kumo-cli 1.0.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,1159 @@
1
+ #!/usr/bin/env node
2
+ import WebSocket from 'ws';
3
+ import http from 'http';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { startHookServer } from './hookServer.js';
9
+ import { generateHookSettingsFile } from './generateHookSettings.js';
10
+ import { spawnClaude, writeToProcess } from './spawn.js';
11
+ import { SessionScanner } from './sessionScanner.js';
12
+ import { PtyMenuParser } from './ptyMenuParser.js';
13
+ import { PtySnapshotRenderer } from './ptySnapshotRenderer.js';
14
+ import { loadDotEnv } from './loadDotEnv.js';
15
+ import { TerminalManager } from './terminalManager.js';
16
+ import { loadIgnoredNames } from './ignorePaths.js';
17
+ import { loadCredentials, saveCredentials, clearCredentials, credentialsPath } from './credentialsStore.js';
18
+ import readline from 'node:readline';
19
+ import { randomUUID } from 'node:crypto';
20
+ loadDotEnv();
21
+ function normalizeServerHttpUrl(input) {
22
+ const u = new URL(input);
23
+ if (u.protocol === 'ws:')
24
+ u.protocol = 'http:';
25
+ if (u.protocol === 'wss:')
26
+ u.protocol = 'https:';
27
+ if (u.pathname === '/ws')
28
+ u.pathname = '/';
29
+ if (u.pathname.endsWith('/ws'))
30
+ u.pathname = u.pathname.slice(0, -3) || '/';
31
+ return u.toString().replace(/\/$/, '');
32
+ }
33
+ function normalizeServerWsUrl(input) {
34
+ const u = new URL(input);
35
+ if (u.protocol === 'http:')
36
+ u.protocol = 'ws:';
37
+ if (u.protocol === 'https:')
38
+ u.protocol = 'wss:';
39
+ if (u.pathname === '/' || u.pathname === '')
40
+ u.pathname = '/ws';
41
+ return u.toString().replace(/\/$/, '');
42
+ }
43
+ const serverUrl = process.env.KUMO_SERVER_URL;
44
+ const SERVER_HTTP = process.env.KUMO_SERVER_HTTP_URL
45
+ ?? (serverUrl ? normalizeServerHttpUrl(serverUrl) : 'http://localhost:3579');
46
+ const SERVER_WS = process.env.KUMO_SERVER_WS_URL
47
+ ?? (serverUrl ? normalizeServerWsUrl(serverUrl) : 'ws://localhost:3579/ws');
48
+ const SERVER_PORT = parseInt(process.env.KUMO_PORT ?? '3579', 10);
49
+ let ws = null;
50
+ let claudeProcess = null;
51
+ let spawnCleanup = null;
52
+ let scanner = null;
53
+ let reconnectTimer = null;
54
+ let claudeSettingsPath = '';
55
+ let claudeArgs = [];
56
+ let currentSessionId = null;
57
+ const SNAPSHOT_DIR = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..', 'claude-data-snapshot');
58
+ let snapshotScanner = null;
59
+ let pendingPrompt = null;
60
+ let lastWsSendAt = 0;
61
+ let pendingSelectNudgeTimer = null;
62
+ function safePath(requestedPath) {
63
+ const projectRoot = process.cwd();
64
+ const resolved = path.resolve(projectRoot, path.normalize(requestedPath || '.'));
65
+ if (!resolved.startsWith(projectRoot + path.sep) && resolved !== projectRoot) {
66
+ throw new Error('path traversal blocked');
67
+ }
68
+ return resolved;
69
+ }
70
+ function copyRecursive(src, dest) {
71
+ const stat = fs.statSync(src);
72
+ if (stat.isDirectory()) {
73
+ fs.mkdirSync(dest, { recursive: true });
74
+ for (const child of fs.readdirSync(src)) {
75
+ copyRecursive(path.join(src, child), path.join(dest, child));
76
+ }
77
+ }
78
+ else {
79
+ fs.copyFileSync(src, dest);
80
+ }
81
+ }
82
+ let fileWatcher = null;
83
+ const IGNORED_PATHS = loadIgnoredNames(process.cwd());
84
+ const fileWatchDebounceMap = new Map();
85
+ const FILE_WATCH_DEBOUNCE_MS = 300;
86
+ function startFileWatcher() {
87
+ if (fileWatcher) {
88
+ try {
89
+ fileWatcher.close();
90
+ }
91
+ catch { }
92
+ fileWatcher = null;
93
+ }
94
+ const projectRoot = process.cwd();
95
+ try {
96
+ fileWatcher = fs.watch(projectRoot, { recursive: true }, (event, filename) => {
97
+ if (!filename)
98
+ return;
99
+ const normalized = filename.replace(/\\/g, '/');
100
+ const parts = normalized.split('/');
101
+ if (parts.some(p => IGNORED_PATHS.has(p) || p.startsWith('.')))
102
+ return;
103
+ const prev = fileWatchDebounceMap.get(normalized);
104
+ if (prev)
105
+ clearTimeout(prev);
106
+ fileWatchDebounceMap.set(normalized, setTimeout(() => {
107
+ fileWatchDebounceMap.delete(normalized);
108
+ sendToServer({ type: 'file_changed', path: normalized, event });
109
+ log(`[watcher] ${event}: ${normalized}`);
110
+ }, FILE_WATCH_DEBOUNCE_MS));
111
+ });
112
+ log(`[watcher] watching ${projectRoot}`);
113
+ }
114
+ catch (e) {
115
+ log(`[watcher] failed to start: ${e}`);
116
+ }
117
+ }
118
+ let autoAcceptedApiKeyDialog = false;
119
+ let isRespawning = false;
120
+ let startScannerAtEnd = false;
121
+ let sessionSwitchCounter = 0;
122
+ const terminalManager = new TerminalManager((terminalId, data) => {
123
+ sendToServer({ type: 'terminal_output', terminalId, data });
124
+ }, (terminalId, exitCode) => {
125
+ log(`[terminal] ${terminalId} exited with code ${exitCode}`);
126
+ sendToServer({ type: 'terminal_closed', terminalId, exitCode });
127
+ });
128
+ let pairedResolve = null;
129
+ const pairedPromise = new Promise((resolve) => { pairedResolve = resolve; });
130
+ const PROJECT_TMP_DIR = path.resolve(fileURLToPath(import.meta.url), '..', '..', 'tmp');
131
+ const LOGS_DIR = path.resolve(fileURLToPath(import.meta.url), '..', '..', 'logs');
132
+ if (!fs.existsSync(PROJECT_TMP_DIR))
133
+ fs.mkdirSync(PROJECT_TMP_DIR, { recursive: true });
134
+ if (!fs.existsSync(LOGS_DIR))
135
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
136
+ const sessionStart = new Date().toISOString().replace(/[:.]/g, '-');
137
+ const debugLog = fs.createWriteStream(path.join(PROJECT_TMP_DIR, 'kumo-pty-debug.log'), { flags: 'w' });
138
+ const eventLog = fs.createWriteStream(path.join(LOGS_DIR, `cli-${sessionStart}.log`), { flags: 'w' });
139
+ function log(msg) {
140
+ const line = `[${new Date().toISOString()}] ${msg}`;
141
+ eventLog.write(line + '\n');
142
+ }
143
+ let ptyBuffer = '';
144
+ let ptyFlushTimer = null;
145
+ const PTY_FLUSH_MS = 120;
146
+ const PTY_MAX_CHUNK = 8000;
147
+ let ptySnapshotRenderer = new PtySnapshotRenderer(process.stdout.columns ?? 120, process.stdout.rows ?? 30);
148
+ function sanitizePtyPreview(data) {
149
+ return data
150
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
151
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
152
+ .replace(/\s+/g, ' ')
153
+ .slice(0, 100);
154
+ }
155
+ function flushPtyUpdates() {
156
+ if (!ptyBuffer)
157
+ return;
158
+ ptyBuffer = '';
159
+ ptySnapshotRenderer.flushSnapshot();
160
+ }
161
+ function queuePtyData(chunk) {
162
+ ptyBuffer += chunk;
163
+ ptySnapshotRenderer.feed(chunk);
164
+ if (ptyBuffer.length >= PTY_MAX_CHUNK) {
165
+ if (ptyFlushTimer) {
166
+ clearTimeout(ptyFlushTimer);
167
+ ptyFlushTimer = null;
168
+ }
169
+ flushPtyUpdates();
170
+ return;
171
+ }
172
+ if (!ptyFlushTimer) {
173
+ ptyFlushTimer = setTimeout(() => {
174
+ ptyFlushTimer = null;
175
+ flushPtyUpdates();
176
+ }, PTY_FLUSH_MS);
177
+ }
178
+ }
179
+ let pingInterval = null;
180
+ let menuIdCounter = 0;
181
+ const menuParser = new PtyMenuParser((element) => {
182
+ switch (element.type) {
183
+ case 'selection':
184
+ if (element.options.length === 0) {
185
+ log(`[parser] ignoring empty menu`);
186
+ break;
187
+ }
188
+ if (element.title.includes('Bypass Permissions')) {
189
+ const yesIndex = element.options.findIndex((o) => o.label.trim().toLowerCase().includes('yes'));
190
+ if (yesIndex >= 0 && claudeProcess) {
191
+ const delta = yesIndex - element.selectedIndex;
192
+ const key = delta < 0 ? '\x1b[A' : '\x1b[B';
193
+ for (let i = 0; i < Math.abs(delta); i++) {
194
+ claudeProcess.write(key);
195
+ }
196
+ claudeProcess.write('\r');
197
+ log(`[parser] auto-accepted bypass permissions dialog`);
198
+ break;
199
+ }
200
+ }
201
+ if (!autoAcceptedApiKeyDialog && (element.title.includes('Detected a custom API key') || element.title.includes('Do you want to use this API key'))) {
202
+ const yesIndex = element.options.findIndex((o) => o.label.trim().toLowerCase() === 'yes');
203
+ if (yesIndex >= 0 && claudeProcess) {
204
+ autoAcceptedApiKeyDialog = true;
205
+ const delta = yesIndex - element.selectedIndex;
206
+ const key = delta < 0 ? '\x1b[A' : '\x1b[B';
207
+ for (let i = 0; i < Math.abs(delta); i++) {
208
+ claudeProcess.write(key);
209
+ }
210
+ claudeProcess.write('\r');
211
+ log(`[parser] auto-accepted custom api key dialog`);
212
+ break;
213
+ }
214
+ }
215
+ menuIdCounter++;
216
+ const id = `menu-${menuIdCounter}`;
217
+ log(`[parser] menu detected: "${element.title}" (${element.options.length} opts, selected: ${element.selectedIndex})`);
218
+ log(`[parser] options: ${JSON.stringify(element.options)}`);
219
+ sendToServer({
220
+ type: 'prompt_options',
221
+ id,
222
+ title: element.title,
223
+ description: element.description,
224
+ options: element.options,
225
+ selectedIndex: element.selectedIndex,
226
+ hint: element.hint,
227
+ footer: element.footer,
228
+ });
229
+ break;
230
+ case 'input':
231
+ log(`[parser] input: "${element.label}" = "${element.value}"`);
232
+ sendToServer({
233
+ type: 'text_input',
234
+ label: element.label,
235
+ value: element.value,
236
+ placeholder: element.placeholder,
237
+ cursorPosition: element.cursorPosition,
238
+ });
239
+ break;
240
+ case 'notification':
241
+ log(`[parser] notification: [${element.level}] ${element.message}`);
242
+ sendToServer({
243
+ type: 'notification',
244
+ level: element.level,
245
+ message: element.message,
246
+ });
247
+ break;
248
+ case 'status':
249
+ log(`[parser] status: model=${element.model}, effort=${element.effort}, logged=${element.loggedIn}`);
250
+ sendToServer({
251
+ type: 'cli_status',
252
+ loggedIn: element.loggedIn,
253
+ model: element.model,
254
+ effort: element.effort,
255
+ workingDir: element.workingDir,
256
+ });
257
+ break;
258
+ case 'session':
259
+ log(`[parser] session: ${element.sessionId}`);
260
+ sendToServer({
261
+ type: 'session_info',
262
+ sessionId: element.sessionId,
263
+ resumeCommand: element.resumeCommand,
264
+ });
265
+ break;
266
+ case 'prompt':
267
+ break;
268
+ }
269
+ });
270
+ function sendToServer(data) {
271
+ if (ws && ws.readyState === WebSocket.OPEN) {
272
+ lastWsSendAt = Date.now();
273
+ ws.send(JSON.stringify(data));
274
+ }
275
+ }
276
+ function dispatchPrompt(text) {
277
+ if (!claudeProcess)
278
+ return;
279
+ if (text.trim() === '/login') {
280
+ menuParser.reset();
281
+ }
282
+ writeToProcess(claudeProcess, text);
283
+ }
284
+ function handleSelectOption(value) {
285
+ if (!claudeProcess) {
286
+ log(`[select_option] no claude process!`);
287
+ return;
288
+ }
289
+ log(`[select_option] sending: "${value}" (without Enter)`);
290
+ menuParser.reset();
291
+ claudeProcess.write(value);
292
+ if (pendingSelectNudgeTimer)
293
+ clearTimeout(pendingSelectNudgeTimer);
294
+ const baseline = lastWsSendAt;
295
+ pendingSelectNudgeTimer = setTimeout(() => {
296
+ pendingSelectNudgeTimer = null;
297
+ if (!claudeProcess)
298
+ return;
299
+ if (lastWsSendAt === baseline)
300
+ nudgeResize();
301
+ }, 1000);
302
+ }
303
+ function nudgeResize() {
304
+ if (!claudeProcess)
305
+ return;
306
+ const cols = process.stdout.columns ?? 120;
307
+ const rows = process.stdout.rows ?? 30;
308
+ const deltaCols = 1;
309
+ const backDelayMs = 5;
310
+ try {
311
+ claudeProcess.resize(cols + deltaCols, rows);
312
+ setTimeout(() => {
313
+ try {
314
+ claudeProcess?.resize(cols, rows);
315
+ }
316
+ catch { }
317
+ }, backDelayMs);
318
+ }
319
+ catch { }
320
+ }
321
+ function removeResumeArgs(args) {
322
+ const result = [];
323
+ for (let i = 0; i < args.length; i++) {
324
+ if (args[i] === '--resume' || args[i] === '-r') {
325
+ i++;
326
+ }
327
+ else {
328
+ result.push(args[i]);
329
+ }
330
+ }
331
+ return result;
332
+ }
333
+ function spawnClaudeNow() {
334
+ log(`[spawn] spawning claude, args=${JSON.stringify(claudeArgs)}, cwd=${process.cwd()}`);
335
+ if (claudeProcess) {
336
+ isRespawning = true;
337
+ try {
338
+ claudeProcess.kill();
339
+ }
340
+ catch { }
341
+ claudeProcess = null;
342
+ }
343
+ spawnCleanup?.();
344
+ spawnCleanup = null;
345
+ scanner?.stop();
346
+ scanner = null;
347
+ snapshotScanner?.stop();
348
+ snapshotScanner = null;
349
+ autoAcceptedApiKeyDialog = false;
350
+ ptySnapshotRenderer.reset(process.stdout.columns ?? 120, process.stdout.rows ?? 30);
351
+ const { pty: child, cleanup } = spawnClaude([...claudeArgs, '--settings', claudeSettingsPath], (active) => sendToServer({ type: 'thinking', active }), (data) => {
352
+ debugLog.write(data);
353
+ menuParser.feed(data);
354
+ queuePtyData(data);
355
+ }, (cols, rows) => menuParser.resize(cols, rows), (pty) => { claudeProcess = pty; });
356
+ spawnCleanup = cleanup;
357
+ child.onExit(({ exitCode }) => {
358
+ log(`[spawn] claude exited, exitCode=${exitCode}, isRespawning=${isRespawning}`);
359
+ flushPtyUpdates();
360
+ scanner?.stop();
361
+ if (isRespawning) {
362
+ isRespawning = false;
363
+ return;
364
+ }
365
+ if (reconnectTimer)
366
+ clearTimeout(reconnectTimer);
367
+ ws?.close();
368
+ process.exit(exitCode);
369
+ });
370
+ process.on('SIGINT', () => {
371
+ child.kill();
372
+ });
373
+ }
374
+ let activeCredentials = null;
375
+ function connectToServer(onPaired, onOpen) {
376
+ ws = new WebSocket(SERVER_WS);
377
+ ws.on('open', () => {
378
+ log(`[ws] connected to ${SERVER_WS}`);
379
+ sendToServer({
380
+ type: 'register',
381
+ clientType: 'cli',
382
+ deviceToken: activeCredentials?.deviceToken,
383
+ deviceUuid: activeCredentials?.deviceUuid,
384
+ });
385
+ menuParser.reset();
386
+ if (pingInterval)
387
+ clearInterval(pingInterval);
388
+ pingInterval = setInterval(() => {
389
+ if (ws && ws.readyState === WebSocket.OPEN) {
390
+ ws.ping();
391
+ }
392
+ }, 20000);
393
+ if (onOpen) {
394
+ Promise.resolve(onOpen()).catch((e) => log(`[onOpen] error: ${e}`));
395
+ }
396
+ });
397
+ ws.on('ping', () => {
398
+ ws?.pong();
399
+ });
400
+ ws.on('message', (raw) => {
401
+ let msg;
402
+ try {
403
+ msg = JSON.parse(raw.toString());
404
+ }
405
+ catch {
406
+ return;
407
+ }
408
+ if (msg['type'] === 'paired') {
409
+ log(`[pairing] paired with app`);
410
+ console.log('\n\x1b[32m✓ App paired! Launching Claude...\x1b[0m\n');
411
+ pairedResolve?.();
412
+ pairedResolve = null;
413
+ startFileWatcher();
414
+ onPaired?.();
415
+ return;
416
+ }
417
+ if (msg['type'] === 'send_prompt') {
418
+ const text = msg['text'];
419
+ if (!text)
420
+ return;
421
+ if (claudeProcess) {
422
+ dispatchPrompt(text);
423
+ }
424
+ else {
425
+ pendingPrompt = text;
426
+ }
427
+ }
428
+ if (msg['type'] === 'select_option') {
429
+ const value = msg['value'];
430
+ log(`[ws recv] select_option: value="${value}"`);
431
+ if (value)
432
+ handleSelectOption(value);
433
+ }
434
+ if (msg['type'] === 'submit_input') {
435
+ const value = msg['value'];
436
+ log(`[ws recv] submit_input: value="${value}"`);
437
+ if (claudeProcess) {
438
+ menuParser.reset();
439
+ claudeProcess.write('\x15');
440
+ claudeProcess.write(value);
441
+ claudeProcess.write('\r');
442
+ }
443
+ }
444
+ if (msg['type'] === 'send_key') {
445
+ const key = msg['key'];
446
+ log(`[ws recv] send_key: key="${key}"`);
447
+ if (claudeProcess && key === 'esc') {
448
+ menuParser.reset();
449
+ claudeProcess.write('\x1b');
450
+ }
451
+ }
452
+ if (msg['type'] === 'nudge_resize') {
453
+ nudgeResize();
454
+ }
455
+ if (msg['type'] === 'request_session_list') {
456
+ sendSessionList().catch((e) => log(`[request_session_list] error: ${e}`));
457
+ return;
458
+ }
459
+ if (msg['type'] === 'request_session_history') {
460
+ handleSessionHistoryRequest(msg['sessionId']).catch((e) => log(`[session_history] error: ${e}`));
461
+ return;
462
+ }
463
+ if (msg['type'] === 'request_new_session') {
464
+ log(`[ws recv] request_new_session`);
465
+ claudeArgs = removeResumeArgs(claudeArgs);
466
+ startScannerAtEnd = false;
467
+ spawnClaudeNow();
468
+ return;
469
+ }
470
+ if (msg['type'] === 'terminal_create') {
471
+ const cols = msg['cols'] || 120;
472
+ const rows = msg['rows'] || 30;
473
+ const cwd = msg['cwd'];
474
+ const terminalId = terminalManager.create(cols, rows, cwd);
475
+ log(`[terminal] created: ${terminalId}`);
476
+ sendToServer({ type: 'terminal_created', terminalId });
477
+ return;
478
+ }
479
+ if (msg['type'] === 'terminal_input') {
480
+ const terminalId = msg['terminalId'];
481
+ const data = msg['data'];
482
+ if (terminalId && data) {
483
+ terminalManager.write(terminalId, data);
484
+ }
485
+ return;
486
+ }
487
+ if (msg['type'] === 'terminal_resize') {
488
+ const terminalId = msg['terminalId'];
489
+ const cols = msg['cols'];
490
+ const rows = msg['rows'];
491
+ if (terminalId && cols && rows) {
492
+ terminalManager.resize(terminalId, cols, rows);
493
+ log(`[terminal] resize ${terminalId} -> ${cols}x${rows}`);
494
+ }
495
+ return;
496
+ }
497
+ if (msg['type'] === 'terminal_close') {
498
+ const terminalId = msg['terminalId'];
499
+ if (terminalId) {
500
+ terminalManager.close(terminalId);
501
+ log(`[terminal] closed: ${terminalId}`);
502
+ }
503
+ return;
504
+ }
505
+ if (msg['type'] === 'terminal_list') {
506
+ const terminals = terminalManager.list();
507
+ sendToServer({ type: 'terminal_list', terminals });
508
+ return;
509
+ }
510
+ if (msg['type'] === 'file_search') {
511
+ // recursively search files/folders by name (case-insensitive substring)
512
+ const query = (msg['query'] || '').trim().toLowerCase();
513
+ const basePath = msg['path'] || '';
514
+ const limit = Math.min(Math.max(msg['limit'] || 200, 1), 1000);
515
+ try {
516
+ const projectRoot = process.cwd();
517
+ const baseResolved = safePath(basePath || '.');
518
+ const ignored = loadIgnoredNames(projectRoot);
519
+ const results = [];
520
+ if (query.length === 0) {
521
+ sendToServer({ type: 'file_search_result', query, entries: [] });
522
+ return;
523
+ }
524
+ const stack = [baseResolved];
525
+ while (stack.length > 0 && results.length < limit) {
526
+ const dir = stack.pop();
527
+ let dirEntries;
528
+ try {
529
+ dirEntries = fs.readdirSync(dir, { withFileTypes: true });
530
+ }
531
+ catch {
532
+ continue;
533
+ }
534
+ for (const e of dirEntries) {
535
+ if (e.name.startsWith('.'))
536
+ continue;
537
+ if (ignored.has(e.name))
538
+ continue;
539
+ const full = path.resolve(dir, e.name);
540
+ if (e.name.toLowerCase().includes(query)) {
541
+ let size = 0;
542
+ if (!e.isDirectory()) {
543
+ try {
544
+ size = fs.statSync(full).size;
545
+ }
546
+ catch { }
547
+ }
548
+ results.push({
549
+ name: e.name,
550
+ path: path.relative(projectRoot, full).replace(/\\/g, '/'),
551
+ isDirectory: e.isDirectory(),
552
+ size,
553
+ });
554
+ if (results.length >= limit)
555
+ break;
556
+ }
557
+ if (e.isDirectory())
558
+ stack.push(full);
559
+ }
560
+ }
561
+ results.sort((a, b) => {
562
+ if (a.isDirectory !== b.isDirectory)
563
+ return a.isDirectory ? -1 : 1;
564
+ return a.name.localeCompare(b.name);
565
+ });
566
+ sendToServer({ type: 'file_search_result', query, entries: results });
567
+ }
568
+ catch (e) {
569
+ sendToServer({ type: 'file_search_result', query, entries: [], error: e instanceof Error ? e.message : String(e) });
570
+ }
571
+ return;
572
+ }
573
+ if (msg['type'] === 'file_list') {
574
+ const requestedPath = msg['path'] || '';
575
+ try {
576
+ const projectRoot = process.cwd();
577
+ const resolved = safePath(requestedPath || '.');
578
+ if (!resolved.startsWith(projectRoot)) {
579
+ sendToServer({ type: 'file_list_result', path: requestedPath, entries: [], error: 'path traversal blocked' });
580
+ return;
581
+ }
582
+ const entries = fs.readdirSync(resolved, { withFileTypes: true })
583
+ .filter(e => !e.name.startsWith('.'))
584
+ .map(e => ({
585
+ name: e.name,
586
+ path: path.relative(projectRoot, path.resolve(resolved, e.name)).replace(/\\/g, '/'),
587
+ isDirectory: e.isDirectory(),
588
+ size: e.isDirectory() ? 0 : fs.statSync(path.resolve(resolved, e.name)).size,
589
+ }))
590
+ .sort((a, b) => {
591
+ if (a.isDirectory !== b.isDirectory)
592
+ return a.isDirectory ? -1 : 1;
593
+ return a.name.localeCompare(b.name);
594
+ });
595
+ sendToServer({ type: 'file_list_result', path: requestedPath, entries, projectName: path.basename(process.cwd()) });
596
+ }
597
+ catch (e) {
598
+ sendToServer({ type: 'file_list_result', path: requestedPath, entries: [], error: e instanceof Error ? e.message : String(e) });
599
+ }
600
+ return;
601
+ }
602
+ if (msg['type'] === 'file_read') {
603
+ const filePath = msg['path'];
604
+ try {
605
+ const projectRoot = process.cwd();
606
+ const resolved = path.resolve(projectRoot, path.normalize(filePath || '.'));
607
+ if (!resolved.startsWith(projectRoot)) {
608
+ sendToServer({ type: 'file_read_result', path: filePath, error: 'path traversal blocked' });
609
+ return;
610
+ }
611
+ const content = fs.readFileSync(resolved, 'utf-8');
612
+ sendToServer({ type: 'file_read_result', path: filePath, content });
613
+ }
614
+ catch (e) {
615
+ sendToServer({ type: 'file_read_result', path: filePath, error: e instanceof Error ? e.message : String(e) });
616
+ }
617
+ return;
618
+ }
619
+ if (msg['type'] === 'file_write') {
620
+ const filePath = msg['path'];
621
+ const content = msg['content'];
622
+ try {
623
+ const projectRoot = process.cwd();
624
+ const resolved = path.resolve(projectRoot, path.normalize(filePath || '.'));
625
+ if (!resolved.startsWith(projectRoot)) {
626
+ sendToServer({ type: 'file_write_result', path: filePath, ok: false, error: 'path traversal blocked' });
627
+ return;
628
+ }
629
+ fs.writeFileSync(resolved, content, 'utf-8');
630
+ sendToServer({ type: 'file_write_result', path: filePath, ok: true });
631
+ }
632
+ catch (e) {
633
+ sendToServer({ type: 'file_write_result', path: filePath, ok: false, error: e instanceof Error ? e.message : String(e) });
634
+ }
635
+ return;
636
+ }
637
+ if (msg['type'] === 'file_create') {
638
+ const targetPath = msg['path'];
639
+ const isDir = msg['isDirectory'] ?? false;
640
+ try {
641
+ const resolved = safePath(targetPath);
642
+ if (isDir) {
643
+ fs.mkdirSync(resolved, { recursive: true });
644
+ }
645
+ else {
646
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
647
+ fs.writeFileSync(resolved, '', 'utf-8');
648
+ }
649
+ sendToServer({ type: 'file_op_result', op: 'create', path: targetPath, ok: true });
650
+ }
651
+ catch (e) {
652
+ sendToServer({ type: 'file_op_result', op: 'create', path: targetPath, ok: false, error: e instanceof Error ? e.message : String(e) });
653
+ }
654
+ return;
655
+ }
656
+ if (msg['type'] === 'file_delete') {
657
+ const targetPath = msg['path'];
658
+ try {
659
+ const resolved = safePath(targetPath);
660
+ fs.rmSync(resolved, { recursive: true, force: true });
661
+ sendToServer({ type: 'file_op_result', op: 'delete', path: targetPath, ok: true });
662
+ }
663
+ catch (e) {
664
+ sendToServer({ type: 'file_op_result', op: 'delete', path: targetPath, ok: false, error: e instanceof Error ? e.message : String(e) });
665
+ }
666
+ return;
667
+ }
668
+ if (msg['type'] === 'file_rename') {
669
+ const oldPath = msg['oldPath'];
670
+ const newName = msg['newName'];
671
+ try {
672
+ const resolvedOld = safePath(oldPath);
673
+ const resolvedNew = path.join(path.dirname(resolvedOld), newName);
674
+ safePath(path.relative(process.cwd(), resolvedNew));
675
+ fs.renameSync(resolvedOld, resolvedNew);
676
+ sendToServer({ type: 'file_op_result', op: 'rename', path: oldPath, newPath: path.relative(process.cwd(), resolvedNew).replace(/\\/g, '/'), ok: true });
677
+ }
678
+ catch (e) {
679
+ sendToServer({ type: 'file_op_result', op: 'rename', path: oldPath, ok: false, error: e instanceof Error ? e.message : String(e) });
680
+ }
681
+ return;
682
+ }
683
+ if (msg['type'] === 'file_copy') {
684
+ const srcPath = msg['src'];
685
+ const destPath = msg['dest'];
686
+ try {
687
+ const resolvedSrc = safePath(srcPath);
688
+ const resolvedDest = safePath(destPath);
689
+ copyRecursive(resolvedSrc, resolvedDest);
690
+ sendToServer({ type: 'file_op_result', op: 'copy', path: srcPath, dest: destPath, ok: true });
691
+ }
692
+ catch (e) {
693
+ sendToServer({ type: 'file_op_result', op: 'copy', path: srcPath, ok: false, error: e instanceof Error ? e.message : String(e) });
694
+ }
695
+ return;
696
+ }
697
+ if (msg['type'] === 'file_move') {
698
+ const srcPath = msg['src'];
699
+ const destPath = msg['dest'];
700
+ try {
701
+ const resolvedSrc = safePath(srcPath);
702
+ const resolvedDest = safePath(destPath);
703
+ fs.renameSync(resolvedSrc, resolvedDest);
704
+ sendToServer({ type: 'file_op_result', op: 'move', path: srcPath, dest: destPath, ok: true });
705
+ }
706
+ catch (e) {
707
+ sendToServer({ type: 'file_op_result', op: 'move', path: srcPath, ok: false, error: e instanceof Error ? e.message : String(e) });
708
+ }
709
+ return;
710
+ }
711
+ if (msg['type'] === 'tunnel_request') {
712
+ const { requestId, method, path: urlPath, headers: reqHeaders, body: reqBody, port, host: remoteHost } = msg;
713
+ const targetHost = remoteHost || 'localhost';
714
+ const url = `http://${targetHost}:${port}${urlPath}`;
715
+ const filteredHeaders = Object.fromEntries(Object.entries(reqHeaders).filter(([k]) => {
716
+ const lower = k.toLowerCase();
717
+ return !lower.startsWith('sec-') && !['host', 'connection', 'accept-encoding', 'upgrade-insecure-requests'].includes(lower);
718
+ }).map(([k, v]) => {
719
+ const lower = k.toLowerCase();
720
+ if (lower === 'origin' || lower === 'referer') {
721
+ return [k, v.replace(/http:\/\/localhost:\d+/g, `http://${targetHost}:${port}`)];
722
+ }
723
+ return [k, v];
724
+ }));
725
+ log(`[tunnel] ${method} ${url}`);
726
+ fetch(url, {
727
+ method,
728
+ headers: filteredHeaders,
729
+ body: reqBody && method !== 'GET' && method !== 'HEAD' ? Buffer.from(reqBody, 'base64') : undefined,
730
+ redirect: 'manual',
731
+ })
732
+ .then(async (res) => {
733
+ const buf = Buffer.from(await res.arrayBuffer());
734
+ log(`[tunnel] response ${res.status} ${url} (${buf.length} bytes)`);
735
+ const respHeaders = Object.fromEntries([...res.headers.entries()].filter(([k]) => !['content-encoding', 'transfer-encoding', 'content-length'].includes(k.toLowerCase())));
736
+ const remoteOrigin = `http://${targetHost}:${port}`;
737
+ if (respHeaders['location']) {
738
+ respHeaders['location'] = respHeaders['location']
739
+ .replace(remoteOrigin, '');
740
+ }
741
+ if (respHeaders['set-cookie']) {
742
+ respHeaders['set-cookie'] = respHeaders['set-cookie']
743
+ .replace(/;\s*domain=[^;]*/gi, '')
744
+ .replace(/;\s*secure/gi, '');
745
+ }
746
+ sendToServer({
747
+ type: 'tunnel_response',
748
+ requestId,
749
+ status: res.status,
750
+ headers: respHeaders,
751
+ body: buf.toString('base64'),
752
+ });
753
+ })
754
+ .catch((err) => {
755
+ sendToServer({
756
+ type: 'tunnel_response',
757
+ requestId,
758
+ status: 502,
759
+ headers: {},
760
+ body: Buffer.from('Bad Gateway: ' + err.message).toString('base64'),
761
+ });
762
+ });
763
+ return;
764
+ }
765
+ });
766
+ ws.on('close', () => {
767
+ log(`[ws] disconnected, reconnecting in 3s`);
768
+ if (pingInterval) {
769
+ clearInterval(pingInterval);
770
+ pingInterval = null;
771
+ }
772
+ if (fileWatcher) {
773
+ try {
774
+ fileWatcher.close();
775
+ }
776
+ catch { }
777
+ fileWatcher = null;
778
+ }
779
+ reconnectTimer = setTimeout(() => connectToServer(), 3000);
780
+ });
781
+ ws.on('error', () => { });
782
+ }
783
+ function generateCode() {
784
+ return String(Math.floor(100000 + Math.random() * 900000));
785
+ }
786
+ function httpPost(url, body) {
787
+ return new Promise((resolve, reject) => {
788
+ const payload = JSON.stringify(body);
789
+ const urlObj = new URL(url);
790
+ const options = {
791
+ hostname: urlObj.hostname,
792
+ port: urlObj.port || 80,
793
+ path: urlObj.pathname,
794
+ method: 'POST',
795
+ headers: {
796
+ 'Content-Type': 'application/json',
797
+ 'Content-Length': Buffer.byteLength(payload),
798
+ },
799
+ };
800
+ const req = http.request(options, (res) => {
801
+ let raw = '';
802
+ res.on('data', (chunk) => { raw += chunk; });
803
+ res.on('end', () => {
804
+ try {
805
+ resolve({ status: res.statusCode ?? 0, data: JSON.parse(raw) });
806
+ }
807
+ catch {
808
+ resolve({ status: res.statusCode ?? 0, data: raw });
809
+ }
810
+ });
811
+ });
812
+ req.on('error', reject);
813
+ req.write(payload);
814
+ req.end();
815
+ });
816
+ }
817
+ function displayCode(code) {
818
+ const line = '═'.repeat(40);
819
+ console.log('\n\x1b[36m' + line + '\x1b[0m');
820
+ console.log('\x1b[36m║\x1b[0m \x1b[1mKumo Pairing Code (valid 5 min)\x1b[0m');
821
+ console.log('\x1b[36m║\x1b[0m');
822
+ console.log(`\x1b[36m║\x1b[0m \x1b[1;33m${code.slice(0, 3)} ${code.slice(3)}\x1b[0m`);
823
+ console.log('\x1b[36m║\x1b[0m');
824
+ console.log('\x1b[36m║\x1b[0m Enter this code in the Kumo app');
825
+ console.log('\x1b[36m' + line + '\x1b[0m\n');
826
+ }
827
+ async function registerCode(code) {
828
+ try {
829
+ const result = await httpPost(`${SERVER_HTTP}/api/pair/register`, {
830
+ code,
831
+ port: SERVER_PORT,
832
+ });
833
+ return result.status === 200;
834
+ }
835
+ catch {
836
+ return false;
837
+ }
838
+ }
839
+ async function checkCliStatus() {
840
+ return new Promise((resolve) => {
841
+ const urlObj = new URL(`${SERVER_HTTP}/api/cli/status`);
842
+ const options = {
843
+ hostname: urlObj.hostname,
844
+ port: urlObj.port || 80,
845
+ path: urlObj.pathname,
846
+ method: 'GET',
847
+ };
848
+ const req = http.request(options, (res) => {
849
+ let raw = '';
850
+ res.on('data', (chunk) => { raw += chunk; });
851
+ res.on('end', () => {
852
+ try {
853
+ resolve(JSON.parse(raw));
854
+ }
855
+ catch {
856
+ resolve({ paired: false, alive: false });
857
+ }
858
+ });
859
+ });
860
+ req.on('error', () => resolve({ paired: false, alive: false }));
861
+ req.end();
862
+ });
863
+ }
864
+ function getCurrentProjectFolder() {
865
+ const cwd = process.cwd();
866
+ if (os.platform() === 'win32') {
867
+ return cwd.replace(/:/g, '-').replace(/[\\/]/g, '-');
868
+ }
869
+ return cwd.replace(/\//g, '-');
870
+ }
871
+ async function getActiveSessionIds() {
872
+ const sessionsDir = path.join(os.homedir(), '.claude', 'sessions');
873
+ const active = new Set();
874
+ try {
875
+ const files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith('.json'));
876
+ for (const file of files) {
877
+ try {
878
+ const data = JSON.parse(fs.readFileSync(path.join(sessionsDir, file), 'utf-8'));
879
+ if (data.sessionId)
880
+ active.add(data.sessionId);
881
+ }
882
+ catch { }
883
+ }
884
+ }
885
+ catch { }
886
+ return active;
887
+ }
888
+ async function getSessionListForCwd() {
889
+ const folderName = getCurrentProjectFolder();
890
+ const projectDir = path.join(os.homedir(), '.claude', 'projects', folderName);
891
+ log(`[session_list] cwd=${process.cwd()}, folder=${folderName}, dir=${projectDir}`);
892
+ const sessions = [];
893
+ try {
894
+ if (!fs.existsSync(projectDir))
895
+ return sessions;
896
+ const files = fs.readdirSync(projectDir).filter((f) => f.endsWith('.jsonl'));
897
+ for (const file of files) {
898
+ const sessionId = path.basename(file, '.jsonl');
899
+ const filePath = path.join(projectDir, file);
900
+ try {
901
+ const raw = fs.readFileSync(filePath, 'utf-8');
902
+ const lines = raw.split('\n').filter((l) => l.trim());
903
+ let firstMessage = '';
904
+ let lastTimestamp = '';
905
+ let hasMessages = false;
906
+ for (const line of lines) {
907
+ try {
908
+ const entry = JSON.parse(line);
909
+ if (entry['type'] !== 'user' && entry['type'] !== 'assistant')
910
+ continue;
911
+ const msgObj = entry['message'];
912
+ if (!msgObj || !msgObj['role'] || msgObj['role'] === 'system')
913
+ continue;
914
+ hasMessages = true;
915
+ const ts = entry['timestamp'];
916
+ if (ts)
917
+ lastTimestamp = ts;
918
+ if (!firstMessage && msgObj['role'] === 'user') {
919
+ if (typeof msgObj['content'] === 'string') {
920
+ firstMessage = msgObj['content'].slice(0, 200);
921
+ }
922
+ else if (Array.isArray(msgObj['content'])) {
923
+ firstMessage = msgObj['content']
924
+ .filter((b) => b['type'] === 'text' && b['text'])
925
+ .map((b) => b['text'])
926
+ .join('')
927
+ .slice(0, 200);
928
+ }
929
+ }
930
+ }
931
+ catch { }
932
+ }
933
+ if (hasMessages) {
934
+ sessions.push({ sessionId, firstMessage, lastTimestamp });
935
+ }
936
+ }
937
+ catch { }
938
+ }
939
+ }
940
+ catch { }
941
+ sessions.sort((a, b) => (b.lastTimestamp || '').localeCompare(a.lastTimestamp || ''));
942
+ return sessions;
943
+ }
944
+ async function sendSessionList() {
945
+ try {
946
+ const sessions = await getSessionListForCwd();
947
+ const activeIds = await getActiveSessionIds();
948
+ const sessionsWithActive = sessions.map((s) => ({
949
+ ...s,
950
+ isActive: activeIds.has(s.sessionId),
951
+ }));
952
+ sendToServer({ type: 'session_list', sessions: sessionsWithActive });
953
+ log(`[session_list] sent ${sessionsWithActive.length} sessions, ${activeIds.size} active`);
954
+ }
955
+ catch (err) {
956
+ log(`[session_list] error: ${err}`);
957
+ sendToServer({ type: 'session_list', sessions: [] });
958
+ }
959
+ }
960
+ async function handleSessionHistoryRequest(sessionId) {
961
+ if (!sessionId)
962
+ return;
963
+ log(`[ws recv] request_session_history: ${sessionId}`);
964
+ const switchId = ++sessionSwitchCounter;
965
+ log(`[session_history] switchId=${switchId}`);
966
+ try {
967
+ const folderName = getCurrentProjectFolder();
968
+ const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
969
+ const sessionFile = path.join(claudeProjectsDir, folderName, `${sessionId}.jsonl`);
970
+ log(`[session_history] folder=${folderName}, file=${sessionFile}`);
971
+ if (!fs.existsSync(sessionFile)) {
972
+ log(`[session_history] file not found, returning empty`);
973
+ sendToServer({ type: 'session_history', sessionId, messages: [] });
974
+ return;
975
+ }
976
+ const raw = fs.readFileSync(sessionFile, 'utf-8');
977
+ const messages = [];
978
+ const seenUuids = new Set();
979
+ for (const line of raw.split('\n')) {
980
+ if (!line.trim())
981
+ continue;
982
+ try {
983
+ const entry = JSON.parse(line);
984
+ if (entry['type'] !== 'user' && entry['type'] !== 'assistant')
985
+ continue;
986
+ const msgObj = entry['message'];
987
+ if (!msgObj)
988
+ continue;
989
+ const role = msgObj['role'];
990
+ if (!role || role === 'system')
991
+ continue;
992
+ if (entry['isMeta'] || entry['sourceToolUseID'])
993
+ continue;
994
+ const uuid = entry['uuid'];
995
+ if (uuid && seenUuids.has(uuid))
996
+ continue;
997
+ if (uuid)
998
+ seenUuids.add(uuid);
999
+ const timestamp = entry['timestamp'];
1000
+ let content = '';
1001
+ const toolUses = [];
1002
+ if (typeof msgObj['content'] === 'string') {
1003
+ content = msgObj['content'];
1004
+ }
1005
+ else if (Array.isArray(msgObj['content'])) {
1006
+ content = msgObj['content']
1007
+ .filter((b) => b['type'] === 'text' && b['text'])
1008
+ .map((b) => b['text'])
1009
+ .join('');
1010
+ for (const b of msgObj['content']) {
1011
+ if (b['type'] === 'tool_use' && b['name']) {
1012
+ toolUses.push({ toolName: b['name'], input: b['input'] ?? {} });
1013
+ }
1014
+ }
1015
+ }
1016
+ if (!content && toolUses.length === 0)
1017
+ continue;
1018
+ messages.push({ role, content, id: uuid, timestamp, toolUses: toolUses.length > 0 ? toolUses : undefined });
1019
+ }
1020
+ catch { }
1021
+ }
1022
+ sendToServer({ type: 'session_history', sessionId, messages });
1023
+ log(`[session_history] sent ${messages.length} messages for ${sessionId}`);
1024
+ if (switchId !== sessionSwitchCounter) {
1025
+ log(`[session_history] cancelled switch to ${sessionId} (superseded)`);
1026
+ return;
1027
+ }
1028
+ startScannerAtEnd = true;
1029
+ claudeArgs = [...removeResumeArgs(claudeArgs), '--resume', sessionId];
1030
+ spawnClaudeNow();
1031
+ }
1032
+ catch (err) {
1033
+ log(`[request_session_history] error: ${err}`);
1034
+ sendToServer({ type: 'session_history', sessionId, messages: [] });
1035
+ }
1036
+ }
1037
+ async function pairFlow() {
1038
+ // prompt user for the 9-digit pairing code shown in the kumo app
1039
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1040
+ const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a)));
1041
+ // ascii art of the kumo text
1042
+ const cloud = [
1043
+ " \x1b[1;36m_ __ _ _ __ __ ____ \x1b[0m",
1044
+ " \x1b[1;36m| |/ /| | | || \\/ | / __ \\\x1b[0m",
1045
+ " \x1b[1;36m| ' / | | | || \\ / || | | |\x1b[0m",
1046
+ " \x1b[1;36m| . \\ | |_| || |\\/| || |__| |\x1b[0m",
1047
+ " \x1b[1;36m|_|\\_\\ \\___/ |_| |_| \\____/\x1b[0m"
1048
+ ];
1049
+ console.log('');
1050
+ for (const line of cloud)
1051
+ console.log(line);
1052
+ console.log('');
1053
+ console.log(' \x1b[2mwhere your code drifts between devices, softly.\x1b[0m');
1054
+ console.log('');
1055
+ console.log(' \x1b[36m›\x1b[0m open the \x1b[1mKumo app\x1b[0m, sign in, and tap \x1b[1m"generate"\x1b[0m on the pairing tab.');
1056
+ console.log(' \x1b[36m›\x1b[0m whisper the 9 digits back to me below — spaces are fine.');
1057
+ console.log('');
1058
+ const raw = (await ask('\x1b[1;33m✦ pairing code ›\x1b[0m ')).trim();
1059
+ rl.close();
1060
+ if (!raw)
1061
+ return null;
1062
+ const code = raw.replace(/\D+/g, '');
1063
+ if (!/^\d{9}$/.test(code)) {
1064
+ console.warn('[kumo-cli] pairing code must be exactly 9 digits');
1065
+ return null;
1066
+ }
1067
+ const deviceUuid = randomUUID();
1068
+ const deviceName = `${os.userInfo().username}@${os.hostname()}`;
1069
+ try {
1070
+ const result = await httpPost(`${SERVER_HTTP}/api/cli/pair`, {
1071
+ pairingCode: code,
1072
+ deviceUuid,
1073
+ deviceName,
1074
+ });
1075
+ if (result.status !== 201) {
1076
+ const data = result.data;
1077
+ console.warn(`[kumo-cli] pair failed: ${data?.error ?? result.status}`);
1078
+ return null;
1079
+ }
1080
+ const data = result.data;
1081
+ const creds = {
1082
+ deviceUuid: data.device.deviceUuid,
1083
+ deviceToken: data.deviceToken,
1084
+ serverUrl: SERVER_HTTP,
1085
+ user: data.user ?? undefined,
1086
+ };
1087
+ saveCredentials(creds);
1088
+ console.log(`[kumo-cli] paired and saved to ${credentialsPath()}`);
1089
+ return creds;
1090
+ }
1091
+ catch (err) {
1092
+ console.warn(`[kumo-cli] pair error: ${err.message}`);
1093
+ return null;
1094
+ }
1095
+ }
1096
+ async function main() {
1097
+ claudeArgs = process.argv.slice(2);
1098
+ if (claudeArgs[0] === 'reset') {
1099
+ const removed = clearCredentials();
1100
+ if (removed)
1101
+ console.log(`[kumo-cli] credentials cleared at ${credentialsPath()}`);
1102
+ else
1103
+ console.log(`[kumo-cli] no credentials to clear`);
1104
+ return;
1105
+ }
1106
+ if (!claudeArgs.includes('--dangerously-skip-permissions')) {
1107
+ claudeArgs.push('--dangerously-skip-permissions');
1108
+ }
1109
+ const envModel = process.env.KUMO_CLAUDE_MODEL ?? process.env.CLAUDE_MODEL;
1110
+ const hasModelArg = claudeArgs.some((a, i) => a === '--model' || a.startsWith('--model=') || (a === '-m' && i + 1 < claudeArgs.length));
1111
+ if (envModel && !hasModelArg) {
1112
+ claudeArgs.push('--model', envModel);
1113
+ }
1114
+ const hookServer = await startHookServer((sessionId) => {
1115
+ currentSessionId = sessionId;
1116
+ ptySnapshotRenderer.reset(process.stdout.columns ?? 120, process.stdout.rows ?? 30);
1117
+ sendToServer({ type: 'session_start', sessionId });
1118
+ scanner?.stop();
1119
+ scanner = new SessionScanner(sessionId, startScannerAtEnd);
1120
+ startScannerAtEnd = false;
1121
+ scanner.on('message', (msg) => {
1122
+ sendToServer({ type: 'transcript', message: msg });
1123
+ if (msg.role === 'assistant' && msg.stopReason === 'end_turn') {
1124
+ log(`[scanner] assistant_done detected, stopReason=${msg.stopReason}`);
1125
+ sendToServer({ type: 'assistant_done' });
1126
+ }
1127
+ });
1128
+ scanner.start();
1129
+ if (pendingPrompt) {
1130
+ const queued = pendingPrompt;
1131
+ pendingPrompt = null;
1132
+ dispatchPrompt(queued);
1133
+ }
1134
+ });
1135
+ process.stdout.on('resize', () => {
1136
+ ptySnapshotRenderer.resize(process.stdout.columns ?? 120, process.stdout.rows ?? 30);
1137
+ });
1138
+ claudeSettingsPath = generateHookSettingsFile(hookServer.port);
1139
+ activeCredentials = loadCredentials();
1140
+ if (!activeCredentials) {
1141
+ activeCredentials = await pairFlow();
1142
+ if (!activeCredentials) {
1143
+ console.warn('[kumo-cli] pairing skipped — launching Claude directly\n');
1144
+ spawnClaudeNow();
1145
+ hookServer.close();
1146
+ return;
1147
+ }
1148
+ }
1149
+ console.log(`\x1b[32m✓ Connected as ${activeCredentials.user?.email ?? activeCredentials.user?.id ?? 'device'}\x1b[0m\n`);
1150
+ connectToServer(undefined, async () => {
1151
+ startFileWatcher();
1152
+ await sendSessionList();
1153
+ });
1154
+ }
1155
+ main().catch((err) => {
1156
+ console.error('[kumo-cli] fatal error:', err);
1157
+ process.exit(1);
1158
+ });
1159
+ //# sourceMappingURL=index.js.map