Files
vscode-cowriting-plugin-con…/plans/2026-06-09-poc-cline-sdk-extension.md
T

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.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.tsout/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.