opencode-leetcode-realworld 0.0.0-stage → 0.2.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.
- package/README.md +209 -2
- package/package.json +33 -4
- package/src/guard.ts +61 -0
- package/src/index.ts +173 -0
- package/src/languages.ts +358 -0
- package/src/leetcode.ts +257 -0
- package/src/patterns.ts +141 -0
- package/src/prompt.ts +111 -0
- package/src/scaffold.ts +310 -0
- package/src/types.ts +92 -0
package/README.md
CHANGED
|
@@ -1,3 +1,210 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-leetcode-realworld
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An [opencode](https://opencode.ai) plugin that turns a LeetCode problem into a
|
|
4
|
+
**realistic software-engineering assignment**.
|
|
5
|
+
|
|
6
|
+
Solving the puzzle and shipping the principle are different skills. This plugin
|
|
7
|
+
fetches a problem, extracts the *underlying algorithm*, and generates a project
|
|
8
|
+
that actually needs that algorithm — a scenario, requirements, a starter stub,
|
|
9
|
+
and a test suite. The original problem is never shown to you.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/practice # random Medium problem
|
|
13
|
+
/practice hard graph # difficulty + tags
|
|
14
|
+
/practice two-sum # a specific problem
|
|
15
|
+
/practice medium dynamic-programming python
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
/practice [options]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Options can be given in any order; the command figures out which is which.
|
|
25
|
+
|
|
26
|
+
### Options
|
|
27
|
+
|
|
28
|
+
| Option | Accepted values | Default | Description |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `difficulty` | `easy` \| `medium` \| `hard` | **asks you** (Medium suggested) | Difficulty of the source problem. |
|
|
31
|
+
| `tags` | One or more topic tags, comma-separated (e.g. `graph` or `dynamic-programming,sliding-window`) | any | Nudges which underlying algorithm and real-world domain you get. |
|
|
32
|
+
| `<id-or-slug>` | A problem number or slug (e.g. `1`, `two-sum`) | random | Derive the assignment from one specific source problem. |
|
|
33
|
+
| `language` | A supported language or alias (see below) | **asks you** | Language of the generated project and tests. |
|
|
34
|
+
|
|
35
|
+
If you leave the language (or difficulty) out, opencode **asks you** with a
|
|
36
|
+
multiple-choice question before generating anything.
|
|
37
|
+
|
|
38
|
+
### Examples
|
|
39
|
+
|
|
40
|
+
| Command | What you get |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `/practice` | Asks for language + difficulty, then builds a random Medium problem. |
|
|
43
|
+
| `/practice easy` | Asks for the language, then an Easy problem. |
|
|
44
|
+
| `/practice hard graph` | Hard problem tagged Graph. |
|
|
45
|
+
| `/practice two-sum` | Asks for the language, then builds from the Two Sum problem. |
|
|
46
|
+
| `/practice medium dynamic-programming python` | Medium DP problem, Python project. |
|
|
47
|
+
| `/practice sliding-window js` | Random sliding-window problem in JavaScript. |
|
|
48
|
+
|
|
49
|
+
### Supported languages
|
|
50
|
+
|
|
51
|
+
| Language | Aliases | Runtime needed |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| TypeScript | `ts` | Node.js 22.6+ |
|
|
54
|
+
| JavaScript | `js`, `node` | Node.js 18+ |
|
|
55
|
+
| Python | `py`, `python3` | Python 3.8+ |
|
|
56
|
+
| Ruby | `rb` | Ruby 2.6+ |
|
|
57
|
+
| PHP | — | PHP 7.4+ |
|
|
58
|
+
| Go | `golang` | Go 1.18+ |
|
|
59
|
+
| Rust | `rs`, `cargo` | Rust 1.70+ (fetches `serde_json`) |
|
|
60
|
+
| C# | `cs`, `dotnet` | .NET 8 SDK |
|
|
61
|
+
| Swift | — | Swift 5.5+ |
|
|
62
|
+
|
|
63
|
+
Every language uses the same contract: implement `src/solution.<ext>`, read one
|
|
64
|
+
JSON value from stdin, write one JSON value to stdout. The shared runner verifies
|
|
65
|
+
any language identically.
|
|
66
|
+
|
|
67
|
+
### Topic tags you can practice
|
|
68
|
+
|
|
69
|
+
Any LeetCode topic tag works. Common ones and the kind of real-world scenario
|
|
70
|
+
they tend to produce:
|
|
71
|
+
|
|
72
|
+
| Tag | Typical generated domain |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `array` | batch audit-log processor |
|
|
75
|
+
| `string` | log sanitizer |
|
|
76
|
+
| `hash-table` | event deduplication pipeline |
|
|
77
|
+
| `two-pointers` | order-book matching engine |
|
|
78
|
+
| `sliding-window` | API rate limiter |
|
|
79
|
+
| `prefix-sum` | rolling revenue aggregator |
|
|
80
|
+
| `binary-search` | price-threshold alert engine |
|
|
81
|
+
| `sorting` | leaderboard ranker |
|
|
82
|
+
| `heap-priority-queue` | background job scheduler |
|
|
83
|
+
| `monotonic-stack` | histogram-based capacity planner |
|
|
84
|
+
| `stack` | config-rule expression evaluator |
|
|
85
|
+
| `queue` | async task pipeline |
|
|
86
|
+
| `greedy` | resource bin-packing scheduler |
|
|
87
|
+
| `dynamic-programming` | delivery route cost optimizer |
|
|
88
|
+
| `graph` | microservice dependency resolver |
|
|
89
|
+
| `depth-first-search` | permission inheritance resolver |
|
|
90
|
+
| `breadth-first-search` | service-mesh shortest-path router |
|
|
91
|
+
| `union-find` | account clustering for fraud rings |
|
|
92
|
+
| `topological-sort` | build-order planner |
|
|
93
|
+
| `trie` | autocomplete search index |
|
|
94
|
+
| `tree` | filesystem index builder |
|
|
95
|
+
| `linked-list` | LRU eviction chain |
|
|
96
|
+
| `backtracking` | deployment permutation explorer |
|
|
97
|
+
| `bit-manipulation` | feature-flag bitmask service |
|
|
98
|
+
| `segment-tree` | range-quota enforcement service |
|
|
99
|
+
| `design` | in-memory key-value store |
|
|
100
|
+
| `database` | analytics reconciliation job |
|
|
101
|
+
| `simulation` | tick-based match engine |
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
/practice
|
|
107
|
+
│
|
|
108
|
+
├─ leetcode_fetch fetch a problem from the LeetCode API (private reasoning input)
|
|
109
|
+
│
|
|
110
|
+
├─ (agent) derive the principle -> invent a real-world scenario
|
|
111
|
+
│
|
|
112
|
+
├─ leetcode_scaffold write README / PROJECT / starter / tests, reject any leak
|
|
113
|
+
│
|
|
114
|
+
└─ guard hooks block reading hidden tests + provenance, keep the exercise honest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The generated project looks like this:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
.practice/<project>/
|
|
121
|
+
├── README.md # quickstart + how to run
|
|
122
|
+
├── PROJECT.md # scenario, requirements, definition of done
|
|
123
|
+
├── harness.json # build + run commands for the chosen language
|
|
124
|
+
├── src/solution.<ext> # implement this (one JSON in, one JSON out)
|
|
125
|
+
├── tests/runner.mjs # language-agnostic test harness (Node)
|
|
126
|
+
├── tests/public/cases.json # visible test cases
|
|
127
|
+
├── tests/hidden/cases.json # extra acceptance cases (gitignored, guarded)
|
|
128
|
+
└── .practice-meta.json # provenance only (gitignored, guarded)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Run the checks with:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
node tests/runner.mjs # all cases
|
|
135
|
+
node tests/runner.mjs --public # visible cases only
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Install
|
|
139
|
+
|
|
140
|
+
### Local (development)
|
|
141
|
+
|
|
142
|
+
Clone/copy this repo, then open opencode inside it. Files under
|
|
143
|
+
`.opencode/plugins/` and `.opencode/commands/` are auto-loaded.
|
|
144
|
+
|
|
145
|
+
### From npm (once published)
|
|
146
|
+
|
|
147
|
+
```jsonc
|
|
148
|
+
// opencode.json
|
|
149
|
+
{
|
|
150
|
+
"$schema": "https://opencode.ai/config.json",
|
|
151
|
+
"plugin": ["opencode-leetcode-realworld"]
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The plugin registers the `/practice` command itself, so no extra config is needed.
|
|
156
|
+
|
|
157
|
+
## Tools
|
|
158
|
+
|
|
159
|
+
| Tool | Purpose |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `leetcode_fetch` | Fetch a problem by `mode` (`random` \| `specific` \| `daily` \| `filter`), `difficulty`, `tags`, `idOrSlug`. Skips premium problems. |
|
|
162
|
+
| `leetcode_scaffold` | Create the real-world project from a spec in any supported language. Rejects specs that mention LeetCode, the source title, or its slug. |
|
|
163
|
+
|
|
164
|
+
## The honest-practice guard
|
|
165
|
+
|
|
166
|
+
Two plugin hooks keep the exercise meaningful:
|
|
167
|
+
|
|
168
|
+
- **Leak rejection** — `leetcode_scaffold` scans every learner-visible string for
|
|
169
|
+
the source title/slug and practice-site names (`leetcode`, `neetcode`, ...). If
|
|
170
|
+
found, it refuses and tells the agent to rewrite.
|
|
171
|
+
- **Access block** — `tool.execute.before` blocks `read`/`write`/`edit`/`glob`/
|
|
172
|
+
`grep`/`bash` from touching `tests/hidden/**`, `.practice-meta.json`, and
|
|
173
|
+
`.practice/meta.json`. You implement against the visible spec; the oracle stays
|
|
174
|
+
out of reach.
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
| Env var | Default | Description |
|
|
179
|
+
| --- | --- | --- |
|
|
180
|
+
| `LEETCODE_API_BASE` | `https://leetcode-api-pied.vercel.app` | Base URL of the LeetCode API. |
|
|
181
|
+
|
|
182
|
+
Data comes from the community [leetcode-api](https://github.com/noworneverev/leetcode-api)
|
|
183
|
+
project. This plugin is not affiliated with LeetCode.
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
bun install
|
|
189
|
+
bun test # unit + integration tests
|
|
190
|
+
bun run typecheck # tsc --noEmit
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Run `opencode debug config` inside the repo to confirm the plugin and `/practice`
|
|
194
|
+
command are resolved.
|
|
195
|
+
|
|
196
|
+
## Project layout
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
src/
|
|
200
|
+
index.ts plugin entry: tools, guard hooks, command registration
|
|
201
|
+
leetcode.ts API client + normalizer (handles endpoint shape differences)
|
|
202
|
+
languages.ts per-language adapters: starter, build, run commands
|
|
203
|
+
patterns.ts topic -> domain seeds, leak detection
|
|
204
|
+
scaffold.ts deterministic project writer (any supported language)
|
|
205
|
+
guard.ts protected-path detection
|
|
206
|
+
prompt.ts the /practice command template
|
|
207
|
+
types.ts shared types
|
|
208
|
+
.opencode/ local wiring so this repo is usable immediately
|
|
209
|
+
tests/ unit + integration tests
|
|
210
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-leetcode-realworld",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"opencode",
|
|
17
|
+
"opencode-plugin",
|
|
18
|
+
"leetcode",
|
|
19
|
+
"practice",
|
|
20
|
+
"interview",
|
|
21
|
+
"algorithms"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "bun test"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@opencode-ai/plugin": "^1.4.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bun": "^1.4.2",
|
|
32
|
+
"@types/node": "^22.13.9",
|
|
33
|
+
"typescript": "^5.8.2"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/guard.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated oracle tests and provenance file are the only things keeping the
|
|
3
|
+
* practice exercise honest. If the learner (or a helpful agent) can just read the
|
|
4
|
+
* hidden tests or the `.practice-meta.json` that points back to the original
|
|
5
|
+
* problem, the exercise collapses into "solve the LeetCode problem".
|
|
6
|
+
*
|
|
7
|
+
* These helpers detect tool calls that would touch those files so the plugin can
|
|
8
|
+
* block them.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const GUARDED_TOOLS = new Set([
|
|
12
|
+
"read",
|
|
13
|
+
"write",
|
|
14
|
+
"edit",
|
|
15
|
+
"glob",
|
|
16
|
+
"grep",
|
|
17
|
+
"list",
|
|
18
|
+
"find",
|
|
19
|
+
"bash",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const HIDDEN_SEGMENT = /(^|[\/\s"'])tests\/hidden(\/|[\s"']|$)/;
|
|
23
|
+
const META_FILE = /(^|[\/\s"'])\.practice-meta\.json/;
|
|
24
|
+
const LEGACY_META = /(^|[\/\s"'])\.practice\/meta\.json/;
|
|
25
|
+
|
|
26
|
+
export function normalizeReference(value: string): string {
|
|
27
|
+
return value.replace(/\\/g, "/");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isProtectedReference(value: string): boolean {
|
|
31
|
+
const normalized = normalizeReference(value);
|
|
32
|
+
return HIDDEN_SEGMENT.test(normalized) || META_FILE.test(normalized) || LEGACY_META.test(normalized);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isGuardedTool(toolName: string): boolean {
|
|
36
|
+
return GUARDED_TOOLS.has(toolName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pull the path-like arguments out of a tool call. Deliberately conservative:
|
|
41
|
+
* we only inspect fields that name a file, directory, or shell command, so we
|
|
42
|
+
* never false-positive on prose or search patterns.
|
|
43
|
+
*/
|
|
44
|
+
export function collectPathReferences(toolName: string, args: Record<string, unknown>): string[] {
|
|
45
|
+
const references: string[] = [];
|
|
46
|
+
const add = (value: unknown) => {
|
|
47
|
+
if (typeof value === "string" && value.length > 0) references.push(value);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
add(args.filePath);
|
|
51
|
+
add(args.path);
|
|
52
|
+
add(args.pattern);
|
|
53
|
+
add(args.command);
|
|
54
|
+
add(args.include);
|
|
55
|
+
|
|
56
|
+
if (toolName === "bash" && typeof args.command === "string") {
|
|
57
|
+
references.push(args.command);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return references;
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { type Plugin, tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { collectPathReferences, isGuardedTool, isProtectedReference } from "./guard";
|
|
4
|
+
import { fetchProblem, renderProblemForAgent } from "./leetcode";
|
|
5
|
+
import { PRACTICE_DESCRIPTION, PRACTICE_TEMPLATE } from "./prompt";
|
|
6
|
+
import { scaffold } from "./scaffold";
|
|
7
|
+
import type { ScaffoldSpec } from "./types";
|
|
8
|
+
|
|
9
|
+
const testCaseSchema = tool.schema.object({
|
|
10
|
+
name: tool.schema.string().describe("Short description of what the case asserts"),
|
|
11
|
+
input: tool.schema.any().describe("The single JSON-shaped payload passed to the entry function"),
|
|
12
|
+
expected: tool.schema.any().describe("The JSON-shaped value the entry function must return"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
16
|
+
const log = async (
|
|
17
|
+
level: "debug" | "info" | "warn" | "error",
|
|
18
|
+
message: string,
|
|
19
|
+
extra?: Record<string, unknown>,
|
|
20
|
+
) => {
|
|
21
|
+
try {
|
|
22
|
+
await client.app.log({
|
|
23
|
+
body: { service: "leetcode-realworld", level, message, extra },
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
/* logging must never break a session */
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const fetchTool = tool({
|
|
31
|
+
description:
|
|
32
|
+
"Fetch a LeetCode problem (random, by id/slug, the daily challenge, or filtered by difficulty/tags). " +
|
|
33
|
+
"Use the returned algorithmic principle to design a real-world assignment. Never surface the raw problem to the learner.",
|
|
34
|
+
args: {
|
|
35
|
+
mode: tool.schema
|
|
36
|
+
.enum(["random", "specific", "daily", "filter"])
|
|
37
|
+
.default("random")
|
|
38
|
+
.describe("How to choose the problem"),
|
|
39
|
+
idOrSlug: tool.schema.string().optional().describe("Problem id or slug (required for mode=specific)"),
|
|
40
|
+
difficulty: tool.schema.enum(["Easy", "Medium", "Hard"]).optional(),
|
|
41
|
+
tags: tool.schema.array(tool.schema.string()).optional().describe("Topic tags, e.g. ['graph']"),
|
|
42
|
+
},
|
|
43
|
+
async execute(args, context) {
|
|
44
|
+
const problem = await fetchProblem({
|
|
45
|
+
mode: args.mode,
|
|
46
|
+
idOrSlug: args.idOrSlug,
|
|
47
|
+
difficulty: args.difficulty,
|
|
48
|
+
tags: args.tags,
|
|
49
|
+
signal: context.abort,
|
|
50
|
+
});
|
|
51
|
+
context.metadata({ title: `${problem.title} (${problem.difficulty})` });
|
|
52
|
+
await log("info", "Fetched problem", { slug: problem.slug, difficulty: problem.difficulty });
|
|
53
|
+
const metadata = {
|
|
54
|
+
id: problem.frontendId || problem.id,
|
|
55
|
+
slug: problem.slug,
|
|
56
|
+
title: problem.title,
|
|
57
|
+
difficulty: problem.difficulty,
|
|
58
|
+
topics: problem.topics,
|
|
59
|
+
url: problem.url,
|
|
60
|
+
};
|
|
61
|
+
return `${renderProblemForAgent(problem)}\n\n<!-- source-metadata ${JSON.stringify(
|
|
62
|
+
metadata,
|
|
63
|
+
)} -->`;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const scaffoldTool = tool({
|
|
68
|
+
description:
|
|
69
|
+
"Create a real-world practice project from a spec (scenario, requirements, starter, tests). " +
|
|
70
|
+
"Rejects any spec that leaks the original problem, its title, slug, or a coding-practice site name.",
|
|
71
|
+
args: {
|
|
72
|
+
title: tool.schema.string().describe("Real-world project title (no coding-practice words)"),
|
|
73
|
+
scenario: tool.schema.string().describe("Business context, written like a real ticket"),
|
|
74
|
+
pattern: tool.schema.string().describe("Underlying algorithmic principle (internal only)"),
|
|
75
|
+
projectName: tool.schema.string().optional().describe("kebab-case directory name"),
|
|
76
|
+
difficulty: tool.schema.string().optional(),
|
|
77
|
+
language: tool.schema
|
|
78
|
+
.enum([
|
|
79
|
+
"typescript",
|
|
80
|
+
"javascript",
|
|
81
|
+
"python",
|
|
82
|
+
"ruby",
|
|
83
|
+
"php",
|
|
84
|
+
"go",
|
|
85
|
+
"rust",
|
|
86
|
+
"csharp",
|
|
87
|
+
"swift",
|
|
88
|
+
])
|
|
89
|
+
.default("typescript")
|
|
90
|
+
.describe("Target language for the practice project"),
|
|
91
|
+
functionName: tool.schema.string().optional().describe("Entry function name (default: solve)"),
|
|
92
|
+
requirements: tool.schema.array(tool.schema.string()).default([]).describe("Testable requirements"),
|
|
93
|
+
edgeCases: tool.schema.array(tool.schema.string()).default([]),
|
|
94
|
+
starterCode: tool.schema.string().optional().describe("Optional starter implementation"),
|
|
95
|
+
publicTests: tool.schema.array(testCaseSchema).default([]).describe("Visible test cases"),
|
|
96
|
+
hiddenTests: tool.schema.array(testCaseSchema).default([]).describe("Extra acceptance test cases"),
|
|
97
|
+
outDir: tool.schema.string().optional().describe("Target directory, relative to the session directory"),
|
|
98
|
+
sourceSlug: tool.schema.string().optional().describe("Provenance only; never surfaced"),
|
|
99
|
+
sourceTitle: tool.schema.string().optional().describe("Provenance only; never surfaced"),
|
|
100
|
+
},
|
|
101
|
+
async execute(rawArgs, context) {
|
|
102
|
+
const spec = rawArgs as ScaffoldSpec;
|
|
103
|
+
const result = await scaffold(spec, { directory: context.directory });
|
|
104
|
+
context.metadata({ title: result.title });
|
|
105
|
+
|
|
106
|
+
const relative = path.relative(context.directory, result.outDir) || ".";
|
|
107
|
+
await log("info", "Scaffolded practice project", {
|
|
108
|
+
outDir: relative,
|
|
109
|
+
language: result.language,
|
|
110
|
+
files: result.files.length,
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
await client.tui.showToast({
|
|
114
|
+
body: {
|
|
115
|
+
title: "Practice project ready",
|
|
116
|
+
message: `${result.title} -> ${relative}`,
|
|
117
|
+
variant: "success",
|
|
118
|
+
duration: 5000,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
/* toast is best-effort */
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [
|
|
126
|
+
`Created a real-world practice project at ${relative}`,
|
|
127
|
+
"",
|
|
128
|
+
`Title: ${result.title}`,
|
|
129
|
+
`Language: ${result.languageName} (${result.runtime})`,
|
|
130
|
+
`Files (${result.files.length}):`,
|
|
131
|
+
...result.files.map((file) => ` - ${file}`),
|
|
132
|
+
"",
|
|
133
|
+
`Check your work with: ${result.checkCommand}`,
|
|
134
|
+
"The solution is intentionally left unimplemented. Do not write it for the learner.",
|
|
135
|
+
].join("\n");
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
config: async (config) => {
|
|
141
|
+
const commands = (config.command ??= {});
|
|
142
|
+
if (!commands.practice) {
|
|
143
|
+
commands.practice = {
|
|
144
|
+
template: PRACTICE_TEMPLATE,
|
|
145
|
+
description: PRACTICE_DESCRIPTION,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
tool: {
|
|
151
|
+
leetcode_fetch: fetchTool,
|
|
152
|
+
leetcode_scaffold: scaffoldTool,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
"tool.execute.before": async (input, output) => {
|
|
156
|
+
if (!isGuardedTool(input.tool)) return;
|
|
157
|
+
const args = (output.args ?? {}) as Record<string, unknown>;
|
|
158
|
+
const reference = collectPathReferences(input.tool, args).find(isProtectedReference);
|
|
159
|
+
if (reference) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`[leetcode-realworld] "${reference}" is protected practice material and cannot be accessed. ` +
|
|
162
|
+
"Hidden acceptance tests and problem provenance are off-limits so the exercise stays honest.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
event: async ({ event }) => {
|
|
168
|
+
if (event.type === "session.idle") {
|
|
169
|
+
await log("debug", "Session idle", { cwd: directory });
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
};
|