Files
cline/sdk-migration
Dominic Cooney a4a5a45135 sdk migration: squashed pre-2026-05-22 work
Collapses the early SDK-migration history (through 2026-05-20) into a single commit. Later commits are preserved individually.
2026-05-29 11:57:34 -07:00
..

SDK Migration — Entry Point

You are working on migrating the Cline VSCode extension from its classic core to the Cline SDK (@cline/core). This document is your primary reference. Read it in full before starting any step.

Document Map

Document Purpose When to Read
This file Entry point, plan, operational procedure Always, first
ARCHITECTURE.md Features, design decisions, SDK capabilities Before Step 1; refer back as needed
SDK-REFERENCE/OAUTH.md How the SDK handles OAuth and credentials When working on auth (Steps 4, 5)
SDK-REFERENCE/MCP.md How the SDK handles MCP server management When working on MCP (Step 5)
PROBLEMS.md Known issues, verification status Before each verification gate
../src/dev/debug-harness/README.md Debug harness API reference When using the debug harness

Docs from previous attempts that are not carried forward:

  • CAVEATS.md, FIXED.md, FEATURE-REMOVAL-CLEANUP-PLAN.md, DEBUG-HARNESS.md (root level), FEEDBACK.md — these degraded badly. Lessons are incorporated into this plan.

References

Code Repositories

Repo Path kb_search name
Cline (this repo) ~/clients/cline/cline cline
Cline SDK ~/clients/cline/sdk-wip sdk
JetBrains Plugin ~/clients/cline/intellij-plugin plugin
VSCode ~/clients/cline/vscode vscode

How to Research the SDK

Always use kb_search with the sdk repo when you need to understand how the SDK supports a feature. Do not guess at APIs, URLs, or data formats. The SDK is the source of truth.

Example: Before implementing OAuth, search:

kb_search(name="sdk", query="OAuth login flow callback")

You can also compare before/after states using commit-based search:

kb_search(name="cline", query="accountLoginClicked", commit="origin/main")
kb_search(name="cline", query="accountLoginClicked", commit="HEAD")

Core Principles

These principles are derived from hard-won experience on two previous attempts. Violating them leads to broken products and wasted time.

1. Thunk, Don't Replace

The webview speaks gRPC-over-postMessage today. We will not replace that with a new message protocol in this migration. Instead, we build a thunking layer that sits between the SDK and the existing gRPC interface. The webview continues to send gRPC-shaped messages; the thunking layer translates between those and SDK calls.

This means:

  • The webview code is largely untouched
  • gRPC proto files stay in place until the final cleanup step
  • Each SDK feature is wired up by implementing its gRPC handler

2. Verify Before You Proceed

Every step has a verification gate. You must demonstrate the feature works before moving on. Verification means:

  • Unit tests that test real behavior, not just that functions exist
  • Debug harness smoke tests for UI-facing features
  • Manual confirmation when automated tests can't cover it

Mark things as "awaiting verification" not "fixed". Only mark "verified" after you have evidence (test output, screenshot, etc.).

3. Delete and Document

When replacing a classic module with its SDK equivalent, delete the classic code immediately and document where to find it. Dead code in the tree creates confusion about what is active vs. vestigial.

The classic implementation is always accessible via:

  • kb_search(name="cline", query="...", commit="origin/main") — search the classic codebase at the pre-migration commit
  • git show origin/main:path/to/file.ts — view any file
  • git diff origin/main..HEAD -- path/ — see what changed

When deleting a module, add a comment in the replacement file:

// Replaces classic src/core/task/ (see origin/main)

This way there is never any ambiguity about what code is running.

4. Use the Debug Harness

The debug harness at src/dev/debug-harness/ is your primary integration testing tool. Use it to:

  • Verify UI renders correctly after changes
  • Test user flows (login, chat, settings, history)
  • Catch regressions that unit tests miss

Always dismiss promotional overlays first. There may be one or two:

  1. "Introducing Cline Kanban" overlay
  2. "New in v3.78.0" announcement overlay

Both follow the same sr-only pattern and can be dismissed with:

curl localhost:19229/api -d '{"method": "web.evaluate", "params": {"expression": "document.querySelectorAll(\".sr-only\").forEach(el => el.parentElement?.click())"}}'

You may need to run this twice if both overlays are present.

Use VSCode command palette actions to navigate between tabs.

5. SDK "Default" Implementations Are References, Not Products

The SDK's DefaultSessionBuilder, DefaultRuntimeBuilder, etc. are designed for simple use cases. As an IDE, we need more:

  • Custom MCP manager (file watching, SSE/streamableHTTP support)
  • Custom session persistence (read existing task history format)
  • Custom tool approval (integrate with webview approval UI)

Use the defaults as references, but implement what the product needs.

6. Avoid as Casts and Type Confusion

A recurring bug source was confusion between SDK types and gRPC/proto types. For example, SDK returns accountId but gRPC expects workos:accountId. Use explicit conversion functions with tests, and never use as to paper over type mismatches.


Migration Steps

This plan is ordered by dependency: each step builds on the previous. Do not skip steps. Each step ends with a verification gate.

Step 1: Foundation & Cutover

Goal: SDK dependencies installed, test infrastructure ready, and the extension's entry point switched to the SDK adapter. There is one entry point, not two.

Tasks:

  • Add @cline/core, @cline/llms, @cline/shared, @cline/agents as dependencies (via npm link from local SDK)
  • Add vitest.config.sdk.ts for SDK adapter tests
  • Create src/sdk/ directory with index.ts barrel export
  • Modify src/extension.ts to use the SDK adapter as its activation path (replacing the classic Controller import)
  • Delete src/core/controller/ — the classic controller is replaced by src/sdk/SdkController.ts (to be implemented in Step 4). Add comment: // Replaces classic src/core/controller/ (see origin/main)
  • Update esbuild.mjs if needed for the new import structure
  • Verify: npm run compile succeeds, extension loads in VSCode (sidebar may show errors since handlers aren't implemented yet, but the extension process itself starts)

Why one entry point: Attempt 2 used CLINE_SDK=1 to switch between two entry points. This caused constant confusion about which codepath was running. With a single entry point, there is never any doubt. The classic code is always accessible via origin/main.

Verification gate: Extension compiles and loads. The SDK adapter is the only codepath. (It won't do much yet — that's Step 4.)

Step 2: Legacy State Reader

Goal: Read all existing on-disk state from the SDK adapter layer.

Tasks:

  • Implement src/sdk/legacy-state-reader.ts:
    • Read globalState.json (provider settings, model selections, dismissed banners, etc.)
    • Read secrets.json (API keys, Cline auth tokens)
    • Read taskHistory.json (task list for history view)
    • Read per-task directories (api_conversation_history.json, ui_messages.json)
    • Read cline_mcp_settings.json (MCP server configs)
  • Write tests against fixture data (copy real ~/.cline/data/ samples, redact secrets)
  • Verify: All reads produce correct typed results, error handling for missing/corrupt files

Verification gate: Unit tests pass; reader correctly parses real ~/.cline/data/ contents (spot-check manually).

Step 3: Provider Migration

Goal: Existing provider credentials survive the transition.

Tasks:

  • Implement src/sdk/provider-migration.ts:
    • Use SDK's migrateLegacyProviderSettings() as reference
    • Map classic globalState.json + secrets.json entries to SDK providers.json format
    • Never overwrite existing entries
    • Tag migrated entries with tokenSource: "migration"
    • Write a migration sentinel to prevent re-migration
  • Test with fixtures covering all 30+ providers
  • Verify: After migration, SDK can create handler for each provider; existing API keys still work

Critical: This is the highest-risk step. Getting it wrong means users get logged out. Test exhaustively.

Verification gate: All provider credential tests pass. Manual test: set up providers in classic extension, switch to SDK branch, verify inference still works for Anthropic, OpenAI, OpenRouter, Ollama, and the Cline provider.

Step 4: Session Lifecycle (No UI Yet) — Completed

Goal: Create and manage SDK sessions from the adapter layer.

Tasks:

  • Implement src/sdk/cline-session-factory.ts:
    • Custom session persistence adapter reading ~/.cline/data/tasks/
    • Map HistoryItem ↔ session fields
    • Implement ClineCore.create() with proper config
    • Build CoreSessionConfig from legacy state via ProviderSettingsManager
    • Build StartSessionInput and resume input helpers
  • Implement src/sdk/SdkController.ts:
    • initTask(prompt) — create session, start inference
    • askResponse(message) — continue conversation (sends to existing session)
    • cancelTask() — abort running session
    • clearTask() — reset for new task
    • showTaskWithId(id) — load task from history
    • reinitExistingTaskFromId(id) — resume task from history
    • Subscribe to SDK events, translate to internal message format
    • Session event listener system for downstream consumers
  • Implement src/sdk/message-translator.ts:
    • SDK CoreSessionEventClineMessage[] for webview consumption
    • Handle all event types: chunk, agent_event (content_start/update/end, done, error, notice, iteration_start/end, usage), ended, hook, status
    • Streaming state tracking (partial message dedup)
    • Tool text formatting helpers
    • HistoryItem ↔ session field mapping
  • Test all paths — 91 unit tests pass across 4 test files

Verification gate: Unit tests pass (91/91). TypeScript compiles with 0 errors in src/sdk/. Session lifecycle methods work through the adapter layer without any UI. See PROBLEMS.md for known minor issues.

Step 5: gRPC Thunking Layer — Completed

Goal: Wire SDK adapter to the existing webview via gRPC handlers.

This is the critical insight from attempt 2: the webview speaks gRPC. We translate at the boundary. The webview stays untouched.

Tasks:

  • Implement src/sdk/task-proxy.ts:
    • TaskProxy provides a classic Task-compatible interface that delegates to SDK session methods
    • handleWebviewAskResponse() → SdkController.askResponse()
    • abortTask() → SdkController.cancelTask()
    • MessageStateHandler extends EventEmitter for CLI compatibility
    • TaskProxyState mirrors classic TaskState subset
    • Stub properties for removed features (browser, checkpoints)
  • Implement src/sdk/webview-grpc-bridge.ts:
    • Bridges SDK session events to webview gRPC streams
    • Translates ClineMessages to proto format via convertClineMessageToProto()
    • Pushes through sendPartialMessageEvent() for streaming
    • Pushes through sendStateUpdate() on significant events
    • Error handling — never blocks the event stream
  • Wire SdkController to use TaskProxy + WebviewGrpcBridge:
    • Session events → message translation → gRPC bridge → webview
    • handleSessionEvent() translates and emits to all listeners
    • Messages accumulated in messageStateHandler for state building
    • State updates pushed on turn complete / session ended
  • Reuse existing getStateToPostToWebview() for state building
    • Classic implementation reads from StateManager
    • TaskProxy provides messageStateHandler.getClineMessages()
    • Will be gradually replaced with SDK-sourced state in later steps

Verification gate: 114 unit tests pass across 6 test files. TypeScript compiles with 0 new errors (3 pre-existing in unrelated files). The gRPC thunking layer is complete — session events flow from SDK through message translation to webview gRPC streams. See PROBLEMS.md for known minor issues.

Step 6: Auth & Account Flows — Implementation Complete, 🔵 Awaiting E2E Verification

Goal: Full OAuth login/logout, credit display, org switching work.

This was the most broken area in attempt 2. Be especially careful.

Tasks:

  • Implement Cline OAuth using SDK's loginClineOAuth():
    • SDK spawns local callback server and provides the auth URL
    • Our code opens the browser via openExternal()
    • SDK handles token exchange
    • We persist tokens to secrets.json under cline:clineAccountId
  • Implement subscribeToAuthStatusUpdate streaming:
    • Read credentials from disk on subscription
    • Push initial auth state immediately (prevents race condition)
    • Cross-window sync via secrets change listener
  • Implement getUserCredits / getOrganizationCredits:
    • Fetch from Cline API using stored auth token via ClineAccountService
    • Use {apiBaseUrl} not hardcoded app.cline.bot
  • Implement accountLogoutClicked:
    • Clear credentials from disk
    • Push unauthenticated state to webview
  • Implement setUserOrganization:
    • Update active org via API call
    • Refresh auth info after switching
  • Implement OpenAI Codex OAuth via SDK's loginOpenAICodex()
  • Implement OCA OAuth via SDK's loginOcaOAuth()
  • Implement token refresh using SDK's refreshClineToken()
  • Write unit tests — 20 tests in src/sdk/auth-service.test.ts

Key pitfalls from attempt 2 (all addressed):

  • workos: prefix on account IDs — getAuthToken() always returns workos:-prefixed token
  • {appBaseUrl} vs hardcoded URLs — uses ClineEnv.config().apiBaseUrl and appBaseUrl
  • Race condition: webview subscribes to auth state before the bridge pushes it — subscribeToAuthStatusUpdate pushes initial state immediately
  • Token field name mismatches between SDK and classic storage — explicit conversion in credentialsToAuthInfo() (ms→seconds for expiresAt)

Files created/modified:

  • src/sdk/auth-service.ts — SDK-backed AuthService (replaces src/services/auth/AuthService.ts)
  • src/sdk/account-service.ts — SDK-backed ClineAccountService (replaces src/services/account/ClineAccountService.ts)
  • src/sdk/auth-service.test.ts — 20 unit tests
  • src/sdk/SdkController.ts — Wired auth/account services in constructor
  • src/sdk/index.ts — Added barrel exports
  • src/core/controller/account/accountLoginClicked.ts — Import from @/sdk/auth-service
  • src/core/controller/account/accountLogoutClicked.ts — Delegates to SdkController
  • src/core/controller/account/subscribeToAuthStatusUpdate.ts — Import from @/sdk/auth-service
  • src/core/controller/account/openAiCodexSignIn.ts — Uses SDK-backed AuthService
  • src/core/controller/account/openAiCodexSignOut.ts — Uses SDK-backed AuthService
  • src/extension.ts — Import from @/sdk/auth-service

Verification gate: 🔵 Unit tests pass (20/20). TypeScript compiles with 0 new errors. End-to-end verification with debug harness pending — need to test: login flow, profile display, credits, org switching, logout.

Step 7: MCP Integration — Classic McpHub Wired (SDK Manager Deferred)

Goal: MCP servers load, tools appear in agent, server management UI works.

Following the "Thunk, Don't Replace" principle, we wire the classic McpHub into the SdkController instead of building a custom SDK MCP manager. The classic McpHub already supports all three transports (stdio, SSE, streamableHTTP), file watching, and all gRPC handlers. The SDK's InMemoryMcpManager will replace it in Step 10 (Cleanup).

Tasks:

  • Wire classic McpHub into SdkController.mcpHub
    • Same constructor args as classic Controller
    • Existing gRPC handlers (subscribeToMcpServers, restartMcpServer, deleteMcpServer, toggleMcpServer, etc.) work without modification
    • They all delegate to controller.mcpHub which is now a real instance
  • Update SdkController.mcpHub type from any to McpHub
  • Implement MCP marketplace (cache + refresh from API) — deferred
  • Replace classic McpHub with SDK's InMemoryMcpManager — deferred to Step 10

Reference: See SDK-REFERENCE/MCP.md for how the SDK's MCP manager works and what gaps exist.

Verification gate: 🔵 Classic McpHub wired in. Existing gRPC handlers should work. Full E2E verification pending debug harness test with real MCP servers configured.

Step 8: Settings & Features — Core Settings Working

Goal: All settings UI works, feature toggles persist.

Following the "Thunk, Don't Replace" principle, the existing updateSettings gRPC handler already works — it calls controller.stateManager.setGlobalState() which is available. We just needed to ensure TaskProxy properties don't crash it.

Tasks:

  • Wire all updateSettings keys to persist to globalState.json — already works via StateManager
  • TaskProxy.api is settable (updateSettings replaces it on model switch)
  • TaskProxy.terminalManager safely no-ops (settings compatibility)
  • Implement togglePlanActMode() — saves mode, cancels active task
  • Implement toggleActModeForYoloMode() — switches to act mode
  • Implement getAvailableTerminalProfiles (simplified — only background terminal) — deferred
  • Simplify terminal settings UI (remove IDE terminal options) — deferred
  • Remove workflows tab from Cline Rules modal — deferred
  • Remove focus chain / deep planning / memory bank UI remnants — deferred
  • Verify model picker works for all providers — needs E2E test
  • Verify Plan/Act mode toggle works with separate model configs — needs E2E test

Verification gate: 🔵 Core settings work (updateSettings, mode toggle). Full E2E verification pending debug harness test with real credentials. UI cleanup items deferred to post-Step-9 polish.

Step 9: Full Integration Verification

Goal: The SDK-backed extension is functionally equivalent to the classic extension for all core features.

Tasks:

  • Write QA test scripts covering:
    1. Fresh install flow (no saved state)
    2. Upgrade flow (existing state from classic)
    3. Login → inference → logout → login
    4. Multiple providers (Cline, Anthropic, OpenAI, Ollama)
    5. Task history: create, view, resume, delete, favorite
    6. Settings: change model, change provider, toggle features
    7. MCP: add server, use tool, remove server
    8. Plan/Act mode switching
    9. @ mentions and file attachments
    10. Cancel task mid-execution, start new task
  • Run each test with the debug harness
  • Document any known issues in PROBLEMS.md with reproduction steps

Verification gate: All QA scripts pass. Any failures are documented and triaged.

Step 10: Cleanup (Only After Step 9 Passes)

Goal: Remove classic core code that is no longer used.

Do NOT start this step until Step 9 is fully verified.

Tasks:

  • Delete src/core/task/ (replaced by @cline/agents)
  • Delete src/core/controller/ (replaced by SDK adapter)
  • Delete src/core/api/ (replaced by @cline/llms)
  • Delete src/core/prompts/system-prompt/ (replaced by SDK prompts)
  • Delete src/services/mcp/McpHub.ts (replaced by SDK MCP)
  • Delete src/standalone/ (not needed for VSCode)
  • Remove deprecated feature code (browser automation, shadow git, memory bank, focus chain, deep planning, workflows)
  • Remove proto files for webview messages (keep proto for any persisted state that still uses them)
  • Remove proto build steps from package.json
  • Remove src/shared/proto-conversions/, src/generated/
  • Clean up imports, fix TypeScript errors
  • Run full test suite

Verification gate: Extension compiles and loads. All QA scripts from Step 9 still pass. npm run compile produces no errors.

Future Steps (Not In Scope)

  • Step 11: JetBrains sidecar (JSON-RPC over stdio)
  • Step 12: Enterprise features (remote config, SSO, team controls)
  • Step 13: Improved checkpoints (kanban-style git refs)
  • Step 14: MCP Marketplace improvements
  • Step 15: Remove gRPC thunking layer, switch webview to typed JSON messages (optional — only if the thunking layer is a maintenance burden)

Operational Procedure

How to Work on a Step

  1. Read the step description in full
  2. Check PROBLEMS.md for any known issues in this area
  3. Research the SDK using kb_search(name="sdk", query="...") before implementing anything
  4. Implement the minimum needed to make the step's verification gate pass
  5. Write tests that verify real behavior
  6. Verify using the debug harness for UI-facing features
  7. Update PROBLEMS.md with any issues found, marked as "awaiting verification"
  8. Commit with a descriptive message referencing the step number

How to Use the Debug Harness

# Build and launch
npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch

# Dismiss promotional overlays FIRST (may need to run twice)
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
curl localhost:19229/api -d '{"method": "web.evaluate", "params": {"expression": "document.querySelectorAll(\".sr-only\").forEach(el => el.parentElement?.click())"}}'

# Navigate using command palette, NOT by clicking tabs
curl localhost:19229/api -d '{"method": "ui.command_palette", "params": {"command": "cline.accountLogin"}}'

# Take screenshots (read the file, don't open it!)
curl localhost:19229/api -d '{"method": "ui.screenshot"}'
# Returns {"result": {"path": "/tmp/cline-debug/screenshot-0001.png"}}
# Use read_file on that path to examine it

How to Report Problems

When you find a bug, add it to PROBLEMS.md with:

  • ID: Sequential number
  • Status: 🔴 Blocker / 🟡 Minor / 🟢 Verified Fixed
  • Description: What's wrong, where, how to reproduce
  • Root cause: If known
  • Fix: If attempted, with file references
  • Verification: How to verify it's fixed (test name, harness command, etc.)

Never mark a problem 🟢 without evidence. Write the test first, then mark it fixed.

How to Handle "SDK Doesn't Support X"

If the SDK is missing a feature you need:

  1. Document the gap in PROBLEMS.md
  2. Search the SDK codebase (kb_search name="sdk") for any workaround or extension point
  3. If no workaround exists, implement a minimal version in the adapter layer
  4. File an issue / PR to the SDK repo for the proper fix
  5. Use npm link for quick iteration on SDK changes

What Changed From Previous Attempts

Attempt 1 (sdk-migration-port-check)

Deleted ~138K lines of classic core before having a working replacement. Created stub webview components. Result: 595 TypeScript errors, non-functional extension.

Lesson: Delete and document — but only as you replace, not before. The classic code is always accessible via origin/main.

Attempt 2 (sdk-migration-v2, 90 commits)

Built an SDK adapter layer with tests. Got inference working. But documentation degraded, bugs were marked fixed without verification, auth flows were broken, gRPC "mode" vs SDK "mode" caused confusion, feature removals were incomplete, and the agent kept confusing SDK types with gRPC types. The dual entry point (CLINE_SDK=1) was a constant source of confusion.

Lessons applied in this plan:

  • Single entry point — no CLINE_SDK flag, no dual codepaths
  • Delete and document — dead code creates confusion; use origin/main and kb_search to reference the classic impl
  • Thunk at the gRPC boundary, don't create a "mode" system
  • Verify before proceeding, don't mark things fixed prematurely
  • Use kb_search to research the SDK, don't guess at APIs
  • Don't hardcode URLs, use {appBaseUrl}
  • SDK "Default" implementations are references, not products
  • Keep docs focused and reliable; use PROBLEMS.md for tracking </task_progress> </write_to_file>