POC: F5 launch config, README, and implementation plan (Feature #2)

.vscode launch/tasks wire F5 -> Run Extension (preLaunchTask: npm build).
README documents run/dev. Plan archived under docs/superpowers/plans/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-09 23:55:39 -07:00
parent 80083c1230
commit 969502eba7
4 changed files with 526 additions and 0 deletions
@@ -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<SdkSummary> {
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.