leetcode-capture-helper 0.1.0 → 0.1.1
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/config.js +1 -1
- package/host.js +75 -1
- package/package.json +1 -1
- package/runtime/bundle.json +1 -1
- package/runtime/companion/companion.js +5 -8
- package/runtime/companion/vault-summary.js +2 -3
- package/runtime/extension/shared/code-ledger.js +1 -1
- package/runtime/extension/shared/roadmap.js +5 -12
- package/runtime/extension/shared/solved.js +39 -0
- package/runtime/relay-server/server.js +1 -1
package/config.js
CHANGED
package/host.js
CHANGED
|
@@ -45,6 +45,7 @@ export const DEFAULTS = {
|
|
|
45
45
|
relayIdleExitMs: 5 * 60 * 1000,
|
|
46
46
|
stopTimeoutMs: 3000,
|
|
47
47
|
startTimeoutMs: 5000,
|
|
48
|
+
reviveMs: 10 * 1000,
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
// Where the companion and relay live: LEETCODE_CAPTURE_ROOT, else the `root`
|
|
@@ -107,6 +108,7 @@ export function createHost({
|
|
|
107
108
|
relayIdleExitMs = DEFAULTS.relayIdleExitMs,
|
|
108
109
|
stopTimeoutMs = DEFAULTS.stopTimeoutMs,
|
|
109
110
|
startTimeoutMs = DEFAULTS.startTimeoutMs,
|
|
111
|
+
reviveMs = DEFAULTS.reviveMs,
|
|
110
112
|
rootError = null,
|
|
111
113
|
}) {
|
|
112
114
|
const port = env.CAPTURE_PORT ? Number(env.CAPTURE_PORT) : 8135;
|
|
@@ -118,6 +120,18 @@ export function createHost({
|
|
|
118
120
|
let starting = null;
|
|
119
121
|
let shuttingDown = null;
|
|
120
122
|
let lastHeard = now();
|
|
123
|
+
let wantRunning = false;
|
|
124
|
+
let lastRevive = 0;
|
|
125
|
+
|
|
126
|
+
// One line in helper.log for each thing that matters when a start goes wrong.
|
|
127
|
+
function note(text) {
|
|
128
|
+
if (typeof logFd !== 'number') return;
|
|
129
|
+
try {
|
|
130
|
+
fs.writeSync(logFd, `[host ${new Date(now()).toISOString()}] ${text}\n`);
|
|
131
|
+
} catch {
|
|
132
|
+
// no log then
|
|
133
|
+
}
|
|
134
|
+
}
|
|
121
135
|
|
|
122
136
|
function send(message) {
|
|
123
137
|
try {
|
|
@@ -154,6 +168,36 @@ export function createHost({
|
|
|
154
168
|
}
|
|
155
169
|
}
|
|
156
170
|
|
|
171
|
+
// Whether a tutor (the companion) is attached to the relay: true, false, or
|
|
172
|
+
// null when that cannot be told. The relay's panel stream opens with a
|
|
173
|
+
// status event that says so.
|
|
174
|
+
async function companionOnline() {
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const timer = timers.setTimeout(() => controller.abort(), 1500);
|
|
177
|
+
try {
|
|
178
|
+
const response = await fetchImpl(`${base}/panel/events?after=0`, { signal: controller.signal });
|
|
179
|
+
if (!response || !response.body || typeof response.body.getReader !== 'function') return null;
|
|
180
|
+
const reader = response.body.getReader();
|
|
181
|
+
const decoder = new TextDecoder();
|
|
182
|
+
let text = '';
|
|
183
|
+
for (;;) {
|
|
184
|
+
// eslint-disable-next-line no-await-in-loop
|
|
185
|
+
const { done, value } = await reader.read();
|
|
186
|
+
if (done) return null;
|
|
187
|
+
text += decoder.decode(value, { stream: true });
|
|
188
|
+
const match = /"companionOnline":(true|false)/.exec(text);
|
|
189
|
+
if (match) {
|
|
190
|
+
controller.abort();
|
|
191
|
+
return match[1] === 'true';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
} finally {
|
|
197
|
+
timers.clearTimeout(timer);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
157
201
|
function startChild(script, extraEnv) {
|
|
158
202
|
const child = spawn(execPath, [path.basename(script)], {
|
|
159
203
|
cwd: path.join(root, path.dirname(script)),
|
|
@@ -167,8 +211,10 @@ export function createHost({
|
|
|
167
211
|
if (child.stdin) child.stdin.on('error', () => {});
|
|
168
212
|
child.on('error', (err) => {
|
|
169
213
|
lastError = `could not start ${script}: ${err.message}`;
|
|
214
|
+
note(lastError);
|
|
170
215
|
});
|
|
171
|
-
child.on('exit', () => {
|
|
216
|
+
child.on('exit', (code, signal) => {
|
|
217
|
+
note(`${script} exited (code ${code}, signal ${signal})`);
|
|
172
218
|
if (!shuttingDown) send(status());
|
|
173
219
|
});
|
|
174
220
|
return child;
|
|
@@ -180,6 +226,15 @@ export function createHost({
|
|
|
180
226
|
relayExternal = false;
|
|
181
227
|
if (await relayUp()) {
|
|
182
228
|
relayExternal = true;
|
|
229
|
+
note('a relay already answers on this port; leaving it alone');
|
|
230
|
+
// A relay somebody else started may have no tutor attached: then the
|
|
231
|
+
// extension's Settings page stays locked. Start one unless it is there.
|
|
232
|
+
if (!alive(companion) && (await companionOnline()) === false) {
|
|
233
|
+
note('no tutor is attached to that relay; starting the companion');
|
|
234
|
+
companion = startChild('companion/companion.js', { COMPANION_HEADLESS: '1' });
|
|
235
|
+
}
|
|
236
|
+
lastError = null;
|
|
237
|
+
send(status());
|
|
183
238
|
return;
|
|
184
239
|
}
|
|
185
240
|
relay = startChild('relay-server/server.js', {
|
|
@@ -196,12 +251,14 @@ export function createHost({
|
|
|
196
251
|
}
|
|
197
252
|
if (!up) {
|
|
198
253
|
lastError = `the relay did not start on ${base}`;
|
|
254
|
+
note(lastError);
|
|
199
255
|
return;
|
|
200
256
|
}
|
|
201
257
|
}
|
|
202
258
|
if (!alive(companion)) {
|
|
203
259
|
companion = startChild('companion/companion.js', { COMPANION_HEADLESS: '1' });
|
|
204
260
|
}
|
|
261
|
+
note(`started: relay pid ${relay && relay.pid}, companion pid ${companion && companion.pid}`);
|
|
205
262
|
lastError = null;
|
|
206
263
|
send(status());
|
|
207
264
|
}
|
|
@@ -265,6 +322,8 @@ export function createHost({
|
|
|
265
322
|
send(status(id));
|
|
266
323
|
return;
|
|
267
324
|
case 'start':
|
|
325
|
+
wantRunning = true;
|
|
326
|
+
note('start requested');
|
|
268
327
|
await ensureStarted();
|
|
269
328
|
send(status(id));
|
|
270
329
|
return;
|
|
@@ -294,8 +353,23 @@ export function createHost({
|
|
|
294
353
|
if (alive(relay)) {
|
|
295
354
|
fetchImpl(`${base}/helper/keepalive`, { method: 'POST', headers: { 'x-lc-companion': '1' } }).catch(() => {});
|
|
296
355
|
}
|
|
356
|
+
revive();
|
|
297
357
|
}, Math.min(keepaliveMs, idleMs));
|
|
298
358
|
|
|
359
|
+
// Once asked to start, notice a relay or tutor that has gone away (a relay
|
|
360
|
+
// that was somebody else's and stopped, a child that crashed) and bring it
|
|
361
|
+
// back, at most every reviveMs.
|
|
362
|
+
async function revive() {
|
|
363
|
+
if (!wantRunning || starting || shuttingDown || rootError || now() - lastRevive < reviveMs) return;
|
|
364
|
+
const relayGone = relayExternal ? !(await relayUp()) : !alive(relay);
|
|
365
|
+
const companionGone = !relayExternal && !alive(companion);
|
|
366
|
+
if (relayGone || companionGone) {
|
|
367
|
+
lastRevive = now();
|
|
368
|
+
note('something went away; starting it again');
|
|
369
|
+
await ensureStarted().catch(() => {});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
299
373
|
const decoder = createDecoder({
|
|
300
374
|
onMessage: (message) => {
|
|
301
375
|
handle(message).catch((err) => send({ type: 'error', id: message && message.id, error: err.message }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leetcode-capture-helper",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Native Messaging helper for LeetCode Capture: lets the browser extension start and stop the local companion and relay for the Claude and ChatGPT subscriptions. The package carries its own copy of the companion and relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/runtime/bundle.json
CHANGED
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
// built-in fetch. "openai" is the same backend against OpenAI's API.
|
|
25
25
|
// - "chatgpt": the Codex CLI on a ChatGPT sign-in (codex-backend.js).
|
|
26
26
|
//
|
|
27
|
-
//
|
|
28
|
-
// environment variables, and exactly what credentials each backend needs.
|
|
27
|
+
// Setup: the environment variables, and exactly what credentials each backend needs.
|
|
29
28
|
|
|
30
29
|
import fs from 'node:fs';
|
|
31
30
|
import os from 'node:os';
|
|
@@ -202,7 +201,7 @@ const LOCAL_RESERVE_TOKENS = Number(process.env.COMPANION_LOCAL_RESERVE_TOKENS |
|
|
|
202
201
|
// The other options also come from settings.js (`effective.options`):
|
|
203
202
|
// - vaultAutoSummary (VAULT_AUTO_SUMMARY), off by default: a portable
|
|
204
203
|
// checkout won't have this vault's Study/Algorithms structure, or even
|
|
205
|
-
// use Obsidian.
|
|
204
|
+
// use Obsidian.
|
|
206
205
|
// - autoClearContext (COMPANION_AUTO_CLEAR_CONTEXT), on by default: see the
|
|
207
206
|
// "automatic context reset on problem switch" section below for why
|
|
208
207
|
// leaving it on is the safer default (a stale problem's code and
|
|
@@ -467,9 +466,8 @@ function backendLabel() {
|
|
|
467
466
|
// it exits - one command instead of two. If a server was already running
|
|
468
467
|
// (started separately, or left over from another companion instance), this
|
|
469
468
|
// instance leaves it alone on both ends: it doesn't spawn a second one, and
|
|
470
|
-
// it doesn't stop it on exit.
|
|
471
|
-
//
|
|
472
|
-
// where companion.js isn't running are no longer durably logged).
|
|
469
|
+
// it doesn't stop it on exit. The tradeoff: captures during a gap
|
|
470
|
+
// where companion.js isn't running are no longer durably logged.
|
|
473
471
|
|
|
474
472
|
// Set only if *this* process spawned the relay server; stays null if an
|
|
475
473
|
// already-running server was found instead, so the exit handler below knows
|
|
@@ -678,8 +676,7 @@ function resetTutorContext() {
|
|
|
678
676
|
// session progresses. The actual logic (building the addendum, parsing the
|
|
679
677
|
// model's reply, writing the notes) lives in vault-summary.js, split out so
|
|
680
678
|
// it can be unit-tested without booting this file's readline loop / relay
|
|
681
|
-
// server lifecycle - see that file's header for the full design
|
|
682
|
-
// README.md → "Vault auto-summary" for user-facing docs.
|
|
679
|
+
// server lifecycle - see that file's header for the full design.
|
|
683
680
|
|
|
684
681
|
// --- terminal chat loop ------------------------------------------------------
|
|
685
682
|
//
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// Split out from companion.js so this feature's actual logic (building the
|
|
4
4
|
// addendum, parsing the model's reply, writing the notes) can be exercised
|
|
5
5
|
// directly by tests without booting companion.js's interactive readline
|
|
6
|
-
// loop or relay-server lifecycle. See
|
|
7
|
-
// the
|
|
8
|
-
// handling path.
|
|
6
|
+
// loop or relay-server lifecycle. See companion.js for how this gets wired
|
|
7
|
+
// into the capture handling path.
|
|
9
8
|
//
|
|
10
9
|
// The core constraint shaping this file: reuse the same backend response
|
|
11
10
|
// already being shown in the terminal for a Submit, and never make a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// extension/shared/code-ledger.js - what code the tutor has already been shown, and
|
|
2
2
|
// what to tell it next.
|
|
3
3
|
//
|
|
4
|
-
// The rules (Thomson's request
|
|
4
|
+
// The rules (Thomson's request):
|
|
5
5
|
// - A Run (the button or Ctrl+') is never sent to the tutor on its own. It
|
|
6
6
|
// is only recorded, as the newest code for its problem.
|
|
7
7
|
// - A Submit is sent automatically, but only when its code differs from
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// plus the pure logic they share. No Node or browser APIs, so the options
|
|
3
3
|
// page, the background worker and the companion can all import it.
|
|
4
4
|
//
|
|
5
|
-
// A list is { id, title, short,
|
|
5
|
+
// A list is { id, title, short, kind, topics, edges, subjects }:
|
|
6
6
|
// topics its groups in the publisher's order, each { id, title, problems }
|
|
7
7
|
// where a problem is { slug, title, difficulty, topic } (title and
|
|
8
8
|
// difficulty come from the one shared catalog, shared/catalog.js)
|
|
@@ -24,7 +24,7 @@ import { CATALOG } from './catalog.js';
|
|
|
24
24
|
const words = (slugs) => slugs.split(/\s+/).filter(Boolean);
|
|
25
25
|
|
|
26
26
|
// Each group: [id, title, slugs], the slugs one per line in the list's order.
|
|
27
|
-
function defineList({ id, title, short,
|
|
27
|
+
function defineList({ id, title, short, groupNoun, groups, edges = [], subjects = null }) {
|
|
28
28
|
const topics = groups.map(([topicId, topicTitle, slugs]) => ({
|
|
29
29
|
id: topicId,
|
|
30
30
|
title: topicTitle,
|
|
@@ -35,7 +35,7 @@ function defineList({ id, title, short, description, groupNoun, groups, edges =
|
|
|
35
35
|
}),
|
|
36
36
|
}));
|
|
37
37
|
const size = topics.reduce((n, t) => n + t.problems.length, 0);
|
|
38
|
-
const list = { id, title, short: short || title,
|
|
38
|
+
const list = { id, title, short: short || title, groupNoun, kind: edges.length ? 'graph' : 'groups', size, topics, edges, subjects: null };
|
|
39
39
|
if (subjects) list.subjects = defineSubjects(list, subjects);
|
|
40
40
|
return list;
|
|
41
41
|
}
|
|
@@ -94,7 +94,6 @@ const EDGES = [
|
|
|
94
94
|
export const NEETCODE_150 = defineList({
|
|
95
95
|
id: 'neetcode-150',
|
|
96
96
|
title: 'NeetCode 150',
|
|
97
|
-
description: 'NeetCode\'s expanded Blind 75, by topic on a roadmap.',
|
|
98
97
|
groupNoun: 'topic',
|
|
99
98
|
edges: EDGES,
|
|
100
99
|
groups: [
|
|
@@ -279,7 +278,6 @@ export const NEETCODE_150 = defineList({
|
|
|
279
278
|
export const NEETCODE_250 = defineList({
|
|
280
279
|
id: 'neetcode-250',
|
|
281
280
|
title: 'NeetCode 250',
|
|
282
|
-
description: 'NeetCode 150 and 100 more, by topic on the same roadmap.',
|
|
283
281
|
groupNoun: 'topic',
|
|
284
282
|
edges: EDGES,
|
|
285
283
|
groups: [
|
|
@@ -566,7 +564,6 @@ export const NEETCODE_250 = defineList({
|
|
|
566
564
|
export const BLIND_75 = defineList({
|
|
567
565
|
id: 'blind-75',
|
|
568
566
|
title: 'Blind 75',
|
|
569
|
-
description: 'The original 75 from Blind, by category. NeetCode 150 extends it.',
|
|
570
567
|
groupNoun: 'category',
|
|
571
568
|
groups: [
|
|
572
569
|
['array', 'Array', `
|
|
@@ -672,7 +669,6 @@ export const BLIND_75 = defineList({
|
|
|
672
669
|
export const GRIND_75 = defineList({
|
|
673
670
|
id: 'grind-75',
|
|
674
671
|
title: 'Grind 75',
|
|
675
|
-
description: 'By the Blind 75 author: the site\'s default eight weeks, or by subject.',
|
|
676
672
|
groupNoun: 'week',
|
|
677
673
|
groups: [
|
|
678
674
|
['week-1', 'Week 1', `
|
|
@@ -872,8 +868,7 @@ export const GRIND_75 = defineList({
|
|
|
872
868
|
// against e2e/leetcode-study-plans.txt by roadmap.test.mjs.
|
|
873
869
|
export const TOP_100_LIKED = defineList({
|
|
874
870
|
id: 'top-100-liked',
|
|
875
|
-
title: 'Top 100 Liked',
|
|
876
|
-
description: 'LeetCode\'s 100 most liked problems, by topic.',
|
|
871
|
+
title: 'Top 100 Liked by LeetCode',
|
|
877
872
|
groupNoun: 'topic',
|
|
878
873
|
groups: [
|
|
879
874
|
['backtracking', 'Backtracking', `
|
|
@@ -999,8 +994,7 @@ export const TOP_100_LIKED = defineList({
|
|
|
999
994
|
// 2026-11-04: e2e/leetcode-study-plans.txt, which roadmap.test.mjs compares.
|
|
1000
995
|
export const TOP_INTERVIEW_150 = defineList({
|
|
1001
996
|
id: 'top-interview-150',
|
|
1002
|
-
title: 'Top Interview 150',
|
|
1003
|
-
description: 'LeetCode\'s own interview study plan, section by section.',
|
|
997
|
+
title: 'Top Interview 150 by LeetCode',
|
|
1004
998
|
groupNoun: 'section',
|
|
1005
999
|
groups: [
|
|
1006
1000
|
['array-string', 'Array / String', `
|
|
@@ -1184,7 +1178,6 @@ export const TOP_INTERVIEW_150 = defineList({
|
|
|
1184
1178
|
export const SQL_50 = defineList({
|
|
1185
1179
|
id: 'sql-50',
|
|
1186
1180
|
title: 'SQL 50',
|
|
1187
|
-
description: 'LeetCode\'s study plan for database problems.',
|
|
1188
1181
|
groupNoun: 'section',
|
|
1189
1182
|
groups: [
|
|
1190
1183
|
['select', 'Select', `
|
|
@@ -44,6 +44,10 @@ import { LISTS, NEETCODE_150, ROADMAPS, listById, problemInList, suggestAfterSol
|
|
|
44
44
|
export const SOLVED_KEY = 'solvedProblems';
|
|
45
45
|
export const STARRED_KEY = 'starredProblems';
|
|
46
46
|
export const ACTIVE_LIST_KEY = 'activeProgressList';
|
|
47
|
+
export const SYNCED_AT_KEY = 'solvedSyncedAt';
|
|
48
|
+
// How long an automatic sync waits for the next one; the button ignores it.
|
|
49
|
+
export const SYNC_EVERY_MS = 12 * 60 * 60 * 1000;
|
|
50
|
+
const MAX_SYNC = 6000;
|
|
47
51
|
|
|
48
52
|
export function isListId(value) {
|
|
49
53
|
return typeof value === 'string' && Object.hasOwn(ROADMAPS, value);
|
|
@@ -85,6 +89,7 @@ export function normalizeSolved(raw) {
|
|
|
85
89
|
const count = Number.isInteger(entry.count) && entry.count >= 0 ? entry.count : 0;
|
|
86
90
|
const clean = { at: validTime(entry.at) ? entry.at : new Date(0).toISOString(), count };
|
|
87
91
|
if (entry.manual === true) clean.manual = true;
|
|
92
|
+
else if (entry.synced === true) clean.synced = true;
|
|
88
93
|
out[slug] = clean;
|
|
89
94
|
}
|
|
90
95
|
return out;
|
|
@@ -113,6 +118,20 @@ export function markSolved(solved, slug, on, at) {
|
|
|
113
118
|
return next;
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
// The problems LeetCode says the account solved, added to the list as
|
|
122
|
+
// "synced". Whatever is marked already stays as it is, and nothing is
|
|
123
|
+
// removed. Returns the new record and how many were added.
|
|
124
|
+
export function mergeSynced(solved, slugs, at) {
|
|
125
|
+
const next = { ...solved };
|
|
126
|
+
let added = 0;
|
|
127
|
+
for (const slug of slugs) {
|
|
128
|
+
if (next[slug]) continue;
|
|
129
|
+
next[slug] = { at, count: 0, synced: true };
|
|
130
|
+
added += 1;
|
|
131
|
+
}
|
|
132
|
+
return { solved: next, added };
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
export function solvedSlugs(solved) {
|
|
117
136
|
return Object.keys(solved);
|
|
118
137
|
}
|
|
@@ -243,6 +262,23 @@ export function createSolvedService({ storage, extensionOrigin, now = () => new
|
|
|
243
262
|
});
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
// True when an automatic sync is due: never synced, or not for SYNC_EVERY_MS.
|
|
266
|
+
async function syncDue() {
|
|
267
|
+
const stored = (await storage.get([SYNCED_AT_KEY])) || {};
|
|
268
|
+
const last = Date.parse(stored[SYNCED_AT_KEY]);
|
|
269
|
+
return { ok: true, due: Number.isNaN(last) || Date.parse(now()) - last >= SYNC_EVERY_MS };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// `slugs` is what a signed-in LeetCode tab reported as accepted.
|
|
273
|
+
async function mergeFromLeetCode(slugs) {
|
|
274
|
+
if (!Array.isArray(slugs) || slugs.length > MAX_SYNC || !slugs.every(isSlug)) return { ok: false };
|
|
275
|
+
return update(async (solved) => {
|
|
276
|
+
const merged = mergeSynced(solved, slugs, now());
|
|
277
|
+
await storage.set({ [SOLVED_KEY]: merged.solved, [SYNCED_AT_KEY]: now() });
|
|
278
|
+
return { ok: true, added: merged.added, total: new Set(slugs).size };
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
246
282
|
async function setStarred(message) {
|
|
247
283
|
if (!isSlug(message.slug) || typeof message.starred !== 'boolean') return { ok: false };
|
|
248
284
|
return update(async (_solved, starred) => {
|
|
@@ -257,9 +293,12 @@ export function createSolvedService({ storage, extensionOrigin, now = () => new
|
|
|
257
293
|
return {
|
|
258
294
|
// Resolves to the answer, or undefined when the message is not for this
|
|
259
295
|
// service.
|
|
296
|
+
mergeFromLeetCode,
|
|
260
297
|
async handleMessage(message, sender) {
|
|
261
298
|
if (!message || typeof message !== 'object') return undefined;
|
|
262
299
|
try {
|
|
300
|
+
if (message.type === 'lc-tutor:sync-check') return sender && sender.tab && slugFromUrl(sender.url) ? await syncDue() : { ok: false };
|
|
301
|
+
if (message.type === 'lc-tutor:sync-solved') return sender && sender.tab && slugFromUrl(sender.url) ? await mergeFromLeetCode(message.slugs) : { ok: false };
|
|
263
302
|
if (message.type === 'lc-tutor:accepted') return await accepted(message, sender);
|
|
264
303
|
if (message.type === 'lc-tutor:set-list') return await setList(message, sender);
|
|
265
304
|
if (message.type === 'lc-tutor:practice') return await practice(message, sender);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// a per-problem monotonic attempt sequence number.
|
|
6
6
|
//
|
|
7
7
|
// Log file location: ./data/captures.jsonl (relative to this file), unless
|
|
8
|
-
// overridden with the CAPTURE_LOG_PATH environment variable.
|
|
8
|
+
// overridden with the CAPTURE_LOG_PATH environment variable.
|
|
9
9
|
// The log is cleared as the companion consumes it (see trimCaptureLog).
|
|
10
10
|
|
|
11
11
|
const http = require("http");
|