14 KiB
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.46loads 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.urlundefined →createRequirethrows). Keeping it external + dynamicimport()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
{
"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
{
"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
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
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:
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:
/**
* 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
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
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
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
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
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
{
"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
{
"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
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
# 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
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.