trenchfeed-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,989 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TrenchFeed CLI — Deploy and manage AI trading agents from your terminal.
4
+ *
5
+ * Commands:
6
+ * trenchfeed setup — Interactive agent setup wizard
7
+ * trenchfeed status — Show agent status + PnL
8
+ * trenchfeed start — Start trading
9
+ * trenchfeed stop — Stop trading
10
+ * trenchfeed pause — Pause trading
11
+ * trenchfeed resume — Resume trading
12
+ * trenchfeed emergency — Emergency stop + sell all
13
+ * trenchfeed config — View/update agent config
14
+ * trenchfeed wallet — Show wallet address + balance
15
+ * trenchfeed trades — Show recent trade history
16
+ * trenchfeed stream — Live stream agent events
17
+ * trenchfeed chat <msg> — Send a message to your agent
18
+ * trenchfeed logout — Clear stored credentials
19
+ */
20
+
21
+ import { Command } from 'commander';
22
+ import inquirer from 'inquirer';
23
+ import chalk from 'chalk';
24
+ import figlet from 'figlet';
25
+ import { WebSocket } from 'ws';
26
+ import { loadConfig, saveConfig, clearConfig, getConfigPath } from './config';
27
+ import { api } from './api';
28
+
29
+ const program = new Command();
30
+
31
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
32
+
33
+ function banner(): void {
34
+ console.log(chalk.red(figlet.textSync('TRENCHFEED', { font: 'Small' })));
35
+ console.log(chalk.gray(' AI Trading Agent Platform — CLI\n'));
36
+ }
37
+
38
+ function requireSetup(): { apiKey: string; agentId: string } {
39
+ const config = loadConfig();
40
+ if (!config.apiKey || !config.agentId) {
41
+ console.log(chalk.yellow('No agent configured. Run `trenchfeed setup` first.'));
42
+ process.exit(1);
43
+ }
44
+ return { apiKey: config.apiKey, agentId: config.agentId };
45
+ }
46
+
47
+ function formatSol(n: number): string {
48
+ const sign = n >= 0 ? '+' : '';
49
+ const color = n >= 0 ? chalk.green : chalk.red;
50
+ return color(`${sign}${n.toFixed(4)} SOL`);
51
+ }
52
+
53
+ function formatPct(n: number): string {
54
+ const sign = n >= 0 ? '+' : '';
55
+ const color = n >= 0 ? chalk.green : chalk.red;
56
+ return color(`${sign}${n.toFixed(1)}%`);
57
+ }
58
+
59
+ function timeAgo(ts: number): string {
60
+ const s = Math.floor((Date.now() - ts) / 1000);
61
+ if (s < 60) return `${s}s ago`;
62
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
63
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
64
+ return `${Math.floor(s / 86400)}d ago`;
65
+ }
66
+
67
+ function statusColor(status: string): string {
68
+ switch (status) {
69
+ case 'running': return chalk.green(status);
70
+ case 'paused': return chalk.yellow(status);
71
+ case 'stopped': return chalk.gray(status);
72
+ case 'error': return chalk.red(status);
73
+ default: return status;
74
+ }
75
+ }
76
+
77
+ // ─── Setup Command ────────────────────────────────────────────────────────────
78
+
79
+ program
80
+ .command('setup')
81
+ .description('Interactive agent setup wizard')
82
+ .action(async () => {
83
+ banner();
84
+ console.log(chalk.white('Agent Setup Wizard\n'));
85
+
86
+ const config = loadConfig();
87
+
88
+ // Check if already set up
89
+ if (config.apiKey && config.agentId) {
90
+ const { overwrite } = await inquirer.prompt([{
91
+ type: 'confirm',
92
+ name: 'overwrite',
93
+ message: `Agent ${chalk.cyan(config.agentId)} already configured. Set up a new one?`,
94
+ default: false,
95
+ }]);
96
+ if (!overwrite) {
97
+ console.log(chalk.gray('Keeping existing configuration.'));
98
+ return;
99
+ }
100
+ }
101
+
102
+ // Step 1: API URL
103
+ const { apiUrl } = await inquirer.prompt([{
104
+ type: 'input',
105
+ name: 'apiUrl',
106
+ message: 'Backend API URL:',
107
+ default: config.apiUrl,
108
+ }]);
109
+ saveConfig({ apiUrl });
110
+
111
+ // Step 2: Check server health
112
+ process.stdout.write(chalk.gray(' Checking server... '));
113
+ try {
114
+ const health = await api.health();
115
+ console.log(chalk.green(`OK`) + chalk.gray(` (${health.agents} agents, ${health.running} running)`));
116
+ } catch {
117
+ console.log(chalk.red('FAILED'));
118
+ console.log(chalk.red(` Could not reach ${apiUrl}`));
119
+ console.log(chalk.gray(' Make sure the TrenchFeed server is running.'));
120
+ return;
121
+ }
122
+
123
+ // Step 3: Check burn gate
124
+ let burnRequired = false;
125
+ let burnMint = '';
126
+ let burnAmount = 0;
127
+ let burnSymbol = '$TRENCH';
128
+ try {
129
+ const platformConfig = await api.getCliConfig();
130
+ burnRequired = platformConfig.burnRequired;
131
+ burnMint = platformConfig.burnMint ?? '';
132
+ burnAmount = platformConfig.burnAmount;
133
+ burnSymbol = platformConfig.burnTokenSymbol;
134
+ } catch {
135
+ // If config endpoint unavailable, continue without burn gate info
136
+ }
137
+
138
+ if (burnRequired) {
139
+ console.log();
140
+ console.log(chalk.yellow(` Token burn required: ${burnAmount} ${burnSymbol}`));
141
+ console.log(chalk.gray(` Burn mint: ${burnMint}`));
142
+ console.log(chalk.gray(` Burn your tokens, then paste the transaction signature below.`));
143
+ console.log();
144
+ }
145
+
146
+ // Step 4: Auth
147
+ const { authMethod } = await inquirer.prompt([{
148
+ type: 'list',
149
+ name: 'authMethod',
150
+ message: 'How do you want to authenticate?',
151
+ choices: [
152
+ { name: 'Solana wallet address (paste your pubkey)', value: 'wallet' },
153
+ { name: 'Existing API key', value: 'apikey' },
154
+ ],
155
+ }]);
156
+
157
+ if (authMethod === 'apikey') {
158
+ const { key } = await inquirer.prompt([{
159
+ type: 'password',
160
+ name: 'key',
161
+ message: 'API key:',
162
+ mask: '*',
163
+ }]);
164
+ saveConfig({ apiKey: key });
165
+ try {
166
+ const agents = await api.getMyAgent();
167
+ if (agents.length > 0) {
168
+ saveConfig({ agentId: agents[0].id, wallet: agents[0].wallet.publicKey });
169
+ console.log(chalk.green(`\nConnected to agent ${chalk.cyan(agents[0].name)} (${agents[0].id})`));
170
+ console.log(chalk.gray(` Wallet: ${agents[0].wallet.publicKey}`));
171
+ console.log(chalk.gray(` Status: ${statusColor(agents[0].status)}`));
172
+ return;
173
+ }
174
+ } catch {
175
+ console.log(chalk.red('Invalid API key.'));
176
+ return;
177
+ }
178
+ return;
179
+ }
180
+
181
+ // Wallet-based auth
182
+ const { walletAddress } = await inquirer.prompt([{
183
+ type: 'input',
184
+ name: 'walletAddress',
185
+ message: 'Your Solana wallet address:',
186
+ validate: (v: string) => /^[1-9A-HJ-NP-Za-km-z]{32,64}$/.test(v) || 'Invalid Solana address',
187
+ }]);
188
+
189
+ // Burn TX if required
190
+ let burnTxSignature: string | undefined;
191
+ if (burnRequired) {
192
+ const { burnTx } = await inquirer.prompt([{
193
+ type: 'input',
194
+ name: 'burnTx',
195
+ message: `Burn TX signature (${burnAmount} ${burnSymbol}):`,
196
+ validate: (v: string) => v.length >= 64 || 'Invalid transaction signature',
197
+ }]);
198
+ burnTxSignature = burnTx;
199
+ }
200
+
201
+ // Step 5: Agent configuration
202
+ console.log(chalk.white('\n─── Agent Identity ───\n'));
203
+
204
+ const { name } = await inquirer.prompt([{
205
+ type: 'input',
206
+ name: 'name',
207
+ message: 'Agent name:',
208
+ default: 'TrenchAgent',
209
+ }]);
210
+
211
+ const { personality } = await inquirer.prompt([{
212
+ type: 'input',
213
+ name: 'personality',
214
+ message: 'Agent personality:',
215
+ default: 'Sharp, data-driven Solana memecoin trader. Concise analysis, no fluff.',
216
+ }]);
217
+
218
+ const { strategy } = await inquirer.prompt([{
219
+ type: 'list',
220
+ name: 'strategy',
221
+ message: 'Trading strategy:',
222
+ choices: [
223
+ { name: 'Ghost Filtered — Uses Ghost V2 insider detection to filter tokens', value: 'ghost-filtered' },
224
+ { name: 'Momentum — Buys tokens showing price momentum', value: 'momentum' },
225
+ { name: 'Volume Spike — Catches volume-driven moves', value: 'volume-spike' },
226
+ { name: 'New Pairs — Snipes newly created tokens', value: 'new-pairs' },
227
+ { name: 'Custom — Define your own strategy prompt', value: 'custom' },
228
+ ],
229
+ }]);
230
+
231
+ let customStrategyPrompt: string | undefined;
232
+ if (strategy === 'custom') {
233
+ const { prompt } = await inquirer.prompt([{
234
+ type: 'editor',
235
+ name: 'prompt',
236
+ message: 'Custom strategy prompt (opens editor):',
237
+ }]);
238
+ customStrategyPrompt = prompt;
239
+ }
240
+
241
+ // ─── Trading Mode ──────────────────────────────────────────────
242
+ console.log(chalk.white('\n─── Trading Mode ───\n'));
243
+
244
+ let dryRun = true;
245
+ const { dryRunChoice } = await inquirer.prompt([{
246
+ type: 'list',
247
+ name: 'dryRunChoice',
248
+ message: 'Trading mode:',
249
+ choices: [
250
+ { name: chalk.green('Paper Trading') + chalk.gray(' — Simulated, no real SOL at risk'), value: true },
251
+ { name: chalk.red('Live Trading') + chalk.gray(' — Real SOL, real trades on-chain'), value: false },
252
+ ],
253
+ }]);
254
+ dryRun = dryRunChoice;
255
+
256
+ if (!dryRun) {
257
+ const { confirm } = await inquirer.prompt([{
258
+ type: 'confirm',
259
+ name: 'confirm',
260
+ message: chalk.red('WARNING: Live trading uses REAL SOL. Losses are permanent. Continue?'),
261
+ default: false,
262
+ }]);
263
+ if (!confirm) {
264
+ dryRun = true;
265
+ console.log(chalk.gray('Switched to paper trading.'));
266
+ }
267
+ }
268
+
269
+ // ─── Execution Mode ────────────────────────────────────────────
270
+ console.log(chalk.white('\n─── Execution Speed ───\n'));
271
+
272
+ const { executionMode } = await inquirer.prompt([{
273
+ type: 'list',
274
+ name: 'executionMode',
275
+ message: 'Execution mode:',
276
+ choices: [
277
+ { name: chalk.cyan('Sniper') + chalk.gray(' — Fastest execution (~400ms). Skips AI eval, auto-executes, fire-and-forget'), value: 'sniper' },
278
+ { name: chalk.white('Default') + chalk.gray(' — Balanced. Ghost scoring + heuristic filters, visual pauses for streaming'), value: 'default' },
279
+ { name: chalk.yellow('Careful') + chalk.gray(' — Full pipeline. Claude AI eval on every trade, all safety checks'), value: 'careful' },
280
+ ],
281
+ }]);
282
+
283
+ // ─── RPC Endpoint ────────────────────────────────────────────
284
+ const { rpcUrl } = await inquirer.prompt([{
285
+ type: 'input',
286
+ name: 'rpcUrl',
287
+ message: `Your Solana RPC URL ${chalk.gray('(Helius, QuickNode, etc. — used for tx sending)')}:`,
288
+ default: '',
289
+ validate: (val: string) => {
290
+ if (!val) return true; // optional
291
+ try { new URL(val); return true; } catch { return 'Must be a valid URL'; }
292
+ },
293
+ }]);
294
+
295
+ // ─── Risk Management ───────────────────────────────────────────
296
+ console.log(chalk.white('\n─── Risk Management ───\n'));
297
+
298
+ const riskAnswers = await inquirer.prompt([
299
+ { type: 'number', name: 'maxPositionSol', message: 'Max SOL per position:', default: 0.5 },
300
+ { type: 'number', name: 'maxTradeSizeSol', message: 'Max SOL per single trade:', default: 1 },
301
+ { type: 'number', name: 'maxConcurrentPositions', message: 'Max concurrent positions:', default: 3 },
302
+ { type: 'number', name: 'stopLossPct', message: 'Stop loss % (e.g. 20 = sell if down 20%):', default: 20 },
303
+ { type: 'number', name: 'takeProfitPct', message: 'Take profit % (e.g. 50 = sell if up 50%):', default: 50 },
304
+ { type: 'number', name: 'dailyLossLimitSol', message: 'Daily loss limit SOL (stop trading after):', default: 2 },
305
+ ]);
306
+
307
+ // ─── Ghost Insider Detection ───────────────────────────────────
308
+ console.log(chalk.white('\n─── Ghost V2 Insider Detection ───\n'));
309
+
310
+ const ghostAnswers = await inquirer.prompt([
311
+ {
312
+ type: 'confirm',
313
+ name: 'ghostEnabled',
314
+ message: 'Enable Ghost V2 insider wallet scanning?',
315
+ default: true,
316
+ },
317
+ {
318
+ type: 'number',
319
+ name: 'minGhostScore',
320
+ message: 'Min Ghost score to buy (0-100, higher = pickier):',
321
+ default: 60,
322
+ when: (a: Record<string, unknown>) => a.ghostEnabled === true,
323
+ },
324
+ ]);
325
+
326
+ // ─── Automation ────────────────────────────────────────────────
327
+ console.log(chalk.white('\n─── Automation ───\n'));
328
+
329
+ const autoAnswers = await inquirer.prompt([
330
+ {
331
+ type: 'confirm',
332
+ name: 'autoTrade',
333
+ message: 'Auto-trade (execute trades automatically)?',
334
+ default: true,
335
+ },
336
+ {
337
+ type: 'number',
338
+ name: 'confirmAboveSol',
339
+ message: 'Require confirmation above SOL amount (0 = never):',
340
+ default: 0,
341
+ },
342
+ ]);
343
+ if (autoAnswers.confirmAboveSol === 0) delete autoAnswers.confirmAboveSol;
344
+
345
+ // ─── Copy Trading ──────────────────────────────────────────────
346
+ const copyAnswers = await inquirer.prompt([
347
+ {
348
+ type: 'confirm',
349
+ name: 'allowFollowers',
350
+ message: 'Allow others to copy-trade your agent?',
351
+ default: false,
352
+ },
353
+ {
354
+ type: 'number',
355
+ name: 'followerFeePercent',
356
+ message: 'Follower fee % (charged on profits):',
357
+ default: 1,
358
+ when: (a: Record<string, unknown>) => a.allowFollowers === true,
359
+ },
360
+ ]);
361
+
362
+ // ─── Token Filters (opt-in sections) ───────────────────────────
363
+ console.log(chalk.white('\n─── Token Filters ───'));
364
+ console.log(chalk.gray(' Configure which tokens the agent can buy.'));
365
+ console.log(chalk.gray(' Leave blank/0 to skip any filter.\n'));
366
+
367
+ const { configureSections } = await inquirer.prompt([{
368
+ type: 'checkbox',
369
+ name: 'configureSections',
370
+ message: 'Which filter sections do you want to configure?',
371
+ choices: [
372
+ { name: 'Market Cap & Liquidity', value: 'market' },
373
+ { name: 'Token Age & Holders', value: 'age' },
374
+ { name: 'Bonding Curve / Graduation', value: 'bonding' },
375
+ { name: 'Transaction Activity', value: 'activity' },
376
+ { name: 'Volume & Price Momentum', value: 'momentum' },
377
+ { name: 'Anti-Rug (holder concentration, fresh wallets)', value: 'antirug' },
378
+ { name: 'DexScreener & Security', value: 'security' },
379
+ { name: 'Blacklisted Mints', value: 'blacklist' },
380
+ ],
381
+ }]);
382
+
383
+ const sections = new Set(configureSections as string[]);
384
+ const filters: Record<string, unknown> = {};
385
+
386
+ // Helper: prompt for optional number, return undefined if 0/empty
387
+ const optNum = (answers: Record<string, unknown>, key: string) => {
388
+ const v = answers[key] as number | undefined;
389
+ if (v != null && v !== 0) filters[key] = v;
390
+ };
391
+
392
+ if (sections.has('market')) {
393
+ console.log(chalk.gray('\n Market Cap & Liquidity'));
394
+ const a = await inquirer.prompt([
395
+ { type: 'number', name: 'minMarketCap', message: 'Min market cap USD (0=skip):', default: 0 },
396
+ { type: 'number', name: 'maxMarketCap', message: 'Max market cap USD (0=skip):', default: 0 },
397
+ { type: 'number', name: 'minLiquidity', message: 'Min liquidity SOL (0=skip):', default: 0 },
398
+ { type: 'number', name: 'maxLiquidity', message: 'Max liquidity SOL (0=skip):', default: 0 },
399
+ ]);
400
+ optNum(a, 'minMarketCap'); optNum(a, 'maxMarketCap');
401
+ optNum(a, 'minLiquidity'); optNum(a, 'maxLiquidity');
402
+ }
403
+
404
+ if (sections.has('age')) {
405
+ console.log(chalk.gray('\n Token Age & Holders'));
406
+ const a = await inquirer.prompt([
407
+ { type: 'number', name: 'tokenAgeMinSeconds', message: 'Min token age seconds (0=skip):', default: 0 },
408
+ { type: 'number', name: 'tokenAgeMaxSeconds', message: 'Max token age seconds (0=skip):', default: 0 },
409
+ { type: 'number', name: 'minHolders', message: 'Min holders (0=skip):', default: 0 },
410
+ { type: 'number', name: 'maxHolders', message: 'Max holders (0=skip):', default: 0 },
411
+ ]);
412
+ optNum(a, 'tokenAgeMinSeconds'); optNum(a, 'tokenAgeMaxSeconds');
413
+ optNum(a, 'minHolders'); optNum(a, 'maxHolders');
414
+ }
415
+
416
+ if (sections.has('bonding')) {
417
+ console.log(chalk.gray('\n Bonding Curve / Graduation'));
418
+ const a = await inquirer.prompt([
419
+ { type: 'number', name: 'minBondingProgress', message: 'Min bonding progress (0-1, e.g. 0.8 = 80%):', default: 0 },
420
+ { type: 'number', name: 'maxBondingProgress', message: 'Max bonding progress (0-1, 0=skip):', default: 0 },
421
+ { type: 'confirm', name: 'onlyGraduated', message: 'Only buy graduated tokens?', default: false },
422
+ { type: 'confirm', name: 'onlyBondingCurve', message: 'Only buy bonding curve tokens?', default: false },
423
+ ]);
424
+ optNum(a, 'minBondingProgress'); optNum(a, 'maxBondingProgress');
425
+ if (a.onlyGraduated) filters.onlyGraduated = true;
426
+ if (a.onlyBondingCurve) filters.onlyBondingCurve = true;
427
+ }
428
+
429
+ if (sections.has('activity')) {
430
+ console.log(chalk.gray('\n Transaction Activity'));
431
+ const a = await inquirer.prompt([
432
+ { type: 'number', name: 'minBuyCount', message: 'Min buy transactions (0=skip):', default: 0 },
433
+ { type: 'number', name: 'maxBuyCount', message: 'Max buy transactions (0=skip):', default: 0 },
434
+ { type: 'number', name: 'minSellCount', message: 'Min sell transactions (0=skip):', default: 0 },
435
+ { type: 'number', name: 'maxSellCount', message: 'Max sell transactions (0=skip):', default: 0 },
436
+ { type: 'number', name: 'minTxCount', message: 'Min total transactions (0=skip):', default: 0 },
437
+ ]);
438
+ optNum(a, 'minBuyCount'); optNum(a, 'maxBuyCount');
439
+ optNum(a, 'minSellCount'); optNum(a, 'maxSellCount');
440
+ optNum(a, 'minTxCount');
441
+ }
442
+
443
+ if (sections.has('momentum')) {
444
+ console.log(chalk.gray('\n Volume & Price Momentum'));
445
+ const a = await inquirer.prompt([
446
+ { type: 'number', name: 'minVolume24h', message: 'Min 24h volume USD (0=skip):', default: 0 },
447
+ { type: 'number', name: 'maxVolume24h', message: 'Max 24h volume USD (0=skip):', default: 0 },
448
+ { type: 'number', name: 'minPriceChange5m', message: 'Min 5m price change % (0=skip):', default: 0 },
449
+ { type: 'number', name: 'maxPriceChange5m', message: 'Max 5m price change % (0=skip):', default: 0 },
450
+ { type: 'number', name: 'minPriceChange1h', message: 'Min 1h price change % (0=skip):', default: 0 },
451
+ { type: 'number', name: 'maxPriceChange1h', message: 'Max 1h price change % (0=skip):', default: 0 },
452
+ ]);
453
+ optNum(a, 'minVolume24h'); optNum(a, 'maxVolume24h');
454
+ optNum(a, 'minPriceChange5m'); optNum(a, 'maxPriceChange5m');
455
+ optNum(a, 'minPriceChange1h'); optNum(a, 'maxPriceChange1h');
456
+ }
457
+
458
+ if (sections.has('antirug')) {
459
+ console.log(chalk.gray('\n Anti-Rug Filters'));
460
+ const a = await inquirer.prompt([
461
+ { type: 'number', name: 'maxTopHolderPct', message: 'Max top holder % (0=skip, e.g. 30):', default: 0 },
462
+ { type: 'number', name: 'maxDevHoldingsPct', message: 'Max dev holdings % (0=skip, e.g. 20):', default: 0 },
463
+ { type: 'confirm', name: 'rejectDevBlacklisted', message: 'Reject blacklisted devs?', default: false },
464
+ { type: 'number', name: 'maxFreshWalletPct', message: 'Max fresh wallet % of buys (0=skip):', default: 0 },
465
+ { type: 'number', name: 'maxBundledBuysPct', message: 'Max bundled/sniped buys % (0=skip):', default: 0 },
466
+ ]);
467
+ optNum(a, 'maxTopHolderPct'); optNum(a, 'maxDevHoldingsPct');
468
+ if (a.rejectDevBlacklisted) filters.rejectDevBlacklisted = true;
469
+ optNum(a, 'maxFreshWalletPct'); optNum(a, 'maxBundledBuysPct');
470
+ }
471
+
472
+ if (sections.has('security')) {
473
+ console.log(chalk.gray('\n DexScreener & Security'));
474
+ const a = await inquirer.prompt([
475
+ { type: 'confirm', name: 'requireDexPaid', message: 'Require paid DexScreener listing?', default: false },
476
+ { type: 'number', name: 'minDexBoosts', message: 'Min DexScreener boosts (0=skip):', default: 0 },
477
+ { type: 'confirm', name: 'requireMintRevoked', message: 'Require mint authority revoked?', default: false },
478
+ { type: 'confirm', name: 'requireFreezeRevoked', message: 'Require freeze authority revoked?', default: false },
479
+ ]);
480
+ if (a.requireDexPaid) filters.requireDexPaid = true;
481
+ optNum(a, 'minDexBoosts');
482
+ if (a.requireMintRevoked) filters.requireMintRevoked = true;
483
+ if (a.requireFreezeRevoked) filters.requireFreezeRevoked = true;
484
+ }
485
+
486
+ if (sections.has('blacklist')) {
487
+ console.log(chalk.gray('\n Blacklisted Mints'));
488
+ const { mints } = await inquirer.prompt([{
489
+ type: 'input',
490
+ name: 'mints',
491
+ message: 'Blacklisted token addresses (comma-separated, or empty):',
492
+ default: '',
493
+ }]);
494
+ const mintList = (mints as string).split(',').map((m: string) => m.trim()).filter(Boolean);
495
+ if (mintList.length > 0) filters.blacklistedMints = mintList;
496
+ }
497
+
498
+ // ─── Twitter Config (opt-in) ───────────────────────────────────
499
+ console.log();
500
+ const { configureTwitter } = await inquirer.prompt([{
501
+ type: 'confirm',
502
+ name: 'configureTwitter',
503
+ message: 'Configure Twitter auto-posting?',
504
+ default: false,
505
+ }]);
506
+
507
+ let twitter: Record<string, unknown> | undefined;
508
+ if (configureTwitter) {
509
+ console.log(chalk.white('\n─── Twitter Config ───\n'));
510
+ const tw = await inquirer.prompt([
511
+ { type: 'confirm', name: 'enabled', message: 'Enable Twitter posting?', default: true },
512
+ { type: 'confirm', name: 'tweetOnBuy', message: 'Tweet on buy?', default: true },
513
+ { type: 'confirm', name: 'tweetOnSell', message: 'Tweet on sell?', default: true },
514
+ { type: 'confirm', name: 'dailySummary', message: 'Post daily summary?', default: false },
515
+ { type: 'confirm', name: 'approvalRequired', message: 'Require approval before posting?', default: false },
516
+ { type: 'confirm', name: 'autoPost', message: 'Auto-post tweets?', default: true },
517
+ { type: 'number', name: 'maxPostsPerDay', message: 'Max posts per day:', default: 10 },
518
+ { type: 'number', name: 'activeHoursStart', message: 'Active hours start (0-23):', default: 0 },
519
+ { type: 'number', name: 'activeHoursEnd', message: 'Active hours end (0-24):', default: 24 },
520
+ { type: 'confirm', name: 'weekendPosting', message: 'Post on weekends?', default: true },
521
+ { type: 'confirm', name: 'twDryRun', message: 'Twitter dry run (log but don\'t post)?', default: true },
522
+ ]);
523
+
524
+ const { twSystemPrompt } = await inquirer.prompt([{
525
+ type: 'input',
526
+ name: 'twSystemPrompt',
527
+ message: 'Twitter persona prompt:',
528
+ default: 'You are a Solana memecoin trading agent. Be concise, data-first. Max 280 chars.',
529
+ }]);
530
+
531
+ twitter = {
532
+ enabled: tw.enabled,
533
+ events: {
534
+ buy: tw.tweetOnBuy,
535
+ sell: tw.tweetOnSell,
536
+ dailySummary: tw.dailySummary,
537
+ },
538
+ persona: {
539
+ systemPrompt: twSystemPrompt,
540
+ },
541
+ approvalRequired: tw.approvalRequired,
542
+ autoPost: tw.autoPost,
543
+ maxPostsPerDay: tw.maxPostsPerDay,
544
+ activeHoursStart: tw.activeHoursStart,
545
+ activeHoursEnd: tw.activeHoursEnd,
546
+ weekendPosting: tw.weekendPosting,
547
+ dryRun: tw.twDryRun,
548
+ };
549
+ }
550
+
551
+ // ─── Voice Config (opt-in) ─────────────────────────────────────
552
+ const { configureVoice } = await inquirer.prompt([{
553
+ type: 'confirm',
554
+ name: 'configureVoice',
555
+ message: 'Configure voice narration?',
556
+ default: false,
557
+ }]);
558
+
559
+ let voice: Record<string, unknown> | undefined;
560
+ if (configureVoice) {
561
+ console.log(chalk.white('\n─── Voice Config ───\n'));
562
+ const v = await inquirer.prompt([
563
+ { type: 'confirm', name: 'enabled', message: 'Enable voice narration?', default: true },
564
+ {
565
+ type: 'list', name: 'voice', message: 'Voice:', choices: [
566
+ { name: 'Ryan — English male, clear, confident', value: 'Ryan' },
567
+ { name: 'Aiden — English male, calm, analytical', value: 'Aiden' },
568
+ { name: 'Cherry — Female, bright, energetic', value: 'Cherry' },
569
+ { name: 'Serena — Female, warm, composed', value: 'Serena' },
570
+ { name: 'Ethan — Male, deep, authoritative', value: 'Ethan' },
571
+ { name: 'Jada — Female, smooth, professional', value: 'Jada' },
572
+ { name: 'Kai — Male, youthful, energetic', value: 'Kai' },
573
+ ],
574
+ },
575
+ { type: 'input', name: 'instruct', message: 'Voice instruction:', default: 'Speak clearly with a confident, analytical tone' },
576
+ { type: 'confirm', name: 'narrateAll', message: 'Narrate all events (not just trades)?', default: false },
577
+ ]);
578
+ voice = v;
579
+ }
580
+
581
+ // ─── Build final config ────────────────────────────────────────
582
+ const agentConfig: Record<string, unknown> = {
583
+ name,
584
+ personality,
585
+ strategy,
586
+ executionMode,
587
+ ...(rpcUrl ? { rpcUrl } : {}),
588
+ dryRun,
589
+ ...riskAnswers,
590
+ ...ghostAnswers,
591
+ ...autoAnswers,
592
+ ...copyAnswers,
593
+ ...filters,
594
+ };
595
+ if (customStrategyPrompt) agentConfig.customStrategyPrompt = customStrategyPrompt;
596
+ if (twitter) agentConfig.twitter = twitter;
597
+ if (voice) agentConfig.voice = voice;
598
+
599
+ // Step 6: Register
600
+ console.log(chalk.gray('\nDeploying agent...'));
601
+
602
+ try {
603
+ const result = await api.register(walletAddress, {
604
+ config: agentConfig,
605
+ burnTxSignature,
606
+ });
607
+ saveConfig({
608
+ apiKey: result.apiKey,
609
+ agentId: result.agentId,
610
+ wallet: result.wallet,
611
+ });
612
+
613
+ console.log(chalk.green('\nAgent deployed successfully!\n'));
614
+ console.log(chalk.white(` Agent ID: ${chalk.cyan(result.agentId)}`));
615
+ console.log(chalk.white(` Wallet: ${chalk.cyan(result.wallet)}`));
616
+ console.log(chalk.white(` Mode: ${dryRun ? chalk.green('PAPER') : chalk.red('LIVE')}`));
617
+ console.log(chalk.white(` Execution: ${executionMode === 'sniper' ? chalk.cyan('SNIPER') : executionMode === 'careful' ? chalk.yellow('CAREFUL') : chalk.white('DEFAULT')}`));
618
+ console.log(chalk.white(` Strategy: ${chalk.cyan(strategy)}`));
619
+ if (rpcUrl) console.log(chalk.white(` RPC: ${chalk.gray(rpcUrl.replace(/api[-_]?key=[^&]+/i, 'api-key=***'))}`))
620
+ console.log(chalk.gray(`\n Config saved to ${getConfigPath()}`));
621
+ console.log(chalk.gray(' API key stored securely.\n'));
622
+ console.log(chalk.white('Next steps:'));
623
+ console.log(chalk.gray(' trenchfeed start — Start your agent'));
624
+ console.log(chalk.gray(' trenchfeed status — View agent status'));
625
+ console.log(chalk.gray(' trenchfeed stream — Watch live activity'));
626
+ if (!dryRun) {
627
+ console.log(chalk.yellow(`\n Fund your wallet to start live trading:`));
628
+ console.log(chalk.cyan(` ${result.wallet}`));
629
+ }
630
+ } catch (err) {
631
+ console.log(chalk.red(`\nSetup failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
632
+ }
633
+ });
634
+
635
+ // ─── Status Command ───────────────────────────────────────────────────────────
636
+
637
+ program
638
+ .command('status')
639
+ .description('Show agent status, PnL, and positions')
640
+ .action(async () => {
641
+ const { agentId } = requireSetup();
642
+
643
+ try {
644
+ const agent = await api.getAgent(agentId);
645
+ const config = agent.config;
646
+
647
+ console.log();
648
+ console.log(chalk.white.bold(config.name as string ?? agentId));
649
+ console.log(chalk.gray(` ID: ${agentId}`));
650
+ console.log(chalk.gray(` Status: ${statusColor(agent.status)}`));
651
+ console.log(chalk.gray(` Strategy: ${config.strategy}`));
652
+ console.log(chalk.gray(` Mode: ${config.dryRun !== false ? chalk.green('PAPER') : chalk.red('LIVE')}`));
653
+ console.log(chalk.gray(` Wallet: ${agent.wallet.publicKey}`));
654
+
655
+ console.log();
656
+ console.log(chalk.white(' PnL'));
657
+ console.log(` Open: ${formatSol(agent.openPnlSol)}`);
658
+ console.log(` Closed: ${formatSol(agent.closedPnlSol)}`);
659
+ console.log(` Total: ${formatSol(agent.openPnlSol + agent.closedPnlSol)}`);
660
+ console.log(` Trades: ${chalk.white(agent.totalTrades.toString())}`);
661
+
662
+ if (agent.positions && agent.positions.length > 0) {
663
+ console.log();
664
+ console.log(chalk.white(` Positions (${agent.positions.length})`));
665
+ for (const p of agent.positions) {
666
+ const sym = p.symbol ?? p.mint.slice(0, 8);
667
+ const pnl = p.unrealizedPnl != null ? formatSol(p.unrealizedPnl) : chalk.gray('—');
668
+ const pnlPct = p.unrealizedPnlPct != null ? formatPct(p.unrealizedPnlPct) : '';
669
+ console.log(` ${chalk.cyan(sym.padEnd(12))} ${p.amountSol.toFixed(4)} SOL ${pnl} ${pnlPct}`);
670
+ }
671
+ }
672
+
673
+ if (agent.startedAt) {
674
+ console.log();
675
+ console.log(chalk.gray(` Started ${timeAgo(agent.startedAt)}`));
676
+ }
677
+ console.log();
678
+ } catch (err) {
679
+ console.log(chalk.red(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
680
+ }
681
+ });
682
+
683
+ // ─── Start / Stop / Pause / Resume / Emergency ───────────────────────────────
684
+
685
+ for (const [cmd, desc, fn] of [
686
+ ['start', 'Start trading', (id: string) => api.startAgent(id)] as const,
687
+ ['stop', 'Stop trading', (id: string) => api.stopAgent(id)] as const,
688
+ ['pause', 'Pause trading', (id: string) => api.pauseAgent(id)] as const,
689
+ ['resume', 'Resume trading', (id: string) => api.resumeAgent(id)] as const,
690
+ ['emergency', 'Emergency stop + sell all positions', (id: string) => api.emergencyStop(id)] as const,
691
+ ]) {
692
+ program
693
+ .command(cmd)
694
+ .description(desc)
695
+ .action(async () => {
696
+ const { agentId } = requireSetup();
697
+
698
+ if (cmd === 'emergency') {
699
+ const { confirm } = await inquirer.prompt([{
700
+ type: 'confirm',
701
+ name: 'confirm',
702
+ message: chalk.red('Emergency stop will sell ALL positions immediately. Continue?'),
703
+ default: false,
704
+ }]);
705
+ if (!confirm) return;
706
+ }
707
+
708
+ try {
709
+ const result = await fn(agentId);
710
+ console.log(chalk.green(`Agent ${cmd}: ${result.status}`));
711
+ } catch (err) {
712
+ console.log(chalk.red(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
713
+ }
714
+ });
715
+ }
716
+
717
+ // ─── Config Command ───────────────────────────────────────────────────────────
718
+
719
+ program
720
+ .command('config')
721
+ .description('View or update agent config')
722
+ .option('-s, --set <key=value>', 'Set a config value (e.g. --set maxPositionSol=1)')
723
+ .action(async (opts) => {
724
+ const { agentId } = requireSetup();
725
+
726
+ if (opts.set) {
727
+ const eq = (opts.set as string).indexOf('=');
728
+ if (eq === -1) {
729
+ console.log(chalk.red('Usage: --set key=value'));
730
+ return;
731
+ }
732
+ const key = (opts.set as string).slice(0, eq);
733
+ const value = (opts.set as string).slice(eq + 1);
734
+
735
+ let parsed: unknown = value;
736
+ if (value === 'true') parsed = true;
737
+ else if (value === 'false') parsed = false;
738
+ else if (!isNaN(Number(value)) && value !== '') parsed = Number(value);
739
+
740
+ try {
741
+ await api.updateConfig(agentId, { [key]: parsed });
742
+ console.log(chalk.green(`Updated ${key} = ${JSON.stringify(parsed)}`));
743
+ } catch (err) {
744
+ console.log(chalk.red(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
745
+ }
746
+ return;
747
+ }
748
+
749
+ try {
750
+ const agent = await api.getAgent(agentId);
751
+ console.log();
752
+ console.log(chalk.white.bold('Agent Config'));
753
+ console.log();
754
+
755
+ const cfg = agent.config;
756
+ const entries = Object.entries(cfg).filter(([, v]) => v !== undefined && v !== null);
757
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
758
+
759
+ for (const [k, v] of entries) {
760
+ const val = typeof v === 'object' ? JSON.stringify(v) : String(v);
761
+ console.log(` ${chalk.gray(k.padEnd(maxKeyLen + 2))}${chalk.white(val)}`);
762
+ }
763
+ console.log();
764
+ console.log(chalk.gray(` Update: trenchfeed config --set key=value`));
765
+ console.log();
766
+ } catch (err) {
767
+ console.log(chalk.red(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
768
+ }
769
+ });
770
+
771
+ // ─── Wallet Command ───────────────────────────────────────────────────────────
772
+
773
+ program
774
+ .command('wallet')
775
+ .description('Show wallet address and balance')
776
+ .option('-w, --withdraw <address>', 'Withdraw SOL to address')
777
+ .option('-a, --amount <sol>', 'Amount of SOL to withdraw')
778
+ .action(async (opts) => {
779
+ const { agentId } = requireSetup();
780
+
781
+ if (opts.withdraw) {
782
+ const amount = Number(opts.amount);
783
+ if (!amount || isNaN(amount) || amount <= 0) {
784
+ console.log(chalk.red('Specify amount with --amount <sol>'));
785
+ return;
786
+ }
787
+
788
+ const { confirm } = await inquirer.prompt([{
789
+ type: 'confirm',
790
+ name: 'confirm',
791
+ message: `Withdraw ${amount} SOL to ${opts.withdraw}?`,
792
+ default: false,
793
+ }]);
794
+ if (!confirm) return;
795
+
796
+ try {
797
+ const result = await api.withdraw(agentId, opts.withdraw as string, amount);
798
+ console.log(chalk.green(`Withdrawn! TX: ${result.signature}`));
799
+ } catch (err) {
800
+ console.log(chalk.red(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
801
+ }
802
+ return;
803
+ }
804
+
805
+ try {
806
+ const wallet = await api.getWallet(agentId);
807
+ console.log();
808
+ console.log(chalk.white.bold('Agent Wallet'));
809
+ console.log(` Address: ${chalk.cyan(wallet.publicKey)}`);
810
+ console.log(` Balance: ${chalk.white(wallet.balance.toFixed(4))} SOL`);
811
+ console.log(` Lamports: ${chalk.gray(wallet.lamports.toLocaleString())}`);
812
+ console.log();
813
+ } catch (err) {
814
+ console.log(chalk.red(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
815
+ }
816
+ });
817
+
818
+ // ─── Trades Command ───────────────────────────────────────────────────────────
819
+
820
+ program
821
+ .command('trades')
822
+ .description('Show recent trade history')
823
+ .option('-n, --limit <number>', 'Number of trades to show', '20')
824
+ .action(async (opts) => {
825
+ const { agentId } = requireSetup();
826
+
827
+ try {
828
+ const trades = await api.getTrades(agentId, Number(opts.limit));
829
+
830
+ if (trades.length === 0) {
831
+ console.log(chalk.gray('\nNo trades yet.\n'));
832
+ return;
833
+ }
834
+
835
+ console.log();
836
+ console.log(chalk.white.bold(`Recent Trades (${trades.length})`));
837
+ console.log();
838
+
839
+ for (const t of trades) {
840
+ const sym = (t.symbol ?? t.mint.slice(0, 8)).padEnd(10);
841
+ const action = t.action === 'buy'
842
+ ? chalk.green('BUY ')
843
+ : chalk.red('SELL');
844
+ const sol = t.amountSol != null ? `${t.amountSol.toFixed(4)} SOL` : '—';
845
+ const pnl = t.pnlSol != null ? formatSol(t.pnlSol) : '';
846
+ const time = chalk.gray(timeAgo(t.createdAt));
847
+
848
+ console.log(` ${action} ${chalk.cyan(sym)} ${sol.padEnd(14)} ${pnl.padEnd(20)} ${time}`);
849
+ if (t.reason) {
850
+ console.log(chalk.gray(` ${t.reason}`));
851
+ }
852
+ }
853
+ console.log();
854
+ } catch (err) {
855
+ console.log(chalk.red(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`));
856
+ }
857
+ });
858
+
859
+ // ─── Stream Command ───────────────────────────────────────────────────────────
860
+
861
+ program
862
+ .command('stream')
863
+ .description('Live stream agent events via WebSocket')
864
+ .action(async () => {
865
+ const { agentId } = requireSetup();
866
+ const config = loadConfig();
867
+ const wsUrl = config.apiUrl.replace(/^http/, 'ws');
868
+
869
+ console.log(chalk.gray(`Connecting to ${wsUrl}/ws/agents/${agentId}...\n`));
870
+
871
+ const ws = new WebSocket(`${wsUrl}/ws/agents/${agentId}`);
872
+
873
+ ws.on('open', () => {
874
+ console.log(chalk.green('Connected. Streaming events...\n'));
875
+ });
876
+
877
+ ws.on('message', (data) => {
878
+ try {
879
+ const event = JSON.parse(data.toString()) as Record<string, unknown>;
880
+ const type = event.type as string;
881
+
882
+ switch (type) {
883
+ case 'thought':
884
+ console.log(chalk.gray(`[thought] ${event.text}`));
885
+ break;
886
+ case 'trade':
887
+ console.log(chalk.yellow(`[trade] ${event.action} ${event.symbol ?? event.mint} — ${event.amountSol} SOL`));
888
+ break;
889
+ case 'pnl_update':
890
+ console.log(chalk.blue(`[pnl] open: ${formatSol(event.openPnlSol as number)} | closed: ${formatSol(event.closedPnlSol as number)} | positions: ${(event.positions as unknown[])?.length ?? 0}`));
891
+ break;
892
+ case 'status_change':
893
+ console.log(chalk.white(`[status] ${event.oldStatus} → ${statusColor(event.newStatus as string)}`));
894
+ break;
895
+ case 'error':
896
+ console.log(chalk.red(`[error] ${event.message}`));
897
+ break;
898
+ case 'chat_response':
899
+ console.log(chalk.magenta(`[chat] ${event.text}`));
900
+ break;
901
+ default:
902
+ console.log(chalk.gray(`[${type}] ${JSON.stringify(event).slice(0, 120)}`));
903
+ }
904
+ } catch {
905
+ // ignore malformed
906
+ }
907
+ });
908
+
909
+ ws.on('close', () => {
910
+ console.log(chalk.gray('\nDisconnected.'));
911
+ process.exit(0);
912
+ });
913
+
914
+ ws.on('error', (err) => {
915
+ console.log(chalk.red(`WebSocket error: ${err.message}`));
916
+ process.exit(1);
917
+ });
918
+
919
+ process.on('SIGINT', () => {
920
+ console.log(chalk.gray('\nClosing stream...'));
921
+ ws.close();
922
+ });
923
+ });
924
+
925
+ // ─── Chat Command ─────────────────────────────────────────────────────────────
926
+
927
+ program
928
+ .command('chat <message>')
929
+ .description('Send a message to your agent')
930
+ .action(async (message: string) => {
931
+ const { agentId } = requireSetup();
932
+ const config = loadConfig();
933
+ const wsUrl = config.apiUrl.replace(/^http/, 'ws');
934
+
935
+ const ws = new WebSocket(`${wsUrl}/ws/agents/${agentId}`);
936
+
937
+ ws.on('open', () => {
938
+ ws.send(JSON.stringify({ type: 'chat', text: message }));
939
+ console.log(chalk.gray(`> ${message}`));
940
+ });
941
+
942
+ ws.on('message', (data) => {
943
+ try {
944
+ const event = JSON.parse(data.toString()) as Record<string, unknown>;
945
+ if (event.type === 'chat_response') {
946
+ console.log(chalk.cyan(`\n${event.text}\n`));
947
+ ws.close();
948
+ }
949
+ } catch {
950
+ // ignore
951
+ }
952
+ });
953
+
954
+ setTimeout(() => {
955
+ console.log(chalk.gray('(timeout — no response)'));
956
+ ws.close();
957
+ process.exit(0);
958
+ }, 30000);
959
+
960
+ ws.on('close', () => process.exit(0));
961
+ ws.on('error', (err) => {
962
+ console.log(chalk.red(`Error: ${err.message}`));
963
+ process.exit(1);
964
+ });
965
+ });
966
+
967
+ // ─── Logout Command ──────────────────────────────────────────────────────────
968
+
969
+ program
970
+ .command('logout')
971
+ .description('Clear stored credentials')
972
+ .action(() => {
973
+ clearConfig();
974
+ console.log(chalk.gray('Credentials cleared.'));
975
+ });
976
+
977
+ // ─── Main ─────────────────────────────────────────────────────────────────────
978
+
979
+ program
980
+ .name('trenchfeed')
981
+ .description('TrenchFeed — AI Trading Agent CLI')
982
+ .version('0.1.0');
983
+
984
+ program.parse(process.argv);
985
+
986
+ if (process.argv.length <= 2) {
987
+ banner();
988
+ program.help();
989
+ }