diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d91aabb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.cjs"], + "preLaunchTask": "npm: build" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..98c3d8b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": "build", + "problemMatcher": ["$esbuild"], + "label": "npm: build", + "detail": "Bundle the extension with esbuild" + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..2939936 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# vscode-cowriting-plugin + +Non-shippable **proof-of-concept** (Feature #2 of Epic #1): a standalone VS Code +extension that drives **[`@cline/sdk`](https://www.npmjs.com/package/@cline/sdk)** +— validating Approach A (own coauthoring extension on the Cline SDK, no fork). + +## What it does + +Registers one command, **`Cowriting: Show Cline SDK Info`**, which loads +`@cline/sdk` and shows the SDK build version plus the agent's builtin tool +catalog (a pure, key-free SDK call) in a notification and the +"Cowriting (Cline SDK)" output channel. + +## Architecture + +- CommonJS extension bundled with esbuild (`src/extension.ts` → `out/extension.cjs`). +- `@cline/sdk` is ESM-only (Node ≥22) and uses `createRequire(import.meta.url)`, + so it is **not** bundled — it is shipped in `node_modules` and loaded at + runtime via dynamic `import()` from the vscode-free `src/cline.ts`. + +## Run it (F5) + +1. `npm install` +2. `npm run build` +3. Press **F5** (or Run → "Run Extension") to launch the Extension Development Host. +4. In the new window: **Cmd/Ctrl+Shift+P** → **"Cowriting: Show Cline SDK Info"**. + +## Develop + +- `npm run watch` — rebuild on change. +- `npm test` — run the unit test for the SDK driver. +- `npm run typecheck` — type-check without emit. diff --git a/docs/superpowers/plans/2026-06-09-poc-cline-sdk-extension.md b/docs/superpowers/plans/2026-06-09-poc-cline-sdk-extension.md new file mode 100644 index 0000000..7a4924f --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-poc-cline-sdk-extension.md @@ -0,0 +1,468 @@ +# POC: Runnable VS Code Extension on `@cline/sdk` — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Scaffold a standalone, non-shippable VS Code extension that builds, launches in the Extension Development Host on F5, and runs one command that drives `@cline/sdk` and shows a real result — validating Approach A (standalone extension on the Cline SDK, no fork). + +**Architecture:** A CommonJS VS Code extension bundled with esbuild (`vscode` and `@cline/sdk` both kept external). `@cline/sdk` is ESM-only (Node ≥22) and uses `createRequire(import.meta.url)`, so it **cannot** be bundled into a CJS extension — instead it is shipped in `node_modules` and loaded at runtime via dynamic `import('@cline/sdk')`. The SDK-driving logic lives in a vscode-free module (`src/cline.ts`) so it is unit-testable in Node; the vscode command wiring lives in `src/extension.ts`. The command calls the SDK's pure `getCoreBuiltinToolCatalog()` + `CORE_BUILD_VERSION` (no API keys, no agent run) and renders the result. + +**Tech Stack:** TypeScript, VS Code Extension API (`engines.vscode`), esbuild (bundler), vitest (unit test), `@cline/sdk@0.0.46`. + +**De-risking already done (empirical, see session 0002 transcript):** +- `@cline/sdk@0.0.46` loads in Node 24; `CORE_BUILD_VERSION === "0.0.46"`; `getCoreBuiltinToolCatalog()` returns 9 entries with shape `{id, description, headlessToolNames, defaultEnabled}` (e.g. `read_files`, `search_codebase`, `run_commands`, `editor`, `fetch_web_content`). +- Bundling the SDK **into** CJS fails (`import.meta.url` undefined → `createRequire` throws). Keeping it external + dynamic `import()` from a CJS bundle works and yields a ~2KB extension bundle. + +--- + +## File Structure + +| File | Responsibility | +| --- | --- | +| `package.json` | Extension manifest: `main`, `engines.vscode`, `contributes.commands`, `activationEvents`, scripts, deps. | +| `tsconfig.json` | TypeScript config (typecheck only; esbuild does the emit). | +| `esbuild.mjs` | Bundle `src/extension.ts` → `out/extension.cjs`, externalize `vscode` + `@cline/sdk`. | +| `src/cline.ts` | vscode-free SDK driver: `fetchSdkSummary()` dynamic-imports `@cline/sdk`, returns `{version, tools}`. Unit-testable. | +| `src/extension.ts` | `activate`/`deactivate`; registers the `cowriting.showClineSdkInfo` command; renders the summary via notification + output channel. | +| `test/cline.test.ts` | vitest unit test asserting `fetchSdkSummary()` returns a semver version and a non-empty tool list including `read_files`. | +| `.vscode/launch.json` | F5 → "Run Extension" launches the Extension Development Host with this extension. | +| `.vscode/tasks.json` | The `npm: build` task the launch config depends on. | +| `.gitignore` | Ignore `node_modules/`, `out/`. | +| `vitest.config.ts` | vitest node environment config. | +| `README.md` | What the POC is + how to run it (F5). | + +--- + +## Task 1: Project manifest and tooling config + +**Files:** +- Create: `package.json` +- Create: `tsconfig.json` +- Create: `.gitignore` +- Create: `vitest.config.ts` + +- [ ] **Step 1: Write `package.json`** + +```json +{ + "name": "vscode-cowriting-plugin", + "displayName": "Cowriting (Cline SDK POC)", + "description": "Non-shippable POC: a standalone VS Code extension that drives @cline/sdk.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "publisher": "benstull", + "engines": { + "vscode": "^1.90.0", + "node": ">=22" + }, + "categories": ["Other"], + "main": "./out/extension.cjs", + "activationEvents": [], + "contributes": { + "commands": [ + { + "command": "cowriting.showClineSdkInfo", + "title": "Cowriting: Show Cline SDK Info", + "category": "Cowriting" + } + ] + }, + "scripts": { + "build": "node esbuild.mjs", + "watch": "node esbuild.mjs --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "vscode:prepublish": "node esbuild.mjs" + }, + "dependencies": { + "@cline/sdk": "0.0.46" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/vscode": "^1.90.0", + "esbuild": "^0.23.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" + } +} +``` + +Note: `contributes.commands` declares the command without a `when`, so it appears in the Command Palette as soon as the extension activates. `activationEvents` is empty because declaring a command auto-generates `onCommand:` activation in VS Code ≥1.74. + +- [ ] **Step 2: Write `tsconfig.json`** + +```json +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src", "test", "esbuild.mjs"] +} +``` + +- [ ] **Step 3: Write `.gitignore`** + +``` +node_modules/ +out/ +*.vsix +``` + +- [ ] **Step 4: Write `vitest.config.ts`** + +```ts +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts"], + }, +}); +``` + +- [ ] **Step 5: Install dependencies** + +Run: `npm install` +Expected: `node_modules/` populated, `@cline/sdk` present, no error exit. + +- [ ] **Step 6: Commit** + +```bash +git add package.json tsconfig.json .gitignore vitest.config.ts package-lock.json +git commit -m "POC scaffold: extension manifest + tooling config (Feature #2)" +``` + +--- + +## Task 2: SDK driver module (TDD) + +**Files:** +- Create: `src/cline.ts` +- Test: `test/cline.test.ts` + +- [ ] **Step 1: Write the failing test** + +`test/cline.test.ts`: + +```ts +import { describe, it, expect } from "vitest"; +import { fetchSdkSummary } from "../src/cline"; + +describe("fetchSdkSummary", () => { + it("loads @cline/sdk and returns its version and builtin tool catalog", async () => { + const summary = await fetchSdkSummary(); + expect(summary.version).toMatch(/^\d+\.\d+\.\d+/); + expect(summary.tools.length).toBeGreaterThan(0); + expect(summary.tools.map((t) => t.id)).toContain("read_files"); + for (const tool of summary.tools) { + expect(typeof tool.id).toBe("string"); + expect(typeof tool.description).toBe("string"); + } + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm test` +Expected: FAIL — cannot resolve `../src/cline` (module not created yet). + +- [ ] **Step 3: Write minimal implementation** + +`src/cline.ts`: + +```ts +/** + * vscode-free driver for @cline/sdk. + * + * @cline/sdk is ESM-only (Node >=22) and uses createRequire(import.meta.url), + * so it must NOT be bundled into the CJS extension. We load it at runtime via + * dynamic import(); the bundler keeps it external (see esbuild.mjs). + */ + +export interface ClineTool { + id: string; + description: string; +} + +export interface SdkSummary { + version: string; + tools: ClineTool[]; +} + +/** + * Drive @cline/sdk with pure, key-free calls and return a renderable summary: + * the SDK build version and the agent's builtin tool catalog. Proves the SDK + * is linked and callable from the extension host. + */ +export async function fetchSdkSummary(): Promise { + const sdk = await import("@cline/sdk"); + const catalog = sdk.getCoreBuiltinToolCatalog(); + return { + version: sdk.CORE_BUILD_VERSION, + tools: catalog.map((entry) => ({ + id: entry.id, + description: entry.description, + })), + }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm test` +Expected: PASS — version matches semver, tools include `read_files`. + +- [ ] **Step 5: Commit** + +```bash +git add src/cline.ts test/cline.test.ts +git commit -m "POC: vscode-free @cline/sdk driver with unit test (Feature #2)" +``` + +--- + +## Task 3: Extension entry point and command wiring + +**Files:** +- Create: `src/extension.ts` + +- [ ] **Step 1: Write `src/extension.ts`** + +```ts +import * as vscode from "vscode"; +import { fetchSdkSummary } from "./cline"; + +const CHANNEL_NAME = "Cowriting (Cline SDK)"; + +export function activate(context: vscode.ExtensionContext): void { + const output = vscode.window.createOutputChannel(CHANNEL_NAME); + context.subscriptions.push(output); + + const command = vscode.commands.registerCommand( + "cowriting.showClineSdkInfo", + async () => { + try { + const summary = await fetchSdkSummary(); + output.clear(); + output.appendLine(`@cline/sdk version: ${summary.version}`); + output.appendLine(`Builtin tools (${summary.tools.length}):`); + for (const tool of summary.tools) { + output.appendLine(` • ${tool.id} — ${tool.description}`); + } + output.show(true); + await vscode.window.showInformationMessage( + `Cline SDK ${summary.version} loaded — ${summary.tools.length} builtin tools. See the "${CHANNEL_NAME}" output channel.` + ); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + output.appendLine(`Failed to drive @cline/sdk: ${message}`); + output.show(true); + await vscode.window.showErrorMessage( + `Cowriting: failed to load @cline/sdk — ${message}` + ); + } + } + ); + context.subscriptions.push(command); +} + +export function deactivate(): void { + // Nothing to clean up beyond the disposables registered on the context. +} +``` + +- [ ] **Step 2: Typecheck** + +Run: `npm run typecheck` +Expected: PASS — no type errors (requires `@types/vscode` and `@types/node`). + +- [ ] **Step 3: Commit** + +```bash +git add src/extension.ts +git commit -m "POC: extension activate + showClineSdkInfo command (Feature #2)" +``` + +--- + +## Task 4: esbuild bundle + +**Files:** +- Create: `esbuild.mjs` + +- [ ] **Step 1: Write `esbuild.mjs`** + +```js +import { build, context } from "esbuild"; + +const watch = process.argv.includes("--watch"); + +/** @type {import('esbuild').BuildOptions} */ +const options = { + entryPoints: ["src/extension.ts"], + outfile: "out/extension.cjs", + bundle: true, + platform: "node", + format: "cjs", + target: "node20", + sourcemap: true, + // vscode is provided by the host; @cline/sdk is ESM-only and is loaded at + // runtime via dynamic import() from node_modules, so keep both external. + external: ["vscode", "@cline/sdk"], + logLevel: "info", +}; + +if (watch) { + const ctx = await context(options); + await ctx.watch(); + console.log("esbuild: watching…"); +} else { + await build(options); + console.log("esbuild: build complete → out/extension.cjs"); +} +``` + +- [ ] **Step 2: Build** + +Run: `npm run build` +Expected: `out/extension.cjs` created; log "build complete". Bundle is small (~few KB) because the SDK is external. + +- [ ] **Step 3: Verify the bundle keeps the SDK external** + +Run: `node -e "const s=require('fs').readFileSync('out/extension.cjs','utf8'); console.log('has dynamic import of sdk:', s.includes('@cline/sdk')); console.log('bytes:', s.length)"` +Expected: `has dynamic import of sdk: true`; byte count in the low thousands (SDK not inlined). + +- [ ] **Step 4: Commit** + +```bash +git add esbuild.mjs +git commit -m "POC: esbuild bundle (vscode + @cline/sdk external) (Feature #2)" +``` + +--- + +## Task 5: F5 launch configuration + +**Files:** +- Create: `.vscode/launch.json` +- Create: `.vscode/tasks.json` + +- [ ] **Step 1: Write `.vscode/tasks.json`** + +```json +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": "build", + "problemMatcher": ["$esbuild"], + "label": "npm: build", + "detail": "Bundle the extension with esbuild" + } + ] +} +``` + +- [ ] **Step 2: Write `.vscode/launch.json`** + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.cjs"], + "preLaunchTask": "npm: build" + } + ] +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add .vscode/launch.json .vscode/tasks.json +git commit -m "POC: F5 Run Extension launch config (Feature #2)" +``` + +--- + +## Task 6: README and verification + +**Files:** +- Create: `README.md` + +- [ ] **Step 1: Write `README.md`** + +```markdown +# vscode-cowriting-plugin + +Non-shippable **proof-of-concept** (Feature #2 of Epic #1): a standalone VS Code +extension that drives **[`@cline/sdk`](https://www.npmjs.com/package/@cline/sdk)** +— validating Approach A (own coauthoring extension on the Cline SDK, no fork). + +## What it does + +Registers one command, **`Cowriting: Show Cline SDK Info`**, which loads +`@cline/sdk` and shows the SDK build version plus the agent's builtin tool +catalog (a pure, key-free SDK call) in a notification and the +"Cowriting (Cline SDK)" output channel. + +## Architecture + +- CommonJS extension bundled with esbuild (`src/extension.ts` → `out/extension.cjs`). +- `@cline/sdk` is ESM-only (Node ≥22) and uses `createRequire(import.meta.url)`, + so it is **not** bundled — it is shipped in `node_modules` and loaded at + runtime via dynamic `import()` from the vscode-free `src/cline.ts`. + +## Run it (F5) + +1. `npm install` +2. `npm run build` +3. Press **F5** (or Run → "Run Extension") to launch the Extension Development Host. +4. In the new window: **Cmd/Ctrl+Shift+P** → **"Cowriting: Show Cline SDK Info"**. + +## Develop + +- `npm run watch` — rebuild on change. +- `npm test` — run the unit test for the SDK driver. +- `npm run typecheck` — type-check without emit. +``` + +- [ ] **Step 2: Full verification sweep** + +Run: `npm install && npm run typecheck && npm test && npm run build` +Expected: install OK; typecheck OK; 1 test passes; build writes `out/extension.cjs`. + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "POC: README with run/dev instructions (Feature #2)" +``` + +--- + +## Manual / operator verification (cannot be automated in-session) + +- [ ] Press **F5** in VS Code → Extension Development Host opens with no activation error. +- [ ] Run **"Cowriting: Show Cline SDK Info"** → notification shows `Cline SDK 0.0.46 loaded — 9 builtin tools`, and the output channel lists the 9 tool IDs with descriptions. + +This is the POC's acceptance: repo builds, F5 launches the host with the +extension active, and one command drives `@cline/sdk` and shows a real result.