# 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.