@exreve/exk 1.0.54 → 1.0.56

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.
@@ -1,347 +0,0 @@
1
- /**
2
- * Benchmark: cold query() vs warm startup() + query()
3
- *
4
- * Measures time-to-first-token for:
5
- * 1. Cold query() (current behavior - spawns subprocess each time)
6
- * 2. Warm startup() -> query() (pre-warmed subprocess)
7
- *
8
- * Usage: cd cli && npx tsx benchmark-startup.ts
9
- */
10
- import { query, startup } from '@anthropic-ai/claude-agent-sdk';
11
- import fs from 'fs';
12
- import path from 'path';
13
- import os from 'os';
14
- import { execSync } from 'child_process';
15
- import { createRequire } from 'module';
16
- // Resolve Claude Code executable (mirrors agentSession.ts logic)
17
- function resolveClaudeExe() {
18
- // Try glibc native binary first (most Linux distros)
19
- for (const pkg of [
20
- '@anthropic-ai/claude-agent-sdk-linux-x64',
21
- '@anthropic-ai/claude-agent-sdk-linux-x64-musl',
22
- ]) {
23
- try {
24
- const req = createRequire(import.meta.url);
25
- const nativePkgPath = req.resolve(`${pkg}/package.json`);
26
- const nativePath = path.join(path.dirname(nativePkgPath), 'claude');
27
- if (fs.existsSync(nativePath)) {
28
- // Test if it's actually executable
29
- try {
30
- execSync(`${nativePath} --version`, { stdio: 'pipe', timeout: 5000 });
31
- return nativePath;
32
- }
33
- catch {
34
- // Binary can't execute (e.g. musl on glibc), skip
35
- }
36
- }
37
- }
38
- catch { }
39
- }
40
- // Fallback: cli.js (Node.js-based)
41
- try {
42
- const req = createRequire(import.meta.url);
43
- const pkgPath = req.resolve('@anthropic-ai/claude-agent-sdk/package.json');
44
- const cliPath = path.join(path.dirname(pkgPath), 'cli.js');
45
- if (fs.existsSync(cliPath))
46
- return cliPath;
47
- }
48
- catch { }
49
- return undefined;
50
- }
51
- const CLAUDE_EXE = resolveClaudeExe();
52
- // ── Config ──────────────────────────────────────────────────────────
53
- const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
54
- function loadConfig() {
55
- try {
56
- return JSON.parse(fs.readFileSync(AI_CONFIG_PATH, 'utf-8'));
57
- }
58
- catch {
59
- return {};
60
- }
61
- }
62
- // ── Helpers ─────────────────────────────────────────────────────────
63
- const GREEN = '\x1b[32m';
64
- const RED = '\x1b[31m';
65
- const CYAN = '\x1b[36m';
66
- const YELLOW = '\x1b[33m';
67
- const BOLD = '\x1b[1m';
68
- const RESET = '\x1b[0m';
69
- function hr() {
70
- console.log(`${'─'.repeat(60)}`);
71
- }
72
- async function measureColdQuery(apiKey, baseUrl, model, env, settingsEnv, label) {
73
- const queryStart = Date.now();
74
- let firstTokenMs = 0;
75
- let initMs = 0;
76
- let totalMs = 0;
77
- try {
78
- const q = query({
79
- prompt: 'Say exactly: "Hello, I am ready." Nothing else.',
80
- options: {
81
- apiKey,
82
- model,
83
- cwd: '/tmp',
84
- permissionMode: 'bypassPermissions',
85
- allowDangerouslySkipPermissions: true,
86
- maxTurns: 1,
87
- env,
88
- settings: { env: settingsEnv },
89
- ...(CLAUDE_EXE ? { pathToClaudeCodeExecutable: CLAUDE_EXE } : {}),
90
- },
91
- });
92
- for await (const event of q) {
93
- const now = Date.now();
94
- if (event.type === 'system' && event.subtype === 'init') {
95
- initMs = now - queryStart;
96
- }
97
- if (event.type === 'assistant') {
98
- if (firstTokenMs === 0) {
99
- firstTokenMs = now - queryStart;
100
- }
101
- }
102
- if (event.type === 'result') {
103
- totalMs = now - queryStart;
104
- }
105
- }
106
- }
107
- catch (err) {
108
- return {
109
- firstTokenMs: firstTokenMs || -1,
110
- totalMs: Date.now() - queryStart,
111
- startupMs: initMs || -1,
112
- error: err.message,
113
- };
114
- }
115
- console.log(` ${label}: init=${initMs}ms, first_token=${firstTokenMs}ms, total=${totalMs}ms`);
116
- return { firstTokenMs, totalMs, startupMs: initMs };
117
- }
118
- async function measureWarmQuery(apiKey, baseUrl, model, env, settingsEnv, label) {
119
- // Phase 1: Pre-warm
120
- const prewarmStart = Date.now();
121
- let warmQuery;
122
- try {
123
- warmQuery = await startup({
124
- options: {
125
- apiKey,
126
- model,
127
- cwd: '/tmp',
128
- permissionMode: 'bypassPermissions',
129
- allowDangerouslySkipPermissions: true,
130
- maxTurns: 1,
131
- env,
132
- settings: { env: settingsEnv },
133
- ...(CLAUDE_EXE ? { pathToClaudeCodeExecutable: CLAUDE_EXE } : {}),
134
- },
135
- });
136
- }
137
- catch (err) {
138
- return {
139
- prewarmMs: Date.now() - prewarmStart,
140
- firstTokenMs: -1,
141
- totalMs: Date.now() - prewarmStart,
142
- error: `startup() failed: ${err.message}`,
143
- };
144
- }
145
- const prewarmMs = Date.now() - prewarmStart;
146
- console.log(` ${label} prewarm: ${prewarmMs}ms`);
147
- // Phase 2: Query on warm subprocess
148
- const queryStart = Date.now();
149
- let firstTokenMs = 0;
150
- let totalMs = 0;
151
- try {
152
- const q = warmQuery.query('Say exactly: "Hello, I am ready." Nothing else.');
153
- for await (const event of q) {
154
- const now = Date.now();
155
- if (event.type === 'assistant') {
156
- if (firstTokenMs === 0) {
157
- firstTokenMs = now - queryStart;
158
- }
159
- }
160
- if (event.type === 'result') {
161
- totalMs = now - queryStart;
162
- }
163
- }
164
- }
165
- catch (err) {
166
- return {
167
- prewarmMs,
168
- firstTokenMs: firstTokenMs || -1,
169
- totalMs: Date.now() - queryStart,
170
- error: `warm query() failed: ${err.message}`,
171
- };
172
- }
173
- console.log(` ${label} query: first_token=${firstTokenMs}ms, total=${totalMs}ms`);
174
- return { prewarmMs, firstTokenMs, totalMs };
175
- }
176
- // ── Main ────────────────────────────────────────────────────────────
177
- async function main() {
178
- console.log(`\n${BOLD}╔══════════════════════════════════════════════════════════╗${RESET}`);
179
- console.log(`${BOLD}║ startup() Benchmark: Cold vs Warm Time-to-First-Token ║${RESET}`);
180
- console.log(`${BOLD}╚══════════════════════════════════════════════════════════╝${RESET}\n`);
181
- const config = loadConfig();
182
- const minimaxKey = config.minimaxApiKey || process.env.MINIMAX_API_KEY || '';
183
- if (!minimaxKey) {
184
- console.log(`${RED}ERROR: No MiniMax API key found. Set minimaxApiKey in ai-config.json${RESET}`);
185
- process.exit(1);
186
- }
187
- const MINIMAX_BASE_URL = 'https://api.minimax.io/anthropic';
188
- const MINIMAX_MODEL = 'MiniMax-M2.7-highspeed';
189
- const env = {
190
- ...process.env,
191
- ANTHROPIC_API_KEY: minimaxKey,
192
- ANTHROPIC_BASE_URL: MINIMAX_BASE_URL,
193
- ANTHROPIC_MODEL: MINIMAX_MODEL,
194
- ANTHROPIC_DEFAULT_SONNET_MODEL: MINIMAX_MODEL,
195
- ANTHROPIC_DEFAULT_OPUS_MODEL: MINIMAX_MODEL,
196
- ANTHROPIC_DEFAULT_HAIKU_MODEL: MINIMAX_MODEL,
197
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
198
- };
199
- const settingsEnv = {
200
- ANTHROPIC_API_KEY: minimaxKey,
201
- ANTHROPIC_BASE_URL: MINIMAX_BASE_URL,
202
- ANTHROPIC_MODEL: MINIMAX_MODEL,
203
- ANTHROPIC_DEFAULT_SONNET_MODEL: MINIMAX_MODEL,
204
- ANTHROPIC_DEFAULT_OPUS_MODEL: MINIMAX_MODEL,
205
- ANTHROPIC_DEFAULT_HAIKU_MODEL: MINIMAX_MODEL,
206
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
207
- };
208
- console.log(`${CYAN}Provider:${RESET} MiniMax`);
209
- console.log(`${CYAN}Model:${RESET} ${MINIMAX_MODEL}`);
210
- console.log(`${CYAN}Base URL:${RESET} ${MINIMAX_BASE_URL}`);
211
- console.log(`${CYAN}Key:${RESET} ${minimaxKey.slice(0, 8)}...`);
212
- console.log(`${CYAN}Exe:${RESET} ${CLAUDE_EXE || '(not found)'}\n`);
213
- if (!CLAUDE_EXE) {
214
- console.log(`${RED}ERROR: Could not resolve Claude Code executable${RESET}`);
215
- process.exit(1);
216
- }
217
- // ============== TEST 1: Cold Query ==============
218
- console.log(`${BOLD}TEST 1: Cold query() — current behavior${RESET}`);
219
- hr();
220
- const coldResults = [];
221
- for (let i = 0; i < 3; i++) {
222
- const result = await measureColdQuery(minimaxKey, MINIMAX_BASE_URL, MINIMAX_MODEL, env, settingsEnv, ` Run ${i + 1}`);
223
- coldResults.push(result);
224
- if (result.error) {
225
- console.log(` ${RED}Error: ${result.error}${RESET}`);
226
- }
227
- }
228
- // ============== TEST 2: Warm startup() + query() ==============
229
- console.log(`\n${BOLD}TEST 2: Warm startup() → query() — pre-warmed subprocess${RESET}`);
230
- hr();
231
- const warmResults = [];
232
- for (let i = 0; i < 3; i++) {
233
- const result = await measureWarmQuery(minimaxKey, MINIMAX_BASE_URL, MINIMAX_MODEL, env, settingsEnv, ` Run ${i + 1}`);
234
- warmResults.push(result);
235
- if (result.error) {
236
- console.log(` ${RED}Error: ${result.error}${RESET}`);
237
- }
238
- }
239
- // ============== Summary ==============
240
- console.log(`\n${BOLD}══════════════════════════════════════════════════════════${RESET}`);
241
- console.log(`${BOLD} SUMMARY${RESET}`);
242
- console.log(`${BOLD}══════════════════════════════════════════════════════════${RESET}\n`);
243
- const validCold = coldResults.filter(r => !r.error && r.firstTokenMs > 0);
244
- const validWarm = warmResults.filter(r => !r.error && r.firstTokenMs > 0);
245
- if (validCold.length > 0) {
246
- const avgColdFirst = Math.round(validCold.reduce((s, r) => s + r.firstTokenMs, 0) / validCold.length);
247
- const avgColdTotal = Math.round(validCold.reduce((s, r) => s + r.totalMs, 0) / validCold.length);
248
- const avgColdInit = Math.round(validCold.reduce((s, r) => s + r.startupMs, 0) / validCold.length);
249
- console.log(` ${YELLOW}Cold query() (avg of ${validCold.length}):${RESET}`);
250
- console.log(` Init/subprocess spawn: ${avgColdInit}ms`);
251
- console.log(` Time to first token: ${avgColdFirst}ms`);
252
- console.log(` Total query time: ${avgColdTotal}ms`);
253
- }
254
- else {
255
- console.log(` ${RED}Cold query: ALL RUNS FAILED${RESET}`);
256
- coldResults.forEach((r, i) => r.error && console.log(` Run ${i + 1}: ${r.error}`));
257
- }
258
- console.log();
259
- if (validWarm.length > 0) {
260
- const avgWarmPrem = Math.round(validWarm.reduce((s, r) => s + r.prewarmMs, 0) / validWarm.length);
261
- const avgWarmFirst = Math.round(validWarm.reduce((s, r) => s + r.firstTokenMs, 0) / validWarm.length);
262
- const avgWarmTotal = Math.round(validWarm.reduce((s, r) => s + r.totalMs, 0) / validWarm.length);
263
- console.log(` ${GREEN}Warm startup() → query() (avg of ${validWarm.length}):${RESET}`);
264
- console.log(` Prewarm (startup()): ${avgWarmPrem}ms`);
265
- console.log(` Time to first token: ${avgWarmFirst}ms`);
266
- console.log(` Total query time: ${avgWarmTotal}ms`);
267
- }
268
- else {
269
- console.log(` ${RED}Warm query: ALL RUNS FAILED${RESET}`);
270
- warmResults.forEach((r, i) => r.error && console.log(` Run ${i + 1}: ${r.error}`));
271
- }
272
- console.log();
273
- if (validCold.length > 0 && validWarm.length > 0) {
274
- const avgColdFirst = Math.round(validCold.reduce((s, r) => s + r.firstTokenMs, 0) / validCold.length);
275
- const avgWarmFirst = Math.round(validWarm.reduce((s, r) => s + r.firstTokenMs, 0) / validWarm.length);
276
- const speedup = avgColdFirst > 0 ? (avgColdFirst / avgWarmFirst).toFixed(1) : 'N/A';
277
- const saved = avgColdFirst - avgWarmFirst;
278
- console.log(` ${BOLD}Speedup (first token):${RESET} ${speedup}x faster with startup()`);
279
- console.log(` ${BOLD}Time saved:${RESET} ${saved > 0 ? saved : 0}ms per query`);
280
- console.log();
281
- if (parseFloat(speedup) >= 5) {
282
- console.log(` ${GREEN}${BOLD}✓ Significant improvement! startup() is highly beneficial here.${RESET}`);
283
- }
284
- else if (parseFloat(speedup) >= 2) {
285
- console.log(` ${YELLOW}! Moderate improvement. startup() helps but not 20x as advertised.${RESET}`);
286
- }
287
- else {
288
- console.log(` ${RED}✗ Minimal improvement. startup() overhead may not be worth it here.${RESET}`);
289
- }
290
- }
291
- console.log(`\n${BOLD}══════════════════════════════════════════════════════════${RESET}\n`);
292
- // ============== Bonus: Can we reuse startup()? ==============
293
- console.log(`${BOLD}TEST 3: Can one WarmQuery be reused for multiple queries?${RESET}`);
294
- hr();
295
- try {
296
- const warmHandle = await startup({
297
- options: {
298
- apiKey: minimaxKey,
299
- model: MINIMAX_MODEL,
300
- cwd: '/tmp',
301
- permissionMode: 'bypassPermissions',
302
- allowDangerouslySkipPermissions: true,
303
- maxTurns: 1,
304
- env,
305
- settings: { env: settingsEnv },
306
- ...(CLAUDE_EXE ? { pathToClaudeCodeExecutable: CLAUDE_EXE } : {}),
307
- },
308
- });
309
- console.log(' startup() succeeded, attempting first query()...');
310
- const q1Start = Date.now();
311
- let q1FirstToken = 0;
312
- const q1 = warmHandle.query('Say "one"');
313
- for await (const event of q1) {
314
- if (event.type === 'assistant' && q1FirstToken === 0) {
315
- q1FirstToken = Date.now() - q1Start;
316
- }
317
- if (event.type === 'result') {
318
- console.log(` Query 1: first_token=${q1FirstToken}ms, total=${Date.now() - q1Start}ms`);
319
- }
320
- }
321
- console.log(' Attempting second query() on same WarmQuery...');
322
- try {
323
- const q2 = warmHandle.query('Say "two"');
324
- for await (const _event of q2) {
325
- // drain
326
- }
327
- console.log(` ${GREEN}Second query succeeded — WarmQuery CAN be reused!${RESET}`);
328
- }
329
- catch (reuseErr) {
330
- console.log(` ${RED}Second query FAILED: ${reuseErr.message}${RESET}`);
331
- console.log(` ${YELLOW}→ WarmQuery is single-use, must call startup() again for each query.${RESET}`);
332
- }
333
- try {
334
- warmHandle.close();
335
- }
336
- catch { }
337
- }
338
- catch (err) {
339
- console.log(` ${RED}startup() failed: ${err.message}${RESET}`);
340
- }
341
- console.log();
342
- process.exit(0);
343
- }
344
- main().catch((err) => {
345
- console.error('Unhandled error:', err);
346
- process.exit(1);
347
- });
@@ -1,279 +0,0 @@
1
- /**
2
- * Cloudflared Handlers Module
3
- *
4
- * Handles cloudflared:check, cloudflared:sync, cloudflared:login,
5
- * cloudflared:regenerate operations.
6
- */
7
- import fs from 'fs/promises';
8
- import path from 'path';
9
- import os from 'os';
10
- import { spawn, execSync } from 'child_process';
11
- export function registerCloudflaredHandlers(socket, foreground) {
12
- socket.on('cloudflared:check:request', async () => {
13
- try {
14
- let installed = false;
15
- let hasCert = false;
16
- try {
17
- execSync('which cloudflared', { stdio: 'ignore' });
18
- installed = true;
19
- const certPath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
20
- try {
21
- const stats = await fs.stat(certPath);
22
- hasCert = stats.isFile();
23
- if (foreground && hasCert) {
24
- console.log(`✓ Found cert.pem at ${certPath}`);
25
- }
26
- }
27
- catch (err) {
28
- hasCert = false;
29
- if (foreground) {
30
- console.log(`✗ cert.pem not found at ${certPath}: ${err.message}`);
31
- }
32
- }
33
- }
34
- catch {
35
- installed = false;
36
- }
37
- socket.emit('cloudflared:check:response', { installed, hasCert });
38
- if (foreground) {
39
- console.log(`Cloudflared check: installed=${installed}, hasCert=${hasCert}`);
40
- }
41
- }
42
- catch (error) {
43
- socket.emit('cloudflared:check:response', { installed: false, hasCert: false });
44
- }
45
- });
46
- socket.on('cloudflared:sync:request', async () => {
47
- try {
48
- const certPath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
49
- if (foreground) {
50
- console.log(`Syncing credentials from ${certPath}`);
51
- }
52
- const certContent = await fs.readFile(certPath, 'utf-8');
53
- const tokenMatch = certContent.match(/-----BEGIN ARGO TUNNEL TOKEN-----\s*([\s\S]*?)\s*-----END ARGO TUNNEL TOKEN-----/);
54
- if (tokenMatch && tokenMatch[1]) {
55
- const tokenBase64 = tokenMatch[1].replace(/\s/g, '');
56
- const tokenJson = Buffer.from(tokenBase64, 'base64').toString('utf-8');
57
- const tokenData = JSON.parse(tokenJson);
58
- const apiToken = tokenData.apiToken;
59
- const accountId = tokenData.accountID;
60
- const zoneId = tokenData.zoneID;
61
- if (foreground) {
62
- console.log(`✓ Extracted credentials: accountId=${accountId}, zoneId=${zoneId}`);
63
- }
64
- socket.emit('cloudflared:sync:complete', {
65
- accountId,
66
- accountName: undefined,
67
- apiToken,
68
- zoneId
69
- });
70
- }
71
- else {
72
- const error = 'Failed to extract token from cert.pem';
73
- if (foreground) {
74
- console.error(`✗ ${error}`);
75
- }
76
- socket.emit('cloudflared:sync:error', { error });
77
- }
78
- }
79
- catch (error) {
80
- const errorMsg = `Failed to read cert.pem: ${error.message}`;
81
- if (foreground) {
82
- console.error(`✗ ${errorMsg}`);
83
- }
84
- socket.emit('cloudflared:sync:error', { error: errorMsg });
85
- }
86
- });
87
- socket.on('cloudflared:login:request', async () => {
88
- try {
89
- // Check if cloudflared is installed
90
- try {
91
- execSync('which cloudflared', { stdio: 'ignore' });
92
- }
93
- catch {
94
- socket.emit('cloudflared:login:error', { error: 'cloudflared is not installed' });
95
- return;
96
- }
97
- const loginProcess = spawn('cloudflared', ['tunnel', 'login'], {
98
- stdio: ['ignore', 'pipe', 'pipe']
99
- });
100
- let stdout = '';
101
- let stderr = '';
102
- let urlEmitted = false;
103
- let alreadyLoggedIn = false;
104
- let certPath = null;
105
- const extractCertPath = (text) => {
106
- const pathMatch = text.match(/existing certificate at\s+([^\s]+)/i) ||
107
- text.match(/certificate at\s+([^\s]+)/i) ||
108
- text.match(/cert\.pem.*?at\s+([^\s]+)/i);
109
- return pathMatch ? pathMatch[1] : null;
110
- };
111
- const extractLoginUrl = (text) => {
112
- const urlPatterns = [
113
- /https:\/\/dash\.cloudflare\.com\/argotunnel[^\s\)]+/g,
114
- /https:\/\/[^\s\)]+cloudflareaccess\.org[^\s\)]+/g,
115
- /https:\/\/[^\s\)]+cloudflare\.com[^\s\)]+/g
116
- ];
117
- for (const pattern of urlPatterns) {
118
- const matches = text.match(pattern);
119
- if (matches && matches.length > 0) {
120
- return matches[0];
121
- }
122
- }
123
- return null;
124
- };
125
- loginProcess.stdout.on('data', (data) => {
126
- const text = data.toString();
127
- stdout += text;
128
- if (text.includes('existing certificate') || text.includes('cert.pem which login would overwrite')) {
129
- alreadyLoggedIn = true;
130
- const extractedPath = extractCertPath(text);
131
- if (extractedPath) {
132
- certPath = extractedPath;
133
- }
134
- }
135
- if (!alreadyLoggedIn && !urlEmitted) {
136
- const url = extractLoginUrl(text);
137
- if (url) {
138
- urlEmitted = true;
139
- socket.emit('cloudflared:login:url', { loginUrl: url });
140
- }
141
- }
142
- });
143
- loginProcess.stderr.on('data', (data) => {
144
- const text = data.toString();
145
- stderr += text;
146
- if (text.includes('existing certificate') || text.includes('cert.pem which login would overwrite')) {
147
- alreadyLoggedIn = true;
148
- const extractedPath = extractCertPath(text);
149
- if (extractedPath) {
150
- certPath = extractedPath;
151
- }
152
- }
153
- if (!alreadyLoggedIn && !urlEmitted) {
154
- const url = extractLoginUrl(text);
155
- if (url) {
156
- urlEmitted = true;
157
- socket.emit('cloudflared:login:url', { loginUrl: url });
158
- }
159
- }
160
- });
161
- loginProcess.on('close', async (code) => {
162
- if (alreadyLoggedIn && certPath) {
163
- // Already logged in - extract credentials from existing cert
164
- try {
165
- if (foreground) {
166
- console.log(`Already logged in, extracting credentials from ${certPath}`);
167
- }
168
- const certContent = await fs.readFile(certPath, 'utf-8');
169
- const tokenMatch = certContent.match(/-----BEGIN ARGO TUNNEL TOKEN-----\s*([\s\S]*?)\s*-----END ARGO TUNNEL TOKEN-----/);
170
- if (tokenMatch && tokenMatch[1]) {
171
- const tokenBase64 = tokenMatch[1].replace(/\s/g, '');
172
- const tokenJson = Buffer.from(tokenBase64, 'base64').toString('utf-8');
173
- const tokenData = JSON.parse(tokenJson);
174
- if (foreground) {
175
- console.log(`✓ Extracted credentials from existing cert`);
176
- }
177
- socket.emit('cloudflared:login:complete', {
178
- accountId: tokenData.accountID,
179
- accountName: undefined,
180
- apiToken: tokenData.apiToken,
181
- zoneId: tokenData.zoneID
182
- });
183
- }
184
- else {
185
- const error = 'Failed to extract token from existing cert.pem';
186
- if (foreground) {
187
- console.error(`✗ ${error}`);
188
- }
189
- socket.emit('cloudflared:login:error', { error });
190
- }
191
- }
192
- catch (error) {
193
- const errorMsg = `Failed to read cert.pem: ${error.message}`;
194
- if (foreground) {
195
- console.error(`✗ ${errorMsg}`);
196
- }
197
- socket.emit('cloudflared:login:error', { error: errorMsg });
198
- }
199
- }
200
- else if (code === 0 && !alreadyLoggedIn) {
201
- // Login completed successfully - wait a moment then extract credentials
202
- if (foreground) {
203
- console.log('Login completed, extracting credentials...');
204
- }
205
- setTimeout(async () => {
206
- try {
207
- const certFilePath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
208
- const certContent = await fs.readFile(certFilePath, 'utf-8');
209
- const tokenMatch = certContent.match(/-----BEGIN ARGO TUNNEL TOKEN-----\s*([\s\S]*?)\s*-----END ARGO TUNNEL TOKEN-----/);
210
- if (tokenMatch && tokenMatch[1]) {
211
- const tokenBase64 = tokenMatch[1].replace(/\s/g, '');
212
- const tokenJson = Buffer.from(tokenBase64, 'base64').toString('utf-8');
213
- const tokenData = JSON.parse(tokenJson);
214
- if (foreground) {
215
- console.log(`✓ Extracted credentials after login`);
216
- }
217
- socket.emit('cloudflared:login:complete', {
218
- accountId: tokenData.accountID,
219
- accountName: undefined,
220
- apiToken: tokenData.apiToken,
221
- zoneId: tokenData.zoneID
222
- });
223
- }
224
- else {
225
- const error = 'Failed to extract token from cert.pem after login';
226
- if (foreground) {
227
- console.error(`✗ ${error}`);
228
- }
229
- socket.emit('cloudflared:login:error', { error });
230
- }
231
- }
232
- catch (error) {
233
- const errorMsg = `Failed to read cert.pem after login: ${error.message}`;
234
- if (foreground) {
235
- console.error(`✗ ${errorMsg}`);
236
- }
237
- socket.emit('cloudflared:login:error', { error: errorMsg });
238
- }
239
- }, 1000);
240
- }
241
- else if (!alreadyLoggedIn) {
242
- const error = `Login failed with code ${code}: ${stderr || stdout}`;
243
- if (foreground) {
244
- console.error(`✗ ${error}`);
245
- }
246
- socket.emit('cloudflared:login:error', { error });
247
- }
248
- });
249
- loginProcess.on('error', (error) => {
250
- socket.emit('cloudflared:login:error', { error: error.message });
251
- });
252
- }
253
- catch (error) {
254
- socket.emit('cloudflared:login:error', { error: error.message });
255
- }
256
- });
257
- socket.on('cloudflared:regenerate:request', async () => {
258
- try {
259
- const certPath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
260
- // Delete existing cert.pem
261
- try {
262
- await fs.unlink(certPath);
263
- if (foreground) {
264
- console.log(`✓ Deleted existing cert.pem`);
265
- }
266
- }
267
- catch (error) {
268
- if (foreground && error.code !== 'ENOENT') {
269
- console.log(`Note: Could not delete cert.pem: ${error.message}`);
270
- }
271
- }
272
- // Emit success - frontend will then trigger login
273
- socket.emit('cloudflared:regenerate:complete', {});
274
- }
275
- catch (error) {
276
- socket.emit('cloudflared:regenerate:error', { error: error.message });
277
- }
278
- });
279
- }