claude-pool 0.0.0-stage → 0.1.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.
package/src/setup.js ADDED
@@ -0,0 +1,218 @@
1
+ import { spawnSync } from 'child_process';
2
+ import readline from 'readline/promises';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { ConfigManager } from './config.js';
7
+ import { colors, printTable, makePill } from './ui.js';
8
+
9
+ export function checkClaudeCliInstalled() {
10
+ try {
11
+ const isWin = process.platform === 'win32';
12
+ const cmd = isWin ? 'where.exe' : 'which';
13
+ const res = spawnSync(cmd, ['claude'], { encoding: 'utf8' });
14
+ return res.status === 0;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ export function runAccountLogin(configDir, email = null) {
21
+ const env = { ...process.env };
22
+ const defaultClaudePath = path.resolve(path.join(os.homedir(), '.claude'));
23
+ if (path.resolve(configDir) !== defaultClaudePath) {
24
+ env.CLAUDE_CONFIG_DIR = configDir;
25
+ } else {
26
+ delete env.CLAUDE_CONFIG_DIR;
27
+ }
28
+
29
+ delete env.ANTHROPIC_BASE_URL;
30
+ delete env.ANTHROPIC_API_KEY;
31
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
32
+
33
+ const cmdArgs = ['auth', 'login'];
34
+ if (email) cmdArgs.push('--email', email);
35
+
36
+ console.log(`${colors.dim}Opening Claude browser authentication...${colors.reset}`);
37
+ const res = spawnSync('claude', cmdArgs, { env, stdio: 'inherit' });
38
+ return res.status === 0;
39
+ }
40
+
41
+ export function checkAuthStatus(configDir) {
42
+ const env = { ...process.env };
43
+ const defaultClaudePath = path.resolve(path.join(os.homedir(), '.claude'));
44
+ if (path.resolve(configDir) !== defaultClaudePath) {
45
+ env.CLAUDE_CONFIG_DIR = configDir;
46
+ } else {
47
+ delete env.CLAUDE_CONFIG_DIR;
48
+ }
49
+
50
+ try {
51
+ const res = spawnSync('claude', ['auth', 'status'], { env, encoding: 'utf8', timeout: 10000 });
52
+ if (res.status === 0) {
53
+ const data = JSON.parse(res.stdout);
54
+ return { ok: data.loggedIn === true, email: data.email, plan: data.subscriptionType };
55
+ }
56
+ } catch {}
57
+ return { ok: false, email: null, plan: null };
58
+ }
59
+
60
+ export function installSlashCommands(configDir) {
61
+ const cmdDir = path.join(configDir, 'commands');
62
+ try {
63
+ fs.mkdirSync(cmdDir, { recursive: true });
64
+ // /pool
65
+ fs.writeFileSync(
66
+ path.join(cmdDir, 'pool.md'),
67
+ '---\ndescription: View Claude Pool token dashboard and status\n---\nRun `claude-pool dashboard` using Bash and display the formatted output.\n',
68
+ 'utf8'
69
+ );
70
+ // /limits
71
+ fs.writeFileSync(
72
+ path.join(cmdDir, 'limits.md'),
73
+ '---\ndescription: View remaining session and weekly quotas across all accounts in the pool\n---\nRun `claude-pool limits` using Bash and display the formatted output.\n',
74
+ 'utf8'
75
+ );
76
+ // /switch
77
+ fs.writeFileSync(
78
+ path.join(cmdDir, 'switch.md'),
79
+ '---\ndescription: Switch active Claude account in the pool\n---\nRun `claude-pool switch $ARGUMENTS` using Bash and display the result.\nIf switched successfully, inform the user that their session was synchronized and they can run `/exit` to restart immediately on the new account.\n',
80
+ 'utf8'
81
+ );
82
+ // Also include /pool-limits and /pool-switch aliases
83
+ fs.copyFileSync(path.join(cmdDir, 'limits.md'), path.join(cmdDir, 'pool-limits.md'));
84
+ fs.copyFileSync(path.join(cmdDir, 'switch.md'), path.join(cmdDir, 'pool-switch.md'));
85
+ } catch {}
86
+ }
87
+
88
+ export async function runSetupWizard() {
89
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
90
+
91
+ console.log();
92
+ console.log(colors.indigo + colors.bold + '╭─ claude-pool setup ' + '─'.repeat(45) + '╮' + colors.reset);
93
+ console.log(colors.border + '│' + colors.reset + ` ${colors.bold}Welcome to Claude Pool Setup Wizard${colors.reset}`);
94
+ console.log(colors.border + '│' + colors.reset + ` Connect multiple Claude subscriptions into one unified token pool.`);
95
+ console.log(colors.border + '│' + colors.reset + ` ${colors.terracotta}Automatic failover · Continuous session memory · Live token metrics${colors.reset}`);
96
+ console.log(colors.indigo + colors.bold + '╰' + '─'.repeat(66) + '╯' + colors.reset);
97
+ console.log();
98
+
99
+ if (!checkClaudeCliInstalled()) {
100
+ console.log(`${colors.red}✗ Claude Code CLI was not found in your PATH.${colors.reset}`);
101
+ console.log(`Please install Claude Code CLI first by running:\n`);
102
+ console.log(` ${colors.indigo}npm install -g @anthropic-ai/claude-code${colors.reset}\n`);
103
+ console.log(`Once installed, run ${colors.indigo}claude-pool setup${colors.reset} again.`);
104
+ rl.close();
105
+ return;
106
+ }
107
+
108
+ console.log(`${colors.emerald}✓ Claude Code CLI detected.${colors.reset}`);
109
+
110
+ const cfg = new ConfigManager();
111
+ const accounts = [];
112
+ const home = os.homedir();
113
+
114
+ // Primary Account
115
+ const primaryDir = path.join(home, '.claude');
116
+ fs.mkdirSync(primaryDir, { recursive: true });
117
+ console.log(`\n${colors.indigo}${colors.bold}Account #1 (Primary):${colors.reset}`);
118
+
119
+ const primaryStatus = checkAuthStatus(primaryDir);
120
+ if (primaryStatus.ok) {
121
+ console.log(` ${colors.emerald}✓ Already authenticated as:${colors.reset} ${colors.bold}${primaryStatus.email || 'Logged In'}${colors.reset}`);
122
+ accounts.push({
123
+ name: 'primary',
124
+ configDir: primaryDir,
125
+ priority: 1,
126
+ enabled: true,
127
+ email: primaryStatus.email,
128
+ subscriptionType: primaryStatus.plan,
129
+ });
130
+ } else {
131
+ const doLogin = await rl.question(' No active session in ~/.claude. Log in now? [Y/n]: ');
132
+ if (doLogin.toLowerCase() !== 'n') {
133
+ const email = await rl.question(' Enter email (optional, press Enter to skip): ');
134
+ runAccountLogin(primaryDir, email.trim() || null);
135
+ const st = checkAuthStatus(primaryDir);
136
+ accounts.push({
137
+ name: 'primary',
138
+ configDir: primaryDir,
139
+ priority: 1,
140
+ enabled: true,
141
+ email: st.email,
142
+ subscriptionType: st.plan,
143
+ });
144
+ }
145
+ }
146
+
147
+ // Standby Accounts
148
+ let count = 2;
149
+ while (true) {
150
+ console.log();
151
+ const addMore = await rl.question(`Would you like to add another Claude account? [y/N]: `);
152
+ if (addMore.trim().toLowerCase() !== 'y') break;
153
+
154
+ const defaultName = `account${count}`;
155
+ const nameInput = await rl.question(` Enter account name [default: ${defaultName}]: `);
156
+ const acctName = nameInput.trim() || defaultName;
157
+
158
+ const acctDir = path.join(home, `.claude-${acctName}`);
159
+ fs.mkdirSync(acctDir, { recursive: true });
160
+
161
+ const st = checkAuthStatus(acctDir);
162
+ let email = st.email;
163
+ let plan = st.plan;
164
+
165
+ if (st.ok) {
166
+ console.log(` ${colors.emerald}✓ Already authenticated as:${colors.reset} ${colors.bold}${email}${colors.reset}`);
167
+ } else {
168
+ console.log(` Opening browser to sign in for '${acctName}'...`);
169
+ const emailInput = await rl.question(' Enter email (optional, press Enter to skip): ');
170
+ runAccountLogin(acctDir, emailInput.trim() || null);
171
+ const newSt = checkAuthStatus(acctDir);
172
+ email = newSt.email;
173
+ plan = newSt.plan;
174
+ }
175
+
176
+ accounts.push({
177
+ name: acctName,
178
+ configDir: acctDir,
179
+ priority: count,
180
+ enabled: true,
181
+ email,
182
+ subscriptionType: plan,
183
+ });
184
+ count++;
185
+ }
186
+
187
+ rl.close();
188
+
189
+ if (accounts.length > 0) {
190
+ for (const a of accounts) {
191
+ installSlashCommands(a.configDir);
192
+ }
193
+ cfg.saveAccounts(accounts);
194
+ console.log(`\n${colors.emerald}✓ Setup complete! Your Claude Pool is configured:${colors.reset}\n`);
195
+
196
+ const headers = [
197
+ { label: 'Rank', align: 'center' },
198
+ { label: 'Priority', align: 'center' },
199
+ { label: 'Account' },
200
+ { label: 'Status', align: 'center' },
201
+ { label: 'Email' },
202
+ { label: 'Plan' },
203
+ { label: 'Config Directory' },
204
+ ];
205
+ const rows = accounts.map((a, i) => [
206
+ i === 0 ? '#1' : `#${i + 1}`,
207
+ `P${a.priority}`,
208
+ a.name,
209
+ makePill(i === 0 ? 'active' : 'standby'),
210
+ a.email || '-',
211
+ a.subscriptionType || '-',
212
+ a.configDir,
213
+ ]);
214
+ printTable(headers, rows);
215
+
216
+ console.log(`\nTo start coding with automated pooling, simply run:\n ${colors.indigo}${colors.bold}claude-pool${colors.reset}\n`);
217
+ }
218
+ }
package/src/ui.js ADDED
@@ -0,0 +1,128 @@
1
+ // Zero-dependency aesthetic UI renderer (Linear / Raycast inspired)
2
+
3
+ export const colors = {
4
+ reset: '\x1b[0m',
5
+ bold: '\x1b[1m',
6
+ dim: '\x1b[2m',
7
+ white: '\x1b[37m',
8
+ indigo: '\x1b[38;2;129;140;248m', // #818cf8
9
+ terracotta: '\x1b[38;2;249;115;22m', // #f97316
10
+ emerald: '\x1b[38;2;16;185;129m', // #10b981
11
+ amber: '\x1b[38;2;245;158;11m', // #f59e0b
12
+ slate: '\x1b[38;2;100;116;139m', // #64748b
13
+ border: '\x1b[38;2;51;65;85m', // #334155
14
+ red: '\x1b[38;2;239;68;68m', // #ef4444
15
+ bgEmerald: '\x1b[48;2;16;185;129m\x1b[30m\x1b[1m',
16
+ bgSlate: '\x1b[48;2;100;116;139m\x1b[37m\x1b[1m',
17
+ bgAmber: '\x1b[48;2;245;158;11m\x1b[30m\x1b[1m',
18
+ bgRed: '\x1b[48;2;239;68;68m\x1b[37m\x1b[1m',
19
+ };
20
+
21
+ export function formatTokenCount(n) {
22
+ if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
23
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
24
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
25
+ return String(n);
26
+ }
27
+
28
+ export function makePill(status) {
29
+ if (status === 'active') return `${colors.bgEmerald} ACTIVE ${colors.reset}`;
30
+ if (status === 'standby') return `${colors.bgSlate} STANDBY ${colors.reset}`;
31
+ if (status === 'rate_limited') return `${colors.bgAmber} COOLDOWN ${colors.reset}`;
32
+ if (status === 'auth_expired') return `${colors.bgRed} EXPIRED ${colors.reset}`;
33
+ return `${colors.dim} ${status.toUpperCase()} ${colors.reset}`;
34
+ }
35
+
36
+ export function makeGauge(pctRemaining, width = 10) {
37
+ if (pctRemaining === null || pctRemaining === undefined) return `${colors.dim}—${colors.reset}`;
38
+ const pct = Math.max(0, Math.min(100, Math.round(pctRemaining)));
39
+ const filled = Math.round((pct / 100) * width);
40
+ const empty = width - filled;
41
+
42
+ const barColor = pct > 40 ? colors.emerald : pct > 15 ? colors.amber : colors.red;
43
+ return `${barColor}${'█'.repeat(filled)}${colors.border}${'░'.repeat(empty)} ${pct}%${colors.reset}`;
44
+ }
45
+
46
+ // Clean aligned terminal table without external dependencies
47
+ export function printTable(headers, rows) {
48
+ const colWidths = headers.map((h, i) => {
49
+ let max = h.label.length;
50
+ for (const row of rows) {
51
+ const valStr = String(row[i] || '').replace(/\x1b\[[0-9;]*m/g, ''); // strip ansi
52
+ if (valStr.length > max) max = valStr.length;
53
+ }
54
+ return max + 2;
55
+ });
56
+
57
+ const sepTop = '┌' + colWidths.map(w => '─'.repeat(w)).join('┬') + '┐';
58
+ const sepMid = '├' + colWidths.map(w => '─'.repeat(w)).join('┼') + '┤';
59
+ const sepBot = '└' + colWidths.map(w => '─'.repeat(w)).join('┴') + '┘';
60
+
61
+ console.log(colors.border + sepTop + colors.reset);
62
+
63
+ // Header row
64
+ const headerStr = headers
65
+ .map((h, i) => {
66
+ const pad = colWidths[i] - h.label.length;
67
+ return h.align === 'right' ? ' '.repeat(pad - 1) + h.label + ' ' : ' ' + h.label + ' '.repeat(pad - 1);
68
+ })
69
+ .join(colors.border + '│' + colors.reset);
70
+ console.log(colors.border + '│' + colors.indigo + colors.bold + headerStr + colors.border + '│' + colors.reset);
71
+ console.log(colors.border + sepMid + colors.reset);
72
+
73
+ // Rows
74
+ for (const row of rows) {
75
+ const rowStr = row
76
+ .map((val, i) => {
77
+ const raw = String(val || '');
78
+ const plainLen = raw.replace(/\x1b\[[0-9;]*m/g, '').length;
79
+ const pad = Math.max(0, colWidths[i] - plainLen);
80
+ const align = headers[i]?.align || 'left';
81
+ return align === 'right' ? ' '.repeat(pad - 1) + raw + ' ' : ' ' + raw + ' '.repeat(pad - 1);
82
+ })
83
+ .join(colors.border + '│' + colors.reset);
84
+ console.log(colors.border + '│' + colors.reset + rowStr + colors.border + '│' + colors.reset);
85
+ }
86
+
87
+ console.log(colors.border + sepBot + colors.reset);
88
+ }
89
+
90
+ export function renderSessionHeader(activeAccount, standbyAccounts, poolStats) {
91
+ const standbyNames = standbyAccounts.length > 0 ? standbyAccounts.map(a => a.name).join(', ') : 'none';
92
+ const todayPoolStr = poolStats ? formatTokenCount(poolStats.todayPoolTokens) : '0';
93
+
94
+ console.log();
95
+ console.log(colors.border + '┌─ ' + colors.indigo + colors.bold + 'claude-pool' + colors.border + ' ─'.repeat(60) + '┐' + colors.reset);
96
+
97
+ let remStr = '';
98
+ if (activeAccount.quota && activeAccount.quota.sessionRemainingPct !== undefined) {
99
+ const rem = activeAccount.quota.sessionRemainingPct;
100
+ const color = rem > 30 ? colors.emerald : colors.amber;
101
+ remStr = ` · ${color}${colors.bold}${rem}% remaining${colors.reset}`;
102
+ if (activeAccount.quota.sessionReset) {
103
+ remStr += ` ${colors.slate}(resets ${activeAccount.quota.sessionReset})${colors.reset}`;
104
+ }
105
+ } else if (activeAccount.email) {
106
+ remStr = ` ${colors.slate}· ${activeAccount.email}${colors.reset}`;
107
+ }
108
+
109
+ console.log(colors.border + '│' + colors.reset + ` ${colors.indigo}Active Account${colors.reset} ${colors.bold}${activeAccount.name}${colors.reset} [Priority ${activeAccount.priority}]${remStr}`);
110
+ console.log(colors.border + '│' + colors.reset + ` ${colors.indigo}Pool Today${colors.reset} ${colors.bold}${todayPoolStr} tokens${colors.reset} ${colors.slate}across ${standbyAccounts.length + 1} accounts${colors.reset}`);
111
+ console.log(colors.border + '│' + colors.reset + ` ${colors.indigo}Standby Queue${colors.reset} ${standbyNames}`);
112
+ console.log(colors.border + '└' + '─'.repeat(73) + '┘' + colors.reset);
113
+ console.log();
114
+ }
115
+
116
+ export function renderFailoverBanner(oldAccount, newAccount, sessionId) {
117
+ console.log();
118
+ console.log(colors.terracotta + '╭─ Auto-Failover Transition ' + '─'.repeat(45) + '╮' + colors.reset);
119
+ console.log(colors.terracotta + '│' + colors.reset + ` ${colors.amber}${colors.bold}Rate limit reached on ${oldAccount.name}${colors.reset}`);
120
+ console.log(colors.terracotta + '│' + colors.reset);
121
+ if (sessionId) {
122
+ console.log(colors.terracotta + '│' + colors.reset + ` ${colors.emerald}✓ Preserved active session:${colors.reset} ${sessionId.slice(0, 16)}...`);
123
+ }
124
+ console.log(colors.terracotta + '│' + colors.reset + ` ${colors.emerald}✓ Synchronized project state to${colors.reset} ${newAccount.name}`);
125
+ console.log(colors.terracotta + '│' + colors.reset + ` ${colors.indigo}→ Continuing on ${newAccount.name} (Priority ${newAccount.priority})${colors.reset}`);
126
+ console.log(colors.terracotta + '╰' + '─'.repeat(72) + '╯' + colors.reset);
127
+ console.log();
128
+ }