opencode-leetcode-realworld 0.2.3 → 0.4.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 CHANGED
@@ -76,6 +76,7 @@ Options can be given in any order; the command figures out which is which.
76
76
  | `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. |
77
77
  | `<id-or-slug>` | A problem number or slug (e.g. `1`, `two-sum`) | random | Derive the assignment from one specific source problem. |
78
78
  | `language` | A supported language or alias (see below) | **asks you** | Language of the generated project and tests. |
79
+ | `scope` | `single` \| `mini` \| `full` | **asks you** | How big the assignment is (see Two modes). |
79
80
 
80
81
  If you leave the language (or difficulty) out, opencode **asks you** with a
81
82
  multiple-choice question before generating anything.
@@ -150,17 +151,34 @@ they tend to produce:
150
151
  ```
151
152
  /practice
152
153
  │
153
- ├─ (agent) if language/difficulty are missing, ask you via the `question` tool
154
+ ├─ (agent) ask you for language/difficulty, then how you want to start
154
155
  │
155
156
  ├─ leetcode_fetch fetch a problem from the LeetCode API (private reasoning input)
156
157
  │
157
158
  ├─ (agent) derive the principle -> invent a real-world scenario
158
159
  │
159
- ├─ leetcode_scaffold write README / PROJECT / starter / tests, reject any leak
160
+ ├─ leetcode_scaffold write the exercises(s): one function, or a multi-file app
160
161
  │
161
162
  └─ guard hooks block reading hidden tests + provenance, keep the exercise honest
162
163
  ```
163
164
 
165
+ ## Two modes
166
+
167
+ `/practice` asks **how you want to start**, and the answer changes what gets built:
168
+
169
+ | Scope | What you get |
170
+ | --- | --- |
171
+ | **Quick exercise** | One entry point with a JSON-in / JSON-out contract (any language). |
172
+ | **Mini project** | A small multi-file app (~3-6 files) with the algorithm behind a single `TODO` (TypeScript, Python, Rust). |
173
+ | **Fuller project** | A multi-file app with more realistic layers (~6-15 files) and the same single `TODO`. |
174
+
175
+ In project mode you get a real little codebase — CLI entry, domain types, service
176
+ layer, wiring — where **everything works except one `TODO`**. You implement that
177
+ piece (the algorithm) and `node tests/runner.mjs` verifies the app end-to-end.
178
+
179
+ Project mode is available for **TypeScript, Python, Rust**; the other languages use
180
+ the quick exercise.
181
+
164
182
  The generated project looks like this:
165
183
 
166
184
  ```
@@ -182,6 +200,43 @@ node tests/runner.mjs # all cases
182
200
  node tests/runner.mjs --public # visible cases only
183
201
  ```
184
202
 
203
+ ## Typed starters
204
+
205
+ The starter file is generated from your test cases, so the input/output contract
206
+ is explicit before you write any logic:
207
+
208
+ - **Type-safe languages** (`typescript`, `go`, `rust`, `csharp`, `swift`) get real
209
+ type declarations inferred from the sample inputs/outputs — an interface /
210
+ struct / class per object, with nested objects and arrays typed too — plus a
211
+ typed function signature. Your editor autocompletes `payload.<field>`.
212
+ - **Dynamic languages** (`javascript`, `python`, `ruby`, `php`) — and Swift when a
213
+ shape can't be expressed statically — get a commented input/output format with a
214
+ concrete example instead.
215
+
216
+ Example (TypeScript), generated from input `{ "values": [1,2,3] }` -> output `[3,2,1]`:
217
+
218
+ ```ts
219
+ interface Payload {
220
+ values: number[];
221
+ }
222
+
223
+ function solve(payload: Payload): number[] {
224
+ throw new Error("Not implemented: solve");
225
+ }
226
+ ```
227
+
228
+ Example (Python):
229
+
230
+ ```python
231
+ # Input format: { values: number[] }
232
+ # Input example: {"values":[1,2,3]}
233
+ # Output format: number[]
234
+ # Output example: [3,2,1]
235
+
236
+ def solve(payload):
237
+ raise NotImplementedError("Not implemented: solve")
238
+ ```
239
+
185
240
  ## Running from source
186
241
 
187
242
  For plugin development, clone the repo and open opencode inside it — files under
@@ -234,6 +289,7 @@ src/
234
289
  index.ts plugin entry: tools, guard hooks, command registration
235
290
  leetcode.ts API client + normalizer (handles endpoint shape differences)
236
291
  languages.ts per-language adapters: starter, build, run commands
292
+ schema.ts input/output schema inference + type generation
237
293
  patterns.ts topic -> domain seeds, leak detection
238
294
  scaffold.ts deterministic project writer (any supported language)
239
295
  guard.ts protected-path detection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-leetcode-realworld",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
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
@@ -71,9 +71,15 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
71
71
 
72
72
  const scaffoldTool = tool({
73
73
  description:
74
- "Create a real-world practice project from a spec (scenario, requirements, starter, tests). " +
74
+ "Create a real-world practice project from a spec. " +
75
+ "Use mode \"single\" for one JSON-in/out function (any language) or mode \"project\" " +
76
+ "(typescript, python, rust) for a multi-file app where one TODO must be implemented. " +
75
77
  "Rejects any spec that leaks the original problem, its title, slug, or a coding-practice site name.",
76
78
  args: {
79
+ mode: tool.schema
80
+ .enum(["single", "project"])
81
+ .default("single")
82
+ .describe("single = one function; project = multi-file app (ts/python/rust)"),
77
83
  title: tool.schema.string().describe("Real-world project title (no coding-practice words)"),
78
84
  scenario: tool.schema.string().describe("Business context, written like a real ticket"),
79
85
  pattern: tool.schema.string().describe("Underlying algorithmic principle (internal only)"),
@@ -102,6 +108,22 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
102
108
  outDir: tool.schema.string().optional().describe("Target directory, relative to the session directory"),
103
109
  sourceSlug: tool.schema.string().optional().describe("Provenance only; never surfaced"),
104
110
  sourceTitle: tool.schema.string().optional().describe("Provenance only; never surfaced"),
111
+ task: tool.schema
112
+ .string()
113
+ .optional()
114
+ .describe("Project mode: markdown brief telling the learner what to implement and where"),
115
+ files: tool.schema
116
+ .array(tool.schema.object({ path: tool.schema.string(), content: tool.schema.string() }))
117
+ .optional()
118
+ .describe("Project mode: the full file tree, leaving one TODO to implement"),
119
+ runCommand: tool.schema
120
+ .string()
121
+ .optional()
122
+ .describe("Project mode: command that runs one case (JSON on stdin, JSON on stdout)"),
123
+ buildCommand: tool.schema
124
+ .string()
125
+ .optional()
126
+ .describe("Project mode: optional one-time build command (compiled languages)"),
105
127
  },
106
128
  async execute(rawArgs, context) {
107
129
  const spec = rawArgs as ScaffoldSpec;
@@ -131,6 +153,7 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
131
153
  `Created a real-world practice project at ${relative}`,
132
154
  "",
133
155
  `Title: ${result.title}`,
156
+ `Mode: ${result.mode}${result.mode === "project" ? " (implement the marked TODO)" : ""}`,
134
157
  `Language: ${result.languageName} (${result.runtime})`,
135
158
  `Files (${result.files.length}):`,
136
159
  ...result.files.map((file) => ` - ${file}`),