forkoff 1.0.17 → 1.0.18

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.
Files changed (156) hide show
  1. package/dist/approval.d.ts +1 -0
  2. package/dist/approval.js +9 -0
  3. package/dist/config.d.ts +3 -0
  4. package/dist/config.js +62 -16
  5. package/dist/crypto/e2eeManager.d.ts +49 -52
  6. package/dist/crypto/e2eeManager.js +256 -181
  7. package/dist/crypto/encryption.d.ts +8 -10
  8. package/dist/crypto/encryption.js +29 -94
  9. package/dist/crypto/index.d.ts +10 -0
  10. package/dist/crypto/index.js +22 -0
  11. package/dist/crypto/keyExchange.d.ts +6 -20
  12. package/dist/crypto/keyExchange.js +18 -110
  13. package/dist/crypto/keyGeneration.d.ts +2 -13
  14. package/dist/crypto/keyGeneration.js +14 -88
  15. package/dist/crypto/keyStorage.d.ts +32 -5
  16. package/dist/crypto/keyStorage.js +152 -8
  17. package/dist/crypto/sessionPersistence.d.ts +7 -13
  18. package/dist/crypto/sessionPersistence.js +108 -33
  19. package/dist/crypto/types.d.ts +24 -3
  20. package/dist/crypto/types.js +2 -1
  21. package/dist/crypto/websocketE2EE.d.ts +6 -17
  22. package/dist/crypto/websocketE2EE.js +21 -38
  23. package/dist/index.js +203 -280
  24. package/dist/integration.d.ts +0 -1
  25. package/dist/integration.js +2 -4
  26. package/dist/logger.d.ts +15 -0
  27. package/dist/logger.js +209 -1
  28. package/dist/server.d.ts +30 -0
  29. package/dist/server.js +162 -0
  30. package/dist/startup.js +15 -6
  31. package/dist/terminal.d.ts +1 -0
  32. package/dist/terminal.js +94 -1
  33. package/dist/tools/claude-process.d.ts +8 -0
  34. package/dist/tools/claude-process.js +199 -26
  35. package/dist/tools/claude-sessions.d.ts +1 -0
  36. package/dist/tools/claude-sessions.js +36 -10
  37. package/dist/tools/detector.js +11 -3
  38. package/dist/tools/permission-hook.js +94 -27
  39. package/dist/tools/permission-ipc.d.ts +1 -0
  40. package/dist/tools/permission-ipc.js +61 -14
  41. package/dist/transcript-streamer.d.ts +1 -0
  42. package/dist/transcript-streamer.js +18 -4
  43. package/dist/usage-tracker.d.ts +45 -0
  44. package/dist/usage-tracker.js +243 -0
  45. package/dist/websocket.d.ts +43 -12
  46. package/dist/websocket.js +418 -214
  47. package/package.json +4 -3
  48. package/dist/__tests__/cli-commands.test.d.ts +0 -6
  49. package/dist/__tests__/cli-commands.test.d.ts.map +0 -1
  50. package/dist/__tests__/cli-commands.test.js +0 -213
  51. package/dist/__tests__/cli-commands.test.js.map +0 -1
  52. package/dist/__tests__/crypto/e2e-integration.test.d.ts +0 -17
  53. package/dist/__tests__/crypto/e2e-integration.test.d.ts.map +0 -1
  54. package/dist/__tests__/crypto/e2e-integration.test.js +0 -338
  55. package/dist/__tests__/crypto/e2e-integration.test.js.map +0 -1
  56. package/dist/__tests__/crypto/e2eeManager.test.d.ts +0 -2
  57. package/dist/__tests__/crypto/e2eeManager.test.d.ts.map +0 -1
  58. package/dist/__tests__/crypto/e2eeManager.test.js +0 -242
  59. package/dist/__tests__/crypto/e2eeManager.test.js.map +0 -1
  60. package/dist/__tests__/crypto/encryption.test.d.ts +0 -2
  61. package/dist/__tests__/crypto/encryption.test.d.ts.map +0 -1
  62. package/dist/__tests__/crypto/encryption.test.js +0 -116
  63. package/dist/__tests__/crypto/encryption.test.js.map +0 -1
  64. package/dist/__tests__/crypto/keyExchange.test.d.ts +0 -2
  65. package/dist/__tests__/crypto/keyExchange.test.d.ts.map +0 -1
  66. package/dist/__tests__/crypto/keyExchange.test.js +0 -84
  67. package/dist/__tests__/crypto/keyExchange.test.js.map +0 -1
  68. package/dist/__tests__/crypto/keyGeneration.test.d.ts +0 -2
  69. package/dist/__tests__/crypto/keyGeneration.test.d.ts.map +0 -1
  70. package/dist/__tests__/crypto/keyGeneration.test.js +0 -61
  71. package/dist/__tests__/crypto/keyGeneration.test.js.map +0 -1
  72. package/dist/__tests__/crypto/keyStorage.test.d.ts +0 -2
  73. package/dist/__tests__/crypto/keyStorage.test.d.ts.map +0 -1
  74. package/dist/__tests__/crypto/keyStorage.test.js +0 -133
  75. package/dist/__tests__/crypto/keyStorage.test.js.map +0 -1
  76. package/dist/__tests__/crypto/websocketIntegration.test.d.ts +0 -2
  77. package/dist/__tests__/crypto/websocketIntegration.test.d.ts.map +0 -1
  78. package/dist/__tests__/crypto/websocketIntegration.test.js +0 -259
  79. package/dist/__tests__/crypto/websocketIntegration.test.js.map +0 -1
  80. package/dist/__tests__/startup.test.d.ts +0 -11
  81. package/dist/__tests__/startup.test.d.ts.map +0 -1
  82. package/dist/__tests__/startup.test.js +0 -241
  83. package/dist/__tests__/startup.test.js.map +0 -1
  84. package/dist/__tests__/tools/claude-process.test.d.ts +0 -8
  85. package/dist/__tests__/tools/claude-process.test.d.ts.map +0 -1
  86. package/dist/__tests__/tools/claude-process.test.js +0 -430
  87. package/dist/__tests__/tools/claude-process.test.js.map +0 -1
  88. package/dist/__tests__/tools/permission-hook.test.d.ts +0 -17
  89. package/dist/__tests__/tools/permission-hook.test.d.ts.map +0 -1
  90. package/dist/__tests__/tools/permission-hook.test.js +0 -616
  91. package/dist/__tests__/tools/permission-hook.test.js.map +0 -1
  92. package/dist/__tests__/tools/permission-ipc.test.d.ts +0 -11
  93. package/dist/__tests__/tools/permission-ipc.test.d.ts.map +0 -1
  94. package/dist/__tests__/tools/permission-ipc.test.js +0 -612
  95. package/dist/__tests__/tools/permission-ipc.test.js.map +0 -1
  96. package/dist/__tests__/websocket.test.d.ts +0 -13
  97. package/dist/__tests__/websocket.test.d.ts.map +0 -1
  98. package/dist/__tests__/websocket.test.js +0 -204
  99. package/dist/__tests__/websocket.test.js.map +0 -1
  100. package/dist/api.d.ts +0 -44
  101. package/dist/api.d.ts.map +0 -1
  102. package/dist/api.js +0 -76
  103. package/dist/api.js.map +0 -1
  104. package/dist/approval.d.ts.map +0 -1
  105. package/dist/approval.js.map +0 -1
  106. package/dist/config.d.ts.map +0 -1
  107. package/dist/config.js.map +0 -1
  108. package/dist/crypto/e2eeManager.d.ts.map +0 -1
  109. package/dist/crypto/e2eeManager.js.map +0 -1
  110. package/dist/crypto/encryption.d.ts.map +0 -1
  111. package/dist/crypto/encryption.js.map +0 -1
  112. package/dist/crypto/keyExchange.d.ts.map +0 -1
  113. package/dist/crypto/keyExchange.js.map +0 -1
  114. package/dist/crypto/keyGeneration.d.ts.map +0 -1
  115. package/dist/crypto/keyGeneration.js.map +0 -1
  116. package/dist/crypto/keyStorage.d.ts.map +0 -1
  117. package/dist/crypto/keyStorage.js.map +0 -1
  118. package/dist/crypto/sessionPersistence.d.ts.map +0 -1
  119. package/dist/crypto/sessionPersistence.js.map +0 -1
  120. package/dist/crypto/types.d.ts.map +0 -1
  121. package/dist/crypto/types.js.map +0 -1
  122. package/dist/crypto/websocketE2EE.d.ts.map +0 -1
  123. package/dist/crypto/websocketE2EE.js.map +0 -1
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js.map +0 -1
  126. package/dist/integration.d.ts.map +0 -1
  127. package/dist/integration.js.map +0 -1
  128. package/dist/logger.d.ts.map +0 -1
  129. package/dist/logger.js.map +0 -1
  130. package/dist/startup.d.ts.map +0 -1
  131. package/dist/startup.js.map +0 -1
  132. package/dist/terminal.d.ts.map +0 -1
  133. package/dist/terminal.js.map +0 -1
  134. package/dist/tools/__tests__/claude-sessions.test.d.ts +0 -2
  135. package/dist/tools/__tests__/claude-sessions.test.d.ts.map +0 -1
  136. package/dist/tools/__tests__/claude-sessions.test.js +0 -306
  137. package/dist/tools/__tests__/claude-sessions.test.js.map +0 -1
  138. package/dist/tools/claude-hooks.d.ts.map +0 -1
  139. package/dist/tools/claude-hooks.js.map +0 -1
  140. package/dist/tools/claude-process.d.ts.map +0 -1
  141. package/dist/tools/claude-process.js.map +0 -1
  142. package/dist/tools/claude-sessions.d.ts.map +0 -1
  143. package/dist/tools/claude-sessions.js.map +0 -1
  144. package/dist/tools/detector.d.ts.map +0 -1
  145. package/dist/tools/detector.js.map +0 -1
  146. package/dist/tools/index.d.ts.map +0 -1
  147. package/dist/tools/index.js.map +0 -1
  148. package/dist/tools/permission-hook.d.ts.map +0 -1
  149. package/dist/tools/permission-hook.js.map +0 -1
  150. package/dist/tools/permission-ipc.d.ts.map +0 -1
  151. package/dist/tools/permission-ipc.js.map +0 -1
  152. package/dist/transcript-streamer.d.ts.map +0 -1
  153. package/dist/transcript-streamer.js.map +0 -1
  154. package/dist/websocket.d.ts.map +0 -1
  155. package/dist/websocket.js.map +0 -1
  156. package/jest.config.js +0 -18
@@ -25,6 +25,5 @@ export { wsClient } from './websocket';
25
25
  export { approvalManager } from './approval';
26
26
  export { terminalManager } from './terminal';
27
27
  export { config } from './config';
28
- export { api } from './api';
29
28
  export { claudeSessionDetector } from './tools';
30
29
  //# sourceMappingURL=integration.d.ts.map
@@ -6,7 +6,7 @@
6
6
  * It handles approval requests and terminal output streaming.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.claudeSessionDetector = exports.api = exports.config = exports.terminalManager = exports.approvalManager = exports.wsClient = void 0;
9
+ exports.claudeSessionDetector = exports.config = exports.terminalManager = exports.approvalManager = exports.wsClient = void 0;
10
10
  exports.createIntegration = createIntegration;
11
11
  const websocket_1 = require("./websocket");
12
12
  const approval_1 = require("./approval");
@@ -30,7 +30,7 @@ function createIntegration() {
30
30
  if (!config_1.config.isPaired) {
31
31
  throw new Error('Device not paired. Run "forkoff pair" and scan the QR code.');
32
32
  }
33
- await websocket_1.wsClient.connect();
33
+ await websocket_1.wsClient.startServer(config_1.config.relayPort);
34
34
  },
35
35
  disconnect: () => {
36
36
  websocket_1.wsClient.disconnect();
@@ -77,8 +77,6 @@ var terminal_1 = require("./terminal");
77
77
  Object.defineProperty(exports, "terminalManager", { enumerable: true, get: function () { return terminal_1.terminalManager; } });
78
78
  var config_2 = require("./config");
79
79
  Object.defineProperty(exports, "config", { enumerable: true, get: function () { return config_2.config; } });
80
- var api_1 = require("./api");
81
- Object.defineProperty(exports, "api", { enumerable: true, get: function () { return api_1.api; } });
82
80
  var tools_2 = require("./tools");
83
81
  Object.defineProperty(exports, "claudeSessionDetector", { enumerable: true, get: function () { return tools_2.claudeSessionDetector; } });
84
82
  //# sourceMappingURL=integration.js.map
package/dist/logger.d.ts CHANGED
@@ -1,5 +1,20 @@
1
1
  import ora from 'ora';
2
2
  export declare function setQuiet(value: boolean): void;
3
+ /**
4
+ * Enable debug mode: tee all console output to a timestamped log file.
5
+ * Also enables process.env.DEBUG for verbose logging throughout the codebase.
6
+ */
7
+ export declare function setDebug(value: boolean): void;
3
8
  export declare function isQuiet(): boolean;
9
+ export declare function isDebug(): boolean;
10
+ export declare function getLogFilePath(): string | null;
11
+ /**
12
+ * Flush and close the debug log stream. Call on process exit.
13
+ */
14
+ export declare function closeDebugLog(): void;
15
+ /**
16
+ * Clean up old debug log files, keeping only the most recent N.
17
+ */
18
+ export declare function cleanupOldLogs(keepCount?: number): void;
4
19
  export declare function createSpinner(text: string): ora.Ora;
5
20
  //# sourceMappingURL=logger.d.ts.map
package/dist/logger.js CHANGED
@@ -1,13 +1,57 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.setQuiet = setQuiet;
40
+ exports.setDebug = setDebug;
7
41
  exports.isQuiet = isQuiet;
42
+ exports.isDebug = isDebug;
43
+ exports.getLogFilePath = getLogFilePath;
44
+ exports.closeDebugLog = closeDebugLog;
45
+ exports.cleanupOldLogs = cleanupOldLogs;
8
46
  exports.createSpinner = createSpinner;
9
47
  const ora_1 = __importDefault(require("ora"));
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const os = __importStar(require("os"));
10
51
  let quiet = false;
52
+ let debugMode = false;
53
+ let logFilePath = null;
54
+ let logStream = null;
11
55
  const originalConsole = {
12
56
  log: console.log,
13
57
  error: console.error,
@@ -15,9 +59,41 @@ const originalConsole = {
15
59
  info: console.info,
16
60
  debug: console.debug,
17
61
  };
62
+ /**
63
+ * Format a log line with ISO timestamp and level prefix.
64
+ */
65
+ function formatLogLine(level, args) {
66
+ const timestamp = new Date().toISOString();
67
+ const message = args.map(a => {
68
+ if (typeof a === 'string')
69
+ return a;
70
+ try {
71
+ return JSON.stringify(a);
72
+ }
73
+ catch {
74
+ return String(a);
75
+ }
76
+ }).join(' ');
77
+ // Strip ANSI color codes for clean log files
78
+ const clean = message.replace(/\x1B\[[0-9;]*m/g, '');
79
+ return `[${timestamp}] [${level}] ${clean}\n`;
80
+ }
81
+ /**
82
+ * Write a line to the debug log file (if open).
83
+ */
84
+ function writeToLogFile(level, args) {
85
+ if (!logStream)
86
+ return;
87
+ try {
88
+ logStream.write(formatLogLine(level, args));
89
+ }
90
+ catch {
91
+ // Swallow write errors — don't crash the CLI for logging
92
+ }
93
+ }
18
94
  function setQuiet(value) {
19
95
  quiet = value;
20
- if (value) {
96
+ if (value && !debugMode) {
21
97
  const noop = () => { };
22
98
  console.log = noop;
23
99
  console.error = noop;
@@ -25,6 +101,14 @@ function setQuiet(value) {
25
101
  console.info = noop;
26
102
  console.debug = noop;
27
103
  }
104
+ else if (value && debugMode) {
105
+ // Quiet + debug: suppress terminal output but still write to log file
106
+ console.log = (...args) => writeToLogFile('LOG', args);
107
+ console.error = (...args) => writeToLogFile('ERROR', args);
108
+ console.warn = (...args) => writeToLogFile('WARN', args);
109
+ console.info = (...args) => writeToLogFile('INFO', args);
110
+ console.debug = (...args) => writeToLogFile('DEBUG', args);
111
+ }
28
112
  else {
29
113
  console.log = originalConsole.log;
30
114
  console.error = originalConsole.error;
@@ -33,9 +117,133 @@ function setQuiet(value) {
33
117
  console.debug = originalConsole.debug;
34
118
  }
35
119
  }
120
+ /**
121
+ * Enable debug mode: tee all console output to a timestamped log file.
122
+ * Also enables process.env.DEBUG for verbose logging throughout the codebase.
123
+ */
124
+ function setDebug(value) {
125
+ debugMode = value;
126
+ if (!value) {
127
+ // Disable debug mode
128
+ if (logStream) {
129
+ logStream.end();
130
+ logStream = null;
131
+ }
132
+ logFilePath = null;
133
+ process.env.DEBUG = '';
134
+ return;
135
+ }
136
+ // Enable DEBUG env var so existing DEBUG-gated logs activate
137
+ process.env.DEBUG = '1';
138
+ // Create log directory
139
+ const logDir = path.join(os.homedir(), '.forkoff-cli', 'logs');
140
+ try {
141
+ fs.mkdirSync(logDir, { recursive: true, mode: 0o700 });
142
+ }
143
+ catch {
144
+ originalConsole.error('[Debug] Failed to create log directory:', logDir);
145
+ return;
146
+ }
147
+ // Create timestamped log file
148
+ const now = new Date();
149
+ const stamp = now.toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
150
+ logFilePath = path.join(logDir, `debug-${stamp}.log`);
151
+ try {
152
+ // SECURITY: Check for pre-existing symlink at the log path
153
+ if (fs.existsSync(logFilePath)) {
154
+ const stat = fs.lstatSync(logFilePath);
155
+ if (stat.isSymbolicLink()) {
156
+ originalConsole.error('[Debug] Symlink detected at log path, refusing to write');
157
+ logFilePath = null;
158
+ return;
159
+ }
160
+ }
161
+ logStream = fs.createWriteStream(logFilePath, { flags: 'w', mode: 0o600 });
162
+ }
163
+ catch {
164
+ originalConsole.error('[Debug] Failed to create log file');
165
+ logFilePath = null;
166
+ return;
167
+ }
168
+ // Write system info header
169
+ const header = [
170
+ `=== ForkOff CLI Debug Log ===`,
171
+ `Date: ${now.toISOString()}`,
172
+ `Platform: ${os.platform()} ${os.release()} (${os.arch()})`,
173
+ `Node: ${process.version}`,
174
+ `PID: ${process.pid}`,
175
+ `User: ${os.userInfo().username}`,
176
+ `Home: ${os.homedir()}`,
177
+ `===`,
178
+ '',
179
+ ].join('\n');
180
+ logStream.write(header + '\n');
181
+ // Wrap console methods to tee to both terminal and log file
182
+ console.log = (...args) => {
183
+ originalConsole.log(...args);
184
+ writeToLogFile('LOG', args);
185
+ };
186
+ console.error = (...args) => {
187
+ originalConsole.error(...args);
188
+ writeToLogFile('ERROR', args);
189
+ };
190
+ console.warn = (...args) => {
191
+ originalConsole.warn(...args);
192
+ writeToLogFile('WARN', args);
193
+ };
194
+ console.info = (...args) => {
195
+ originalConsole.info(...args);
196
+ writeToLogFile('INFO', args);
197
+ };
198
+ console.debug = (...args) => {
199
+ originalConsole.debug(...args);
200
+ writeToLogFile('DEBUG', args);
201
+ };
202
+ originalConsole.log(`[Debug] Logging to: ${logFilePath}`);
203
+ }
36
204
  function isQuiet() {
37
205
  return quiet;
38
206
  }
207
+ function isDebug() {
208
+ return debugMode;
209
+ }
210
+ function getLogFilePath() {
211
+ return logFilePath;
212
+ }
213
+ /**
214
+ * Flush and close the debug log stream. Call on process exit.
215
+ */
216
+ function closeDebugLog() {
217
+ if (logStream) {
218
+ logStream.write(formatLogLine('LOG', ['=== Debug log closed ===']));
219
+ logStream.end();
220
+ logStream = null;
221
+ }
222
+ }
223
+ /**
224
+ * Clean up old debug log files, keeping only the most recent N.
225
+ */
226
+ function cleanupOldLogs(keepCount = 10) {
227
+ const logDir = path.join(os.homedir(), '.forkoff-cli', 'logs');
228
+ try {
229
+ const files = fs.readdirSync(logDir)
230
+ .filter(f => f.startsWith('debug-') && f.endsWith('.log'))
231
+ .sort()
232
+ .reverse();
233
+ // Delete files beyond keepCount
234
+ for (let i = keepCount; i < files.length; i++) {
235
+ try {
236
+ fs.unlinkSync(path.join(logDir, files[i]));
237
+ }
238
+ catch {
239
+ // Ignore deletion errors
240
+ }
241
+ }
242
+ }
243
+ catch {
244
+ // Log directory may not exist
245
+ }
246
+ }
39
247
  function createSpinner(text) {
40
248
  return (0, ora_1.default)({ text, isSilent: quiet });
41
249
  }
@@ -0,0 +1,30 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface EmbeddedServerOptions {
3
+ port: number;
4
+ deviceId: string;
5
+ deviceName: string;
6
+ }
7
+ export declare class EmbeddedRelayServer extends EventEmitter {
8
+ private httpServer;
9
+ private io;
10
+ private mobileSocket;
11
+ private port;
12
+ private deviceId;
13
+ private deviceName;
14
+ /** The pairing code the CLI generated — validated in-process */
15
+ private currentPairingCode;
16
+ constructor(options: EmbeddedServerOptions);
17
+ /** Set the pairing code for in-process validation */
18
+ setPairingCode(code: string): void;
19
+ /** Start the HTTP + Socket.io server */
20
+ start(): Promise<void>;
21
+ /** Emit an event to the connected mobile socket */
22
+ emitToMobile(event: string, data: any): void;
23
+ /** Check if a mobile client is connected */
24
+ hasMobileConnection(): boolean;
25
+ /** Get the connected mobile device ID (for E2EE targeting) */
26
+ getMobileDeviceId(): string | null;
27
+ /** Graceful shutdown */
28
+ stop(): Promise<void>;
29
+ }
30
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmbeddedRelayServer = void 0;
4
+ /**
5
+ * Embedded relay server — turns the CLI into a Socket.io server.
6
+ * Mobile connects directly to the CLI over the local network.
7
+ * No rooms, no multi-device routing — just one CLI serving mobile connections.
8
+ */
9
+ const http_1 = require("http");
10
+ const socket_io_1 = require("socket.io");
11
+ const events_1 = require("events");
12
+ /** Events the server forwards from mobile → CLI (internal EventEmitter) */
13
+ const MOBILE_EVENTS = [
14
+ 'terminal_command', 'terminal_create', 'user_message',
15
+ 'claude_start_session', 'claude_resume_session', 'claude_sessions_request',
16
+ 'directory_list', 'read_file', 'transcript_fetch', 'transcript_subscribe',
17
+ 'transcript_unsubscribe', 'approval_response', 'claude_approval_response',
18
+ 'permission_response', 'permission_rules_sync', 'claude_abort', 'tab_complete',
19
+ 'subscribe_device', 'unsubscribe_device',
20
+ 'encrypted_key_exchange_init', 'encrypted_key_exchange_ack', 'encrypted_message',
21
+ ];
22
+ class EmbeddedRelayServer extends events_1.EventEmitter {
23
+ constructor(options) {
24
+ super();
25
+ this.httpServer = null;
26
+ this.io = null;
27
+ this.mobileSocket = null;
28
+ /** The pairing code the CLI generated — validated in-process */
29
+ this.currentPairingCode = null;
30
+ this.port = options.port;
31
+ this.deviceId = options.deviceId;
32
+ this.deviceName = options.deviceName;
33
+ }
34
+ /** Set the pairing code for in-process validation */
35
+ setPairingCode(code) {
36
+ this.currentPairingCode = code;
37
+ }
38
+ /** Start the HTTP + Socket.io server */
39
+ start() {
40
+ return new Promise((resolve, reject) => {
41
+ this.httpServer = (0, http_1.createServer)((req, res) => {
42
+ if (req.url === '/health' && req.method === 'GET') {
43
+ res.writeHead(200, { 'Content-Type': 'application/json' });
44
+ res.end(JSON.stringify({ status: 'ok', mode: 'embedded' }));
45
+ return;
46
+ }
47
+ res.writeHead(404);
48
+ res.end();
49
+ });
50
+ this.io = new socket_io_1.Server(this.httpServer, {
51
+ cors: {
52
+ origin: '*',
53
+ methods: ['GET', 'POST'],
54
+ },
55
+ transports: ['websocket', 'polling'],
56
+ });
57
+ // Auth middleware — only accept mobile clients
58
+ this.io.use((socket, next) => {
59
+ const auth = socket.handshake.auth;
60
+ if (auth.clientType !== 'mobile') {
61
+ return next(new Error('CLI only accepts mobile connections'));
62
+ }
63
+ if (!auth.mobileDeviceId || auth.mobileDeviceId.length < 8) {
64
+ return next(new Error('Invalid mobileDeviceId'));
65
+ }
66
+ socket.data = {
67
+ clientType: 'mobile',
68
+ deviceId: auth.mobileDeviceId,
69
+ deviceName: auth.deviceName || 'Mobile',
70
+ };
71
+ next();
72
+ });
73
+ this.io.on('connection', (socket) => {
74
+ console.log(`[Server] Mobile connected: ${socket.data.deviceId}`);
75
+ // Track the mobile socket (only one active connection)
76
+ if (this.mobileSocket) {
77
+ console.log(`[Server] Replacing existing mobile connection`);
78
+ this.mobileSocket.disconnect(true);
79
+ }
80
+ this.mobileSocket = socket;
81
+ this.emit('mobile_connected', { deviceId: socket.data.deviceId });
82
+ // Handle pairing
83
+ socket.on('pair_device', (data) => {
84
+ if (this.currentPairingCode && data.pairingCode === this.currentPairingCode) {
85
+ console.log(`[Server] Pairing successful`);
86
+ // Notify internal listeners (wsClient)
87
+ this.emit('pair_device', { mobileDeviceId: socket.data.deviceId });
88
+ // Send ack directly to mobile
89
+ socket.emit('pair_device_ack', {
90
+ deviceId: this.deviceId,
91
+ deviceName: this.deviceName,
92
+ platform: process.platform,
93
+ mobileDeviceId: socket.data.deviceId,
94
+ });
95
+ }
96
+ else {
97
+ console.log(`[Server] Pairing rejected — invalid code`);
98
+ socket.emit('pair_device_reject', { reason: 'Invalid pairing code' });
99
+ }
100
+ });
101
+ // Forward all mobile events → internal EventEmitter
102
+ for (const event of MOBILE_EVENTS) {
103
+ socket.on(event, (data) => {
104
+ this.emit(event, data);
105
+ });
106
+ }
107
+ socket.on('disconnect', (reason) => {
108
+ console.log(`[Server] Mobile disconnected: ${reason}`);
109
+ if (this.mobileSocket === socket) {
110
+ this.mobileSocket = null;
111
+ }
112
+ this.emit('mobile_disconnected', { deviceId: socket.data.deviceId, reason });
113
+ });
114
+ });
115
+ this.httpServer.on('error', (err) => {
116
+ if (err.code === 'EADDRINUSE') {
117
+ reject(new Error(`Port ${this.port} is already in use. Use "forkoff config --port <port>" to change it.`));
118
+ }
119
+ else {
120
+ reject(err);
121
+ }
122
+ });
123
+ this.httpServer.listen(this.port, '0.0.0.0', () => {
124
+ console.log(`[Server] Listening on 0.0.0.0:${this.port}`);
125
+ resolve();
126
+ });
127
+ });
128
+ }
129
+ /** Emit an event to the connected mobile socket */
130
+ emitToMobile(event, data) {
131
+ if (this.mobileSocket) {
132
+ this.mobileSocket.emit(event, data);
133
+ }
134
+ }
135
+ /** Check if a mobile client is connected */
136
+ hasMobileConnection() {
137
+ return this.mobileSocket !== null && this.mobileSocket.connected;
138
+ }
139
+ /** Get the connected mobile device ID (for E2EE targeting) */
140
+ getMobileDeviceId() {
141
+ return this.mobileSocket?.data?.deviceId ?? null;
142
+ }
143
+ /** Graceful shutdown */
144
+ stop() {
145
+ return new Promise((resolve) => {
146
+ if (this.io) {
147
+ this.io.close(() => {
148
+ this.httpServer?.close(() => resolve());
149
+ });
150
+ }
151
+ else if (this.httpServer) {
152
+ this.httpServer.close(() => resolve());
153
+ }
154
+ else {
155
+ resolve();
156
+ }
157
+ this.mobileSocket = null;
158
+ });
159
+ }
160
+ }
161
+ exports.EmbeddedRelayServer = EmbeddedRelayServer;
162
+ //# sourceMappingURL=server.js.map
package/dist/startup.js CHANGED
@@ -124,7 +124,7 @@ async function disableStartup() {
124
124
  async function enableStartupWindows(binaryPath) {
125
125
  const startupDir = getStartupDir();
126
126
  if (!fs.existsSync(startupDir)) {
127
- fs.mkdirSync(startupDir, { recursive: true });
127
+ fs.mkdirSync(startupDir, { recursive: true, mode: 0o700 });
128
128
  }
129
129
  // Resolve binaryPath to the .cmd shim if it exists (npm global installs create .cmd on Windows)
130
130
  let cmdPath = binaryPath;
@@ -134,12 +134,21 @@ async function enableStartupWindows(binaryPath) {
134
134
  cmdPath = candidate;
135
135
  }
136
136
  }
137
+ // SECURITY: Validate binary path exists
138
+ if (!fs.existsSync(cmdPath)) {
139
+ throw new Error(`Startup binary not found: ${cmdPath}`);
140
+ }
141
+ // SECURITY: Reject paths with VBScript metacharacters to prevent injection
142
+ if (/[^a-zA-Z0-9_\-./\\ :]/.test(cmdPath)) {
143
+ throw new Error(`Startup binary path contains disallowed characters: ${cmdPath}`);
144
+ }
137
145
  // Write a .vbs (VBScript) wrapper that launches the command with a hidden window.
138
146
  // A .bat would open a visible cmd.exe window on every login — bad UX.
139
147
  // WScript.Shell.Run with windowStyle 0 = hidden, False = don't wait.
140
148
  const vbsPath = getVbsPath();
141
- const vbsContent = `CreateObject("WScript.Shell").Run """${cmdPath}"" connect --quiet", 0, False\r\n`;
142
- fs.writeFileSync(vbsPath, vbsContent);
149
+ const escapedPath = cmdPath.replace(/"/g, '""');
150
+ const vbsContent = `CreateObject("WScript.Shell").Run """${escapedPath}"" connect --quiet", 0, False\r\n`;
151
+ fs.writeFileSync(vbsPath, vbsContent, { mode: 0o600 });
143
152
  // Use HKCU Run key — no admin required, runs on user logon
144
153
  (0, child_process_1.execSync)(`reg add "${REG_KEY}" /v ${REG_VALUE} /t REG_SZ /d "\\"${vbsPath}\\"" /f`, { stdio: 'pipe' });
145
154
  }
@@ -170,7 +179,7 @@ async function enableStartupMacOS(binaryPath) {
170
179
  await disableStartupMacOS();
171
180
  const configDir = path.join(os.homedir(), '.config', 'forkoff-cli');
172
181
  if (!fs.existsSync(configDir)) {
173
- fs.mkdirSync(configDir, { recursive: true });
182
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
174
183
  }
175
184
  // Use the current node binary explicitly as the first ProgramArgument.
176
185
  // Users with nvm/fnm have node outside the default PATH, so launchd
@@ -218,9 +227,9 @@ async function enableStartupMacOS(binaryPath) {
218
227
  const plistPath = getPlistPath();
219
228
  const launchAgentsDir = path.dirname(plistPath);
220
229
  if (!fs.existsSync(launchAgentsDir)) {
221
- fs.mkdirSync(launchAgentsDir, { recursive: true });
230
+ fs.mkdirSync(launchAgentsDir, { recursive: true, mode: 0o700 });
222
231
  }
223
- fs.writeFileSync(plistPath, plist);
232
+ fs.writeFileSync(plistPath, plist, { mode: 0o600 });
224
233
  try {
225
234
  (0, child_process_1.execSync)(`launchctl load "${plistPath}"`, { stdio: 'pipe' });
226
235
  }
@@ -6,6 +6,7 @@ interface TerminalSession {
6
6
  cwd: string;
7
7
  }
8
8
  declare class TerminalManager extends EventEmitter {
9
+ private static readonly MAX_SESSIONS;
9
10
  private sessions;
10
11
  private defaultShell;
11
12
  constructor();
package/dist/terminal.js CHANGED
@@ -38,6 +38,90 @@ const child_process_1 = require("child_process");
38
38
  const events_1 = require("events");
39
39
  const os = __importStar(require("os"));
40
40
  const path = __importStar(require("path"));
41
+ function getSafeEnv() {
42
+ const sensitivePatterns = [
43
+ /^AWS_/i,
44
+ /^AZURE_/i,
45
+ /^GCP_/i,
46
+ /^GOOGLE_/i,
47
+ /SECRET/i,
48
+ /PASSWORD/i,
49
+ /PRIVATE_KEY/i,
50
+ /^SUPABASE_SERVICE/i,
51
+ /^DATABASE_URL$/i,
52
+ /^ADMIN_API_KEY$/i,
53
+ // Prevent code injection via environment variables
54
+ /^NODE_OPTIONS$/i,
55
+ /^NODE_EXTRA_CA_CERTS$/i,
56
+ /^LD_PRELOAD$/i,
57
+ /^LD_LIBRARY_PATH$/i,
58
+ /^DYLD_INSERT_LIBRARIES$/i,
59
+ /^DYLD_LIBRARY_PATH$/i,
60
+ /^ELECTRON_RUN_AS_NODE$/i,
61
+ // Language-specific code injection vectors
62
+ /^PYTHONPATH$/i,
63
+ /^PYTHONSTARTUP$/i,
64
+ /^RUBYLIB$/i,
65
+ /^PERL5LIB$/i,
66
+ /^PERL5OPT$/i,
67
+ /^JAVA_TOOL_OPTIONS$/i,
68
+ /^_JAVA_OPTIONS$/i,
69
+ // Git/SSH injection
70
+ /^GIT_SSH_COMMAND$/i,
71
+ /^GIT_EXEC_PATH$/i,
72
+ // Pager/editor injection
73
+ /^LESSOPEN$/i,
74
+ /^LESSCLOSE$/i,
75
+ // Shell startup injection
76
+ /^BASH_ENV$/i,
77
+ /^ENV$/i,
78
+ /^PROMPT_COMMAND$/i,
79
+ /^SHELLOPTS$/i,
80
+ // Field separator injection
81
+ /^IFS$/i,
82
+ // Editor/browser auto-launch injection
83
+ /^EDITOR$/i,
84
+ /^VISUAL$/i,
85
+ /^BROWSER$/i,
86
+ // Proxy injection (MITM child process HTTP traffic)
87
+ /^HTTPS?_PROXY$/i,
88
+ /^ALL_PROXY$/i,
89
+ /^NO_PROXY$/i,
90
+ // TLS verification bypass
91
+ /^SSL_CERT_FILE$/i,
92
+ /^SSL_CERT_DIR$/i,
93
+ /^NODE_TLS_REJECT_UNAUTHORIZED$/i,
94
+ // npm config injection
95
+ /^npm_config_/i,
96
+ // Pager injection (git, man, etc.)
97
+ /^PAGER$/i,
98
+ // Zsh startup injection
99
+ /^ZDOTDIR$/i,
100
+ // Curl config injection
101
+ /^CURL_HOME$/i,
102
+ // Third-party API keys (defense-in-depth for child processes)
103
+ /^OPENAI_/i,
104
+ /^ANTHROPIC_/i,
105
+ /^GITHUB_TOKEN$/i,
106
+ /^GITLAB_TOKEN$/i,
107
+ /^NPM_TOKEN$/i,
108
+ /^DOCKER_PASSWORD$/i,
109
+ /^SLACK_TOKEN$/i,
110
+ /^SLACK_BOT_TOKEN$/i,
111
+ /^SENDGRID_/i,
112
+ /^TWILIO_/i,
113
+ /^DATADOG_/i,
114
+ /TOKEN$/i,
115
+ /API_KEY$/i,
116
+ ];
117
+ const filtered = {};
118
+ for (const [key, value] of Object.entries(process.env)) {
119
+ if (!sensitivePatterns.some(pattern => pattern.test(key))) {
120
+ filtered[key] = value;
121
+ }
122
+ }
123
+ return filtered;
124
+ }
41
125
  class TerminalManager extends events_1.EventEmitter {
42
126
  constructor() {
43
127
  super();
@@ -51,6 +135,14 @@ class TerminalManager extends events_1.EventEmitter {
51
135
  return process.env.SHELL || '/bin/bash';
52
136
  }
53
137
  createSession(terminalSessionId, cwd) {
138
+ // Evict oldest session if at cap (FIFO)
139
+ if (this.sessions.size >= TerminalManager.MAX_SESSIONS) {
140
+ const oldestKey = this.sessions.keys().next().value;
141
+ if (oldestKey) {
142
+ console.warn(`[Terminal] MAX_SESSIONS (${TerminalManager.MAX_SESSIONS}) reached, evicting oldest: ${oldestKey}`);
143
+ this.closeSession(oldestKey);
144
+ }
145
+ }
54
146
  // Default to home directory, not process.cwd()
55
147
  let resolvedCwd = cwd || os.homedir();
56
148
  // Resolve ~ to home directory
@@ -82,7 +174,7 @@ class TerminalManager extends events_1.EventEmitter {
82
174
  const shellArgs = isWindows ? ['/c', command] : ['-c', command];
83
175
  const proc = (0, child_process_1.spawn)(shell, shellArgs, {
84
176
  cwd: session.cwd,
85
- env: process.env,
177
+ env: getSafeEnv(),
86
178
  shell: false,
87
179
  });
88
180
  let stdout = '';
@@ -166,6 +258,7 @@ class TerminalManager extends events_1.EventEmitter {
166
258
  }
167
259
  }
168
260
  }
261
+ TerminalManager.MAX_SESSIONS = 50;
169
262
  exports.terminalManager = new TerminalManager();
170
263
  exports.default = exports.terminalManager;
171
264
  //# sourceMappingURL=terminal.js.map