forkoff 1.0.17 → 1.0.19
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 +11 -7
- package/README.md +77 -118
- package/dist/approval.d.ts +1 -0
- package/dist/approval.js +9 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +62 -16
- package/dist/crypto/e2eeManager.d.ts +49 -52
- package/dist/crypto/e2eeManager.js +256 -181
- package/dist/crypto/encryption.d.ts +8 -10
- package/dist/crypto/encryption.js +29 -94
- package/dist/crypto/index.d.ts +10 -0
- package/dist/crypto/index.js +22 -0
- package/dist/crypto/keyExchange.d.ts +6 -20
- package/dist/crypto/keyExchange.js +18 -110
- package/dist/crypto/keyGeneration.d.ts +2 -13
- package/dist/crypto/keyGeneration.js +14 -88
- package/dist/crypto/keyStorage.d.ts +32 -5
- package/dist/crypto/keyStorage.js +152 -8
- package/dist/crypto/sessionPersistence.d.ts +7 -13
- package/dist/crypto/sessionPersistence.js +108 -33
- package/dist/crypto/types.d.ts +24 -3
- package/dist/crypto/types.js +2 -1
- package/dist/crypto/websocketE2EE.d.ts +6 -17
- package/dist/crypto/websocketE2EE.js +21 -38
- package/dist/index.js +203 -280
- package/dist/integration.d.ts +0 -1
- package/dist/integration.js +2 -4
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +209 -1
- package/dist/server.d.ts +30 -0
- package/dist/server.js +162 -0
- package/dist/startup.js +15 -6
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +94 -1
- package/dist/tools/claude-process.d.ts +8 -0
- package/dist/tools/claude-process.js +199 -26
- package/dist/tools/claude-sessions.d.ts +1 -0
- package/dist/tools/claude-sessions.js +36 -10
- package/dist/tools/detector.js +11 -3
- package/dist/tools/permission-hook.js +94 -27
- package/dist/tools/permission-ipc.d.ts +1 -0
- package/dist/tools/permission-ipc.js +61 -14
- package/dist/transcript-streamer.d.ts +1 -0
- package/dist/transcript-streamer.js +18 -4
- package/dist/usage-tracker.d.ts +45 -0
- package/dist/usage-tracker.js +243 -0
- package/dist/websocket.d.ts +43 -12
- package/dist/websocket.js +418 -214
- package/package.json +5 -4
- package/dist/__tests__/cli-commands.test.d.ts +0 -6
- package/dist/__tests__/cli-commands.test.d.ts.map +0 -1
- package/dist/__tests__/cli-commands.test.js +0 -213
- package/dist/__tests__/cli-commands.test.js.map +0 -1
- package/dist/__tests__/crypto/e2e-integration.test.d.ts +0 -17
- package/dist/__tests__/crypto/e2e-integration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/e2e-integration.test.js +0 -338
- package/dist/__tests__/crypto/e2e-integration.test.js.map +0 -1
- package/dist/__tests__/crypto/e2eeManager.test.d.ts +0 -2
- package/dist/__tests__/crypto/e2eeManager.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/e2eeManager.test.js +0 -242
- package/dist/__tests__/crypto/e2eeManager.test.js.map +0 -1
- package/dist/__tests__/crypto/encryption.test.d.ts +0 -2
- package/dist/__tests__/crypto/encryption.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/encryption.test.js +0 -116
- package/dist/__tests__/crypto/encryption.test.js.map +0 -1
- package/dist/__tests__/crypto/keyExchange.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyExchange.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyExchange.test.js +0 -84
- package/dist/__tests__/crypto/keyExchange.test.js.map +0 -1
- package/dist/__tests__/crypto/keyGeneration.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyGeneration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyGeneration.test.js +0 -61
- package/dist/__tests__/crypto/keyGeneration.test.js.map +0 -1
- package/dist/__tests__/crypto/keyStorage.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyStorage.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyStorage.test.js +0 -133
- package/dist/__tests__/crypto/keyStorage.test.js.map +0 -1
- package/dist/__tests__/crypto/websocketIntegration.test.d.ts +0 -2
- package/dist/__tests__/crypto/websocketIntegration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/websocketIntegration.test.js +0 -259
- package/dist/__tests__/crypto/websocketIntegration.test.js.map +0 -1
- package/dist/__tests__/startup.test.d.ts +0 -11
- package/dist/__tests__/startup.test.d.ts.map +0 -1
- package/dist/__tests__/startup.test.js +0 -241
- package/dist/__tests__/startup.test.js.map +0 -1
- package/dist/__tests__/tools/claude-process.test.d.ts +0 -8
- package/dist/__tests__/tools/claude-process.test.d.ts.map +0 -1
- package/dist/__tests__/tools/claude-process.test.js +0 -430
- package/dist/__tests__/tools/claude-process.test.js.map +0 -1
- package/dist/__tests__/tools/permission-hook.test.d.ts +0 -17
- package/dist/__tests__/tools/permission-hook.test.d.ts.map +0 -1
- package/dist/__tests__/tools/permission-hook.test.js +0 -616
- package/dist/__tests__/tools/permission-hook.test.js.map +0 -1
- package/dist/__tests__/tools/permission-ipc.test.d.ts +0 -11
- package/dist/__tests__/tools/permission-ipc.test.d.ts.map +0 -1
- package/dist/__tests__/tools/permission-ipc.test.js +0 -612
- package/dist/__tests__/tools/permission-ipc.test.js.map +0 -1
- package/dist/__tests__/websocket.test.d.ts +0 -13
- package/dist/__tests__/websocket.test.d.ts.map +0 -1
- package/dist/__tests__/websocket.test.js +0 -204
- package/dist/__tests__/websocket.test.js.map +0 -1
- package/dist/api.d.ts +0 -44
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -76
- package/dist/api.js.map +0 -1
- package/dist/approval.d.ts.map +0 -1
- package/dist/approval.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/crypto/e2eeManager.d.ts.map +0 -1
- package/dist/crypto/e2eeManager.js.map +0 -1
- package/dist/crypto/encryption.d.ts.map +0 -1
- package/dist/crypto/encryption.js.map +0 -1
- package/dist/crypto/keyExchange.d.ts.map +0 -1
- package/dist/crypto/keyExchange.js.map +0 -1
- package/dist/crypto/keyGeneration.d.ts.map +0 -1
- package/dist/crypto/keyGeneration.js.map +0 -1
- package/dist/crypto/keyStorage.d.ts.map +0 -1
- package/dist/crypto/keyStorage.js.map +0 -1
- package/dist/crypto/sessionPersistence.d.ts.map +0 -1
- package/dist/crypto/sessionPersistence.js.map +0 -1
- package/dist/crypto/types.d.ts.map +0 -1
- package/dist/crypto/types.js.map +0 -1
- package/dist/crypto/websocketE2EE.d.ts.map +0 -1
- package/dist/crypto/websocketE2EE.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integration.d.ts.map +0 -1
- package/dist/integration.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/startup.d.ts.map +0 -1
- package/dist/startup.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/tools/__tests__/claude-sessions.test.d.ts +0 -2
- package/dist/tools/__tests__/claude-sessions.test.d.ts.map +0 -1
- package/dist/tools/__tests__/claude-sessions.test.js +0 -306
- package/dist/tools/__tests__/claude-sessions.test.js.map +0 -1
- package/dist/tools/claude-hooks.d.ts.map +0 -1
- package/dist/tools/claude-hooks.js.map +0 -1
- package/dist/tools/claude-process.d.ts.map +0 -1
- package/dist/tools/claude-process.js.map +0 -1
- package/dist/tools/claude-sessions.d.ts.map +0 -1
- package/dist/tools/claude-sessions.js.map +0 -1
- package/dist/tools/detector.d.ts.map +0 -1
- package/dist/tools/detector.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/permission-hook.d.ts.map +0 -1
- package/dist/tools/permission-hook.js.map +0 -1
- package/dist/tools/permission-ipc.d.ts.map +0 -1
- package/dist/tools/permission-ipc.js.map +0 -1
- package/dist/transcript-streamer.d.ts.map +0 -1
- package/dist/transcript-streamer.js.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/dist/websocket.js.map +0 -1
- package/jest.config.js +0 -18
package/dist/index.js
CHANGED
|
@@ -41,36 +41,53 @@ exports.createProgram = createProgram;
|
|
|
41
41
|
const commander_1 = require("commander");
|
|
42
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
43
43
|
const qrcode_terminal_1 = __importDefault(require("qrcode-terminal"));
|
|
44
|
+
const crypto = __importStar(require("crypto"));
|
|
44
45
|
const config_1 = require("./config");
|
|
45
|
-
const api_1 = require("./api");
|
|
46
46
|
const websocket_1 = require("./websocket");
|
|
47
47
|
const terminal_1 = require("./terminal");
|
|
48
48
|
const approval_1 = require("./approval");
|
|
49
49
|
const tools_1 = require("./tools");
|
|
50
50
|
const transcript_streamer_1 = require("./transcript-streamer");
|
|
51
51
|
const logger_1 = require("./logger");
|
|
52
|
+
const usage_tracker_1 = require("./usage-tracker");
|
|
52
53
|
const startup_1 = require("./startup");
|
|
53
54
|
const fs = __importStar(require("fs"));
|
|
54
55
|
const path = __importStar(require("path"));
|
|
55
56
|
const os = __importStar(require("os"));
|
|
57
|
+
/** Get the local network IP (first non-internal IPv4 address) */
|
|
58
|
+
function getLocalIp() {
|
|
59
|
+
const interfaces = os.networkInterfaces();
|
|
60
|
+
for (const name of Object.keys(interfaces)) {
|
|
61
|
+
for (const iface of interfaces[name]) {
|
|
62
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
63
|
+
return iface.address;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return '127.0.0.1';
|
|
68
|
+
}
|
|
56
69
|
function createProgram() {
|
|
57
70
|
const program = new commander_1.Command();
|
|
58
71
|
program
|
|
59
72
|
.name('forkoff')
|
|
60
73
|
.description('CLI tool for ForkOff - Connect your AI coding tools to mobile')
|
|
61
74
|
.version(require('../package.json').version)
|
|
62
|
-
.option('-q, --quiet', 'Suppress all output (for background operation)')
|
|
75
|
+
.option('-q, --quiet', 'Suppress all output (for background operation)')
|
|
76
|
+
.option('--debug', 'Enable debug logging to file (~/.forkoff-cli/logs/)');
|
|
63
77
|
program.hook('preAction', () => {
|
|
78
|
+
if (program.opts().debug) {
|
|
79
|
+
(0, logger_1.setDebug)(true);
|
|
80
|
+
(0, logger_1.cleanupOldLogs)(10);
|
|
81
|
+
}
|
|
64
82
|
if (program.opts().quiet) {
|
|
65
83
|
(0, logger_1.setQuiet)(true);
|
|
66
84
|
}
|
|
67
85
|
});
|
|
68
|
-
// Configure
|
|
86
|
+
// Configure CLI settings
|
|
69
87
|
program
|
|
70
88
|
.command('config')
|
|
71
89
|
.description('Configure ForkOff CLI settings')
|
|
72
|
-
.option('-
|
|
73
|
-
.option('-w, --ws <url>', 'Set WebSocket URL')
|
|
90
|
+
.option('-p, --port <port>', 'Set relay server port')
|
|
74
91
|
.option('-n, --name <name>', 'Set device name')
|
|
75
92
|
.option('--show', 'Show current configuration')
|
|
76
93
|
.option('--reset', 'Reset all configuration')
|
|
@@ -80,22 +97,24 @@ function createProgram() {
|
|
|
80
97
|
console.log(chalk_1.default.green('Configuration reset successfully'));
|
|
81
98
|
return;
|
|
82
99
|
}
|
|
83
|
-
if (options.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
if (options.port) {
|
|
101
|
+
const port = parseInt(options.port, 10);
|
|
102
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
103
|
+
console.log(chalk_1.default.red('Invalid port number. Must be between 1 and 65535.'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
config_1.config.relayPort = port;
|
|
107
|
+
console.log(chalk_1.default.green(`Relay port set to: ${port}`));
|
|
90
108
|
}
|
|
91
109
|
if (options.name) {
|
|
92
110
|
config_1.config.deviceName = options.name;
|
|
93
111
|
console.log(chalk_1.default.green(`Device name set to: ${options.name}`));
|
|
94
112
|
}
|
|
95
|
-
if (options.show || (!options.
|
|
113
|
+
if (options.show || (!options.port && !options.name && !options.reset)) {
|
|
114
|
+
const localIp = getLocalIp();
|
|
96
115
|
console.log(chalk_1.default.bold('\nCurrent Configuration:'));
|
|
97
|
-
console.log(`
|
|
98
|
-
console.log(`
|
|
116
|
+
console.log(` Relay URL: ${chalk_1.default.cyan(`ws://${localIp}:${config_1.config.relayPort}`)}`);
|
|
117
|
+
console.log(` Relay Port: ${chalk_1.default.cyan(String(config_1.config.relayPort))}`);
|
|
99
118
|
console.log(` Device Name: ${chalk_1.default.cyan(config_1.config.deviceName)}`);
|
|
100
119
|
console.log(` Device ID: ${chalk_1.default.cyan(config_1.config.deviceId || 'Not registered')}`);
|
|
101
120
|
console.log(` Paired: ${config_1.config.isPaired ? chalk_1.default.green('Yes') : chalk_1.default.yellow('No')}`);
|
|
@@ -111,52 +130,39 @@ function createProgram() {
|
|
|
111
130
|
.command('pair')
|
|
112
131
|
.description('Generate pairing code to connect with mobile app')
|
|
113
132
|
.action(async () => {
|
|
114
|
-
const spinner = (0, logger_1.createSpinner)('
|
|
133
|
+
const spinner = (0, logger_1.createSpinner)('Starting relay server...').start();
|
|
115
134
|
try {
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
result = await api_1.api.refreshPairingCode(config_1.config.deviceId);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// Device might not exist anymore, register fresh
|
|
133
|
-
result = await api_1.api.registerDevice();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
result = await api_1.api.registerDevice();
|
|
138
|
-
}
|
|
139
|
-
// Save device info
|
|
140
|
-
config_1.config.deviceId = result.device.id;
|
|
141
|
-
config_1.config.pairingCode = result.pairingCode;
|
|
142
|
-
spinner.succeed('Device registered successfully!\n');
|
|
135
|
+
// Ensure we have a deviceId
|
|
136
|
+
config_1.config.ensureDeviceId();
|
|
137
|
+
// Start embedded relay server
|
|
138
|
+
await websocket_1.wsClient.startServer(config_1.config.relayPort);
|
|
139
|
+
// Generate random 8-char pairing code
|
|
140
|
+
const pairingCode = crypto.randomBytes(4).toString('hex').toUpperCase().slice(0, 8);
|
|
141
|
+
config_1.config.pairingCode = pairingCode;
|
|
142
|
+
// Set pairing code on server for in-process validation
|
|
143
|
+
websocket_1.wsClient.setPairingCode(pairingCode);
|
|
144
|
+
const localIp = getLocalIp();
|
|
145
|
+
const relayUrl = `ws://${localIp}:${config_1.config.relayPort}`;
|
|
146
|
+
spinner.succeed(`Relay server started on ${relayUrl}\n`);
|
|
143
147
|
// Display pairing info
|
|
144
148
|
console.log(chalk_1.default.bold('Scan this QR code with the ForkOff mobile app:\n'));
|
|
145
|
-
|
|
146
|
-
const pairingUrl = `forkoff://pair/${result.pairingCode}`;
|
|
149
|
+
const pairingUrl = `forkoff://pair/${pairingCode}?relay=${encodeURIComponent(relayUrl)}`;
|
|
147
150
|
qrcode_terminal_1.default.generate(pairingUrl, { small: true }, (code) => {
|
|
148
151
|
console.log(code);
|
|
149
152
|
});
|
|
150
153
|
console.log(chalk_1.default.bold('\nOr enter this code manually:\n'));
|
|
151
|
-
console.log(chalk_1.default.bgBlue.white.bold(` ${
|
|
154
|
+
console.log(chalk_1.default.bgBlue.white.bold(` ${pairingCode} `));
|
|
152
155
|
console.log();
|
|
153
|
-
|
|
154
|
-
console.log(chalk_1.default.dim(`Code expires at: ${expiresAt.toLocaleTimeString()}`));
|
|
156
|
+
console.log(chalk_1.default.dim(`Relay: ${relayUrl}`));
|
|
155
157
|
console.log();
|
|
156
158
|
// Wait for pairing
|
|
157
159
|
console.log(chalk_1.default.yellow('Waiting for mobile app to scan...'));
|
|
158
160
|
console.log(chalk_1.default.dim('Press Ctrl+C to cancel\n'));
|
|
159
|
-
await waitForPairing(
|
|
161
|
+
const pairData = await waitForPairing();
|
|
162
|
+
// Server already sent pair_device_ack to mobile
|
|
163
|
+
config_1.config.pairedAt = new Date().toISOString();
|
|
164
|
+
console.log(chalk_1.default.green('\n\u2713 Device paired successfully!'));
|
|
165
|
+
console.log(chalk_1.default.dim('\nStarting connection...\n'));
|
|
160
166
|
// Auto-register startup if not explicitly disabled
|
|
161
167
|
if (config_1.config.startupEnabled !== false) {
|
|
162
168
|
try {
|
|
@@ -167,11 +173,11 @@ function createProgram() {
|
|
|
167
173
|
// Non-critical — don't fail pairing over this
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
|
-
//
|
|
176
|
+
// Continue to main connection (server already running)
|
|
171
177
|
await startConnection();
|
|
172
178
|
}
|
|
173
179
|
catch (error) {
|
|
174
|
-
spinner.fail('Failed to
|
|
180
|
+
spinner.fail('Failed to pair device');
|
|
175
181
|
console.error(chalk_1.default.red(error.message || 'Unknown error'));
|
|
176
182
|
}
|
|
177
183
|
});
|
|
@@ -184,29 +190,15 @@ function createProgram() {
|
|
|
184
190
|
console.log(chalk_1.default.yellow('Device not registered. Run "forkoff pair" first.'));
|
|
185
191
|
return;
|
|
186
192
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
config_1.config.userId = status.userId;
|
|
197
|
-
config_1.config.pairedAt = config_1.config.pairedAt || new Date().toISOString();
|
|
198
|
-
console.log(` User ID: ${chalk_1.default.cyan(status.userId)}`);
|
|
199
|
-
}
|
|
200
|
-
if (websocket_1.wsClient.isConnected) {
|
|
201
|
-
console.log(` WebSocket: ${chalk_1.default.green('Connected')}`);
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
console.log(` WebSocket: ${chalk_1.default.yellow('Disconnected')}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
spinner.fail('Failed to check status');
|
|
209
|
-
console.error(chalk_1.default.red(error.message || 'Unknown error'));
|
|
193
|
+
const localIp = getLocalIp();
|
|
194
|
+
console.log(chalk_1.default.bold('\nDevice Status:'));
|
|
195
|
+
console.log(` Device ID: ${chalk_1.default.cyan(config_1.config.deviceId)}`);
|
|
196
|
+
console.log(` Device Name: ${chalk_1.default.cyan(config_1.config.deviceName)}`);
|
|
197
|
+
console.log(` Paired: ${config_1.config.isPaired ? chalk_1.default.green('Yes') : chalk_1.default.yellow('No')}`);
|
|
198
|
+
console.log(` Relay URL: ${chalk_1.default.cyan(`ws://${localIp}:${config_1.config.relayPort}`)}`);
|
|
199
|
+
console.log(` Mobile: ${websocket_1.wsClient.isConnected ? chalk_1.default.green('Connected') : chalk_1.default.yellow('Not connected')}`);
|
|
200
|
+
if (config_1.config.pairedAt) {
|
|
201
|
+
console.log(` Paired At: ${chalk_1.default.dim(config_1.config.pairedAt)}`);
|
|
210
202
|
}
|
|
211
203
|
});
|
|
212
204
|
// Connect and stay online (for returning users who already paired)
|
|
@@ -232,6 +224,12 @@ function createProgram() {
|
|
|
232
224
|
// Non-critical
|
|
233
225
|
}
|
|
234
226
|
}
|
|
227
|
+
// Start embedded relay server before entering connection flow
|
|
228
|
+
const localIp = getLocalIp();
|
|
229
|
+
const relayUrl = `ws://${localIp}:${config_1.config.relayPort}`;
|
|
230
|
+
await websocket_1.wsClient.startServer(config_1.config.relayPort);
|
|
231
|
+
console.log(chalk_1.default.cyan(`Relay server started on ${relayUrl}`));
|
|
232
|
+
console.log(chalk_1.default.dim('Waiting for mobile app to connect...\n'));
|
|
235
233
|
await startConnection();
|
|
236
234
|
});
|
|
237
235
|
// Disconnect/unpair device
|
|
@@ -247,7 +245,6 @@ function createProgram() {
|
|
|
247
245
|
}
|
|
248
246
|
catch { }
|
|
249
247
|
}
|
|
250
|
-
config_1.config.userId = null;
|
|
251
248
|
config_1.config.pairedAt = null;
|
|
252
249
|
config_1.config.pairingCode = null;
|
|
253
250
|
console.log(chalk_1.default.green('Device disconnected and unpaired.'));
|
|
@@ -404,24 +401,17 @@ function createProgram() {
|
|
|
404
401
|
console.error(chalk_1.default.red(error.message));
|
|
405
402
|
}
|
|
406
403
|
});
|
|
407
|
-
// Helper function to
|
|
404
|
+
// Helper function to set up event handlers (server already started by caller)
|
|
408
405
|
async function startConnection() {
|
|
409
|
-
const spinner = (0, logger_1.createSpinner)('
|
|
406
|
+
const spinner = (0, logger_1.createSpinner)('Initializing...').start();
|
|
410
407
|
try {
|
|
411
|
-
await websocket_1.wsClient.connect();
|
|
412
408
|
tools_1.PermissionIpcManager.cleanupStaleTempFiles();
|
|
413
|
-
spinner.succeed('
|
|
414
|
-
// Detect
|
|
409
|
+
spinner.succeed('Ready!\n');
|
|
410
|
+
// Detect connected tools
|
|
415
411
|
spinner.start('Detecting AI coding tools...');
|
|
416
412
|
try {
|
|
417
413
|
const toolResult = await tools_1.toolDetector.detectAll();
|
|
418
414
|
if (toolResult.tools.length > 0) {
|
|
419
|
-
const toolsToReport = toolResult.tools.map(tool => ({
|
|
420
|
-
type: tool.type,
|
|
421
|
-
name: tool.name,
|
|
422
|
-
version: tool.version || null,
|
|
423
|
-
}));
|
|
424
|
-
await api_1.api.reportConnectedTools(config_1.config.deviceId, toolsToReport);
|
|
425
415
|
spinner.succeed(`Detected ${toolResult.tools.length} AI tool(s): ${toolResult.tools.map(t => t.name).join(', ')}`);
|
|
426
416
|
}
|
|
427
417
|
else {
|
|
@@ -443,7 +433,7 @@ function createProgram() {
|
|
|
443
433
|
});
|
|
444
434
|
// When a session is auto-created (command received before terminal_create), send the cwd
|
|
445
435
|
terminal_1.terminalManager.on('session_created', (data) => {
|
|
446
|
-
console.log(chalk_1.default.dim(`[Terminal] Session auto-created: ${data.terminalSessionId}
|
|
436
|
+
console.log(chalk_1.default.dim(`[Terminal] Session auto-created: ${data.terminalSessionId}`));
|
|
447
437
|
websocket_1.wsClient.sendTerminalCwd({
|
|
448
438
|
terminalSessionId: data.terminalSessionId,
|
|
449
439
|
cwd: data.cwd,
|
|
@@ -465,7 +455,7 @@ function createProgram() {
|
|
|
465
455
|
terminalSessionId: data.terminalSessionId,
|
|
466
456
|
cwd: session.cwd,
|
|
467
457
|
});
|
|
468
|
-
console.log(chalk_1.default.dim(`[Terminal] Session created
|
|
458
|
+
console.log(chalk_1.default.dim(`[Terminal] Session created`));
|
|
469
459
|
});
|
|
470
460
|
// Set up event handlers
|
|
471
461
|
websocket_1.wsClient.on('terminal_command', async (data) => {
|
|
@@ -496,11 +486,11 @@ function createProgram() {
|
|
|
496
486
|
console.log(chalk_1.default.cyan('[Claude] Scanning for Claude sessions...'));
|
|
497
487
|
// Attach event listeners BEFORE starting to watch (so we catch initial events)
|
|
498
488
|
tools_1.claudeSessionDetector.on('session_detected', (session) => {
|
|
499
|
-
console.log(chalk_1.default.cyan(`[Claude] New session detected
|
|
489
|
+
console.log(chalk_1.default.cyan(`[Claude] New session detected`));
|
|
500
490
|
websocket_1.wsClient.sendClaudeSessionUpdate(session);
|
|
501
491
|
});
|
|
502
492
|
tools_1.claudeSessionDetector.on('session_changed', (session) => {
|
|
503
|
-
console.log(chalk_1.default.dim(`[Claude] Session updated
|
|
493
|
+
console.log(chalk_1.default.dim(`[Claude] Session updated (${session.state})`));
|
|
504
494
|
websocket_1.wsClient.sendClaudeSessionUpdate(session);
|
|
505
495
|
});
|
|
506
496
|
tools_1.claudeSessionDetector.on('claude_running_changed', (isRunning) => {
|
|
@@ -535,18 +525,20 @@ function createProgram() {
|
|
|
535
525
|
}
|
|
536
526
|
// Log approval events
|
|
537
527
|
approval_1.approvalManager.on('approved', (approval) => {
|
|
538
|
-
console.log(chalk_1.default.green(`[Approval] Approved: ${approval.
|
|
528
|
+
console.log(chalk_1.default.green(`[Approval] Approved: ${approval.approvalId}`));
|
|
539
529
|
});
|
|
540
530
|
approval_1.approvalManager.on('rejected', (approval) => {
|
|
541
|
-
console.log(chalk_1.default.red(`[Approval] Rejected: ${approval.
|
|
531
|
+
console.log(chalk_1.default.red(`[Approval] Rejected: ${approval.approvalId}`));
|
|
542
532
|
});
|
|
543
533
|
// Handle Claude start session request from mobile
|
|
544
534
|
websocket_1.wsClient.on('claude_start_session', async (data) => {
|
|
545
|
-
console.log(chalk_1.default.cyan(`[Claude] Start session request
|
|
535
|
+
console.log(chalk_1.default.cyan(`[Claude] Start session request for ${data.terminalSessionId}`));
|
|
546
536
|
try {
|
|
547
537
|
const result = await tools_1.claudeProcessManager.startSession(data.directory, data.terminalSessionId, data.dangerouslySkipPermissions, data.interactivePermissions);
|
|
548
538
|
websocket_1.wsClient.sendToolStatusUpdate('claude_code', 'active');
|
|
549
539
|
websocket_1.wsClient.sendTerminalCwd({ terminalSessionId: data.terminalSessionId, cwd: result.cwd });
|
|
540
|
+
// Track session start for analytics
|
|
541
|
+
usageTracker.recordSessionStart();
|
|
550
542
|
// Notify mobile that the session is ready for input
|
|
551
543
|
websocket_1.wsClient.sendClaudeSessionEvent({
|
|
552
544
|
sessionKey: data.terminalSessionId,
|
|
@@ -563,13 +555,13 @@ function createProgram() {
|
|
|
563
555
|
// Claude is only spawned when the user actually sends a message (via user_message event)
|
|
564
556
|
// This prevents duplicate transcript entries from double spawns
|
|
565
557
|
websocket_1.wsClient.on('claude_resume_session', async (data) => {
|
|
566
|
-
console.log(chalk_1.default.cyan(`[Claude] Resume session request
|
|
558
|
+
console.log(chalk_1.default.cyan(`[Claude] Resume session request`));
|
|
567
559
|
// Look up the correct directory from our locally scanned sessions
|
|
568
560
|
// The mobile app may have a cached/stale directory (e.g. with corrupted hyphens)
|
|
569
561
|
let resolvedDir = data.directory;
|
|
570
562
|
const knownSession = tools_1.claudeSessionDetector.getSessions().find(s => s.sessionKey === data.sessionKey);
|
|
571
563
|
if (knownSession && knownSession.directory) {
|
|
572
|
-
console.log(chalk_1.default.dim(`[Claude] Using local directory for session
|
|
564
|
+
console.log(chalk_1.default.dim(`[Claude] Using local directory for session`));
|
|
573
565
|
resolvedDir = knownSession.directory;
|
|
574
566
|
}
|
|
575
567
|
if (resolvedDir === '~' || resolvedDir.startsWith('~/')) {
|
|
@@ -607,7 +599,7 @@ function createProgram() {
|
|
|
607
599
|
sessionKey: data.sessionKey,
|
|
608
600
|
event: { type: 'ready' },
|
|
609
601
|
});
|
|
610
|
-
console.log(chalk_1.default.green(`[Claude] Session ready (will spawn on first message)
|
|
602
|
+
console.log(chalk_1.default.green(`[Claude] Session ready (will spawn on first message)`));
|
|
611
603
|
});
|
|
612
604
|
// Handle directory listing requests
|
|
613
605
|
websocket_1.wsClient.on('directory_list', async (data) => {
|
|
@@ -622,8 +614,10 @@ function createProgram() {
|
|
|
622
614
|
const homeDir = os.homedir();
|
|
623
615
|
// SECURITY: Only allow access to directories under home directory
|
|
624
616
|
// This prevents accessing sensitive system files like /etc/passwd
|
|
625
|
-
|
|
626
|
-
|
|
617
|
+
// Uses path.relative() for cross-platform safety (handles Windows case-insensitivity)
|
|
618
|
+
const dirRelative = path.relative(homeDir, resolvedPath);
|
|
619
|
+
if (dirRelative.startsWith('..') || path.isAbsolute(dirRelative)) {
|
|
620
|
+
console.warn(chalk_1.default.yellow(`[Dir] Access denied — path outside home directory`));
|
|
627
621
|
websocket_1.wsClient.sendDirectoryListResponse({ requestId: data.requestId, entries: [], currentPath: data.path });
|
|
628
622
|
return;
|
|
629
623
|
}
|
|
@@ -648,13 +642,13 @@ function createProgram() {
|
|
|
648
642
|
});
|
|
649
643
|
// Handle read file requests from mobile (e.g., CLAUDE.md)
|
|
650
644
|
websocket_1.wsClient.on('read_file', async (data) => {
|
|
651
|
-
console.log(chalk_1.default.dim(`[File] Read request
|
|
645
|
+
console.log(chalk_1.default.dim(`[File] Read request received`));
|
|
652
646
|
try {
|
|
653
647
|
// SECURITY: Whitelist of allowed filenames
|
|
654
648
|
const allowedFiles = ['CLAUDE.md', 'README.md', 'package.json'];
|
|
655
649
|
const fileName = path.basename(data.filePath);
|
|
656
650
|
if (!allowedFiles.includes(fileName)) {
|
|
657
|
-
console.warn(chalk_1.default.yellow(`[File] Access denied
|
|
651
|
+
console.warn(chalk_1.default.yellow(`[File] Access denied — file not in whitelist`));
|
|
658
652
|
websocket_1.wsClient.sendReadFileResponse({
|
|
659
653
|
requestId: data.requestId,
|
|
660
654
|
exists: false,
|
|
@@ -670,14 +664,16 @@ function createProgram() {
|
|
|
670
664
|
}
|
|
671
665
|
resolvedPath = path.resolve(resolvedPath);
|
|
672
666
|
// SECURITY: Only allow access under home directory
|
|
667
|
+
// Uses path.relative() for cross-platform safety (handles Windows case-insensitivity)
|
|
673
668
|
const homeDir = os.homedir();
|
|
674
|
-
|
|
675
|
-
|
|
669
|
+
const fileRelative = path.relative(homeDir, resolvedPath);
|
|
670
|
+
if (fileRelative.startsWith('..') || path.isAbsolute(fileRelative)) {
|
|
671
|
+
console.warn(chalk_1.default.yellow(`[File] Access denied — path outside home directory`));
|
|
676
672
|
websocket_1.wsClient.sendReadFileResponse({
|
|
677
673
|
requestId: data.requestId,
|
|
678
674
|
exists: false,
|
|
679
675
|
fileName,
|
|
680
|
-
error: '
|
|
676
|
+
error: 'Access denied',
|
|
681
677
|
});
|
|
682
678
|
return;
|
|
683
679
|
}
|
|
@@ -710,20 +706,28 @@ function createProgram() {
|
|
|
710
706
|
});
|
|
711
707
|
}
|
|
712
708
|
catch (error) {
|
|
713
|
-
console.error(
|
|
709
|
+
console.error(`[File] Error reading ${path.basename(data.filePath)}:`, error.message);
|
|
714
710
|
websocket_1.wsClient.sendReadFileResponse({
|
|
715
711
|
requestId: data.requestId,
|
|
716
712
|
exists: false,
|
|
717
713
|
fileName: path.basename(data.filePath),
|
|
718
|
-
error:
|
|
714
|
+
error: 'File read failed',
|
|
719
715
|
});
|
|
720
716
|
}
|
|
721
717
|
});
|
|
722
718
|
// Handle transcript fetch requests from mobile
|
|
723
719
|
websocket_1.wsClient.on('transcript_fetch', async (data) => {
|
|
724
|
-
console.log(chalk_1.default.dim(`[Transcript] Fetching:
|
|
720
|
+
console.log(chalk_1.default.dim(`[Transcript] Fetching: offset: ${data.offset}, limit: ${data.limit}`));
|
|
725
721
|
try {
|
|
726
|
-
|
|
722
|
+
// SECURITY: Validate transcript path is under ~/.claude/projects/ to prevent path traversal
|
|
723
|
+
const resolvedTranscriptPath = path.resolve(data.transcriptPath);
|
|
724
|
+
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
725
|
+
const relPath = path.relative(claudeProjectsDir, resolvedTranscriptPath);
|
|
726
|
+
if (relPath.startsWith('..') || path.isAbsolute(relPath)) {
|
|
727
|
+
console.warn(chalk_1.default.yellow(`[Transcript] Access denied — path outside ~/.claude/projects/`));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const result = await transcript_streamer_1.transcriptStreamer.fetchHistory(resolvedTranscriptPath, data.offset || 0, data.limit || 100, data.reverse !== false // Default to true (most recent first)
|
|
727
731
|
);
|
|
728
732
|
const payload = JSON.stringify(result.entries);
|
|
729
733
|
console.log(chalk_1.default.dim(`[Transcript] Sending history: ${result.entries.length} entries, ${result.totalEntries} total, payload ~${(payload.length / 1024).toFixed(0)}KB`));
|
|
@@ -740,29 +744,22 @@ function createProgram() {
|
|
|
740
744
|
});
|
|
741
745
|
// Handle transcript subscribe
|
|
742
746
|
websocket_1.wsClient.on('transcript_subscribe', (data) => {
|
|
743
|
-
console.log(chalk_1.default.dim(`[Transcript] Subscribing
|
|
744
|
-
|
|
747
|
+
console.log(chalk_1.default.dim(`[Transcript] Subscribing to session`));
|
|
748
|
+
// SECURITY: Validate transcript path to prevent path traversal
|
|
749
|
+
const resolvedSubPath = path.resolve(data.transcriptPath);
|
|
750
|
+
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
751
|
+
const subRelPath = path.relative(claudeDir, resolvedSubPath);
|
|
752
|
+
if (subRelPath.startsWith('..') || path.isAbsolute(subRelPath)) {
|
|
753
|
+
console.warn(chalk_1.default.yellow(`[Transcript] Subscribe denied — path outside ~/.claude/projects/`));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(data.sessionKey, resolvedSubPath);
|
|
745
757
|
});
|
|
746
758
|
// Handle transcript unsubscribe
|
|
747
759
|
websocket_1.wsClient.on('transcript_unsubscribe', (data) => {
|
|
748
|
-
console.log(chalk_1.default.dim(`[Transcript] Unsubscribing
|
|
760
|
+
console.log(chalk_1.default.dim(`[Transcript] Unsubscribing from session`));
|
|
749
761
|
transcript_streamer_1.transcriptStreamer.unsubscribeFromUpdates(data.sessionKey);
|
|
750
762
|
});
|
|
751
|
-
// Handle SDK subscribe start - mobile wants live updates for a session
|
|
752
|
-
// This is sent by API when mobile uses transcript_subscribe_sdk
|
|
753
|
-
websocket_1.wsClient.on('transcript_subscribe_sdk_start', async (data) => {
|
|
754
|
-
console.log(chalk_1.default.cyan(`[Transcript] SDK subscribe start: ${data.sessionKey}`));
|
|
755
|
-
// Find the transcript file for this session
|
|
756
|
-
const sessions = tools_1.claudeSessionDetector.scanSessions();
|
|
757
|
-
const session = sessions.find(s => s.sessionKey === data.sessionKey);
|
|
758
|
-
if (session?.transcriptPath) {
|
|
759
|
-
console.log(chalk_1.default.dim(`[Transcript] Starting watch for: ${session.transcriptPath}`));
|
|
760
|
-
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(data.sessionKey, session.transcriptPath);
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
console.log(chalk_1.default.yellow(`[Transcript] No transcript found for session: ${data.sessionKey}`));
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
763
|
// Handle claude sessions request - mobile app wants current sessions
|
|
767
764
|
websocket_1.wsClient.on('claude_sessions_request', () => {
|
|
768
765
|
console.log(chalk_1.default.cyan(`[Claude] Sessions requested by mobile`));
|
|
@@ -786,118 +783,6 @@ function createProgram() {
|
|
|
786
783
|
websocket_1.wsClient.sendToolStatusUpdate('claude_code', hasActiveSession ? 'active' : 'inactive');
|
|
787
784
|
}
|
|
788
785
|
});
|
|
789
|
-
// Handle RPC requests from the API gateway
|
|
790
|
-
websocket_1.wsClient.on('rpc_request', async (data) => {
|
|
791
|
-
console.log(chalk_1.default.cyan(`[RPC] Request: ${data.method}, requestId: ${data.requestId}`));
|
|
792
|
-
try {
|
|
793
|
-
if (data.method === 'get_session_history') {
|
|
794
|
-
const { claudeSessionId, sessionKey, directory, limit = 400, offset = 0 } = data.params;
|
|
795
|
-
console.log(chalk_1.default.dim(`[RPC] get_session_history request received`));
|
|
796
|
-
// Find the transcript file
|
|
797
|
-
let transcriptPath;
|
|
798
|
-
// If claudeSessionId is provided, search for the JSONL file directly
|
|
799
|
-
if (claudeSessionId) {
|
|
800
|
-
// SECURITY: Validate claudeSessionId to prevent path traversal
|
|
801
|
-
// Session IDs should be alphanumeric with hyphens/underscores only
|
|
802
|
-
const sessionIdRegex = /^[a-zA-Z0-9_-]+$/;
|
|
803
|
-
if (!sessionIdRegex.test(claudeSessionId)) {
|
|
804
|
-
console.warn(chalk_1.default.yellow(`[RPC] Invalid claudeSessionId format - rejected`));
|
|
805
|
-
websocket_1.wsClient.sendRpcResponse({
|
|
806
|
-
requestId: data.requestId,
|
|
807
|
-
error: { code: -32602, message: 'Invalid session ID format' }
|
|
808
|
-
});
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
812
|
-
if (fs.existsSync(claudeProjectsDir)) {
|
|
813
|
-
const projectDirs = fs.readdirSync(claudeProjectsDir);
|
|
814
|
-
for (const projectDir of projectDirs) {
|
|
815
|
-
// SECURITY: Validate projectDir as well to prevent traversal
|
|
816
|
-
if (!sessionIdRegex.test(projectDir) && !/^[a-zA-Z0-9_.-]+$/.test(projectDir)) {
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
const potentialPath = path.join(claudeProjectsDir, projectDir, `${claudeSessionId}.jsonl`);
|
|
820
|
-
// SECURITY: Verify the resolved path is still under claudeProjectsDir
|
|
821
|
-
const resolvedPotentialPath = path.resolve(potentialPath);
|
|
822
|
-
if (!resolvedPotentialPath.startsWith(claudeProjectsDir)) {
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
if (fs.existsSync(potentialPath)) {
|
|
826
|
-
transcriptPath = potentialPath;
|
|
827
|
-
console.log(chalk_1.default.dim(`[RPC] Found JSONL transcript`));
|
|
828
|
-
break;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
// If not found by claudeSessionId, search all sessions
|
|
834
|
-
if (!transcriptPath && tools_1.claudeSessionDetector.isClaudeInstalled()) {
|
|
835
|
-
const sessions = tools_1.claudeSessionDetector.scanSessions();
|
|
836
|
-
// First try to match by sessionKey
|
|
837
|
-
let session = sessions.find(s => s.sessionKey === sessionKey);
|
|
838
|
-
// If no match by sessionKey, try to find most recent session for the same directory
|
|
839
|
-
if (!session && sessions.length > 0 && directory) {
|
|
840
|
-
const normalizedDir = path.resolve(directory);
|
|
841
|
-
session = sessions.find(s => path.resolve(s.directory) === normalizedDir);
|
|
842
|
-
if (session) {
|
|
843
|
-
console.log(chalk_1.default.dim(`[RPC] Using directory-matched session as fallback: ${session.sessionKey}`));
|
|
844
|
-
}
|
|
845
|
-
else {
|
|
846
|
-
console.log(chalk_1.default.dim(`[RPC] No session found for directory: ${normalizedDir}`));
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
if (session?.transcriptPath) {
|
|
850
|
-
transcriptPath = session.transcriptPath;
|
|
851
|
-
console.log(chalk_1.default.dim(`[RPC] Found transcript path: ${transcriptPath}`));
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
855
|
-
console.log(chalk_1.default.yellow(`[RPC] No transcript file found for session`));
|
|
856
|
-
websocket_1.wsClient.sendRpcResponse({
|
|
857
|
-
requestId: data.requestId,
|
|
858
|
-
result: { entries: [], totalEntries: 0, hasMore: false }
|
|
859
|
-
});
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
// Read the transcript file
|
|
863
|
-
const result = await transcript_streamer_1.transcriptStreamer.fetchHistory(transcriptPath, offset, limit, true);
|
|
864
|
-
console.log(chalk_1.default.green(`[RPC] Loaded ${result.entries.length} entries from transcript`));
|
|
865
|
-
// IMPORTANT: Start watching this transcript for live updates
|
|
866
|
-
// This is needed because SDK mode doesn't send transcript_subscribe
|
|
867
|
-
// Use sessionKey from mobile for updates (must match what mobile is listening for)
|
|
868
|
-
const updateSessionKey = sessionKey;
|
|
869
|
-
if (!updateSessionKey) {
|
|
870
|
-
console.log(chalk_1.default.yellow(`[RPC] No sessionKey provided, using claudeSessionId - updates may not route correctly`));
|
|
871
|
-
}
|
|
872
|
-
const watchKey = updateSessionKey || claudeSessionId || data.requestId;
|
|
873
|
-
console.log(chalk_1.default.cyan(`[RPC] Starting file watch for live updates: ${watchKey}`));
|
|
874
|
-
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(watchKey, transcriptPath);
|
|
875
|
-
websocket_1.wsClient.sendRpcResponse({
|
|
876
|
-
requestId: data.requestId,
|
|
877
|
-
result: {
|
|
878
|
-
entries: result.entries,
|
|
879
|
-
totalEntries: result.totalEntries,
|
|
880
|
-
hasMore: result.hasMore,
|
|
881
|
-
sessionKey: watchKey, // Tell mobile which sessionKey to listen for updates
|
|
882
|
-
}
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
else {
|
|
886
|
-
console.log(chalk_1.default.yellow(`[RPC] Unknown method: ${data.method}`));
|
|
887
|
-
websocket_1.wsClient.sendRpcResponse({
|
|
888
|
-
requestId: data.requestId,
|
|
889
|
-
error: { code: -32601, message: `Method not found: ${data.method}` }
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
catch (error) {
|
|
894
|
-
console.error(chalk_1.default.red(`[RPC] Error handling ${data.method}:`, error.message));
|
|
895
|
-
websocket_1.wsClient.sendRpcResponse({
|
|
896
|
-
requestId: data.requestId,
|
|
897
|
-
error: { code: -32603, message: error.message || 'Internal error' }
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
});
|
|
901
786
|
// Forward live transcript updates to WebSocket
|
|
902
787
|
transcript_streamer_1.transcriptStreamer.on('update', (data) => {
|
|
903
788
|
console.log(chalk_1.default.green(`[Transcript] Sending update for ${data.sessionKey}: ${data.entry?.type}`));
|
|
@@ -927,7 +812,7 @@ function createProgram() {
|
|
|
927
812
|
});
|
|
928
813
|
// Forward tool activity events to mobile (non-blocking notifications)
|
|
929
814
|
tools_1.claudeProcessManager.on('tool_activity', (data) => {
|
|
930
|
-
console.log(chalk_1.default.dim(`[Claude] Tool activity: ${data.toolName}
|
|
815
|
+
console.log(chalk_1.default.dim(`[Claude] Tool activity: ${data.toolName}`));
|
|
931
816
|
websocket_1.wsClient.sendToolActivity(data);
|
|
932
817
|
});
|
|
933
818
|
// Forward permission prompts from hook system to mobile (interactive approval)
|
|
@@ -943,7 +828,7 @@ function createProgram() {
|
|
|
943
828
|
});
|
|
944
829
|
// Handle permission responses from mobile → route back to hook IPC
|
|
945
830
|
websocket_1.wsClient.on('permission_response', (data) => {
|
|
946
|
-
console.log(chalk_1.default.green(`[Claude] Permission response: ${data.promptId}
|
|
831
|
+
console.log(chalk_1.default.green(`[Claude] Permission response: ${data.promptId}`));
|
|
947
832
|
tools_1.claudeProcessManager.handlePermissionResponse(data.promptId, data.decision, data.reason);
|
|
948
833
|
});
|
|
949
834
|
// Handle permission rules sync from mobile — write rules to disk for hook script
|
|
@@ -953,16 +838,9 @@ function createProgram() {
|
|
|
953
838
|
tools_1.claudeProcessManager.updatePermissionRules(data.rules);
|
|
954
839
|
}
|
|
955
840
|
});
|
|
956
|
-
// Handle mobile disconnect — full reset: clear taken-over, auto-allow pending, tear down hooks
|
|
957
|
-
websocket_1.wsClient.on('mobile_disconnected', () => {
|
|
958
|
-
console.log(chalk_1.default.yellow(`[Claude] Mobile disconnected — full permission reset`));
|
|
959
|
-
tools_1.claudeProcessManager.clearAllTakenOver();
|
|
960
|
-
tools_1.claudeProcessManager.autoAllowAllPendingPrompts();
|
|
961
|
-
tools_1.claudeProcessManager.cleanupAllPermissionState();
|
|
962
|
-
});
|
|
963
841
|
// Handle Claude approval responses from mobile
|
|
964
842
|
websocket_1.wsClient.on('claude_approval_response', (data) => {
|
|
965
|
-
console.log(chalk_1.default.green(`[Claude] Approval response: ${data.approvalId}
|
|
843
|
+
console.log(chalk_1.default.green(`[Claude] Approval response: ${data.approvalId}`));
|
|
966
844
|
tools_1.claudeProcessManager.handleApprovalResponse(data.approvalId, data.response);
|
|
967
845
|
});
|
|
968
846
|
// Handle user messages from mobile app (send to Claude session)
|
|
@@ -980,7 +858,7 @@ function createProgram() {
|
|
|
980
858
|
if (!tools_1.claudeProcessManager.isTakenOver(terminalSessionId)) {
|
|
981
859
|
// Fresh session from auto-prompt (quick action): start new session with directory
|
|
982
860
|
if (data.directory) {
|
|
983
|
-
console.log(chalk_1.default.cyan(`[Claude] Starting fresh session for auto-prompt
|
|
861
|
+
console.log(chalk_1.default.cyan(`[Claude] Starting fresh session for auto-prompt`));
|
|
984
862
|
const sent = await tools_1.claudeProcessManager.startAndSendMessage(data.directory, terminalSessionId, data.message, data.mode?.permissionMode === 'bypassPermissions', data.interactivePermissions);
|
|
985
863
|
if (sent) {
|
|
986
864
|
// Notify mobile that session is ready
|
|
@@ -1034,7 +912,7 @@ function createProgram() {
|
|
|
1034
912
|
// Forward thinking content to mobile
|
|
1035
913
|
tools_1.claudeProcessManager.on('thinking_content', (data) => {
|
|
1036
914
|
if (data.content || !data.partial) {
|
|
1037
|
-
console.log(chalk_1.default.magenta(`[Claude] Thinking${data.partial ? ' (streaming)' : ' (complete)'}: ${data.content?.
|
|
915
|
+
console.log(chalk_1.default.magenta(`[Claude] Thinking${data.partial ? ' (streaming)' : ' (complete)'}: ${data.content?.length || 0} chars`));
|
|
1038
916
|
}
|
|
1039
917
|
websocket_1.wsClient.sendThinkingContent({
|
|
1040
918
|
sessionKey: data.sessionKey,
|
|
@@ -1043,26 +921,35 @@ function createProgram() {
|
|
|
1043
921
|
partial: data.partial,
|
|
1044
922
|
});
|
|
1045
923
|
});
|
|
1046
|
-
//
|
|
924
|
+
// Persistent usage tracker
|
|
925
|
+
const usageTracker = new usage_tracker_1.UsageTracker();
|
|
926
|
+
websocket_1.wsClient.setUsageTracker(usageTracker);
|
|
927
|
+
// Forward token usage to mobile and persist locally
|
|
1047
928
|
tools_1.claudeProcessManager.on('token_usage', (data) => {
|
|
1048
929
|
console.log(chalk_1.default.blue(`[Claude] Tokens: ${data.usage.inputTokens} in / ${data.usage.outputTokens} out`));
|
|
930
|
+
usageTracker.recordUsage(data.usage.inputTokens || 0, data.usage.outputTokens || 0);
|
|
1049
931
|
websocket_1.wsClient.sendTokenUsage({
|
|
1050
932
|
sessionKey: data.sessionKey,
|
|
1051
933
|
usage: data.usage,
|
|
1052
934
|
});
|
|
1053
935
|
});
|
|
936
|
+
// Handle usage stats request from mobile (pull-refresh)
|
|
937
|
+
websocket_1.wsClient.on('usage_stats_request', () => {
|
|
938
|
+
console.log(chalk_1.default.blue('[Analytics] Usage stats requested by mobile'));
|
|
939
|
+
websocket_1.wsClient.sendAllUsageStats();
|
|
940
|
+
});
|
|
1054
941
|
// When a fresh session captures a real session_id, update the transcript
|
|
1055
942
|
// file watcher so the mobile gets real-time updates from the correct JSONL file.
|
|
1056
943
|
// This is critical for brainstorm/quick-action sessions where startAndSendMessage
|
|
1057
944
|
// creates a new Claude session with a new transcript file.
|
|
1058
945
|
tools_1.claudeProcessManager.on('session_id_captured', (data) => {
|
|
1059
946
|
const { terminalSessionId, sessionId } = data;
|
|
1060
|
-
console.log(chalk_1.default.green(`[Claude] New session_id captured for ${terminalSessionId}
|
|
947
|
+
console.log(chalk_1.default.green(`[Claude] New session_id captured for ${terminalSessionId}`));
|
|
1061
948
|
// Re-scan to pick up the new session's transcript path
|
|
1062
949
|
const sessions = tools_1.claudeSessionDetector.scanSessions();
|
|
1063
950
|
const newSession = sessions.find((s) => s.sessionKey === sessionId);
|
|
1064
951
|
if (newSession?.transcriptPath) {
|
|
1065
|
-
console.log(chalk_1.default.green(`[Claude] Updating transcript watcher
|
|
952
|
+
console.log(chalk_1.default.green(`[Claude] Updating transcript watcher`));
|
|
1066
953
|
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(terminalSessionId, newSession.transcriptPath);
|
|
1067
954
|
}
|
|
1068
955
|
else {
|
|
@@ -1071,11 +958,11 @@ function createProgram() {
|
|
|
1071
958
|
const retrySessions = tools_1.claudeSessionDetector.scanSessions();
|
|
1072
959
|
const retrySession = retrySessions.find((s) => s.sessionKey === sessionId);
|
|
1073
960
|
if (retrySession?.transcriptPath) {
|
|
1074
|
-
console.log(chalk_1.default.green(`[Claude] Retry: updating transcript watcher
|
|
961
|
+
console.log(chalk_1.default.green(`[Claude] Retry: updating transcript watcher`));
|
|
1075
962
|
transcript_streamer_1.transcriptStreamer.subscribeToUpdates(terminalSessionId, retrySession.transcriptPath);
|
|
1076
963
|
}
|
|
1077
964
|
else {
|
|
1078
|
-
console.log(chalk_1.default.yellow(`[Claude] Retry: still not found
|
|
965
|
+
console.log(chalk_1.default.yellow(`[Claude] Retry: transcript still not found`));
|
|
1079
966
|
}
|
|
1080
967
|
}, 1000);
|
|
1081
968
|
}
|
|
@@ -1086,7 +973,7 @@ function createProgram() {
|
|
|
1086
973
|
console.log(chalk_1.default.cyan(`[Claude] Task list: ${data.tasks?.length || 0} tasks`));
|
|
1087
974
|
}
|
|
1088
975
|
else {
|
|
1089
|
-
console.log(chalk_1.default.cyan(`[Claude] Task ${data.type}: ${data.task?.
|
|
976
|
+
console.log(chalk_1.default.cyan(`[Claude] Task ${data.type}: ${data.task?.id || 'unknown'}`));
|
|
1090
977
|
}
|
|
1091
978
|
websocket_1.wsClient.sendTaskProgress({
|
|
1092
979
|
sessionKey: data.sessionKey,
|
|
@@ -1096,10 +983,8 @@ function createProgram() {
|
|
|
1096
983
|
});
|
|
1097
984
|
});
|
|
1098
985
|
websocket_1.wsClient.on('disconnected', (reason) => {
|
|
1099
|
-
console.log(chalk_1.default.yellow(`\
|
|
1100
|
-
|
|
1101
|
-
console.log(chalk_1.default.dim('Attempting to reconnect...'));
|
|
1102
|
-
}
|
|
986
|
+
console.log(chalk_1.default.yellow(`\nMobile disconnected: ${reason}`));
|
|
987
|
+
console.log(chalk_1.default.dim('Waiting for mobile to reconnect...'));
|
|
1103
988
|
});
|
|
1104
989
|
websocket_1.wsClient.on('error', (error) => {
|
|
1105
990
|
console.error(chalk_1.default.red(`Connection error: ${error.message}`));
|
|
@@ -1107,9 +992,17 @@ function createProgram() {
|
|
|
1107
992
|
// Keep the process running
|
|
1108
993
|
process.on('SIGINT', () => {
|
|
1109
994
|
console.log(chalk_1.default.yellow('\nDisconnecting...'));
|
|
995
|
+
tools_1.claudeProcessManager.cleanupAllPermissionState();
|
|
996
|
+
usageTracker.flush();
|
|
1110
997
|
tools_1.claudeSessionDetector.stopWatching();
|
|
1111
998
|
transcript_streamer_1.transcriptStreamer.cleanup();
|
|
1112
999
|
websocket_1.wsClient.disconnect();
|
|
1000
|
+
const logPath = (0, logger_1.getLogFilePath)();
|
|
1001
|
+
(0, logger_1.closeDebugLog)();
|
|
1002
|
+
if (logPath) {
|
|
1003
|
+
// Use original console since we just closed the debug log
|
|
1004
|
+
process.stdout.write(`\nDebug log saved: ${logPath}\n`);
|
|
1005
|
+
}
|
|
1113
1006
|
process.exit(0);
|
|
1114
1007
|
});
|
|
1115
1008
|
// Keep alive
|
|
@@ -1120,34 +1013,64 @@ function createProgram() {
|
|
|
1120
1013
|
console.error(chalk_1.default.red(error.message || 'Unknown error'));
|
|
1121
1014
|
}
|
|
1122
1015
|
}
|
|
1123
|
-
// Helper function to wait for pairing
|
|
1124
|
-
async function waitForPairing(
|
|
1125
|
-
return new Promise((resolve
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
if (status.isPaired) {
|
|
1130
|
-
clearInterval(checkInterval);
|
|
1131
|
-
config_1.config.userId = status.userId;
|
|
1132
|
-
config_1.config.pairedAt = new Date().toISOString();
|
|
1133
|
-
console.log(chalk_1.default.green('\n✓ Device paired successfully!'));
|
|
1134
|
-
console.log(chalk_1.default.dim('\nConnecting to receive commands...\n'));
|
|
1135
|
-
// Auto-connect after successful pairing
|
|
1136
|
-
resolve();
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
catch (error) {
|
|
1140
|
-
// Continue waiting
|
|
1141
|
-
}
|
|
1142
|
-
}, 2000);
|
|
1016
|
+
// Helper function to wait for pairing via WebSocket event
|
|
1017
|
+
async function waitForPairing() {
|
|
1018
|
+
return new Promise((resolve) => {
|
|
1019
|
+
websocket_1.wsClient.on('pair_device', (data) => {
|
|
1020
|
+
resolve({ mobileDeviceId: data.mobileDeviceId });
|
|
1021
|
+
});
|
|
1143
1022
|
// Handle Ctrl+C
|
|
1144
1023
|
process.on('SIGINT', () => {
|
|
1145
|
-
clearInterval(checkInterval);
|
|
1146
1024
|
console.log(chalk_1.default.yellow('\nPairing cancelled.'));
|
|
1025
|
+
websocket_1.wsClient.disconnect();
|
|
1147
1026
|
process.exit(0);
|
|
1148
1027
|
});
|
|
1149
1028
|
});
|
|
1150
1029
|
}
|
|
1030
|
+
// Logs command — list and manage debug log files
|
|
1031
|
+
program
|
|
1032
|
+
.command('logs')
|
|
1033
|
+
.description('List debug log files for troubleshooting')
|
|
1034
|
+
.option('--clean', 'Delete all debug log files')
|
|
1035
|
+
.option('--latest', 'Print path to the most recent log file')
|
|
1036
|
+
.action((options) => {
|
|
1037
|
+
const logDir = path.join(os.homedir(), '.forkoff-cli', 'logs');
|
|
1038
|
+
if (!fs.existsSync(logDir)) {
|
|
1039
|
+
console.log(chalk_1.default.dim('No debug logs found. Run with --debug to generate logs.'));
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const logFiles = fs.readdirSync(logDir)
|
|
1043
|
+
.filter(f => f.startsWith('debug-') && f.endsWith('.log'))
|
|
1044
|
+
.sort()
|
|
1045
|
+
.reverse();
|
|
1046
|
+
if (logFiles.length === 0) {
|
|
1047
|
+
console.log(chalk_1.default.dim('No debug logs found. Run with --debug to generate logs.'));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (options.clean) {
|
|
1051
|
+
for (const file of logFiles) {
|
|
1052
|
+
try {
|
|
1053
|
+
fs.unlinkSync(path.join(logDir, file));
|
|
1054
|
+
}
|
|
1055
|
+
catch { }
|
|
1056
|
+
}
|
|
1057
|
+
console.log(chalk_1.default.green(`Deleted ${logFiles.length} log file(s).`));
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (options.latest) {
|
|
1061
|
+
console.log(path.join(logDir, logFiles[0]));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
console.log(chalk_1.default.bold('Debug log files:\n'));
|
|
1065
|
+
for (const file of logFiles) {
|
|
1066
|
+
const filePath = path.join(logDir, file);
|
|
1067
|
+
const stat = fs.statSync(filePath);
|
|
1068
|
+
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
1069
|
+
console.log(` ${file} ${chalk_1.default.dim(`(${sizeKB} KB)`)}`);
|
|
1070
|
+
}
|
|
1071
|
+
console.log(chalk_1.default.dim(`\nLog directory: ${logDir}`));
|
|
1072
|
+
console.log(chalk_1.default.dim('Run with --debug to generate a new log file.'));
|
|
1073
|
+
});
|
|
1151
1074
|
// Help command
|
|
1152
1075
|
program
|
|
1153
1076
|
.command('help')
|