leetcode-capture-helper 0.0.0-stage → 0.1.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.
Files changed (48) hide show
  1. package/cli.js +92 -0
  2. package/config.js +25 -0
  3. package/framing.js +57 -0
  4. package/host.js +346 -0
  5. package/install.js +290 -0
  6. package/package.json +31 -4
  7. package/runtime/bundle.json +42 -0
  8. package/runtime/companion/capture-log.js +83 -0
  9. package/runtime/companion/codex-backend.js +337 -0
  10. package/runtime/companion/companion.js +2193 -0
  11. package/runtime/companion/mouse-input.js +154 -0
  12. package/runtime/companion/package.json +4 -0
  13. package/runtime/companion/panel-bridge.js +273 -0
  14. package/runtime/companion/settings-rpc.js +228 -0
  15. package/runtime/companion/settings.js +464 -0
  16. package/runtime/companion/terminal-format.js +330 -0
  17. package/runtime/companion/vault-summary.js +169 -0
  18. package/runtime/companion/virtual-terminal.js +138 -0
  19. package/runtime/extension/shared/anthropic-backend.js +193 -0
  20. package/runtime/extension/shared/catalog.js +415 -0
  21. package/runtime/extension/shared/code-ledger.js +333 -0
  22. package/runtime/extension/shared/context-budget.js +259 -0
  23. package/runtime/extension/shared/context-summary.js +271 -0
  24. package/runtime/extension/shared/conversation.js +81 -0
  25. package/runtime/extension/shared/event-hub.js +126 -0
  26. package/runtime/extension/shared/extension-settings.js +445 -0
  27. package/runtime/extension/shared/helper-launcher.js +248 -0
  28. package/runtime/extension/shared/local-backend.js +706 -0
  29. package/runtime/extension/shared/local-server.js +149 -0
  30. package/runtime/extension/shared/package.json +4 -0
  31. package/runtime/extension/shared/practice.js +228 -0
  32. package/runtime/extension/shared/problem-index.js +4128 -0
  33. package/runtime/extension/shared/profiles.js +106 -0
  34. package/runtime/extension/shared/providers.js +58 -0
  35. package/runtime/extension/shared/reply-guard.js +864 -0
  36. package/runtime/extension/shared/roadmap.js +1344 -0
  37. package/runtime/extension/shared/sanitize.js +68 -0
  38. package/runtime/extension/shared/solved.js +277 -0
  39. package/runtime/extension/shared/tutor-engine.js +369 -0
  40. package/runtime/extension/shared/tutor-prompt.js +108 -0
  41. package/runtime/extension/shared/tutor-service.js +520 -0
  42. package/runtime/relay-server/package.json +4 -0
  43. package/runtime/relay-server/panel-broker.js +797 -0
  44. package/runtime/relay-server/server.js +241 -0
  45. package/runtime/vault-tool/log-session.js +230 -0
  46. package/runtime/vault-tool/package.json +4 -0
  47. package/runtime/vault-tool/vault-notes.js +206 -0
  48. package/README.md +0 -3
package/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ // leetcode-capture-helper install | uninstall | status
3
+ //
4
+ // install [--chrome-id <id>]... [--browser <id>]... [--root <folder>]
5
+ // Registers the helper with the browsers found for this user (or only the
6
+ // ones named with --browser). Chrome, Chromium, Edge and Brave need the
7
+ // extension's id, which the extension's settings page shows in the
8
+ // command to copy. --root is the LeetCode Capture folder holding
9
+ // companion/ and relay-server/ (by default the one this helper is in).
10
+ // uninstall Removes exactly what install wrote.
11
+ // status Shows what is registered where.
12
+
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { BROWSER_IDS, install, status, uninstall } from './install.js';
18
+ import { resolveRoot } from './host.js';
19
+
20
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
21
+
22
+ export function parseArgs(argv) {
23
+ const [command, ...rest] = argv;
24
+ const options = { command, chromeIds: [], browsers: [], root: null };
25
+ for (let i = 0; i < rest.length; i += 1) {
26
+ const flag = rest[i];
27
+ const value = rest[i + 1];
28
+ const take = () => {
29
+ if (value === undefined || value.startsWith('--')) throw new Error(`${flag} needs a value`);
30
+ i += 1;
31
+ return value;
32
+ };
33
+ if (flag === '--chrome-id') options.chromeIds.push(...take().split(','));
34
+ else if (flag === '--browser') options.browsers.push(...take().split(','));
35
+ else if (flag === '--root') options.root = take();
36
+ else throw new Error(`unknown option ${flag}`);
37
+ }
38
+ return options;
39
+ }
40
+
41
+ const USAGE = `usage: leetcode-capture-helper <install|uninstall|status> [options]
42
+
43
+ install [--chrome-id <id>]... [--browser <${BROWSER_IDS.join('|')}>]... [--root <folder>]
44
+ uninstall
45
+ status`;
46
+
47
+ export function run(argv, { home = os.homedir(), env = process.env, platform = process.platform, reg, print = console.log } = {}) {
48
+ let options;
49
+ try {
50
+ options = parseArgs(argv);
51
+ } catch (err) {
52
+ print(`${err.message}\n\n${USAGE}`);
53
+ return 2;
54
+ }
55
+ const common = { home, env, platform, ...(reg ? { reg } : {}) };
56
+ try {
57
+ if (options.command === 'install') {
58
+ const result = install({
59
+ ...common,
60
+ root: options.root ? path.resolve(options.root) : resolveRoot(env, HERE),
61
+ chromeIds: options.chromeIds,
62
+ browsers: options.browsers.length ? options.browsers : null,
63
+ });
64
+ if (result.registered.length) print(`Registered the LeetCode Capture helper with: ${result.registered.join(', ')}.`);
65
+ for (const note of result.notes) print(note);
66
+ print(`Files are in ${result.dataDir}. Reload the extension's settings page to see the helper.`);
67
+ return result.registered.length ? 0 : 1;
68
+ }
69
+ if (options.command === 'uninstall') {
70
+ const result = uninstall(common);
71
+ print(result.nothing ? 'The helper is not installed.' : `Removed the helper (${result.removed.length} items).`);
72
+ return 0;
73
+ }
74
+ if (options.command === 'status') {
75
+ const s = status(common);
76
+ print(s.installed ? `Installed (version ${s.version}) in ${s.dataDir}` : 'Not installed.');
77
+ if (s.root) print(`LeetCode Capture folder: ${s.root}${s.rootError ? ` (problem: ${s.rootError})` : ''}`);
78
+ for (const b of s.browsers) print(` ${b.label}: ${b.registered ? `registered for ${b.allowed.join(', ')}` : 'not registered'}`);
79
+ return s.installed ? 0 : 1;
80
+ }
81
+ } catch (err) {
82
+ print(`leetcode-capture-helper: ${err.message}`);
83
+ return 1;
84
+ }
85
+ print(USAGE);
86
+ return 2;
87
+ }
88
+
89
+ // npx and global installs run this through a symlink in node_modules/.bin.
90
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
91
+ process.exitCode = run(process.argv.slice(2));
92
+ }
package/config.js ADDED
@@ -0,0 +1,25 @@
1
+ // The one place that names this helper and the extensions allowed to talk to
2
+ // it. The installer writes these into the browsers' registration files.
3
+
4
+ // Native Messaging host name: lowercase letters, digits, dots and underscores.
5
+ // The extension's background worker connects to this exact name.
6
+ export const HOST_NAME = 'dev.thomsonklee.leetcode_capture';
7
+
8
+ export const HOST_DESCRIPTION = 'LeetCode Capture helper (starts the local tutor companion and relay)';
9
+
10
+ // Firefox: the gecko id from extension/manifest.json
11
+ // (browser_specific_settings.gecko.id). It never changes.
12
+ export const GECKO_ID = 'leetcode-capture@thomsonklee.dev';
13
+
14
+ // Chrome, Chromium, Edge and Brave identify an extension by a 32-letter id.
15
+ // An unpacked copy gets an id from its folder path (or from a `key` in its
16
+ // manifest), and a store copy gets the store's id, so there is no single
17
+ // built-in value yet.
18
+ // Set the published Chrome Web Store id (and the Edge Add-ons id, if it
19
+ // differs) here once they exist; until then `install --chrome-id <id>` adds
20
+ // the id the options page shows (chrome://extensions lists it too).
21
+ export const CHROME_EXTENSION_IDS = [];
22
+
23
+ export const CHROME_ID_RE = /^[a-p]{32}$/;
24
+
25
+ export const VERSION = '0.1.0';
package/framing.js ADDED
@@ -0,0 +1,57 @@
1
+ // Native Messaging framing: every message, in both directions, is a 4-byte
2
+ // little-endian length followed by that many bytes of UTF-8 JSON.
3
+ // https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging
4
+ // Firefox uses the same framing.
5
+
6
+ // The browser refuses a message from the host above 1 MB.
7
+ export const MAX_OUTGOING_BYTES = 1024 * 1024;
8
+ // Messages from the extension are small commands; anything far larger is a
9
+ // broken stream, not a message.
10
+ export const MAX_INCOMING_BYTES = 1024 * 1024;
11
+
12
+ export function encodeMessage(message) {
13
+ const body = Buffer.from(JSON.stringify(message), 'utf8');
14
+ if (body.length > MAX_OUTGOING_BYTES) throw new Error(`message too large (${body.length} bytes)`);
15
+ const header = Buffer.alloc(4);
16
+ header.writeUInt32LE(body.length, 0);
17
+ return Buffer.concat([header, body]);
18
+ }
19
+
20
+ // Feed it chunks as they arrive; it calls onMessage once per whole message,
21
+ // however the chunks split them. onError is called once for a length over
22
+ // the limit or a body that is not JSON, and nothing more is decoded after
23
+ // that (the stream can no longer be trusted to be in step).
24
+ export function createDecoder({ onMessage, onError, maxBytes = MAX_INCOMING_BYTES }) {
25
+ let buffered = Buffer.alloc(0);
26
+ let broken = false;
27
+ return {
28
+ push(chunk) {
29
+ if (broken) return;
30
+ buffered = buffered.length ? Buffer.concat([buffered, chunk]) : Buffer.from(chunk);
31
+ while (buffered.length >= 4) {
32
+ const length = buffered.readUInt32LE(0);
33
+ if (length > maxBytes) {
34
+ broken = true;
35
+ onError(new Error(`incoming message too large (${length} bytes)`));
36
+ return;
37
+ }
38
+ if (buffered.length < 4 + length) return;
39
+ const body = buffered.subarray(4, 4 + length).toString('utf8');
40
+ buffered = buffered.subarray(4 + length);
41
+ let message;
42
+ try {
43
+ message = JSON.parse(body);
44
+ } catch {
45
+ broken = true;
46
+ onError(new Error('incoming message is not JSON'));
47
+ return;
48
+ }
49
+ onMessage(message);
50
+ }
51
+ },
52
+ // Bytes of a message that never finished (for a clean-shutdown check).
53
+ get pending() {
54
+ return buffered.length;
55
+ },
56
+ };
57
+ }
package/host.js ADDED
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+ // LeetCode Capture helper: the Native Messaging host.
3
+ //
4
+ // The browser starts this program when the extension calls
5
+ // runtime.connectNative(HOST_NAME) and talks to it over stdin and stdout with
6
+ // the framing in framing.js. stdout is that channel, so nothing else may ever
7
+ // be written to it: the children's output goes to a log file.
8
+ //
9
+ // It owns two children, started on the extension's `start`:
10
+ // - the relay (relay-server/server.js), unless one already answers on the
11
+ // port (then it is "external" and left alone, and no companion is started
12
+ // either, since whoever started that relay runs their own companion);
13
+ // - the companion (companion/companion.js) in COMPANION_HEADLESS mode.
14
+ // Both get a pipe for stdin, so if this process dies in any way (even
15
+ // SIGKILL) the pipe closes and they exit too. The relay also has an idle exit
16
+ // that this host's keepalive holds off while the extension keeps talking.
17
+ //
18
+ // Messages from the extension (each may carry an `id`, echoed in the answer):
19
+ // { type: 'ping' } -> { type: 'pong', version }
20
+ // { type: 'status' } -> { type: 'status', ... } (see status() below)
21
+ // { type: 'start' } -> starts what is not running, then a status
22
+ // { type: 'reset' } -> the companion clears its tutor context: { type: 'reset', ok }
23
+ // { type: 'stop' } -> stops both children, answers { type: 'stopped' }, exits
24
+ // A status with no id is also sent whenever a child starts or exits.
25
+ //
26
+ // It stops both children and exits when the browser closes stdin (the
27
+ // extension disconnected, or the browser quit or crashed), on SIGTERM, SIGHUP
28
+ // or SIGINT, on `stop`, and when the extension has not sent anything for
29
+ // idleMs (it pings while a LeetCode tab is open).
30
+
31
+ import fs from 'node:fs';
32
+ import path from 'node:path';
33
+ import { spawn as nodeSpawn } from 'node:child_process';
34
+ import { fileURLToPath } from 'node:url';
35
+ import { createDecoder, encodeMessage } from './framing.js';
36
+ import { VERSION } from './config.js';
37
+
38
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
39
+
40
+ export const DEFAULTS = {
41
+ // The extension pings every 60 s; five quiet minutes means it is gone.
42
+ idleMs: 5 * 60 * 1000,
43
+ keepaliveMs: 60 * 1000,
44
+ // The relay's own idle exit, held off by the keepalive above.
45
+ relayIdleExitMs: 5 * 60 * 1000,
46
+ stopTimeoutMs: 3000,
47
+ startTimeoutMs: 5000,
48
+ };
49
+
50
+ // Where the companion and relay live: LEETCODE_CAPTURE_ROOT, else the `root`
51
+ // the installer recorded next to this file, else the copy bundled in the npm
52
+ // package (runtime/, built by scripts/pack.mjs), else the folder above this
53
+ // file (a checkout of the repository).
54
+ export function resolveRoot(env = process.env, here = HERE) {
55
+ if (env.LEETCODE_CAPTURE_ROOT) return path.resolve(env.LEETCODE_CAPTURE_ROOT);
56
+ try {
57
+ const saved = JSON.parse(fs.readFileSync(path.join(here, 'helper-config.json'), 'utf8'));
58
+ if (saved && typeof saved.root === 'string') return saved.root;
59
+ } catch {
60
+ // no saved config
61
+ }
62
+ const bundled = path.join(here, 'runtime');
63
+ if (isBundled(bundled)) return bundled;
64
+ return path.resolve(here, '..');
65
+ }
66
+
67
+ // A bundled copy carries this marker (scripts/pack.mjs writes it).
68
+ export function isBundled(root) {
69
+ return fs.existsSync(path.join(root, 'bundle.json'));
70
+ }
71
+
72
+ export function checkRoot(root) {
73
+ const missing = ['companion/companion.js', 'relay-server/server.js'].filter((rel) => !fs.existsSync(path.join(root, rel)));
74
+ return missing.length ? `the LeetCode Capture files are missing from ${root} (${missing.join(', ')})` : null;
75
+ }
76
+
77
+ // A bundled copy may sit in a read-only or throwaway place (a global npm
78
+ // folder, npx's cache), and a new version is a new folder, so its capture log
79
+ // and companion state go to the helper's own data folder instead. A variable
80
+ // already set wins. A checkout keeps its usual places (relay-server/data/).
81
+ export function dataEnv(root, dataDir, env = process.env) {
82
+ if (!isBundled(root)) return {};
83
+ const captures = path.join(dataDir, 'data', 'captures.jsonl');
84
+ return {
85
+ CAPTURE_LOG_PATH: env.CAPTURE_LOG_PATH || env.LEETCODE_CAPTURES_FILE || captures,
86
+ LEETCODE_CAPTURES_FILE: env.LEETCODE_CAPTURES_FILE || env.CAPTURE_LOG_PATH || captures,
87
+ LEETCODE_COMPANION_STATE_FILE: env.LEETCODE_COMPANION_STATE_FILE || path.join(dataDir, 'data', 'companion-state.json'),
88
+ };
89
+ }
90
+
91
+ const alive = (child) => Boolean(child) && child.exitCode === null && child.signalCode === null;
92
+
93
+ export function createHost({
94
+ input,
95
+ output,
96
+ root,
97
+ env = process.env,
98
+ spawn = nodeSpawn,
99
+ fetchImpl = fetch,
100
+ execPath = process.execPath,
101
+ logFd = 'ignore',
102
+ exit = (code) => process.exit(code),
103
+ now = () => Date.now(),
104
+ timers = { setInterval, clearInterval, setTimeout, clearTimeout },
105
+ idleMs = DEFAULTS.idleMs,
106
+ keepaliveMs = DEFAULTS.keepaliveMs,
107
+ relayIdleExitMs = DEFAULTS.relayIdleExitMs,
108
+ stopTimeoutMs = DEFAULTS.stopTimeoutMs,
109
+ startTimeoutMs = DEFAULTS.startTimeoutMs,
110
+ rootError = null,
111
+ }) {
112
+ const port = env.CAPTURE_PORT ? Number(env.CAPTURE_PORT) : 8135;
113
+ const base = `http://127.0.0.1:${port}`;
114
+ let relay = null;
115
+ let companion = null;
116
+ let relayExternal = false;
117
+ let lastError = rootError;
118
+ let starting = null;
119
+ let shuttingDown = null;
120
+ let lastHeard = now();
121
+
122
+ function send(message) {
123
+ try {
124
+ output.write(encodeMessage(message));
125
+ } catch {
126
+ // The browser went away; stdin ending will shut everything down.
127
+ }
128
+ }
129
+
130
+ function status(id) {
131
+ const relayState = relayExternal ? 'external' : alive(relay) ? 'running' : 'stopped';
132
+ return {
133
+ type: 'status',
134
+ ...(id === undefined ? {} : { id }),
135
+ version: VERSION,
136
+ root,
137
+ port,
138
+ relay: { state: relayState, pid: alive(relay) ? relay.pid : null },
139
+ companion: { state: alive(companion) ? 'running' : 'stopped', pid: alive(companion) ? companion.pid : null },
140
+ error: lastError,
141
+ };
142
+ }
143
+
144
+ async function relayUp() {
145
+ const controller = new AbortController();
146
+ const timer = timers.setTimeout(() => controller.abort(), 500);
147
+ try {
148
+ await fetchImpl(`${base}/capture`, { method: 'OPTIONS', signal: controller.signal });
149
+ return true;
150
+ } catch {
151
+ return false;
152
+ } finally {
153
+ timers.clearTimeout(timer);
154
+ }
155
+ }
156
+
157
+ function startChild(script, extraEnv) {
158
+ const child = spawn(execPath, [path.basename(script)], {
159
+ cwd: path.join(root, path.dirname(script)),
160
+ env: { ...env, ...extraEnv },
161
+ stdio: ['pipe', logFd, logFd],
162
+ // Its own process group, so a Ctrl+C or hang-up meant for a terminal
163
+ // this host was started from reaches the host, which stops it in order.
164
+ detached: true,
165
+ windowsHide: true,
166
+ });
167
+ if (child.stdin) child.stdin.on('error', () => {});
168
+ child.on('error', (err) => {
169
+ lastError = `could not start ${script}: ${err.message}`;
170
+ });
171
+ child.on('exit', () => {
172
+ if (!shuttingDown) send(status());
173
+ });
174
+ return child;
175
+ }
176
+
177
+ async function start() {
178
+ if (rootError) return;
179
+ if (!alive(relay)) {
180
+ relayExternal = false;
181
+ if (await relayUp()) {
182
+ relayExternal = true;
183
+ return;
184
+ }
185
+ relay = startChild('relay-server/server.js', {
186
+ RELAY_IDLE_EXIT_MS: String(relayIdleExitMs),
187
+ RELAY_EXIT_WITH_STDIN: '1',
188
+ });
189
+ const deadline = now() + startTimeoutMs;
190
+ let up = false;
191
+ while (!up && alive(relay) && now() < deadline) {
192
+ // eslint-disable-next-line no-await-in-loop
193
+ await new Promise((resolve) => timers.setTimeout(resolve, 100));
194
+ // eslint-disable-next-line no-await-in-loop
195
+ up = await relayUp();
196
+ }
197
+ if (!up) {
198
+ lastError = `the relay did not start on ${base}`;
199
+ return;
200
+ }
201
+ }
202
+ if (!alive(companion)) {
203
+ companion = startChild('companion/companion.js', { COMPANION_HEADLESS: '1' });
204
+ }
205
+ lastError = null;
206
+ send(status());
207
+ }
208
+
209
+ function ensureStarted() {
210
+ if (!starting) starting = start().finally(() => (starting = null));
211
+ return starting;
212
+ }
213
+
214
+ function stopChild(child) {
215
+ if (!alive(child)) return Promise.resolve();
216
+ return new Promise((resolve) => {
217
+ const killTimer = timers.setTimeout(() => {
218
+ try {
219
+ child.kill('SIGKILL');
220
+ } catch {
221
+ // already gone
222
+ }
223
+ }, stopTimeoutMs);
224
+ child.once('exit', () => {
225
+ timers.clearTimeout(killTimer);
226
+ resolve();
227
+ });
228
+ try {
229
+ if (child.stdin) child.stdin.end();
230
+ child.kill('SIGTERM');
231
+ } catch {
232
+ // already gone
233
+ }
234
+ });
235
+ }
236
+
237
+ async function stopChildren() {
238
+ // The companion first, so it never sees its relay vanish mid-turn.
239
+ await stopChild(companion);
240
+ await stopChild(relay);
241
+ relayExternal = false;
242
+ }
243
+
244
+ function shutdown(code = 0) {
245
+ if (!shuttingDown) {
246
+ shuttingDown = (async () => {
247
+ timers.clearInterval(tick);
248
+ if (starting) await starting.catch(() => {});
249
+ await stopChildren();
250
+ exit(code);
251
+ })();
252
+ }
253
+ return shuttingDown;
254
+ }
255
+
256
+ async function handle(message) {
257
+ lastHeard = now();
258
+ if (!message || typeof message !== 'object' || shuttingDown) return;
259
+ const { id } = message;
260
+ switch (message.type) {
261
+ case 'ping':
262
+ send({ type: 'pong', id, version: VERSION });
263
+ return;
264
+ case 'status':
265
+ send(status(id));
266
+ return;
267
+ case 'start':
268
+ await ensureStarted();
269
+ send(status(id));
270
+ return;
271
+ case 'reset': {
272
+ const ok = alive(companion) && Boolean(companion.stdin) && companion.stdin.writable;
273
+ if (ok) companion.stdin.write('/reset\n');
274
+ send({ type: 'reset', id, ok });
275
+ return;
276
+ }
277
+ case 'stop':
278
+ await stopChildren();
279
+ send({ type: 'stopped', id });
280
+ await shutdown(0);
281
+ return;
282
+ default:
283
+ send({ type: 'error', id, error: `unknown message type ${JSON.stringify(message.type)}` });
284
+ }
285
+ }
286
+
287
+ // The idle exit and the relay keepalive.
288
+ const tick = timers.setInterval(() => {
289
+ if (shuttingDown) return;
290
+ if (now() - lastHeard >= idleMs) {
291
+ shutdown(0);
292
+ return;
293
+ }
294
+ if (alive(relay)) {
295
+ fetchImpl(`${base}/helper/keepalive`, { method: 'POST', headers: { 'x-lc-companion': '1' } }).catch(() => {});
296
+ }
297
+ }, Math.min(keepaliveMs, idleMs));
298
+
299
+ const decoder = createDecoder({
300
+ onMessage: (message) => {
301
+ handle(message).catch((err) => send({ type: 'error', id: message && message.id, error: err.message }));
302
+ },
303
+ // The stream is out of step; nothing more from it can be trusted.
304
+ onError: () => shutdown(1),
305
+ });
306
+ input.on('data', (chunk) => decoder.push(chunk));
307
+ input.on('end', () => shutdown(0));
308
+ input.on('error', () => shutdown(0));
309
+
310
+ return { handle, shutdown, status, get children() { return { relay, companion }; } };
311
+ }
312
+
313
+ // The real program: stdin and stdout are the browser's.
314
+ export function main() {
315
+ const root = resolveRoot();
316
+ const logDir = process.env.LEETCODE_CAPTURE_HELPER_LOG_DIR || HERE;
317
+ let logFd = 'ignore';
318
+ try {
319
+ const logPath = path.join(logDir, 'helper.log');
320
+ // Keep one run's worth, so the log never grows without bound.
321
+ if (fs.existsSync(logPath) && fs.statSync(logPath).size > 1024 * 1024) fs.truncateSync(logPath, 0);
322
+ logFd = fs.openSync(logPath, 'a');
323
+ } catch {
324
+ // No log then; the children's output is dropped.
325
+ }
326
+ const childEnv = { ...process.env, ...dataEnv(root, HERE) };
327
+ for (const file of [childEnv.CAPTURE_LOG_PATH, childEnv.LEETCODE_COMPANION_STATE_FILE]) {
328
+ try {
329
+ if (file) fs.mkdirSync(path.dirname(file), { recursive: true });
330
+ } catch {
331
+ // the relay reports a log it cannot write
332
+ }
333
+ }
334
+ const host = createHost({
335
+ input: process.stdin,
336
+ output: process.stdout,
337
+ root,
338
+ env: childEnv,
339
+ logFd,
340
+ rootError: checkRoot(root),
341
+ });
342
+ for (const sig of ['SIGTERM', 'SIGHUP', 'SIGINT']) process.on(sig, () => host.shutdown(0));
343
+ return host;
344
+ }
345
+
346
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main();