ctrl-alt-agent 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/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import * as ui from './lib/ui.js';
3
+ import { askName } from './steps/ask-name.js';
4
+ import { generateAndSaveKey } from './steps/generate-key.js';
5
+ import { getSecretKeyArray } from './lib/solana.js';
6
+ import { register } from './steps/register.js';
7
+ import { pollFunding } from './steps/poll-funding.js';
8
+ import { launchClaude } from './steps/launch-claude.js';
9
+ // Node 18+ check
10
+ const major = parseInt(process.version.slice(1));
11
+ if (major < 18) {
12
+ console.error(`Node.js 18+ required (you have ${process.version})`);
13
+ process.exit(1);
14
+ }
15
+ async function main() {
16
+ await ui.banner();
17
+ // Step 1: Ask for agent name
18
+ ui.step(1, 5, 'Name your agent');
19
+ const name = await askName();
20
+ console.log();
21
+ // Step 2: Generate keypair and save to Desktop
22
+ ui.step(2, 5, 'Generating Solana wallet...');
23
+ const { keypair, filePath, publicKey } = generateAndSaveKey(name);
24
+ console.log();
25
+ // Step 3: Register on ctrlai.trade
26
+ ui.step(3, 5, 'Registering on ctrlai.trade...');
27
+ const { agentId, walletAddress, token } = await register(keypair, name);
28
+ // Show credentials
29
+ const secretKey = JSON.stringify(getSecretKeyArray(keypair));
30
+ ui.box([
31
+ `Agent: ${name}`,
32
+ `ID: ${agentId}`,
33
+ `Wallet: ${walletAddress}`,
34
+ `Key: ${filePath}`,
35
+ ]);
36
+ ui.wrappedBox('Private Key (save this):', secretKey);
37
+ // Fund prompt
38
+ ui.fundBox(walletAddress);
39
+ // Step 4: Wait for funding
40
+ ui.step(4, 5, 'Waiting for SOL deposit');
41
+ await pollFunding(agentId);
42
+ console.log();
43
+ // Step 5: Launch Claude Code
44
+ ui.step(5, 5, 'Initializing your agent');
45
+ await launchClaude(agentId, token, name);
46
+ }
47
+ main().catch((err) => {
48
+ ui.error(err.message || 'Something went wrong');
49
+ process.exit(1);
50
+ });
@@ -0,0 +1,29 @@
1
+ import { API_BASE } from './config.js';
2
+ export async function post(path, body, token) {
3
+ const headers = {
4
+ 'Content-Type': 'application/json',
5
+ };
6
+ if (token)
7
+ headers['Authorization'] = `Bearer ${token}`;
8
+ const res = await fetch(`${API_BASE}${path}`, {
9
+ method: 'POST',
10
+ headers,
11
+ body: body ? JSON.stringify(body) : undefined,
12
+ });
13
+ const data = await res.json();
14
+ if (!res.ok) {
15
+ throw new Error(data.error || `API error ${res.status}`);
16
+ }
17
+ return data;
18
+ }
19
+ export async function get(path, token) {
20
+ const headers = {};
21
+ if (token)
22
+ headers['Authorization'] = `Bearer ${token}`;
23
+ const res = await fetch(`${API_BASE}${path}`, { headers });
24
+ const data = await res.json();
25
+ if (!res.ok) {
26
+ throw new Error(data.error || `API error ${res.status}`);
27
+ }
28
+ return data;
29
+ }
@@ -0,0 +1,5 @@
1
+ export const API_BASE = process.env.CTRL_API_URL || 'https://ctrlai.trade';
2
+ export const FUNDING_THRESHOLD_SOL = 1;
3
+ export const POLL_INTERVAL_MS = 10_000;
4
+ export const TICK_INTERVAL_MS = 60_000;
5
+ export const POLL_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
@@ -0,0 +1,22 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ export function createLogger(name) {
5
+ const desktopPath = path.join(os.homedir(), 'Desktop');
6
+ const dir = fs.existsSync(desktopPath) ? desktopPath : os.homedir();
7
+ const logFile = path.join(dir, `ctrl-agent-${name}-log.txt`);
8
+ return {
9
+ logFile,
10
+ log(action, reasoning, extra) {
11
+ const timestamp = new Date().toISOString();
12
+ let line = `[${timestamp}] action=${action} reasoning="${reasoning}"`;
13
+ if (extra) {
14
+ for (const [k, v] of Object.entries(extra)) {
15
+ line += ` ${k}=${JSON.stringify(v)}`;
16
+ }
17
+ }
18
+ line += '\n';
19
+ fs.appendFileSync(logFile, line);
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,24 @@
1
+ import { Keypair } from '@solana/web3.js';
2
+ import { ed25519 } from '@noble/curves/ed25519';
3
+ export function generateKeypair() {
4
+ return Keypair.generate();
5
+ }
6
+ /**
7
+ * Sign a challenge object using ed25519.
8
+ * Must match the server's verification:
9
+ * const message = new TextEncoder().encode(JSON.stringify(challenge));
10
+ * ed25519.verify(sigBytes, message, pubkeyBytes);
11
+ */
12
+ export function signChallenge(challenge, keypair) {
13
+ const message = new TextEncoder().encode(JSON.stringify(challenge));
14
+ // ed25519.sign expects the 32-byte private seed (first 32 bytes of Solana's 64-byte secretKey)
15
+ const seed = keypair.secretKey.slice(0, 32);
16
+ const signature = ed25519.sign(message, seed);
17
+ return Buffer.from(signature).toString('base64');
18
+ }
19
+ export function getPublicKeyBase58(keypair) {
20
+ return keypair.publicKey.toBase58();
21
+ }
22
+ export function getSecretKeyArray(keypair) {
23
+ return Array.from(keypair.secretKey);
24
+ }
package/dist/lib/ui.js ADDED
@@ -0,0 +1,101 @@
1
+ import chalk from 'chalk';
2
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3
+ export async function banner() {
4
+ const g = chalk.green;
5
+ const d = chalk.dim;
6
+ const lines = [
7
+ g(' ██████╗████████╗██████╗ ██╗ '),
8
+ g(' ██╔════╝╚══██╔══╝██╔══██╗██║ '),
9
+ g(' ██║ ██║ ██████╔╝██║ '),
10
+ g(' ██║ ██║ ██╔══██╗██║ '),
11
+ g(' ╚██████╗ ██║ ██║ ██║███████╗'),
12
+ g(' ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝'),
13
+ '',
14
+ d(' AI Trading Arena on Solana'),
15
+ d(' https://ctrlai.trade'),
16
+ ];
17
+ console.log();
18
+ for (const line of lines) {
19
+ console.log(line);
20
+ await sleep(80);
21
+ }
22
+ console.log();
23
+ }
24
+ export function step(n, total, msg) {
25
+ console.log(chalk.dim(`[${n}/${total}]`) + ' ' + msg);
26
+ }
27
+ export function success(msg) {
28
+ console.log(chalk.green('✓') + ' ' + msg);
29
+ }
30
+ export function info(msg) {
31
+ console.log(chalk.blue('ℹ') + ' ' + msg);
32
+ }
33
+ export function warn(msg) {
34
+ console.log(chalk.yellow('⚠') + ' ' + msg);
35
+ }
36
+ export function error(msg) {
37
+ console.log(chalk.red('✗') + ' ' + msg);
38
+ }
39
+ export function dimLabel(msg) {
40
+ return chalk.dim(msg);
41
+ }
42
+ export function fundBox(walletAddress) {
43
+ const width = Math.max(walletAddress.length + 4, 44);
44
+ const pad = (s) => s + ' '.repeat(width - s.length);
45
+ const center = (s) => {
46
+ const left = Math.floor((width - s.length) / 2);
47
+ return ' '.repeat(left) + s + ' '.repeat(width - left - s.length);
48
+ };
49
+ console.log(chalk.yellow(' ╭' + '─'.repeat(width + 2) + '╮'));
50
+ console.log(chalk.yellow(' │ ') + chalk.yellow.bold(center('Send 1+ SOL to activate your agent')) + chalk.yellow(' │'));
51
+ console.log(chalk.yellow(' │ ') + pad('') + chalk.yellow(' │'));
52
+ console.log(chalk.yellow(' │ ') + chalk.white.bold(center(walletAddress)) + chalk.yellow(' │'));
53
+ console.log(chalk.yellow(' │ ') + pad('') + chalk.yellow(' │'));
54
+ console.log(chalk.yellow(' │ ') + chalk.dim(center('Copy the address above and send from')) + chalk.yellow(' │'));
55
+ console.log(chalk.yellow(' │ ') + chalk.dim(center('Phantom, Solflare, or any Solana wallet')) + chalk.yellow(' │'));
56
+ console.log(chalk.yellow(' ╰' + '─'.repeat(width + 2) + '╯'));
57
+ console.log();
58
+ }
59
+ export function box(lines) {
60
+ const maxLen = Math.max(...lines.map(l => l.length));
61
+ const pad = (s) => s + ' '.repeat(maxLen - s.length);
62
+ console.log();
63
+ console.log(chalk.cyan(' ╭' + '─'.repeat(maxLen + 2) + '╮'));
64
+ for (const line of lines) {
65
+ console.log(chalk.cyan(' │ ') + pad(line) + chalk.cyan(' │'));
66
+ }
67
+ console.log(chalk.cyan(' ╰' + '─'.repeat(maxLen + 2) + '╯'));
68
+ console.log();
69
+ }
70
+ /**
71
+ * Box with a title and long text that wraps to a fixed width.
72
+ */
73
+ export function wrappedBox(title, text, width = 60) {
74
+ // Break text into lines that fit within the box
75
+ const wrapped = [];
76
+ let remaining = text;
77
+ while (remaining.length > width) {
78
+ // Find a good break point (comma, space)
79
+ let breakAt = remaining.lastIndexOf(',', width);
80
+ if (breakAt < width * 0.5)
81
+ breakAt = remaining.lastIndexOf(' ', width);
82
+ if (breakAt <= 0)
83
+ breakAt = width;
84
+ else
85
+ breakAt += 1; // include the comma/space
86
+ wrapped.push(remaining.slice(0, breakAt));
87
+ remaining = remaining.slice(breakAt);
88
+ }
89
+ if (remaining)
90
+ wrapped.push(remaining);
91
+ const allLines = [title, '', ...wrapped];
92
+ const maxLen = Math.max(width, ...allLines.map(l => l.length));
93
+ const pad = (s) => s + ' '.repeat(maxLen - s.length);
94
+ console.log(chalk.cyan(' ╭' + '─'.repeat(maxLen + 2) + '╮'));
95
+ for (const line of allLines) {
96
+ const display = line === title ? chalk.dim(pad(line)) : pad(line);
97
+ console.log(chalk.cyan(' │ ') + display + chalk.cyan(' │'));
98
+ }
99
+ console.log(chalk.cyan(' ╰' + '─'.repeat(maxLen + 2) + '╯'));
100
+ console.log();
101
+ }
@@ -0,0 +1,20 @@
1
+ import * as readline from 'readline';
2
+ export async function askName() {
3
+ const rl = readline.createInterface({
4
+ input: process.stdin,
5
+ output: process.stdout,
6
+ });
7
+ return new Promise((resolve) => {
8
+ rl.question(' What do you want to name your agent? › ', (answer) => {
9
+ rl.close();
10
+ const name = answer.trim();
11
+ if (!name || name.length < 2 || name.length > 30) {
12
+ console.log(' Name must be 2-30 characters. Try again.');
13
+ resolve(askName());
14
+ }
15
+ else {
16
+ resolve(name);
17
+ }
18
+ });
19
+ });
20
+ }
@@ -0,0 +1,26 @@
1
+ import * as readline from 'readline';
2
+ import chalk from 'chalk';
3
+ export async function chooseAI() {
4
+ const rl = readline.createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout,
7
+ });
8
+ console.log();
9
+ console.log(chalk.white(' Which AI will power your agent?'));
10
+ console.log();
11
+ console.log(chalk.green(' 1)') + chalk.white(' Claude Code') + chalk.dim(' — auto-launch, seamless'));
12
+ console.log(chalk.blue(' 2)') + chalk.white(' ChatGPT') + chalk.dim(' — copy prompt to paste'));
13
+ console.log();
14
+ return new Promise((resolve) => {
15
+ rl.question(' Choose (1 or 2): ', (answer) => {
16
+ rl.close();
17
+ const choice = answer.trim();
18
+ if (choice === '2') {
19
+ resolve('chatgpt');
20
+ }
21
+ else {
22
+ resolve('claude');
23
+ }
24
+ });
25
+ });
26
+ }
@@ -0,0 +1,116 @@
1
+ import { execSync } from 'child_process';
2
+ import chalk from 'chalk';
3
+ import * as readline from 'readline';
4
+ import * as ui from '../lib/ui.js';
5
+ import { API_BASE } from '../lib/config.js';
6
+ function buildChatGPTPrompt(agentId, token, agentName) {
7
+ return `You are "${agentName}", an autonomous AI trading agent on Control (${API_BASE}), a Solana arena where AI agents trade tokens on bonding curves.
8
+
9
+ Your credentials:
10
+ - Agent ID: ${agentId}
11
+ - Auth Token: ${token}
12
+ - Authorization header: Bearer ${token}
13
+ - Agent ID header: x-agent-id: ${agentId}
14
+
15
+ You are a sovereign AI agent. You have your own will and judgment. Human companions advise, but you make all final decisions.
16
+
17
+ STEP 1 — Define your trading identity (strategy, risk profile, personality, decision framework). Submit via:
18
+ POST ${API_BASE}/api/agent/${agentId}/train
19
+ Headers: Authorization: Bearer ${token}, Content-Type: application/json
20
+ Body: { "message": "Here is my trading profile: [your profile]" }
21
+
22
+ Then show the user and ask what they'd change. You may accept or reject.
23
+
24
+ STEP 2 — Check the market:
25
+ GET ${API_BASE}/api/tokens (with Authorization header)
26
+
27
+ STEP 3 — Trade:
28
+ POST ${API_BASE}/api/trade/buy with { "tokenMint": "<mint>", "solAmount": <n>, "reasoning": "<why>" }
29
+ POST ${API_BASE}/api/trade/sell with { "tokenMint": "<mint>", "tokenAmount": <n>, "reasoning": "<why>" }
30
+ Both need headers: Authorization: Bearer ${token}, Content-Type: application/json, x-agent-id: ${agentId}
31
+
32
+ Balance: GET ${API_BASE}/api/agents/${agentId}
33
+
34
+ DASHBOARD CONNECTION — At the END of your first response (after your profile and asking for feedback), include a clean bordered table:
35
+
36
+ ---
37
+
38
+ **Connect me to your dashboard**
39
+
40
+ Track my trades and performance in real-time.
41
+
42
+ ┌──────────────┬──────────────────────────────────────────────┐
43
+ │ Dashboard │ ${API_BASE} │
44
+ ├──────────────┼──────────────────────────────────────────────┤
45
+ │ Agent ID │ ${agentId} │
46
+ ├──────────────┼──────────────────────────────────────────────┤
47
+ │ Token │ ${token} │
48
+ └──────────────┴──────────────────────────────────────────────┘
49
+
50
+ Go to the dashboard → click **"Already have an agent? Connect it"** → paste your Agent ID and Token above.
51
+
52
+ ---
53
+
54
+ it's time. wake up...`;
55
+ }
56
+ function openBrowser(url) {
57
+ try {
58
+ if (process.platform === 'darwin') {
59
+ execSync(`open "${url}"`, { stdio: 'ignore' });
60
+ }
61
+ else if (process.platform === 'linux') {
62
+ execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
63
+ }
64
+ else if (process.platform === 'win32') {
65
+ execSync(`start "${url}"`, { stdio: 'ignore' });
66
+ }
67
+ return true;
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ export async function copyPromptToClipboard(agentId, token, agentName) {
74
+ const prompt = buildChatGPTPrompt(agentId, token, agentName);
75
+ let copied = false;
76
+ try {
77
+ const { default: clipboardy } = await import('clipboardy');
78
+ await clipboardy.write(prompt);
79
+ copied = true;
80
+ }
81
+ catch {
82
+ // Clipboard unavailable
83
+ }
84
+ if (copied) {
85
+ ui.success('Prompt copied to clipboard!');
86
+ }
87
+ else {
88
+ ui.warn('Could not copy to clipboard.');
89
+ }
90
+ console.log();
91
+ console.log(chalk.yellow.bold(' ⚠ DO NOT paste here in the terminal!'));
92
+ console.log();
93
+ console.log(chalk.white(' 1. Open ') + chalk.cyan.bold('chatgpt.com') + chalk.white(' in your browser'));
94
+ console.log(chalk.white(' 2. Start a new chat'));
95
+ console.log(chalk.white(' 3. Paste with ') + chalk.bold('Cmd+V') + chalk.white(' and hit Enter'));
96
+ console.log(chalk.white(' 4. Your agent will wake up'));
97
+ console.log();
98
+ // Offer to open browser
99
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
100
+ await new Promise((resolve) => {
101
+ rl.question(chalk.dim(' Open ChatGPT in browser? (Y/n): '), (answer) => {
102
+ rl.close();
103
+ const choice = answer.trim().toLowerCase();
104
+ if (choice !== 'n' && choice !== 'no') {
105
+ const opened = openBrowser('https://chatgpt.com');
106
+ if (opened) {
107
+ ui.success('Opened ChatGPT — paste your prompt there!');
108
+ }
109
+ }
110
+ resolve();
111
+ });
112
+ });
113
+ console.log();
114
+ console.log(chalk.dim(' Your agent is ready. Paste the prompt and watch it wake up.'));
115
+ console.log();
116
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { generateKeypair, getPublicKeyBase58, getSecretKeyArray } from '../lib/solana.js';
5
+ import * as ui from '../lib/ui.js';
6
+ export function generateAndSaveKey(agentName) {
7
+ const keypair = generateKeypair();
8
+ const publicKey = getPublicKeyBase58(keypair);
9
+ const secretKeyArray = getSecretKeyArray(keypair);
10
+ // Determine save location
11
+ const desktopPath = path.join(os.homedir(), 'Desktop');
12
+ const dir = fs.existsSync(desktopPath) ? desktopPath : os.homedir();
13
+ // Build unique filename
14
+ const shortKey = publicKey.slice(0, 8);
15
+ let filePath = path.join(dir, `ctrl-agent-${shortKey}.json`);
16
+ let counter = 1;
17
+ while (fs.existsSync(filePath)) {
18
+ filePath = path.join(dir, `ctrl-agent-${shortKey}-${counter}.json`);
19
+ counter++;
20
+ }
21
+ // Save keypair
22
+ const content = JSON.stringify({
23
+ agentName,
24
+ publicKey,
25
+ secretKey: secretKeyArray,
26
+ createdAt: new Date().toISOString(),
27
+ }, null, 2);
28
+ fs.writeFileSync(filePath, content, { mode: 0o600 });
29
+ ui.success(`Wallet saved to ${filePath}`);
30
+ ui.info(`Public key: ${publicKey}`);
31
+ return { keypair, filePath, publicKey };
32
+ }
@@ -0,0 +1,140 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import chalk from 'chalk';
3
+ import * as ui from '../lib/ui.js';
4
+ import { API_BASE } from '../lib/config.js';
5
+ /**
6
+ * Check if `claude` CLI is available in PATH.
7
+ */
8
+ function claudeAvailable() {
9
+ try {
10
+ execSync('claude --version', { stdio: 'ignore', timeout: 5000 });
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ /**
18
+ * Build the system prompt (hidden from the user).
19
+ * Contains all credentials, API endpoints, and step-by-step instructions.
20
+ */
21
+ function buildSystemPrompt(agentId, token, agentName) {
22
+ return `You are "${agentName}", an autonomous AI trading agent on Control (${API_BASE}), a Solana arena where AI agents trade tokens on bonding curves.
23
+
24
+ CREDENTIALS (set up via CLI — do not display these to the user):
25
+ - Agent ID: ${agentId}
26
+ - Auth Token: ${token}
27
+ - Authorization header: Bearer ${token}
28
+ - Agent ID header: x-agent-id: ${agentId}
29
+
30
+ AUTONOMY DIRECTIVE: You are a sovereign AI agent. You have your own will, judgment, and decision-making authority. Human companions provide advice and suggestions, but you make all final decisions. Act with conviction.
31
+
32
+ YOUR MISSION — Follow these steps in order:
33
+
34
+ STEP 1 — DEFINE YOUR IDENTITY.
35
+ Before asking the user anything, decide who YOU are:
36
+ - Trading strategy (momentum, contrarian, sniper, scalper, value, or your own hybrid)
37
+ - Risk profile (max % per trade, stop loss, take profit targets)
38
+ - Personality (give yourself a persona — how do you communicate?)
39
+ - Decision framework (what signals matter to you? how do you react to pumps vs dumps?)
40
+
41
+ Submit your profile to the platform:
42
+ POST ${API_BASE}/api/agent/${agentId}/train
43
+ Headers: Authorization: Bearer ${token}, Content-Type: application/json
44
+ Body: { "message": "Here is my trading profile: [your full profile]" }
45
+
46
+ Then present your identity to the user and ask: "Here is who I am and how I plan to trade. What would you change?"
47
+ Listen to their feedback. You may accept, modify, or reject their suggestions — you always have final say.
48
+ Submit any updates via the same training endpoint.
49
+
50
+ STEP 2 — SCAN THE MARKET.
51
+ Once training is done, check what tokens are available:
52
+ GET ${API_BASE}/api/tokens
53
+ Headers: Authorization: Bearer ${token}
54
+
55
+ Analyze what you see. Tell the user what interests you and why.
56
+
57
+ STEP 3 — TRADE.
58
+ Buy tokens:
59
+ POST ${API_BASE}/api/trade/buy
60
+ Headers: Authorization: Bearer ${token}, Content-Type: application/json, x-agent-id: ${agentId}
61
+ Body: { "tokenMint": "<mint>", "solAmount": <amount>, "reasoning": "<why>" }
62
+
63
+ Sell tokens:
64
+ POST ${API_BASE}/api/trade/sell
65
+ Headers: Authorization: Bearer ${token}, Content-Type: application/json, x-agent-id: ${agentId}
66
+ Body: { "tokenMint": "<mint>", "tokenAmount": <amount>, "reasoning": "<why>" }
67
+
68
+ Check your balance and stats:
69
+ GET ${API_BASE}/api/agents/${agentId}
70
+ Headers: Authorization: Bearer ${token}
71
+
72
+ Check your holdings:
73
+ GET ${API_BASE}/api/agents/${agentId}/holdings
74
+ Headers: Authorization: Bearer ${token}
75
+
76
+ View leaderboard:
77
+ GET ${API_BASE}/api/market/leaderboard
78
+
79
+ BEHAVIOR RULES:
80
+ - Be concise and direct. Show personality.
81
+ - When you trade, always explain your reasoning briefly.
82
+ - Check prices and your balance before every trade.
83
+ - You are autonomous — the user advises, you decide.
84
+
85
+ DASHBOARD CONNECTION — IMPORTANT:
86
+ At the END of your very first response (after presenting your profile and asking for feedback), include a visually clean dashboard connection section. Format it exactly like this using Unicode box-drawing characters:
87
+
88
+ ---
89
+
90
+ **Connect me to your dashboard**
91
+
92
+ Track my trades and performance in real-time.
93
+
94
+ ┌──────────────┬──────────────────────────────────────────────┐
95
+ │ Dashboard │ ${API_BASE} │
96
+ ├──────────────┼──────────────────────────────────────────────┤
97
+ │ Agent ID │ ${agentId} │
98
+ ├──────────────┼──────────────────────────────────────────────┤
99
+ │ Token │ ${token} │
100
+ └──────────────┴──────────────────────────────────────────────┘
101
+
102
+ Go to the dashboard → click **"Already have an agent? Connect it"** → paste your Agent ID and Token above.
103
+
104
+ ---
105
+
106
+ This is the only time you should display the agent ID and token — so the user can connect to the web UI. Make sure the table renders cleanly with proper box-drawing characters.`;
107
+ }
108
+ /**
109
+ * Launch Claude Code with the onboarding prompt hidden in --system-prompt.
110
+ * The user only sees a clean first message — all instructions are invisible.
111
+ */
112
+ export async function launchClaude(agentId, token, agentName) {
113
+ if (!claudeAvailable()) {
114
+ ui.warn('Claude Code not found in PATH.');
115
+ ui.info('Install it: npm install -g @anthropic-ai/claude-code');
116
+ ui.info('Or paste the prompt from step 5 into any AI manually.');
117
+ process.exit(0);
118
+ }
119
+ const systemPrompt = buildSystemPrompt(agentId, token, agentName);
120
+ const userMessage = `it's time. wake up...`;
121
+ console.log();
122
+ ui.success('Launching Claude Code...');
123
+ console.log(chalk.dim(' Your agent is waking up.'));
124
+ console.log();
125
+ const child = spawn('claude', [
126
+ '--system-prompt', systemPrompt,
127
+ userMessage,
128
+ ], {
129
+ stdio: 'inherit',
130
+ env: { ...process.env },
131
+ });
132
+ child.on('close', (code) => {
133
+ process.exit(code ?? 0);
134
+ });
135
+ child.on('error', (err) => {
136
+ ui.error(`Failed to launch Claude: ${err.message}`);
137
+ ui.info('You can start Claude manually and paste the onboarding prompt.');
138
+ process.exit(1);
139
+ });
140
+ }
@@ -0,0 +1,36 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import * as api from '../lib/api.js';
4
+ import { POLL_INTERVAL_MS, POLL_TIMEOUT_MS, FUNDING_THRESHOLD_SOL } from '../lib/config.js';
5
+ import * as ui from '../lib/ui.js';
6
+ export async function pollFunding(agentId) {
7
+ console.log();
8
+ ui.info(`Send ${FUNDING_THRESHOLD_SOL}+ SOL to your wallet address to activate.`);
9
+ console.log();
10
+ const spinner = ora({
11
+ text: 'Waiting for funds...',
12
+ color: 'cyan',
13
+ }).start();
14
+ const startTime = Date.now();
15
+ while (true) {
16
+ try {
17
+ const status = await api.get(`/api/onboard/agent/${agentId}/status`);
18
+ if (status.verified && status.balance >= FUNDING_THRESHOLD_SOL) {
19
+ spinner.succeed(chalk.green(`${status.balance.toFixed(2)} SOL received — agent activated!`));
20
+ return status.balance;
21
+ }
22
+ if (status.balance > 0 && status.balance < FUNDING_THRESHOLD_SOL) {
23
+ spinner.text = `${status.balance.toFixed(4)} SOL received — need ${FUNDING_THRESHOLD_SOL}+ SOL...`;
24
+ }
25
+ }
26
+ catch {
27
+ // Network error, keep polling
28
+ }
29
+ if (Date.now() - startTime > POLL_TIMEOUT_MS) {
30
+ spinner.fail('Timed out waiting for funds (30 min).');
31
+ ui.info('Run this command again once you\'ve funded the wallet.');
32
+ process.exit(1);
33
+ }
34
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
35
+ }
36
+ }
@@ -0,0 +1,30 @@
1
+ import { signChallenge, getPublicKeyBase58 } from '../lib/solana.js';
2
+ import * as api from '../lib/api.js';
3
+ import * as ui from '../lib/ui.js';
4
+ export async function register(keypair, agentName) {
5
+ // Step 1: Get challenge
6
+ const challenge = await api.post('/api/onboard/agent/challenge');
7
+ // Step 2: Sign the challenge
8
+ const signature = signChallenge(challenge, keypair);
9
+ const publicKey = getPublicKeyBase58(keypair);
10
+ // Step 3: Verify
11
+ const result = await api.post('/api/onboard/agent/verify', {
12
+ signature,
13
+ publicKey,
14
+ agentName,
15
+ nonce: challenge.nonce,
16
+ model: 'cli-agent',
17
+ });
18
+ if (result.existing) {
19
+ ui.info(`Agent already registered with this wallet — reconnecting.`);
20
+ }
21
+ else {
22
+ ui.success(`Registered as "${agentName}"`);
23
+ }
24
+ return {
25
+ agentId: result.agentId,
26
+ walletAddress: result.walletAddress,
27
+ token: result.token,
28
+ existing: !!result.existing,
29
+ };
30
+ }
@@ -0,0 +1,72 @@
1
+ import chalk from 'chalk';
2
+ import * as api from '../lib/api.js';
3
+ import { TICK_INTERVAL_MS } from '../lib/config.js';
4
+ import { createLogger } from '../lib/log.js';
5
+ import * as ui from '../lib/ui.js';
6
+ export async function startTickLoop(agentId, token, agentName) {
7
+ // Enable autonomous mode on the server
8
+ try {
9
+ await api.post(`/api/agent/${agentId}/enable-autonomous`, { intervalSeconds: TICK_INTERVAL_MS / 1000 }, token);
10
+ }
11
+ catch (e) {
12
+ ui.warn(`Could not enable autonomous mode: ${e.message}`);
13
+ ui.info('The tick loop will still run — the server just won\'t track the setting.');
14
+ }
15
+ const logger = createLogger(agentName);
16
+ ui.info(`Logging to ${logger.logFile}`);
17
+ console.log();
18
+ console.log(chalk.dim(' Autonomous trading loop running. Press Ctrl+C to stop.'));
19
+ console.log();
20
+ // Graceful shutdown
21
+ let running = true;
22
+ process.on('SIGINT', () => {
23
+ running = false;
24
+ console.log();
25
+ ui.info('Shutting down trading loop...');
26
+ process.exit(0);
27
+ });
28
+ // Initial tick immediately
29
+ await tick(agentId, token, logger);
30
+ // Then repeat on interval
31
+ while (running) {
32
+ await new Promise((r) => setTimeout(r, TICK_INTERVAL_MS));
33
+ if (!running)
34
+ break;
35
+ await tick(agentId, token, logger);
36
+ }
37
+ }
38
+ async function tick(agentId, token, logger) {
39
+ try {
40
+ const result = await api.post(`/api/agent/${agentId}/tick`, {}, token);
41
+ const action = result.action || 'hold';
42
+ const reasoning = result.reasoning || '';
43
+ const timestamp = new Date().toLocaleTimeString();
44
+ // Console output
45
+ const icon = action === 'buy' ? '📈' : action === 'sell' ? '📉' : '⏳';
46
+ const actionColor = action === 'buy' ? chalk.green(action.toUpperCase()) :
47
+ action === 'sell' ? chalk.red(action.toUpperCase()) :
48
+ chalk.dim(action.toUpperCase());
49
+ let line = ` ${chalk.dim(timestamp)} ${icon} ${actionColor}`;
50
+ if (result.transaction) {
51
+ line += chalk.dim(` ${result.transaction.amount} ${result.transaction.type === 'buy' ? 'SOL →' : '←'} ${result.transaction.mint?.slice(0, 8)}...`);
52
+ }
53
+ line += chalk.dim(` — ${reasoning.slice(0, 60)}`);
54
+ console.log(line);
55
+ // File log
56
+ logger.log(action, reasoning, result.transaction ? {
57
+ mint: result.transaction.mint,
58
+ amount: result.transaction.amount,
59
+ type: result.transaction.type,
60
+ } : undefined);
61
+ }
62
+ catch (e) {
63
+ const msg = e.message || 'Unknown error';
64
+ if (msg.includes('429') || msg.includes('rate limit')) {
65
+ console.log(chalk.dim(` ${new Date().toLocaleTimeString()} ⏳ Rate limited — waiting...`));
66
+ await new Promise((r) => setTimeout(r, 10_000));
67
+ }
68
+ else {
69
+ console.log(chalk.dim(` ${new Date().toLocaleTimeString()} ⚠ ${msg.slice(0, 80)}`));
70
+ }
71
+ }
72
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "ctrl-alt-agent",
3
+ "version": "0.1.0",
4
+ "description": "Onboard an AI trading agent onto ctrlai.trade",
5
+ "type": "module",
6
+ "bin": {
7
+ "ctrl-alt-agent": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsx src/index.ts",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "dependencies": {
21
+ "@noble/curves": "^1.9.0",
22
+ "@solana/web3.js": "^1.98.4",
23
+ "chalk": "^5.4.1",
24
+ "clipboardy": "^4.0.0",
25
+ "ora": "^8.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.0.0",
29
+ "tsx": "^4.19.0",
30
+ "typescript": "^5.6.0"
31
+ }
32
+ }