forkoff 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/LICENSE +17 -0
- package/README.md +173 -0
- package/dist/api.d.ts +44 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +76 -0
- package/dist/api.js.map +1 -0
- package/dist/approval.d.ts +46 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +119 -0
- package/dist/approval.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +209 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +868 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +30 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +84 -0
- package/dist/integration.js.map +1 -0
- package/dist/terminal.d.ts +25 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +171 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tools/claude-hooks.d.ts +97 -0
- package/dist/tools/claude-hooks.d.ts.map +1 -0
- package/dist/tools/claude-hooks.js +348 -0
- package/dist/tools/claude-hooks.js.map +1 -0
- package/dist/tools/claude-process.d.ts +271 -0
- package/dist/tools/claude-process.d.ts.map +1 -0
- package/dist/tools/claude-process.js +931 -0
- package/dist/tools/claude-process.js.map +1 -0
- package/dist/tools/claude-sessions.d.ts +60 -0
- package/dist/tools/claude-sessions.d.ts.map +1 -0
- package/dist/tools/claude-sessions.js +285 -0
- package/dist/tools/claude-sessions.js.map +1 -0
- package/dist/tools/detector.d.ts +64 -0
- package/dist/tools/detector.d.ts.map +1 -0
- package/dist/tools/detector.js +383 -0
- package/dist/tools/detector.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/transcript-streamer.d.ts +68 -0
- package/dist/transcript-streamer.d.ts.map +1 -0
- package/dist/transcript-streamer.js +459 -0
- package/dist/transcript-streamer.js.map +1 -0
- package/dist/websocket.d.ts +133 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +247 -0
- package/dist/websocket.js.map +1 -0
- package/nul +0 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const ora_1 = __importDefault(require("ora"));
|
|
43
|
+
const qrcode_terminal_1 = __importDefault(require("qrcode-terminal"));
|
|
44
|
+
const config_1 = require("./config");
|
|
45
|
+
const api_1 = require("./api");
|
|
46
|
+
const websocket_1 = require("./websocket");
|
|
47
|
+
const terminal_1 = require("./terminal");
|
|
48
|
+
const approval_1 = require("./approval");
|
|
49
|
+
const tools_1 = require("./tools");
|
|
50
|
+
const transcript_streamer_1 = require("./transcript-streamer");
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
const program = new commander_1.Command();
|
|
55
|
+
program
|
|
56
|
+
.name('forkoff')
|
|
57
|
+
.description('CLI tool for ForkOff - Connect your AI coding tools to mobile')
|
|
58
|
+
.version('1.0.0');
|
|
59
|
+
// Configure API/WS URLs
|
|
60
|
+
program
|
|
61
|
+
.command('config')
|
|
62
|
+
.description('Configure ForkOff CLI settings')
|
|
63
|
+
.option('-a, --api <url>', 'Set API URL')
|
|
64
|
+
.option('-w, --ws <url>', 'Set WebSocket URL')
|
|
65
|
+
.option('-n, --name <name>', 'Set device name')
|
|
66
|
+
.option('--show', 'Show current configuration')
|
|
67
|
+
.option('--reset', 'Reset all configuration')
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
if (options.reset) {
|
|
70
|
+
config_1.config.reset();
|
|
71
|
+
console.log(chalk_1.default.green('Configuration reset successfully'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (options.api) {
|
|
75
|
+
config_1.config.apiUrl = options.api;
|
|
76
|
+
console.log(chalk_1.default.green(`API URL set to: ${options.api}`));
|
|
77
|
+
}
|
|
78
|
+
if (options.ws) {
|
|
79
|
+
config_1.config.wsUrl = options.ws;
|
|
80
|
+
console.log(chalk_1.default.green(`WebSocket URL set to: ${options.ws}`));
|
|
81
|
+
}
|
|
82
|
+
if (options.name) {
|
|
83
|
+
config_1.config.deviceName = options.name;
|
|
84
|
+
console.log(chalk_1.default.green(`Device name set to: ${options.name}`));
|
|
85
|
+
}
|
|
86
|
+
if (options.show || (!options.api && !options.ws && !options.name && !options.reset)) {
|
|
87
|
+
console.log(chalk_1.default.bold('\nCurrent Configuration:'));
|
|
88
|
+
console.log(` API URL: ${chalk_1.default.cyan(config_1.config.apiUrl)}`);
|
|
89
|
+
console.log(` WebSocket: ${chalk_1.default.cyan(config_1.config.wsUrl)}`);
|
|
90
|
+
console.log(` Device Name: ${chalk_1.default.cyan(config_1.config.deviceName)}`);
|
|
91
|
+
console.log(` Device ID: ${chalk_1.default.cyan(config_1.config.deviceId || 'Not registered')}`);
|
|
92
|
+
console.log(` Paired: ${config_1.config.isPaired ? chalk_1.default.green('Yes') : chalk_1.default.yellow('No')}`);
|
|
93
|
+
console.log(` Config Path: ${chalk_1.default.dim(config_1.config.getPath())}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Pair device with mobile app
|
|
97
|
+
program
|
|
98
|
+
.command('pair')
|
|
99
|
+
.description('Generate pairing code to connect with mobile app')
|
|
100
|
+
.action(async () => {
|
|
101
|
+
const spinner = (0, ora_1.default)('Connecting to ForkOff server...').start();
|
|
102
|
+
try {
|
|
103
|
+
// Check server health
|
|
104
|
+
const isHealthy = await api_1.api.healthCheck();
|
|
105
|
+
if (!isHealthy) {
|
|
106
|
+
spinner.fail('Cannot connect to ForkOff server');
|
|
107
|
+
console.log(chalk_1.default.yellow(`\nMake sure the server is running at ${config_1.config.apiUrl}`));
|
|
108
|
+
console.log(chalk_1.default.dim('Use "forkoff config --api <url>" to change the server URL'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
spinner.text = 'Registering device...';
|
|
112
|
+
// Register device or refresh pairing code
|
|
113
|
+
let result;
|
|
114
|
+
if (config_1.config.deviceId) {
|
|
115
|
+
try {
|
|
116
|
+
result = await api_1.api.refreshPairingCode(config_1.config.deviceId);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Device might not exist anymore, register fresh
|
|
120
|
+
result = await api_1.api.registerDevice();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
result = await api_1.api.registerDevice();
|
|
125
|
+
}
|
|
126
|
+
// Save device info
|
|
127
|
+
config_1.config.deviceId = result.device.id;
|
|
128
|
+
config_1.config.pairingCode = result.pairingCode;
|
|
129
|
+
spinner.succeed('Device registered successfully!\n');
|
|
130
|
+
// Display pairing info
|
|
131
|
+
console.log(chalk_1.default.bold('Scan this QR code with the ForkOff mobile app:\n'));
|
|
132
|
+
// Generate QR code with pairing URL
|
|
133
|
+
const pairingUrl = `forkoff://pair/${result.pairingCode}`;
|
|
134
|
+
qrcode_terminal_1.default.generate(pairingUrl, { small: true }, (code) => {
|
|
135
|
+
console.log(code);
|
|
136
|
+
});
|
|
137
|
+
console.log(chalk_1.default.bold('\nOr enter this code manually:\n'));
|
|
138
|
+
console.log(chalk_1.default.bgBlue.white.bold(` ${result.pairingCode} `));
|
|
139
|
+
console.log();
|
|
140
|
+
const expiresAt = new Date(result.expiresAt);
|
|
141
|
+
console.log(chalk_1.default.dim(`Code expires at: ${expiresAt.toLocaleTimeString()}`));
|
|
142
|
+
console.log();
|
|
143
|
+
// Wait for pairing
|
|
144
|
+
console.log(chalk_1.default.yellow('Waiting for mobile app to scan...'));
|
|
145
|
+
console.log(chalk_1.default.dim('Press Ctrl+C to cancel\n'));
|
|
146
|
+
await waitForPairing(result.device.id);
|
|
147
|
+
// Auto-connect after successful pairing
|
|
148
|
+
await startConnection();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
spinner.fail('Failed to register device');
|
|
152
|
+
console.error(chalk_1.default.red(error.message || 'Unknown error'));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// Check device status
|
|
156
|
+
program
|
|
157
|
+
.command('status')
|
|
158
|
+
.description('Check device connection status')
|
|
159
|
+
.action(async () => {
|
|
160
|
+
if (!config_1.config.deviceId) {
|
|
161
|
+
console.log(chalk_1.default.yellow('Device not registered. Run "forkoff pair" first.'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const spinner = (0, ora_1.default)('Checking status...').start();
|
|
165
|
+
try {
|
|
166
|
+
const status = await api_1.api.checkPairingStatus(config_1.config.deviceId);
|
|
167
|
+
spinner.stop();
|
|
168
|
+
console.log(chalk_1.default.bold('\nDevice Status:'));
|
|
169
|
+
console.log(` Device ID: ${chalk_1.default.cyan(config_1.config.deviceId)}`);
|
|
170
|
+
console.log(` Device Name: ${chalk_1.default.cyan(config_1.config.deviceName)}`);
|
|
171
|
+
console.log(` Paired: ${status.isPaired ? chalk_1.default.green('Yes') : chalk_1.default.yellow('No')}`);
|
|
172
|
+
if (status.isPaired) {
|
|
173
|
+
config_1.config.userId = status.userId;
|
|
174
|
+
config_1.config.pairedAt = config_1.config.pairedAt || new Date().toISOString();
|
|
175
|
+
console.log(` User ID: ${chalk_1.default.cyan(status.userId)}`);
|
|
176
|
+
}
|
|
177
|
+
if (websocket_1.wsClient.isConnected) {
|
|
178
|
+
console.log(` WebSocket: ${chalk_1.default.green('Connected')}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(` WebSocket: ${chalk_1.default.yellow('Disconnected')}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
spinner.fail('Failed to check status');
|
|
186
|
+
console.error(chalk_1.default.red(error.message || 'Unknown error'));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// Connect and stay online (for returning users who already paired)
|
|
190
|
+
program
|
|
191
|
+
.command('connect')
|
|
192
|
+
.description('Reconnect to ForkOff (for previously paired devices)')
|
|
193
|
+
.action(async () => {
|
|
194
|
+
if (!config_1.config.deviceId) {
|
|
195
|
+
console.log(chalk_1.default.yellow('Device not registered. Run "forkoff pair" first.'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!config_1.config.isPaired) {
|
|
199
|
+
console.log(chalk_1.default.yellow('Device not paired. Run "forkoff pair" and scan the QR code.'));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await startConnection();
|
|
203
|
+
});
|
|
204
|
+
// Disconnect/unpair device
|
|
205
|
+
program
|
|
206
|
+
.command('disconnect')
|
|
207
|
+
.description('Disconnect and unpair device')
|
|
208
|
+
.action(async () => {
|
|
209
|
+
websocket_1.wsClient.disconnect();
|
|
210
|
+
config_1.config.userId = null;
|
|
211
|
+
config_1.config.pairedAt = null;
|
|
212
|
+
config_1.config.pairingCode = null;
|
|
213
|
+
console.log(chalk_1.default.green('Device disconnected and unpaired.'));
|
|
214
|
+
console.log(chalk_1.default.dim('Run "forkoff pair" to pair again.'));
|
|
215
|
+
});
|
|
216
|
+
// Detect and manage AI coding tools
|
|
217
|
+
program
|
|
218
|
+
.command('tools')
|
|
219
|
+
.description('Detect and manage AI coding tools')
|
|
220
|
+
.option('-d, --detect', 'Detect installed AI tools')
|
|
221
|
+
.option('-i, --install-hooks', 'Install ForkOff hooks for Claude Code')
|
|
222
|
+
.option('-u, --uninstall-hooks', 'Remove ForkOff hooks from Claude Code')
|
|
223
|
+
.option('-w, --watch', 'Watch tool status changes')
|
|
224
|
+
.action(async (options) => {
|
|
225
|
+
if (options.installHooks) {
|
|
226
|
+
const spinner = (0, ora_1.default)('Installing Claude Code hooks...').start();
|
|
227
|
+
try {
|
|
228
|
+
if (!tools_1.claudeHooksManager.canConfigure()) {
|
|
229
|
+
spinner.fail('Claude Code not found');
|
|
230
|
+
console.log(chalk_1.default.yellow('\nClaude Code must be installed to use hooks.'));
|
|
231
|
+
console.log(chalk_1.default.dim('Install Claude Code from: https://claude.ai/download'));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await tools_1.claudeHooksManager.installHooks();
|
|
235
|
+
spinner.succeed('Claude Code hooks installed!');
|
|
236
|
+
console.log(chalk_1.default.green('\nForkOff will now receive events from Claude Code.'));
|
|
237
|
+
console.log(chalk_1.default.dim('Run "forkoff connect" to start receiving events.'));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
spinner.fail('Failed to install hooks');
|
|
241
|
+
console.error(chalk_1.default.red(error.message));
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (options.uninstallHooks) {
|
|
246
|
+
const spinner = (0, ora_1.default)('Removing Claude Code hooks...').start();
|
|
247
|
+
try {
|
|
248
|
+
await tools_1.claudeHooksManager.uninstallHooks();
|
|
249
|
+
spinner.succeed('Claude Code hooks removed!');
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
spinner.fail('Failed to remove hooks');
|
|
253
|
+
console.error(chalk_1.default.red(error.message));
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (options.watch) {
|
|
258
|
+
console.log(chalk_1.default.bold('\nWatching for tool status changes...'));
|
|
259
|
+
console.log(chalk_1.default.dim('Press Ctrl+C to stop\n'));
|
|
260
|
+
tools_1.toolDetector.watchToolStatus((tools) => {
|
|
261
|
+
console.log(chalk_1.default.cyan(`[${new Date().toLocaleTimeString()}] Tool status update:`));
|
|
262
|
+
tools.forEach(tool => {
|
|
263
|
+
const statusColor = tool.status === 'running' ? chalk_1.default.green :
|
|
264
|
+
tool.status === 'configured' ? chalk_1.default.yellow : chalk_1.default.dim;
|
|
265
|
+
console.log(` ${tool.name}: ${statusColor(tool.status)}`);
|
|
266
|
+
});
|
|
267
|
+
console.log();
|
|
268
|
+
}, 3000);
|
|
269
|
+
// Keep alive
|
|
270
|
+
await new Promise(() => { });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// Default: detect tools
|
|
274
|
+
const spinner = (0, ora_1.default)('Detecting AI coding tools...').start();
|
|
275
|
+
try {
|
|
276
|
+
const result = await tools_1.toolDetector.detectAll();
|
|
277
|
+
spinner.stop();
|
|
278
|
+
console.log(chalk_1.default.bold('\nDetected AI Coding Tools:\n'));
|
|
279
|
+
if (result.tools.length === 0) {
|
|
280
|
+
console.log(chalk_1.default.yellow(' No AI coding tools detected.'));
|
|
281
|
+
console.log(chalk_1.default.dim('\n Supported tools:'));
|
|
282
|
+
console.log(chalk_1.default.dim(' - Claude Code (https://claude.ai/download)'));
|
|
283
|
+
console.log(chalk_1.default.dim(' - Cursor (https://cursor.sh)'));
|
|
284
|
+
console.log(chalk_1.default.dim(' - GitHub Copilot (VS Code extension)'));
|
|
285
|
+
console.log(chalk_1.default.dim(' - Continue.dev (VS Code extension)'));
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
result.tools.forEach(tool => {
|
|
289
|
+
const statusIcon = tool.status === 'running' ? chalk_1.default.green('●') :
|
|
290
|
+
tool.status === 'configured' ? chalk_1.default.yellow('○') :
|
|
291
|
+
chalk_1.default.dim('○');
|
|
292
|
+
console.log(` ${statusIcon} ${chalk_1.default.bold(tool.name)}`);
|
|
293
|
+
console.log(` Type: ${chalk_1.default.cyan(tool.type)}`);
|
|
294
|
+
if (tool.version) {
|
|
295
|
+
console.log(` Version: ${chalk_1.default.dim(tool.version)}`);
|
|
296
|
+
}
|
|
297
|
+
if (tool.path) {
|
|
298
|
+
console.log(` Path: ${chalk_1.default.dim(tool.path)}`);
|
|
299
|
+
}
|
|
300
|
+
console.log(` Status: ${tool.status === 'running' ? chalk_1.default.green('Running') :
|
|
301
|
+
tool.status === 'configured' ? chalk_1.default.yellow('Configured') :
|
|
302
|
+
chalk_1.default.dim('Detected')}`);
|
|
303
|
+
// Check if hooks are configured for Claude Code
|
|
304
|
+
if (tool.type === 'claude-code') {
|
|
305
|
+
const hooksConfigured = tools_1.claudeHooksManager.isHookConfigured();
|
|
306
|
+
console.log(` Hooks: ${hooksConfigured ? chalk_1.default.green('Installed') : chalk_1.default.yellow('Not installed')}`);
|
|
307
|
+
if (!hooksConfigured) {
|
|
308
|
+
console.log(chalk_1.default.dim(' Run "forkoff tools --install-hooks" to enable'));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
console.log(chalk_1.default.dim(`Platform: ${result.platform}`));
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
spinner.fail('Tool detection failed');
|
|
318
|
+
console.error(chalk_1.default.red(error.message));
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
// Helper function to start connection and set up event handlers
|
|
322
|
+
async function startConnection() {
|
|
323
|
+
const spinner = (0, ora_1.default)('Connecting to ForkOff...').start();
|
|
324
|
+
try {
|
|
325
|
+
await websocket_1.wsClient.connect();
|
|
326
|
+
spinner.succeed('Connected to ForkOff!\n');
|
|
327
|
+
// Detect and report connected tools
|
|
328
|
+
spinner.start('Detecting AI coding tools...');
|
|
329
|
+
try {
|
|
330
|
+
const toolResult = await tools_1.toolDetector.detectAll();
|
|
331
|
+
if (toolResult.tools.length > 0) {
|
|
332
|
+
const toolsToReport = toolResult.tools.map(tool => ({
|
|
333
|
+
type: tool.type,
|
|
334
|
+
name: tool.name,
|
|
335
|
+
version: tool.version || null,
|
|
336
|
+
}));
|
|
337
|
+
await api_1.api.reportConnectedTools(config_1.config.deviceId, toolsToReport);
|
|
338
|
+
spinner.succeed(`Detected ${toolResult.tools.length} AI tool(s): ${toolResult.tools.map(t => t.name).join(', ')}`);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
spinner.info('No AI coding tools detected');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (toolError) {
|
|
345
|
+
spinner.warn('Tool detection skipped: ' + (toolError.message || 'unknown error'));
|
|
346
|
+
}
|
|
347
|
+
console.log();
|
|
348
|
+
console.log(chalk_1.default.green('Device is now online and ready to receive commands.'));
|
|
349
|
+
console.log(chalk_1.default.dim('Press Ctrl+C to disconnect\n'));
|
|
350
|
+
// Set up terminal output forwarding
|
|
351
|
+
terminal_1.terminalManager.on('output', (data) => {
|
|
352
|
+
websocket_1.wsClient.sendTerminalOutput(data);
|
|
353
|
+
});
|
|
354
|
+
terminal_1.terminalManager.on('cwd_changed', (data) => {
|
|
355
|
+
websocket_1.wsClient.sendTerminalCwd(data);
|
|
356
|
+
});
|
|
357
|
+
// When a session is auto-created (command received before terminal_create), send the cwd
|
|
358
|
+
terminal_1.terminalManager.on('session_created', (data) => {
|
|
359
|
+
console.log(chalk_1.default.dim(`[Terminal] Session auto-created: ${data.terminalSessionId} at ${data.cwd}`));
|
|
360
|
+
websocket_1.wsClient.sendTerminalCwd({
|
|
361
|
+
terminalSessionId: data.terminalSessionId,
|
|
362
|
+
cwd: data.cwd,
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
// Set up terminal create handler
|
|
366
|
+
websocket_1.wsClient.on('terminal_create', (data) => {
|
|
367
|
+
console.log(chalk_1.default.blue(`[Terminal] Creating session: ${data.terminalSessionId}`));
|
|
368
|
+
// Resolve the cwd (~ to home directory)
|
|
369
|
+
let resolvedCwd = data.cwd || process.cwd();
|
|
370
|
+
if (resolvedCwd === '~' || resolvedCwd.startsWith('~/')) {
|
|
371
|
+
const homedir = require('os').homedir();
|
|
372
|
+
resolvedCwd = resolvedCwd === '~' ? homedir : resolvedCwd.replace('~', homedir);
|
|
373
|
+
}
|
|
374
|
+
// Create the session
|
|
375
|
+
const session = terminal_1.terminalManager.createSession(data.terminalSessionId, resolvedCwd);
|
|
376
|
+
// Send back the resolved cwd
|
|
377
|
+
websocket_1.wsClient.sendTerminalCwd({
|
|
378
|
+
terminalSessionId: data.terminalSessionId,
|
|
379
|
+
cwd: session.cwd,
|
|
380
|
+
});
|
|
381
|
+
console.log(chalk_1.default.dim(`[Terminal] Session created with cwd: ${session.cwd}`));
|
|
382
|
+
});
|
|
383
|
+
// Set up event handlers
|
|
384
|
+
websocket_1.wsClient.on('terminal_command', async (data) => {
|
|
385
|
+
// Check if this is a Claude terminal session
|
|
386
|
+
if (tools_1.claudeProcessManager.isClaudeSession(data.terminalSessionId)) {
|
|
387
|
+
// SECURITY: Don't log command content - may contain sensitive data
|
|
388
|
+
console.log(chalk_1.default.cyan(`[Claude] Input received (${data.command.length} chars)`));
|
|
389
|
+
await tools_1.claudeProcessManager.sendInput(data.terminalSessionId, data.command);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Regular terminal command
|
|
393
|
+
// SECURITY: Don't log command content - may contain passwords, API keys, etc.
|
|
394
|
+
console.log(chalk_1.default.blue(`[Terminal] Executing command (${data.command.length} chars)`));
|
|
395
|
+
try {
|
|
396
|
+
const result = await terminal_1.terminalManager.executeCommand(data.terminalSessionId, data.command);
|
|
397
|
+
console.log(chalk_1.default.dim(`[Terminal] Exit code: ${result.exitCode}`));
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
console.error(chalk_1.default.red(`[Terminal] Error: ${error.message}`));
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
websocket_1.wsClient.on('approval_response', (data) => {
|
|
404
|
+
console.log(chalk_1.default.blue(`[Approval] ${data.status}: ${data.approvalId}`));
|
|
405
|
+
approval_1.approvalManager.handleApprovalResponse(data.approvalId, data.status);
|
|
406
|
+
});
|
|
407
|
+
// Set up Claude session detection
|
|
408
|
+
if (tools_1.claudeSessionDetector.isClaudeInstalled()) {
|
|
409
|
+
console.log(chalk_1.default.cyan('[Claude] Scanning for Claude sessions...'));
|
|
410
|
+
// Attach event listeners BEFORE starting to watch (so we catch initial events)
|
|
411
|
+
tools_1.claudeSessionDetector.on('session_detected', (session) => {
|
|
412
|
+
console.log(chalk_1.default.cyan(`[Claude] New session detected: ${session.directory}`));
|
|
413
|
+
websocket_1.wsClient.sendClaudeSessionUpdate(session);
|
|
414
|
+
});
|
|
415
|
+
tools_1.claudeSessionDetector.on('session_changed', (session) => {
|
|
416
|
+
console.log(chalk_1.default.dim(`[Claude] Session updated: ${session.directory} (${session.state})`));
|
|
417
|
+
websocket_1.wsClient.sendClaudeSessionUpdate(session);
|
|
418
|
+
});
|
|
419
|
+
tools_1.claudeSessionDetector.on('claude_running_changed', (isRunning) => {
|
|
420
|
+
console.log(chalk_1.default.cyan(`[Claude] Claude is now ${isRunning ? 'ACTIVE' : 'inactive'}`));
|
|
421
|
+
websocket_1.wsClient.sendToolStatusUpdate('claude_code', isRunning ? 'active' : 'inactive');
|
|
422
|
+
});
|
|
423
|
+
// Scan and report existing sessions
|
|
424
|
+
const sessions = tools_1.claudeSessionDetector.scanSessions();
|
|
425
|
+
if (sessions.length > 0) {
|
|
426
|
+
console.log(chalk_1.default.cyan(`[Claude] Found ${sessions.length} session(s)`));
|
|
427
|
+
// Update session states based on file modification time before sending
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
let hasActiveSession = false;
|
|
430
|
+
for (const session of sessions) {
|
|
431
|
+
const sessionTime = new Date(session.lastUsedAt).getTime();
|
|
432
|
+
if (now - sessionTime < 60000) {
|
|
433
|
+
session.state = 'active';
|
|
434
|
+
session.lastUsedAt = new Date().toISOString(); // Update to NOW for active sessions
|
|
435
|
+
hasActiveSession = true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
websocket_1.wsClient.sendClaudeSessions(sessions);
|
|
439
|
+
if (hasActiveSession) {
|
|
440
|
+
console.log(chalk_1.default.cyan(`[Claude] Claude is now ACTIVE`));
|
|
441
|
+
websocket_1.wsClient.sendToolStatusUpdate('claude_code', 'active');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Start watching for session changes
|
|
445
|
+
tools_1.claudeSessionDetector.startWatching(5000);
|
|
446
|
+
}
|
|
447
|
+
// Log approval events
|
|
448
|
+
approval_1.approvalManager.on('approved', (approval) => {
|
|
449
|
+
console.log(chalk_1.default.green(`[Approval] Approved: ${approval.description}`));
|
|
450
|
+
});
|
|
451
|
+
approval_1.approvalManager.on('rejected', (approval) => {
|
|
452
|
+
console.log(chalk_1.default.red(`[Approval] Rejected: ${approval.description}`));
|
|
453
|
+
});
|
|
454
|
+
websocket_1.wsClient.on('git_clone', async (data) => {
|
|
455
|
+
console.log(chalk_1.default.blue(`[Git] Clone request: ${data.repo.fullName}`));
|
|
456
|
+
try {
|
|
457
|
+
const result = await terminal_1.terminalManager.executeCommand(`git-clone-${Date.now()}`, data.command);
|
|
458
|
+
console.log(chalk_1.default.green(`[Git] Clone completed with exit code: ${result.exitCode}`));
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.error(chalk_1.default.red(`[Git] Clone failed: ${error.message}`));
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
// Handle Claude start session request from mobile
|
|
465
|
+
websocket_1.wsClient.on('claude_start_session', async (data) => {
|
|
466
|
+
console.log(chalk_1.default.cyan(`[Claude] Start session request: ${data.directory}`));
|
|
467
|
+
try {
|
|
468
|
+
const result = await tools_1.claudeProcessManager.startSession(data.directory, data.terminalSessionId);
|
|
469
|
+
websocket_1.wsClient.sendToolStatusUpdate('claude_code', 'active');
|
|
470
|
+
websocket_1.wsClient.sendTerminalCwd({ terminalSessionId: data.terminalSessionId, cwd: result.cwd });
|
|
471
|
+
console.log(chalk_1.default.green(`[Claude] Session started: ${data.terminalSessionId}`));
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
console.error(chalk_1.default.red(`[Claude] Failed to start: ${error.message}`));
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
// Handle Claude resume session request from mobile
|
|
478
|
+
// NOTE: This is called when mobile opens a session view - we DON'T spawn Claude here
|
|
479
|
+
// Claude is only spawned when the user actually sends a message (via user_message event)
|
|
480
|
+
// This prevents duplicate transcript entries from double spawns
|
|
481
|
+
websocket_1.wsClient.on('claude_resume_session', async (data) => {
|
|
482
|
+
console.log(chalk_1.default.cyan(`[Claude] Resume session request: ${data.sessionKey} in ${data.directory}`));
|
|
483
|
+
// Resolve the directory path
|
|
484
|
+
let resolvedDir = data.directory;
|
|
485
|
+
if (resolvedDir === '~' || resolvedDir.startsWith('~/')) {
|
|
486
|
+
resolvedDir = resolvedDir === '~' ? os.homedir() : resolvedDir.replace('~', os.homedir());
|
|
487
|
+
}
|
|
488
|
+
resolvedDir = path.resolve(resolvedDir);
|
|
489
|
+
// Register session info for later use when message is sent (don't spawn yet)
|
|
490
|
+
tools_1.claudeProcessManager.registerSession(data.sessionKey, resolvedDir, data.terminalSessionId);
|
|
491
|
+
websocket_1.wsClient.sendToolStatusUpdate('claude_code', 'active');
|
|
492
|
+
websocket_1.wsClient.sendClaudeSessionUpdate({
|
|
493
|
+
sessionKey: data.sessionKey,
|
|
494
|
+
directory: data.directory,
|
|
495
|
+
state: 'active',
|
|
496
|
+
lastUsedAt: new Date().toISOString(),
|
|
497
|
+
});
|
|
498
|
+
websocket_1.wsClient.sendTerminalCwd({ terminalSessionId: data.terminalSessionId, cwd: resolvedDir });
|
|
499
|
+
console.log(chalk_1.default.green(`[Claude] Session ready (will spawn on first message): ${data.sessionKey}`));
|
|
500
|
+
});
|
|
501
|
+
// Handle directory listing requests
|
|
502
|
+
websocket_1.wsClient.on('directory_list', async (data) => {
|
|
503
|
+
console.log(chalk_1.default.dim(`[Dir] Listing request received`));
|
|
504
|
+
try {
|
|
505
|
+
let resolvedPath = data.path;
|
|
506
|
+
if (resolvedPath === '~' || resolvedPath.startsWith('~/')) {
|
|
507
|
+
resolvedPath = resolvedPath === '~' ? os.homedir() : resolvedPath.replace('~', os.homedir());
|
|
508
|
+
}
|
|
509
|
+
// SECURITY: Normalize and validate path to prevent traversal attacks
|
|
510
|
+
resolvedPath = path.resolve(resolvedPath);
|
|
511
|
+
const homeDir = os.homedir();
|
|
512
|
+
// SECURITY: Only allow access to directories under home directory
|
|
513
|
+
// This prevents accessing sensitive system files like /etc/passwd
|
|
514
|
+
if (!resolvedPath.startsWith(homeDir)) {
|
|
515
|
+
console.warn(chalk_1.default.yellow(`[Dir] Access denied - path outside home directory: ${resolvedPath}`));
|
|
516
|
+
websocket_1.wsClient.sendDirectoryListResponse({ requestId: data.requestId, entries: [], currentPath: data.path });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const entries = fs.readdirSync(resolvedPath, { withFileTypes: true })
|
|
520
|
+
.filter(entry => !entry.name.startsWith('.'))
|
|
521
|
+
.map(entry => ({
|
|
522
|
+
name: entry.name,
|
|
523
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
524
|
+
path: path.join(resolvedPath, entry.name),
|
|
525
|
+
}))
|
|
526
|
+
.sort((a, b) => {
|
|
527
|
+
if (a.type !== b.type)
|
|
528
|
+
return a.type === 'directory' ? -1 : 1;
|
|
529
|
+
return a.name.localeCompare(b.name);
|
|
530
|
+
});
|
|
531
|
+
websocket_1.wsClient.sendDirectoryListResponse({ requestId: data.requestId, entries, currentPath: resolvedPath });
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
console.error(chalk_1.default.red(`[Dir] Error: ${error.message}`));
|
|
535
|
+
websocket_1.wsClient.sendDirectoryListResponse({ requestId: data.requestId, entries: [], currentPath: data.path });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
// Handle transcript fetch requests from mobile
|
|
539
|
+
websocket_1.wsClient.on('transcript_fetch', async (data) => {
|
|
540
|
+
console.log(chalk_1.default.dim(`[Transcript] Fetching: ${data.sessionKey}, offset: ${data.offset}, limit: ${data.limit}, reverse: ${data.reverse}`));
|
|
541
|
+
try {
|
|
542
|
+
const result = await transcript_streamer_1.transcriptStreamer.fetchHistory(data.transcriptPath, data.offset || 0, data.limit || 100, data.reverse !== false // Default to true (most recent first)
|
|
543
|
+
);
|
|
544
|
+
websocket_1.wsClient.sendTranscriptHistory({
|
|
545
|
+
sessionKey: data.sessionKey,
|
|
546
|
+
...result,
|
|
547
|
+
offset: data.offset || 0,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
console.error(chalk_1.default.red(`[Transcript] Error: ${error.message}`));
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
// Handle transcript subscribe
|
|
555
|
+
websocket_1.wsClient.on('transcript_subscribe', (data) => {
|
|
556
|
+
console.log(chalk_1.default.dim(`[Transcript] Subscribing: ${data.sessionKey}`));
|
|
557
|
+
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(data.sessionKey, data.transcriptPath);
|
|
558
|
+
});
|
|
559
|
+
// Handle transcript unsubscribe
|
|
560
|
+
websocket_1.wsClient.on('transcript_unsubscribe', (data) => {
|
|
561
|
+
console.log(chalk_1.default.dim(`[Transcript] Unsubscribing: ${data.sessionKey}`));
|
|
562
|
+
transcript_streamer_1.transcriptStreamer.unsubscribeFromUpdates(data.sessionKey);
|
|
563
|
+
});
|
|
564
|
+
// Handle SDK subscribe start - mobile wants live updates for a session
|
|
565
|
+
// This is sent by API when mobile uses transcript_subscribe_sdk
|
|
566
|
+
websocket_1.wsClient.on('transcript_subscribe_sdk_start', async (data) => {
|
|
567
|
+
console.log(chalk_1.default.cyan(`[Transcript] SDK subscribe start: ${data.sessionKey}`));
|
|
568
|
+
// Find the transcript file for this session
|
|
569
|
+
const sessions = tools_1.claudeSessionDetector.scanSessions();
|
|
570
|
+
const session = sessions.find(s => s.sessionKey === data.sessionKey);
|
|
571
|
+
if (session?.transcriptPath) {
|
|
572
|
+
console.log(chalk_1.default.dim(`[Transcript] Starting watch for: ${session.transcriptPath}`));
|
|
573
|
+
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(data.sessionKey, session.transcriptPath);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
console.log(chalk_1.default.yellow(`[Transcript] No transcript found for session: ${data.sessionKey}`));
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// Handle claude sessions request - mobile app wants current sessions
|
|
580
|
+
websocket_1.wsClient.on('claude_sessions_request', () => {
|
|
581
|
+
console.log(chalk_1.default.cyan(`[Claude] Sessions requested by mobile`));
|
|
582
|
+
if (tools_1.claudeSessionDetector.isClaudeInstalled()) {
|
|
583
|
+
const sessions = tools_1.claudeSessionDetector.scanSessions();
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
let hasActiveSession = false;
|
|
586
|
+
// Update session states based on file modification time
|
|
587
|
+
for (const session of sessions) {
|
|
588
|
+
const sessionTime = new Date(session.lastUsedAt).getTime();
|
|
589
|
+
if (now - sessionTime < 60000) {
|
|
590
|
+
session.state = 'active';
|
|
591
|
+
session.lastUsedAt = new Date().toISOString(); // Update to NOW for active sessions
|
|
592
|
+
hasActiveSession = true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// Send sessions
|
|
596
|
+
if (sessions.length > 0) {
|
|
597
|
+
websocket_1.wsClient.sendClaudeSessions(sessions);
|
|
598
|
+
}
|
|
599
|
+
// Send tool status
|
|
600
|
+
websocket_1.wsClient.sendToolStatusUpdate('claude_code', hasActiveSession ? 'active' : 'inactive');
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
// Handle RPC requests from the API gateway
|
|
604
|
+
websocket_1.wsClient.on('rpc_request', async (data) => {
|
|
605
|
+
console.log(chalk_1.default.cyan(`[RPC] Request: ${data.method}, requestId: ${data.requestId}`));
|
|
606
|
+
try {
|
|
607
|
+
if (data.method === 'get_session_history') {
|
|
608
|
+
const { claudeSessionId, sessionKey, limit = 400, offset = 0 } = data.params;
|
|
609
|
+
console.log(chalk_1.default.dim(`[RPC] get_session_history request received`));
|
|
610
|
+
// Find the transcript file
|
|
611
|
+
let transcriptPath;
|
|
612
|
+
// If claudeSessionId is provided, search for the JSONL file directly
|
|
613
|
+
if (claudeSessionId) {
|
|
614
|
+
// SECURITY: Validate claudeSessionId to prevent path traversal
|
|
615
|
+
// Session IDs should be alphanumeric with hyphens/underscores only
|
|
616
|
+
const sessionIdRegex = /^[a-zA-Z0-9_-]+$/;
|
|
617
|
+
if (!sessionIdRegex.test(claudeSessionId)) {
|
|
618
|
+
console.warn(chalk_1.default.yellow(`[RPC] Invalid claudeSessionId format - rejected`));
|
|
619
|
+
websocket_1.wsClient.sendRpcResponse({
|
|
620
|
+
requestId: data.requestId,
|
|
621
|
+
error: { code: -32602, message: 'Invalid session ID format' }
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
626
|
+
if (fs.existsSync(claudeProjectsDir)) {
|
|
627
|
+
const projectDirs = fs.readdirSync(claudeProjectsDir);
|
|
628
|
+
for (const projectDir of projectDirs) {
|
|
629
|
+
// SECURITY: Validate projectDir as well to prevent traversal
|
|
630
|
+
if (!sessionIdRegex.test(projectDir) && !/^[a-zA-Z0-9_.-]+$/.test(projectDir)) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const potentialPath = path.join(claudeProjectsDir, projectDir, `${claudeSessionId}.jsonl`);
|
|
634
|
+
// SECURITY: Verify the resolved path is still under claudeProjectsDir
|
|
635
|
+
const resolvedPotentialPath = path.resolve(potentialPath);
|
|
636
|
+
if (!resolvedPotentialPath.startsWith(claudeProjectsDir)) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (fs.existsSync(potentialPath)) {
|
|
640
|
+
transcriptPath = potentialPath;
|
|
641
|
+
console.log(chalk_1.default.dim(`[RPC] Found JSONL transcript`));
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// If not found by claudeSessionId, search all sessions
|
|
648
|
+
if (!transcriptPath && tools_1.claudeSessionDetector.isClaudeInstalled()) {
|
|
649
|
+
const sessions = tools_1.claudeSessionDetector.scanSessions();
|
|
650
|
+
// First try to match by sessionKey
|
|
651
|
+
let session = sessions.find(s => s.sessionKey === sessionKey);
|
|
652
|
+
// If no match by sessionKey, try to find most recent session for the same directory
|
|
653
|
+
if (!session && sessions.length > 0) {
|
|
654
|
+
// Just use the most recent session as fallback
|
|
655
|
+
session = sessions[0];
|
|
656
|
+
console.log(chalk_1.default.dim(`[RPC] Using most recent session as fallback: ${session.sessionKey}`));
|
|
657
|
+
}
|
|
658
|
+
if (session?.transcriptPath) {
|
|
659
|
+
transcriptPath = session.transcriptPath;
|
|
660
|
+
console.log(chalk_1.default.dim(`[RPC] Found transcript path: ${transcriptPath}`));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
664
|
+
console.log(chalk_1.default.yellow(`[RPC] No transcript file found for session`));
|
|
665
|
+
websocket_1.wsClient.sendRpcResponse({
|
|
666
|
+
requestId: data.requestId,
|
|
667
|
+
result: { entries: [], totalEntries: 0, hasMore: false }
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
// Read the transcript file
|
|
672
|
+
const result = await transcript_streamer_1.transcriptStreamer.fetchHistory(transcriptPath, offset, limit, true);
|
|
673
|
+
console.log(chalk_1.default.green(`[RPC] Loaded ${result.entries.length} entries from transcript`));
|
|
674
|
+
// IMPORTANT: Start watching this transcript for live updates
|
|
675
|
+
// This is needed because SDK mode doesn't send transcript_subscribe
|
|
676
|
+
// Use sessionKey from mobile for updates (must match what mobile is listening for)
|
|
677
|
+
const updateSessionKey = sessionKey;
|
|
678
|
+
if (!updateSessionKey) {
|
|
679
|
+
console.log(chalk_1.default.yellow(`[RPC] No sessionKey provided, using claudeSessionId - updates may not route correctly`));
|
|
680
|
+
}
|
|
681
|
+
const watchKey = updateSessionKey || claudeSessionId || data.requestId;
|
|
682
|
+
console.log(chalk_1.default.cyan(`[RPC] Starting file watch for live updates: ${watchKey}`));
|
|
683
|
+
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(watchKey, transcriptPath);
|
|
684
|
+
websocket_1.wsClient.sendRpcResponse({
|
|
685
|
+
requestId: data.requestId,
|
|
686
|
+
result: {
|
|
687
|
+
entries: result.entries,
|
|
688
|
+
totalEntries: result.totalEntries,
|
|
689
|
+
hasMore: result.hasMore,
|
|
690
|
+
sessionKey: watchKey, // Tell mobile which sessionKey to listen for updates
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
console.log(chalk_1.default.yellow(`[RPC] Unknown method: ${data.method}`));
|
|
696
|
+
websocket_1.wsClient.sendRpcResponse({
|
|
697
|
+
requestId: data.requestId,
|
|
698
|
+
error: { code: -32601, message: `Method not found: ${data.method}` }
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
console.error(chalk_1.default.red(`[RPC] Error handling ${data.method}:`, error.message));
|
|
704
|
+
websocket_1.wsClient.sendRpcResponse({
|
|
705
|
+
requestId: data.requestId,
|
|
706
|
+
error: { code: -32603, message: error.message || 'Internal error' }
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
// Forward live transcript updates to WebSocket
|
|
711
|
+
transcript_streamer_1.transcriptStreamer.on('update', (data) => {
|
|
712
|
+
console.log(chalk_1.default.green(`[Transcript] Sending update for ${data.sessionKey}: ${data.entry?.type}`));
|
|
713
|
+
websocket_1.wsClient.sendTranscriptUpdate(data);
|
|
714
|
+
// Also update session lastUsedAt to keep it fresh
|
|
715
|
+
// Find the session to get the directory
|
|
716
|
+
const sessions = tools_1.claudeSessionDetector.getSessions();
|
|
717
|
+
const session = sessions.find(s => s.sessionKey === data.sessionKey);
|
|
718
|
+
if (session) {
|
|
719
|
+
websocket_1.wsClient.sendClaudeSessionUpdate({
|
|
720
|
+
sessionKey: data.sessionKey,
|
|
721
|
+
directory: session.directory,
|
|
722
|
+
state: 'active',
|
|
723
|
+
lastUsedAt: new Date().toISOString(),
|
|
724
|
+
transcriptPath: session.transcriptPath,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
// Forward Claude process output to WebSocket
|
|
729
|
+
tools_1.claudeProcessManager.on('output', (data) => {
|
|
730
|
+
websocket_1.wsClient.sendTerminalOutput(data);
|
|
731
|
+
});
|
|
732
|
+
// Forward Claude approval requests to WebSocket (mobile approval)
|
|
733
|
+
tools_1.claudeProcessManager.on('claude_approval_request', (data) => {
|
|
734
|
+
console.log(chalk_1.default.yellow(`[Claude] Approval request: ${data.approvalId}`));
|
|
735
|
+
websocket_1.wsClient.sendClaudeApprovalRequest(data);
|
|
736
|
+
});
|
|
737
|
+
// Handle Claude approval responses from mobile
|
|
738
|
+
websocket_1.wsClient.on('claude_approval_response', (data) => {
|
|
739
|
+
console.log(chalk_1.default.green(`[Claude] Approval response: ${data.approvalId} -> ${data.response}`));
|
|
740
|
+
tools_1.claudeProcessManager.handleApprovalResponse(data.approvalId, data.response);
|
|
741
|
+
});
|
|
742
|
+
// Handle user messages from mobile app (send to Claude session)
|
|
743
|
+
websocket_1.wsClient.on('user_message', async (data) => {
|
|
744
|
+
// SECURITY: Don't log message content - may contain sensitive prompts
|
|
745
|
+
console.log(chalk_1.default.cyan(`[Claude] User message received (${data.message.length} chars)`));
|
|
746
|
+
// The session should have been registered via claude_resume_session
|
|
747
|
+
// sendInput will spawn the process if needed using the registered session info
|
|
748
|
+
const terminalSessionId = data.sessionKey; // Use sessionKey as terminalSessionId
|
|
749
|
+
if (!terminalSessionId) {
|
|
750
|
+
console.log(chalk_1.default.yellow(`[Claude] No sessionKey provided in user_message`));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
// Check if this session is registered (either active or registered for later spawn)
|
|
754
|
+
if (!tools_1.claudeProcessManager.isClaudeSession(terminalSessionId)) {
|
|
755
|
+
console.log(chalk_1.default.yellow(`[Claude] Session not registered: ${terminalSessionId}`));
|
|
756
|
+
console.log(chalk_1.default.dim(`[Claude] Hint: Mobile should send claude_resume_session first`));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
console.log(chalk_1.default.dim(`[Claude] Sending to session: ${terminalSessionId}`));
|
|
760
|
+
const sent = await tools_1.claudeProcessManager.sendInput(terminalSessionId, data.message + '\n');
|
|
761
|
+
if (!sent) {
|
|
762
|
+
console.log(chalk_1.default.yellow(`[Claude] Failed to send message - session may need restart`));
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
// Handle Claude process end
|
|
766
|
+
tools_1.claudeProcessManager.on('session_ended', (data) => {
|
|
767
|
+
console.log(chalk_1.default.dim(`[Claude] Session ended: ${data.terminalSessionId}`));
|
|
768
|
+
websocket_1.wsClient.sendToolStatusUpdate('claude_code', 'inactive');
|
|
769
|
+
if (data.sessionKey) {
|
|
770
|
+
websocket_1.wsClient.sendClaudeSessionUpdate({
|
|
771
|
+
sessionKey: data.sessionKey,
|
|
772
|
+
directory: data.directory,
|
|
773
|
+
state: 'inactive',
|
|
774
|
+
lastUsedAt: new Date().toISOString(),
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
// Forward thinking content to mobile
|
|
779
|
+
tools_1.claudeProcessManager.on('thinking_content', (data) => {
|
|
780
|
+
if (data.content || !data.partial) {
|
|
781
|
+
console.log(chalk_1.default.magenta(`[Claude] Thinking${data.partial ? ' (streaming)' : ' (complete)'}: ${data.content?.substring(0, 50) || '...'}`));
|
|
782
|
+
}
|
|
783
|
+
websocket_1.wsClient.sendThinkingContent({
|
|
784
|
+
sessionKey: data.sessionKey,
|
|
785
|
+
thinkingId: data.thinkingId,
|
|
786
|
+
content: data.content,
|
|
787
|
+
partial: data.partial,
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
// Forward token usage to mobile
|
|
791
|
+
tools_1.claudeProcessManager.on('token_usage', (data) => {
|
|
792
|
+
console.log(chalk_1.default.blue(`[Claude] Tokens: ${data.usage.inputTokens} in / ${data.usage.outputTokens} out`));
|
|
793
|
+
websocket_1.wsClient.sendTokenUsage({
|
|
794
|
+
sessionKey: data.sessionKey,
|
|
795
|
+
usage: data.usage,
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
// Forward task progress to mobile
|
|
799
|
+
tools_1.claudeProcessManager.on('task_progress', (data) => {
|
|
800
|
+
if (data.type === 'list') {
|
|
801
|
+
console.log(chalk_1.default.cyan(`[Claude] Task list: ${data.tasks?.length || 0} tasks`));
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
console.log(chalk_1.default.cyan(`[Claude] Task ${data.type}: ${data.task?.subject || data.task?.id}`));
|
|
805
|
+
}
|
|
806
|
+
websocket_1.wsClient.sendTaskProgress({
|
|
807
|
+
sessionKey: data.sessionKey,
|
|
808
|
+
type: data.type,
|
|
809
|
+
task: data.task,
|
|
810
|
+
tasks: data.tasks,
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
websocket_1.wsClient.on('disconnected', (reason) => {
|
|
814
|
+
console.log(chalk_1.default.yellow(`\nDisconnected: ${reason}`));
|
|
815
|
+
if (reason !== 'io client disconnect') {
|
|
816
|
+
console.log(chalk_1.default.dim('Attempting to reconnect...'));
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
websocket_1.wsClient.on('error', (error) => {
|
|
820
|
+
console.error(chalk_1.default.red(`Connection error: ${error.message}`));
|
|
821
|
+
});
|
|
822
|
+
// Keep the process running
|
|
823
|
+
process.on('SIGINT', () => {
|
|
824
|
+
console.log(chalk_1.default.yellow('\nDisconnecting...'));
|
|
825
|
+
tools_1.claudeSessionDetector.stopWatching();
|
|
826
|
+
transcript_streamer_1.transcriptStreamer.cleanup();
|
|
827
|
+
websocket_1.wsClient.disconnect();
|
|
828
|
+
process.exit(0);
|
|
829
|
+
});
|
|
830
|
+
// Keep alive
|
|
831
|
+
await new Promise(() => { });
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
spinner.fail('Failed to connect');
|
|
835
|
+
console.error(chalk_1.default.red(error.message || 'Unknown error'));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Helper function to wait for pairing
|
|
839
|
+
async function waitForPairing(deviceId) {
|
|
840
|
+
return new Promise((resolve, reject) => {
|
|
841
|
+
const checkInterval = setInterval(async () => {
|
|
842
|
+
try {
|
|
843
|
+
const status = await api_1.api.checkPairingStatus(deviceId);
|
|
844
|
+
if (status.isPaired) {
|
|
845
|
+
clearInterval(checkInterval);
|
|
846
|
+
config_1.config.userId = status.userId;
|
|
847
|
+
config_1.config.pairedAt = new Date().toISOString();
|
|
848
|
+
console.log(chalk_1.default.green('\n✓ Device paired successfully!'));
|
|
849
|
+
console.log(chalk_1.default.dim('\nConnecting to receive commands...\n'));
|
|
850
|
+
// Auto-connect after successful pairing
|
|
851
|
+
resolve();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
// Continue waiting
|
|
856
|
+
}
|
|
857
|
+
}, 2000);
|
|
858
|
+
// Handle Ctrl+C
|
|
859
|
+
process.on('SIGINT', () => {
|
|
860
|
+
clearInterval(checkInterval);
|
|
861
|
+
console.log(chalk_1.default.yellow('\nPairing cancelled.'));
|
|
862
|
+
process.exit(0);
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
// Run the CLI
|
|
867
|
+
program.parse();
|
|
868
|
+
//# sourceMappingURL=index.js.map
|