kumo-cli 1.0.0 → 1.0.2
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/README.md +21 -32
- package/dist/auth/credentialsStore.js +41 -0
- package/dist/auth/credentialsStore.js.map +1 -0
- package/dist/claude/generateHookSettings.js +23 -0
- package/dist/claude/generateHookSettings.js.map +1 -0
- package/dist/claude/hookServer.js +33 -0
- package/dist/claude/hookServer.js.map +1 -0
- package/dist/claude/sessionScanner.js +250 -0
- package/dist/claude/sessionScanner.js.map +1 -0
- package/dist/config.js +40 -0
- package/dist/config.js.map +1 -0
- package/dist/core/config.js +48 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/loadDotEnv.js +46 -0
- package/dist/core/loadDotEnv.js.map +1 -0
- package/dist/core/logger.js +29 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/handlers/messageRouter.js +223 -0
- package/dist/handlers/messageRouter.js.map +1 -0
- package/dist/http/httpClient.js +41 -0
- package/dist/http/httpClient.js.map +1 -0
- package/dist/index.js +141 -1090
- package/dist/index.js.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/logger.js.map +1 -0
- package/dist/pty/ansiCodes.js +14 -0
- package/dist/pty/ansiCodes.js.map +1 -0
- package/dist/pty/ptyMenuParser.js +357 -0
- package/dist/pty/ptyMenuParser.js.map +1 -0
- package/dist/pty/ptySnapshotRenderer.js +71 -0
- package/dist/pty/ptySnapshotRenderer.js.map +1 -0
- package/dist/pty/spawn.js +77 -0
- package/dist/pty/spawn.js.map +1 -0
- package/dist/pty/terminalManager.js +66 -0
- package/dist/pty/terminalManager.js.map +1 -0
- package/dist/services/claudeService.js +218 -0
- package/dist/services/claudeService.js.map +1 -0
- package/dist/services/effortService.js +89 -0
- package/dist/services/effortService.js.map +1 -0
- package/dist/services/fileService.js +127 -0
- package/dist/services/fileService.js.map +1 -0
- package/dist/services/modelService.js +84 -0
- package/dist/services/modelService.js.map +1 -0
- package/dist/services/pairingService.js +129 -0
- package/dist/services/pairingService.js.map +1 -0
- package/dist/services/sessionService.js +168 -0
- package/dist/services/sessionService.js.map +1 -0
- package/dist/services/tunnelService.js +47 -0
- package/dist/services/tunnelService.js.map +1 -0
- package/dist/sessionScanner.js +131 -5
- package/dist/sessionScanner.js.map +1 -1
- package/dist/snapshotScanner.js +3 -1
- package/dist/snapshotScanner.js.map +1 -1
- package/dist/spawn.js +12 -2
- package/dist/spawn.js.map +1 -1
- package/dist/transport/directWsServer.js +135 -0
- package/dist/transport/directWsServer.js.map +1 -0
- package/dist/transport/wsClient.js +87 -0
- package/dist/transport/wsClient.js.map +1 -0
- package/dist/utils/ignorePaths.js +54 -0
- package/dist/utils/ignorePaths.js.map +1 -0
- package/dist/utils/ptyBuffer.js +46 -0
- package/dist/utils/ptyBuffer.js.map +1 -0
- package/dist/utils/safePath.js +43 -0
- package/dist/utils/safePath.js.map +1 -0
- package/package.json +2 -4
package/dist/index.js
CHANGED
|
@@ -1,89 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import WebSocket from 'ws';
|
|
3
|
-
import http from 'http';
|
|
4
2
|
import fs from 'node:fs';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
function
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
3
|
+
import { startHookServer } from './claude/hookServer.js';
|
|
4
|
+
import { generateHookSettingsFile } from './claude/generateHookSettings.js';
|
|
5
|
+
import { SessionScanner } from './claude/sessionScanner.js';
|
|
6
|
+
import { TerminalManager } from './pty/terminalManager.js';
|
|
7
|
+
import { loadIgnoredNames } from './utils/ignorePaths.js';
|
|
8
|
+
import { loadCredentials, saveCredentials, clearCredentials, credentialsPath } from './auth/credentialsStore.js';
|
|
9
|
+
import { loadDotEnv } from './core/loadDotEnv.js';
|
|
10
|
+
import { log } from './core/logger.js';
|
|
11
|
+
import { SERVER_HTTP, FILE_WATCH_DEBOUNCE_MS } from './core/config.js';
|
|
12
|
+
import { ModelService } from './services/modelService.js';
|
|
13
|
+
import { EffortService } from './services/effortService.js';
|
|
14
|
+
import { ClaudeService } from './services/claudeService.js';
|
|
15
|
+
import * as sessionService from './services/sessionService.js';
|
|
16
|
+
import { pairFlow, checkBackendReachable, verifyCredentials } from './services/pairingService.js';
|
|
17
|
+
import { WsClient } from './transport/wsClient.js';
|
|
18
|
+
import { createMessageRouter } from './handlers/messageRouter.js';
|
|
19
|
+
function extractEnvFileArg(argv) {
|
|
20
|
+
const cliArgs = [];
|
|
21
|
+
let envFilePath = null;
|
|
22
|
+
for (let idx = 0; idx < argv.length; idx += 1) {
|
|
23
|
+
const arg = argv[idx];
|
|
24
|
+
if (arg === '--env-file') {
|
|
25
|
+
const nextArg = argv[idx + 1];
|
|
26
|
+
if (!nextArg) {
|
|
27
|
+
throw new Error('[kumo-cli] missing value for --env-file');
|
|
28
|
+
}
|
|
29
|
+
envFilePath = nextArg;
|
|
30
|
+
idx += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg.startsWith('--env-file=')) {
|
|
34
|
+
envFilePath = arg.slice('--env-file='.length);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
cliArgs.push(arg);
|
|
38
|
+
}
|
|
39
|
+
return { envFilePath, cliArgs };
|
|
40
|
+
}
|
|
41
|
+
const { envFilePath, cliArgs: bootArgs } = extractEnvFileArg(process.argv.slice(2));
|
|
42
|
+
if (envFilePath) {
|
|
43
|
+
loadDotEnv(envFilePath);
|
|
32
44
|
}
|
|
33
|
-
|
|
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 = [];
|
|
45
|
+
let activeCredentials = null;
|
|
56
46
|
let currentSessionId = null;
|
|
57
|
-
const SNAPSHOT_DIR = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..', 'claude-data-snapshot');
|
|
58
|
-
let snapshotScanner = null;
|
|
59
47
|
let pendingPrompt = null;
|
|
60
|
-
let
|
|
61
|
-
let
|
|
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
|
-
}
|
|
48
|
+
let startScannerAtEnd = false;
|
|
49
|
+
let scanner = null;
|
|
82
50
|
let fileWatcher = null;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function startFileWatcher() {
|
|
51
|
+
let pairedResolved = false;
|
|
52
|
+
let pairedResolve = null;
|
|
53
|
+
function startFileWatcher(send) {
|
|
87
54
|
if (fileWatcher) {
|
|
88
55
|
try {
|
|
89
56
|
fileWatcher.close();
|
|
@@ -92,20 +59,22 @@ function startFileWatcher() {
|
|
|
92
59
|
fileWatcher = null;
|
|
93
60
|
}
|
|
94
61
|
const projectRoot = process.cwd();
|
|
62
|
+
const ignored = loadIgnoredNames(projectRoot);
|
|
63
|
+
const debounceMap = new Map();
|
|
95
64
|
try {
|
|
96
65
|
fileWatcher = fs.watch(projectRoot, { recursive: true }, (event, filename) => {
|
|
97
66
|
if (!filename)
|
|
98
67
|
return;
|
|
99
68
|
const normalized = filename.replace(/\\/g, '/');
|
|
100
69
|
const parts = normalized.split('/');
|
|
101
|
-
if (parts.some(p =>
|
|
70
|
+
if (parts.some((p) => ignored.has(p) || p.startsWith('.')))
|
|
102
71
|
return;
|
|
103
|
-
const prev =
|
|
72
|
+
const prev = debounceMap.get(normalized);
|
|
104
73
|
if (prev)
|
|
105
74
|
clearTimeout(prev);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
75
|
+
debounceMap.set(normalized, setTimeout(() => {
|
|
76
|
+
debounceMap.delete(normalized);
|
|
77
|
+
send({ type: 'file_changed', path: normalized, event });
|
|
109
78
|
log(`[watcher] ${event}: ${normalized}`);
|
|
110
79
|
}, FILE_WATCH_DEBOUNCE_MS));
|
|
111
80
|
});
|
|
@@ -115,1042 +84,124 @@ function startFileWatcher() {
|
|
|
115
84
|
log(`[watcher] failed to start: ${e}`);
|
|
116
85
|
}
|
|
117
86
|
}
|
|
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
87
|
async function main() {
|
|
1097
|
-
claudeArgs =
|
|
88
|
+
let claudeArgs = [...bootArgs];
|
|
1098
89
|
if (claudeArgs[0] === 'reset') {
|
|
1099
90
|
const removed = clearCredentials();
|
|
1100
|
-
|
|
1101
|
-
console.log(`[kumo-cli] credentials cleared at ${credentialsPath()}`);
|
|
1102
|
-
else
|
|
1103
|
-
console.log(`[kumo-cli] no credentials to clear`);
|
|
91
|
+
console.log(removed ? `[kumo-cli] credentials cleared at ${credentialsPath()}` : `[kumo-cli] no credentials to clear`);
|
|
1104
92
|
return;
|
|
1105
93
|
}
|
|
1106
|
-
if (!claudeArgs.includes('--dangerously-skip-permissions'))
|
|
94
|
+
if (!claudeArgs.includes('--dangerously-skip-permissions'))
|
|
1107
95
|
claudeArgs.push('--dangerously-skip-permissions');
|
|
1108
|
-
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
}
|
|
96
|
+
let ws;
|
|
97
|
+
const send = (data) => ws.send(data);
|
|
98
|
+
const model = new ModelService();
|
|
99
|
+
const effort = new EffortService();
|
|
100
|
+
effort.load();
|
|
101
|
+
const terminals = new TerminalManager((terminalId, data) => send({ type: 'terminal_output', terminalId, data }), (terminalId, exitCode) => {
|
|
102
|
+
log(`[terminal] ${terminalId} exited with code ${exitCode}`);
|
|
103
|
+
send({ type: 'terminal_closed', terminalId, exitCode });
|
|
104
|
+
});
|
|
105
|
+
const claude = new ClaudeService(send, model, effort, () => activeCredentials?.deviceToken);
|
|
106
|
+
claude.args = claudeArgs;
|
|
107
|
+
claude.onExit = (code) => { ws.close(); process.exit(code); };
|
|
1114
108
|
const hookServer = await startHookServer((sessionId) => {
|
|
1115
109
|
currentSessionId = sessionId;
|
|
1116
|
-
|
|
1117
|
-
|
|
110
|
+
claude.resetSnapshot();
|
|
111
|
+
send({ type: 'session_start', sessionId });
|
|
1118
112
|
scanner?.stop();
|
|
1119
113
|
scanner = new SessionScanner(sessionId, startScannerAtEnd);
|
|
1120
114
|
startScannerAtEnd = false;
|
|
1121
115
|
scanner.on('message', (msg) => {
|
|
1122
|
-
|
|
116
|
+
send({ type: 'transcript', message: msg });
|
|
1123
117
|
if (msg.role === 'assistant' && msg.stopReason === 'end_turn') {
|
|
1124
118
|
log(`[scanner] assistant_done detected, stopReason=${msg.stopReason}`);
|
|
1125
|
-
|
|
119
|
+
send({ type: 'assistant_done' });
|
|
1126
120
|
}
|
|
1127
121
|
});
|
|
1128
122
|
scanner.start();
|
|
1129
123
|
if (pendingPrompt) {
|
|
1130
124
|
const queued = pendingPrompt;
|
|
1131
125
|
pendingPrompt = null;
|
|
1132
|
-
dispatchPrompt(queued);
|
|
126
|
+
claude.dispatchPrompt(queued);
|
|
1133
127
|
}
|
|
1134
128
|
});
|
|
1135
|
-
process.stdout.on('resize', () =>
|
|
1136
|
-
|
|
1137
|
-
});
|
|
1138
|
-
claudeSettingsPath = generateHookSettingsFile(hookServer.port);
|
|
129
|
+
process.stdout.on('resize', () => claude.resizeSnapshot());
|
|
130
|
+
claude.settingsPath = generateHookSettingsFile(hookServer.port);
|
|
1139
131
|
activeCredentials = loadCredentials();
|
|
132
|
+
if (activeCredentials) {
|
|
133
|
+
if (!await checkBackendReachable()) {
|
|
134
|
+
console.error(`[kumo-cli] cannot reach backend at ${SERVER_HTTP()}. check KUMO_SERVER_URL and that the server is running.`);
|
|
135
|
+
hookServer.close();
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const verified = await verifyCredentials(activeCredentials);
|
|
139
|
+
if (!verified) {
|
|
140
|
+
console.warn('[kumo-cli] cached device was revoked or deleted, clearing credentials and re-pairing...');
|
|
141
|
+
clearCredentials();
|
|
142
|
+
activeCredentials = null;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
activeCredentials = verified;
|
|
146
|
+
saveCredentials(verified);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
1140
149
|
if (!activeCredentials) {
|
|
150
|
+
if (!await checkBackendReachable()) {
|
|
151
|
+
console.error(`[kumo-cli] cannot reach backend at ${SERVER_HTTP()}. check KUMO_SERVER_URL and that the server is running.`);
|
|
152
|
+
hookServer.close();
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
1141
155
|
activeCredentials = await pairFlow();
|
|
1142
156
|
if (!activeCredentials) {
|
|
1143
|
-
console.
|
|
1144
|
-
spawnClaudeNow();
|
|
157
|
+
console.error('[kumo-cli] pairing aborted');
|
|
1145
158
|
hookServer.close();
|
|
1146
|
-
|
|
159
|
+
process.exit(1);
|
|
1147
160
|
}
|
|
1148
161
|
}
|
|
1149
162
|
console.log(`\x1b[32m✓ Connected as ${activeCredentials.user?.email ?? activeCredentials.user?.id ?? 'device'}\x1b[0m\n`);
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
163
|
+
const router = createMessageRouter({
|
|
164
|
+
send,
|
|
165
|
+
claude, model, effort, terminals,
|
|
166
|
+
onPaired: () => {
|
|
167
|
+
if (pairedResolved)
|
|
168
|
+
return;
|
|
169
|
+
pairedResolved = true;
|
|
170
|
+
pairedResolve?.();
|
|
171
|
+
startFileWatcher(send);
|
|
172
|
+
},
|
|
173
|
+
onAuthError: () => { ws.close(); },
|
|
174
|
+
scheduleScannerAtEnd: () => { startScannerAtEnd = true; },
|
|
175
|
+
setCurrentSessionId: (id) => { currentSessionId = id; },
|
|
176
|
+
getCurrentSessionId: () => currentSessionId,
|
|
177
|
+
setPendingPrompt: (text) => { pendingPrompt = text; },
|
|
178
|
+
});
|
|
179
|
+
ws = new WsClient(async () => {
|
|
180
|
+
send({
|
|
181
|
+
type: 'register',
|
|
182
|
+
clientType: 'cli',
|
|
183
|
+
deviceToken: activeCredentials?.deviceToken,
|
|
184
|
+
deviceUuid: activeCredentials?.deviceUuid,
|
|
185
|
+
cwd: process.cwd(),
|
|
186
|
+
});
|
|
187
|
+
claude.resetMenu();
|
|
188
|
+
startFileWatcher(send);
|
|
189
|
+
const sessions = await sessionService.getSessionListForCwd();
|
|
190
|
+
const activeIds = await sessionService.getActiveSessionIds();
|
|
191
|
+
if (currentSessionId)
|
|
192
|
+
activeIds.add(currentSessionId);
|
|
193
|
+
send({ type: 'session_list', sessions: sessions.map((s) => ({ ...s, isActive: activeIds.has(s.sessionId) })) });
|
|
194
|
+
}, router, () => {
|
|
195
|
+
if (fileWatcher) {
|
|
196
|
+
try {
|
|
197
|
+
fileWatcher.close();
|
|
198
|
+
}
|
|
199
|
+
catch { }
|
|
200
|
+
fileWatcher = null;
|
|
201
|
+
}
|
|
1153
202
|
});
|
|
203
|
+
ws.onSent(() => claude.notifySent());
|
|
204
|
+
ws.connect();
|
|
1154
205
|
}
|
|
1155
206
|
main().catch((err) => {
|
|
1156
207
|
console.error('[kumo-cli] fatal error:', err);
|