opencode-leetcode-realworld 0.2.2 → 0.2.3
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/package.json +1 -1
- package/src/index.ts +6 -1
- package/src/leetcode.ts +120 -33
- package/src/prompt.ts +15 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-leetcode-realworld",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "OpenCode plugin that turns a LeetCode problem into a realistic software-engineering assignment, so you practice applying the underlying algorithm instead of solving the puzzle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -38,7 +38,12 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
|
38
38
|
.describe("How to choose the problem"),
|
|
39
39
|
idOrSlug: tool.schema.string().optional().describe("Problem id or slug (required for mode=specific)"),
|
|
40
40
|
difficulty: tool.schema.enum(["Easy", "Medium", "Hard"]).optional(),
|
|
41
|
-
tags: tool.schema
|
|
41
|
+
tags: tool.schema
|
|
42
|
+
.array(tool.schema.string())
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
"Topic tag slugs to constrain the problem, e.g. ['array'] or ['dynamic-programming']",
|
|
46
|
+
),
|
|
42
47
|
},
|
|
43
48
|
async execute(args, context) {
|
|
44
49
|
const problem = await fetchProblem({
|
package/src/leetcode.ts
CHANGED
|
@@ -157,19 +157,29 @@ export async function getProblem(idOrSlug: string, signal?: AbortSignal): Promis
|
|
|
157
157
|
return normalizeProblem(raw, preferredSlug(idOrSlug));
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
interface
|
|
160
|
+
interface ProblemListEntry {
|
|
161
161
|
title_slug?: string;
|
|
162
162
|
titleSlug?: string;
|
|
163
|
+
difficulty?: string;
|
|
164
|
+
paid_only?: boolean;
|
|
165
|
+
isPaidOnly?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function slugifyTag(input: string): string {
|
|
169
|
+
return input
|
|
170
|
+
.trim()
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
173
|
+
.replace(/^-|-$/g, "");
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
export async function getRandom(
|
|
166
|
-
opts: { difficulty?: string;
|
|
177
|
+
opts: { difficulty?: string; signal?: AbortSignal } = {},
|
|
167
178
|
): Promise<NormalizedProblem> {
|
|
168
179
|
const params = new URLSearchParams();
|
|
169
180
|
if (opts.difficulty) params.set("difficulty", opts.difficulty);
|
|
170
|
-
if (opts.tags?.length) params.set("tags", opts.tags.join(","));
|
|
171
181
|
const query = params.toString();
|
|
172
|
-
const raw = await getJson<
|
|
182
|
+
const raw = await getJson<ProblemListEntry>(`/random${query ? `?${query}` : ""}`, opts.signal);
|
|
173
183
|
const slug = raw.title_slug ?? raw.titleSlug;
|
|
174
184
|
if (!slug) throw new Error("LeetCode API /random did not return a problem slug.");
|
|
175
185
|
return getProblem(slug, opts.signal);
|
|
@@ -181,20 +191,102 @@ export async function getDaily(signal?: AbortSignal): Promise<NormalizedProblem>
|
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
interface FilterResponse {
|
|
184
|
-
problems?:
|
|
194
|
+
problems?: ProblemListEntry[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* IMPORTANT: `/problems/filter` and `/random` silently IGNORE the `tags` query
|
|
199
|
+
* parameter, so the only endpoint that actually filters by topic is
|
|
200
|
+
* `/problems/tag/{slug}`. Difficulty still has to be applied client-side there.
|
|
201
|
+
*/
|
|
202
|
+
interface TagResponse {
|
|
203
|
+
tag?: string;
|
|
204
|
+
problems?: ProblemListEntry[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function listByTag(
|
|
208
|
+
tag: string,
|
|
209
|
+
difficulty: string | undefined,
|
|
210
|
+
signal: AbortSignal | undefined,
|
|
211
|
+
pages = 4,
|
|
212
|
+
): Promise<ProblemListEntry[]> {
|
|
213
|
+
const results: ProblemListEntry[] = [];
|
|
214
|
+
const seen = new Set<string>();
|
|
215
|
+
for (let page = 0; page < pages; page += 1) {
|
|
216
|
+
const query = new URLSearchParams({ limit: "50", skip: String(page * 50) });
|
|
217
|
+
const raw = await getJson<TagResponse>(
|
|
218
|
+
`/problems/tag/${encodeURIComponent(tag)}?${query.toString()}`,
|
|
219
|
+
signal,
|
|
220
|
+
);
|
|
221
|
+
const problems = raw.problems ?? [];
|
|
222
|
+
if (problems.length === 0) break;
|
|
223
|
+
for (const problem of problems) {
|
|
224
|
+
const slug = String(problem.title_slug ?? problem.titleSlug ?? "");
|
|
225
|
+
if (!slug || seen.has(slug)) continue;
|
|
226
|
+
seen.add(slug);
|
|
227
|
+
if (problem.paid_only || problem.isPaidOnly) continue;
|
|
228
|
+
if (
|
|
229
|
+
difficulty &&
|
|
230
|
+
String(problem.difficulty ?? "").toLowerCase() !== difficulty.toLowerCase()
|
|
231
|
+
) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
results.push(problem);
|
|
235
|
+
}
|
|
236
|
+
if (problems.length < 50) break;
|
|
237
|
+
}
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function matchesTags(problem: NormalizedProblem, tags: string[]): boolean {
|
|
242
|
+
const present = problem.topics.map(slugifyTag);
|
|
243
|
+
// Some LeetCode tag slugs differ from the topic label, e.g. tag "graph" ->
|
|
244
|
+
// topic "Graph Theory" (slug "graph-theory"). Allow prefix aliases both ways.
|
|
245
|
+
return tags.every((tag) =>
|
|
246
|
+
present.some(
|
|
247
|
+
(topic) => topic === tag || topic.startsWith(`${tag}-`) || tag.startsWith(`${topic}-`),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function fetchByTags(
|
|
253
|
+
tags: string[],
|
|
254
|
+
difficulty: string | undefined,
|
|
255
|
+
signal?: AbortSignal,
|
|
256
|
+
): Promise<NormalizedProblem> {
|
|
257
|
+
const candidates = await listByTag(tags[0]!, difficulty, signal);
|
|
258
|
+
if (candidates.length === 0) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`No ${difficulty ?? "matching"} problems found for tag "${tags[0]}". ` +
|
|
261
|
+
"Check the tag slug (e.g. array, hash-table, dynamic-programming).",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const strict = tags.length > 1;
|
|
265
|
+
const shuffled = [...candidates].sort(() => Math.random() - 0.5);
|
|
266
|
+
const attempts = Math.min(shuffled.length, 12);
|
|
267
|
+
let fallback: NormalizedProblem | undefined;
|
|
268
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
269
|
+
const slug = String(shuffled[index]!.title_slug ?? shuffled[index]!.titleSlug ?? "");
|
|
270
|
+
if (!slug) continue;
|
|
271
|
+
const problem = await getProblem(slug, signal);
|
|
272
|
+
if (!isUsableProblem(problem)) continue;
|
|
273
|
+
fallback = fallback ?? problem;
|
|
274
|
+
if (!strict || matchesTags(problem, tags)) return problem;
|
|
275
|
+
}
|
|
276
|
+
if (fallback) return fallback;
|
|
277
|
+
throw new Error(`Could not find a usable problem for tag "${tags[0]}".`);
|
|
185
278
|
}
|
|
186
279
|
|
|
187
280
|
export async function getFiltered(
|
|
188
|
-
opts: { difficulty?: string;
|
|
281
|
+
opts: { difficulty?: string; limit?: number; signal?: AbortSignal } = {},
|
|
189
282
|
): Promise<NormalizedProblem> {
|
|
190
283
|
const params = new URLSearchParams();
|
|
191
284
|
if (opts.difficulty) params.set("difficulty", opts.difficulty);
|
|
192
|
-
if (opts.tags?.length) params.set("tags", opts.tags.join(","));
|
|
193
285
|
params.set("limit", String(opts.limit ?? 20));
|
|
194
286
|
const raw = await getJson<FilterResponse>(`/problems/filter?${params.toString()}`, opts.signal);
|
|
195
287
|
const candidates = raw.problems ?? [];
|
|
196
288
|
if (candidates.length === 0) throw new Error("LeetCode API returned no problems for the given filter.");
|
|
197
|
-
const choice = candidates[Math.floor(Math.random() * candidates.length)]
|
|
289
|
+
const choice = candidates[Math.floor(Math.random() * candidates.length)]!;
|
|
198
290
|
const slug = String(choice.title_slug ?? choice.titleSlug ?? "");
|
|
199
291
|
if (!slug) throw new Error("Filtered problem is missing a slug.");
|
|
200
292
|
return getProblem(slug, opts.signal);
|
|
@@ -202,32 +294,27 @@ export async function getFiltered(
|
|
|
202
294
|
|
|
203
295
|
export async function fetchProblem(options: FetchOptions = {}): Promise<NormalizedProblem> {
|
|
204
296
|
const mode = options.mode ?? "random";
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
225
|
-
case "random":
|
|
226
|
-
default:
|
|
227
|
-
return pickUsable(() =>
|
|
228
|
-
getRandom({ difficulty: options.difficulty, tags: options.tags, signal: options.signal }),
|
|
229
|
-
);
|
|
297
|
+
const tags = (options.tags ?? []).map(slugifyTag).filter((tag) => tag.length > 0);
|
|
298
|
+
|
|
299
|
+
if (mode === "specific") {
|
|
300
|
+
if (!options.idOrSlug) throw new Error("`idOrSlug` is required when mode is `specific`.");
|
|
301
|
+
return getProblem(options.idOrSlug, options.signal);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (mode === "daily" && tags.length === 0) {
|
|
305
|
+
const daily = await getDaily(options.signal);
|
|
306
|
+
if (isUsableProblem(daily)) return daily;
|
|
307
|
+
return pickUsable(() => getRandom({ difficulty: options.difficulty, signal: options.signal }));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (tags.length > 0) {
|
|
311
|
+
return fetchByTags(tags, options.difficulty, options.signal);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (mode === "filter") {
|
|
315
|
+
return pickUsable(() => getFiltered({ difficulty: options.difficulty, signal: options.signal }));
|
|
230
316
|
}
|
|
317
|
+
return pickUsable(() => getRandom({ difficulty: options.difficulty, signal: options.signal }));
|
|
231
318
|
}
|
|
232
319
|
|
|
233
320
|
export interface TagInfo {
|
package/src/prompt.ts
CHANGED
|
@@ -29,10 +29,19 @@ puzzle.
|
|
|
29
29
|
Arguments: "$ARGUMENTS"
|
|
30
30
|
Recognise any of these (all optional):
|
|
31
31
|
- difficulty: easy | medium | hard
|
|
32
|
-
- tags:
|
|
32
|
+
- tags: topic names or slugs, e.g. "graph" or "dynamic-programming,sliding-window"
|
|
33
33
|
- a specific problem id or slug, e.g. "two-sum" or "1"
|
|
34
34
|
- language: a supported language name or alias (below)
|
|
35
35
|
|
|
36
|
+
Mapping rules:
|
|
37
|
+
- A word that names a topic (array, string, graph, tree, dp/dynamic-programming,
|
|
38
|
+
sliding-window, two-pointers, greedy, ...) is a TAG. Pass it to \`leetcode_fetch\`
|
|
39
|
+
as \`tags\` (an array of slugs, e.g. \`["array"]\`).
|
|
40
|
+
- Only pass \`idOrSlug\` when the argument is a number or a known problem slug
|
|
41
|
+
(e.g. "1", "two-sum"). Never pass a topic name as \`idOrSlug\`.
|
|
42
|
+
- When the user gives a tag, it MUST be respected. After fetching, if the problem's
|
|
43
|
+
topics do not include the requested tag, the fetch is wrong: retry with the tag.
|
|
44
|
+
|
|
36
45
|
## 2. Ask when unclear (use the \`question\` tool)
|
|
37
46
|
Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}.
|
|
38
47
|
|
|
@@ -47,9 +56,11 @@ If the user did specify values, skip the corresponding question. Only ask at all
|
|
|
47
56
|
when something is missing.
|
|
48
57
|
|
|
49
58
|
## 3. Fetch the source problem (private)
|
|
50
|
-
Call the \`leetcode_fetch\` tool with the difficulty
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
Call the \`leetcode_fetch\` tool with the difficulty, and pass any requested topic
|
|
60
|
+
as \`tags\` (array of slugs) — not as \`idOrSlug\`. Verify the returned topics
|
|
61
|
+
include every requested tag; if not, retry. The fetched statement is for YOUR
|
|
62
|
+
reasoning only. Never paste it, paraphrase it closely, or name its source anywhere
|
|
63
|
+
in the generated project.
|
|
53
64
|
|
|
54
65
|
## 4. Derive the principle
|
|
55
66
|
Identify the underlying algorithmic idea (e.g. sliding window, heap-based
|