mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-16 19:44:14 +00:00
Add Cline provider
This commit is contained in:
parent
cd8c646818
commit
9187bc590a
15 changed files with 298 additions and 19 deletions
|
|
@ -3,6 +3,12 @@
|
|||
## 0.9.8 - 2026-05-10
|
||||
|
||||
### Added (CLI)
|
||||
- **Cline provider support.** CodeBurn now reads Cline task usage from both
|
||||
VS Code globalStorage (`saoudrizwan.claude-dev`) and Cline's
|
||||
`~/.cline/data` task root. It reuses the existing Cline-family parser for
|
||||
`ui_messages.json` usage entries, deduplicates migrated tasks by the newest
|
||||
`ui_messages.json`, and exposes Cline in CLI provider filters, docs, and the
|
||||
macOS menubar provider tabs. Closes #130.
|
||||
- **Multiple Claude config directories.** Set `CLAUDE_CONFIG_DIRS` to an
|
||||
OS-delimited list of paths (`:`-separated on POSIX, `;`-separated on
|
||||
Windows) to scan more than one Claude data directory in a single run.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ Arrow keys switch between Today, 7 Days, 30 Days, Month, and 6 Months (use `--fr
|
|||
|---|----------|-----------|-----|
|
||||
| <img src="assets/providers/claude.jpg" width="28" /> | Claude Code | Yes | [claude.md](docs/providers/claude.md) |
|
||||
| <img src="assets/providers/claude.jpg" width="28" /> | Claude Desktop | Yes | [claude.md](docs/providers/claude.md) |
|
||||
| <img src="assets/providers/cline.svg" width="28" /> | Cline | Yes | [cline.md](docs/providers/cline.md) |
|
||||
| <img src="assets/providers/codex.png" width="28" /> | Codex (OpenAI) | Yes | [codex.md](docs/providers/codex.md) |
|
||||
| <img src="assets/providers/cursor.jpg" width="28" /> | Cursor | Yes | [cursor.md](docs/providers/cursor.md) |
|
||||
| <img src="assets/providers/cursor-agent.jpg" width="28" /> | cursor-agent | Yes | [cursor-agent.md](docs/providers/cursor-agent.md) |
|
||||
|
|
@ -378,7 +379,7 @@ These are starting points, not verdicts. A 60% cache hit on a single experimenta
|
|||
|
||||
**OpenClaw** stores agent sessions as JSONL at `~/.openclaw/agents/*.jsonl`. Also checks legacy paths `.clawdbot`, `.moltbot`, `.moldbot`. Token usage comes from assistant message `usage` blocks; model from `modelId` or `message.model` fields.
|
||||
|
||||
**Roo Code / KiloCode** are Cline-family VS Code extensions. CodeBurn reads `ui_messages.json` from each task directory in VS Code's `globalStorage`, filtering `type: "say"` entries with `say: "api_req_started"` to extract token counts.
|
||||
**Cline / Roo Code / KiloCode** are Cline-family coding agents. CodeBurn reads `ui_messages.json` from each task directory, filtering `type: "say"` entries with `say: "api_req_started"` to extract token counts. Cline scans both VS Code's `globalStorage/saoudrizwan.claude-dev` and `~/.cline/data`.
|
||||
|
||||
CodeBurn deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session ID for Gemini, by session+message ID for OpenCode, by responseId for Pi/OMP), filters by date range per entry, and classifies each turn.
|
||||
|
||||
|
|
|
|||
4
assets/providers/cline.svg
Normal file
4
assets/providers/cline.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Cline">
|
||||
<rect width="64" height="64" rx="14" fill="#0f2f2c"/>
|
||||
<path d="M45.5 42.2c-3.4 3.2-7.6 4.8-12.7 4.8-4.7 0-8.6-1.5-11.6-4.4-3-3-4.5-6.7-4.5-11.2s1.5-8.2 4.5-11.1c3-2.9 6.9-4.4 11.6-4.4 5.1 0 9.3 1.6 12.7 4.8l-5.2 5.8c-2-1.9-4.3-2.8-7-2.8-2.4 0-4.4.7-5.9 2.2-1.5 1.4-2.2 3.3-2.2 5.5 0 2.3.7 4.2 2.2 5.6 1.5 1.4 3.5 2.2 5.9 2.2 2.8 0 5.1-.9 7-2.8l5.2 5.8z" fill="#5eead4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 473 B |
|
|
@ -128,14 +128,14 @@ type Provider = {
|
|||
}
|
||||
```
|
||||
|
||||
`src/providers/index.ts` registers eighteen providers across two tiers:
|
||||
`src/providers/index.ts` registers nineteen providers across two tiers:
|
||||
|
||||
- **Eager**: `claude`, `codex`, `copilot`, `droid`, `gemini`, `kilo-code`, `kiro`, `openclaw`, `pi`, `omp`, `qwen`, `roo-code`. Imported at module load.
|
||||
- **Eager**: `claude`, `cline`, `codex`, `copilot`, `droid`, `gemini`, `kilo-code`, `kiro`, `openclaw`, `pi`, `omp`, `qwen`, `roo-code`. Imported at module load.
|
||||
- **Lazy**: `antigravity`, `goose`, `cursor`, `opencode`, `cursor-agent`, `crush`. Imported via dynamic `import()` so the heavy dependencies (SQLite, protobuf) do not touch users who do not have those tools installed.
|
||||
|
||||
Both lists hit the same `getAllProviders()` aggregator. A failed lazy import is silent and excludes that provider from the run.
|
||||
|
||||
`src/providers/vscode-cline-parser.ts` is a shared helper consumed by `kilo-code` and `roo-code`. It is not registered as a provider on its own.
|
||||
`src/providers/vscode-cline-parser.ts` is a shared helper consumed by `cline`, `kilo-code`, and `roo-code`. It is not registered as a provider on its own.
|
||||
|
||||
For the per-provider data location, storage format, parser quirks, and test coverage, see `docs/providers/`.
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ The `prepublishOnly` hook in `package.json` runs `npm run build` so `npm publish
|
|||
|
||||
- `tests/` root (27 files) covers CLI, parser, optimize, cache, format, models, plans.
|
||||
- `tests/security/` (1 file) covers prototype-pollution guards.
|
||||
- `tests/providers/` (14 files) covers per-provider parsing.
|
||||
- `tests/providers/` (15 files) covers per-provider parsing.
|
||||
- `tests/fixtures/` holds redacted real-world session data.
|
||||
|
||||
Five providers ship without dedicated test files today: `antigravity`, `claude`, `gemini`, `goose`, `qwen`. Closing this gap is a standing good-first-issue.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ For the architectural picture, see `../architecture.md`.
|
|||
| Provider | Storage | Source | Test |
|
||||
|---|---|---|---|
|
||||
| [Claude](claude.md) | JSONL (no parser) | `src/providers/claude.ts` | none (covered indirectly) |
|
||||
| [Cline](cline.md) | JSON | `src/providers/cline.ts` | `tests/providers/cline.test.ts` |
|
||||
| [Codex](codex.md) | JSONL | `src/providers/codex.ts` | `tests/providers/codex.test.ts` |
|
||||
| [Copilot](copilot.md) | JSONL | `src/providers/copilot.ts` | `tests/providers/copilot.test.ts` |
|
||||
| [Droid](droid.md) | JSONL | `src/providers/droid.ts` | `tests/providers/droid.test.ts` |
|
||||
|
|
@ -38,7 +39,7 @@ For the architectural picture, see `../architecture.md`.
|
|||
|
||||
| Helper | Used by | Source |
|
||||
|---|---|---|
|
||||
| [vscode-cline-parser](vscode-cline-parser.md) | `kilo-code`, `roo-code` | `src/providers/vscode-cline-parser.ts` |
|
||||
| [vscode-cline-parser](vscode-cline-parser.md) | `cline`, `kilo-code`, `roo-code` | `src/providers/vscode-cline-parser.ts` |
|
||||
|
||||
## File Format
|
||||
|
||||
|
|
|
|||
50
docs/providers/cline.md
Normal file
50
docs/providers/cline.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Cline
|
||||
|
||||
Cline VS Code extension and Cline home-data task storage.
|
||||
|
||||
- **Source:** `src/providers/cline.ts`
|
||||
- **Loading:** eager (`src/providers/index.ts:2`)
|
||||
- **Test:** `tests/providers/cline.test.ts`
|
||||
|
||||
## Where it reads from
|
||||
|
||||
Two task roots are scanned:
|
||||
|
||||
1. VS Code extension globalStorage for `saoudrizwan.claude-dev`.
|
||||
2. Cline's home-data root at `~/.cline/data`.
|
||||
|
||||
Both roots are expected to contain a `tasks/` child directory. Discovery is delegated to `discoverClineTasks` in `src/providers/vscode-cline-parser.ts`, so a task is only included when it has a `ui_messages.json` file.
|
||||
|
||||
## Storage format
|
||||
|
||||
Per-task directories with:
|
||||
|
||||
```
|
||||
tasks/<taskId>/
|
||||
ui_messages.json
|
||||
api_conversation_history.json
|
||||
task_metadata.json
|
||||
```
|
||||
|
||||
`ui_messages.json` provides the `api_req_started` usage entries. `api_conversation_history.json` is used for model extraction. See [`vscode-cline-parser`](vscode-cline-parser.md) for the full schema description.
|
||||
`task_metadata.json` is part of Cline's task layout but is not read by CodeBurn today.
|
||||
|
||||
## Caching
|
||||
|
||||
None at the provider level; delegates to the shared helper and normal parser/cache layers.
|
||||
|
||||
## Deduplication
|
||||
|
||||
Discovery deduplicates by task id across the two Cline roots so a migrated task is not scanned twice. If the same task id exists in multiple roots, the one with the newest `ui_messages.json` wins. Parsing still uses the shared per-call key: `<providerName>:<taskId>:<index>`.
|
||||
|
||||
## Quirks
|
||||
|
||||
- This provider is intentionally a thin wrapper over the shared Cline-family parser.
|
||||
- Cline can keep data in both VS Code globalStorage and `~/.cline/data`, depending on version and workflow.
|
||||
- If Cline changes the JSON shape, fix `vscode-cline-parser.ts` only if Roo Code and KiloCode still pass. Branch provider-specific parsing rather than duplicating the whole parser.
|
||||
|
||||
## When fixing a bug here
|
||||
|
||||
1. Reproduce with a minimal task directory containing `ui_messages.json` and `api_conversation_history.json`.
|
||||
2. Run `tests/providers/cline.test.ts`, plus `tests/providers/roo-code.test.ts` and `tests/providers/kilo-code.test.ts` if the shared parser changes.
|
||||
3. Keep the provider name `cline`; downstream filters and dedup keys depend on it.
|
||||
|
|
@ -25,10 +25,10 @@ Delegated. Per `<providerName>:<taskId>:<index>` (handled in `vscode-cline-parse
|
|||
## Quirks
|
||||
|
||||
- This file is a thin wrapper. Almost every bug for KiloCode actually lives in `vscode-cline-parser.ts`.
|
||||
- The two providers using the cline parser (KiloCode and Roo Code) differ **only** by extension ID.
|
||||
- The VS Code extension wrappers using the Cline-family parser differ **only** by extension ID.
|
||||
|
||||
## When fixing a bug here
|
||||
|
||||
1. If the bug is "KiloCode and Roo Code both broken in the same way", fix it in `vscode-cline-parser.ts`.
|
||||
1. If the bug is "Cline, KiloCode, and Roo Code all broken in the same way", fix it in `vscode-cline-parser.ts`.
|
||||
2. If the bug is "KiloCode broken, Roo Code fine", the difference is upstream (KiloCode's emitted JSON differs slightly). Reproduce with a fixture and consider whether the cline parser needs to branch on extension ID.
|
||||
3. Read [`vscode-cline-parser.md`](vscode-cline-parser.md) before editing.
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ Delegated. Per `<providerName>:<taskId>:<index>` (in `vscode-cline-parser.ts:109
|
|||
## Quirks
|
||||
|
||||
- Thin wrapper. Almost every Roo Code bug actually lives in `vscode-cline-parser.ts`.
|
||||
- The two providers using the cline parser (KiloCode and Roo Code) differ **only** by extension ID.
|
||||
- The VS Code extension wrappers using the Cline-family parser differ **only** by extension ID.
|
||||
|
||||
## When fixing a bug here
|
||||
|
||||
1. If the bug also reproduces against KiloCode, fix it in `vscode-cline-parser.ts`.
|
||||
1. If the bug also reproduces against Cline or KiloCode, fix it in `vscode-cline-parser.ts`.
|
||||
2. If the bug is Roo Code-specific, the difference is upstream JSON shape. Reproduce with a fixture and consider whether the cline parser needs to branch on extension ID.
|
||||
3. Read [`vscode-cline-parser.md`](vscode-cline-parser.md) before editing.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
# vscode-cline-parser (Shared Helper)
|
||||
|
||||
Shared discovery and parsing for VS Code extensions descended from Cline.
|
||||
Shared discovery and parsing for Cline and VS Code extensions descended from Cline.
|
||||
|
||||
- **Source:** `src/providers/vscode-cline-parser.ts`
|
||||
- **Loading:** not a provider; imported by `kilo-code.ts` and `roo-code.ts`.
|
||||
- **Test:** none directly. Coverage comes from `tests/providers/kilo-code.test.ts` and `tests/providers/roo-code.test.ts`.
|
||||
- **Loading:** not a provider; imported by `cline.ts`, `kilo-code.ts`, and `roo-code.ts`.
|
||||
- **Test:** none directly. Coverage comes from `tests/providers/cline.test.ts`, `tests/providers/kilo-code.test.ts`, and `tests/providers/roo-code.test.ts`.
|
||||
|
||||
## What it does
|
||||
|
||||
Two responsibilities:
|
||||
|
||||
1. `discoverClineTasks(extensionId)` walks VS Code's `globalStorage/<extensionId>/tasks/` directories and returns one source per task that has a `ui_messages.json` file (`vscode-cline-parser.ts:25-50`).
|
||||
1. `discoverClineTasks(extensionId)` walks a base directory's `tasks/` child and returns one source per task that has a `ui_messages.json` file (`vscode-cline-parser.ts:25-50`). Without an override directory it uses VS Code's `globalStorage/<extensionId>/` path.
|
||||
2. `createClineParser` reads each task's `ui_messages.json` and `api_conversation_history.json`, extracts model, tools, and token counts, and yields `ParsedProviderCall` objects.
|
||||
|
||||
## Storage layout
|
||||
|
|
@ -18,7 +18,7 @@ Two responsibilities:
|
|||
Per task directory:
|
||||
|
||||
```
|
||||
<globalStorage>/<extensionId>/tasks/<taskId>/
|
||||
<baseDir>/tasks/<taskId>/
|
||||
ui_messages.json # event stream
|
||||
api_conversation_history.json # full prompt history with model tags
|
||||
```
|
||||
|
|
@ -44,6 +44,6 @@ Per `<providerName>:<taskId>:<index>` where `index` is the position of the `api_
|
|||
|
||||
## When fixing a bug here
|
||||
|
||||
1. A change here ripples to **both** KiloCode and Roo Code. Run both test files (`tests/providers/kilo-code.test.ts` and `tests/providers/roo-code.test.ts`) before opening a PR.
|
||||
1. A change here ripples to Cline, KiloCode, and Roo Code. Run all three provider test files before opening a PR.
|
||||
2. If you find that one of the two extensions emits a different shape, branch on the extension ID parameter that the discovery function already takes; do not duplicate the parser.
|
||||
3. If you add support for a third Cline-derivative extension, register it as a thin wrapper file in the same shape as `kilo-code.ts` and `roo-code.ts`.
|
||||
3. If you add support for another Cline-derivative extension, register it as a thin wrapper file in the same shape as `kilo-code.ts` and `roo-code.ts`.
|
||||
|
|
|
|||
|
|
@ -731,6 +731,7 @@ enum SupportedCurrency: String, CaseIterable, Identifiable {
|
|||
enum ProviderFilter: String, CaseIterable, Identifiable {
|
||||
case all = "All"
|
||||
case claude = "Claude"
|
||||
case cline = "Cline"
|
||||
case codex = "Codex"
|
||||
case cursor = "Cursor"
|
||||
case copilot = "Copilot"
|
||||
|
|
@ -751,6 +752,7 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
|
|||
var providerKeys: [String] {
|
||||
switch self {
|
||||
case .cursor: ["cursor", "cursor agent"]
|
||||
case .cline: ["cline"]
|
||||
case .rooCode: ["roo-code", "roo code"]
|
||||
case .kiloCode: ["kilo-code", "kilocode"]
|
||||
case .openclaw: ["openclaw"]
|
||||
|
|
@ -762,6 +764,7 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
case .all: "all"
|
||||
case .claude: "claude"
|
||||
case .cline: "cline"
|
||||
case .codex: "codex"
|
||||
case .cursor: "cursor"
|
||||
case .copilot: "copilot"
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ extension ProviderFilter {
|
|||
switch self {
|
||||
case .all: return Theme.brandAccent
|
||||
case .claude: return Theme.categoricalClaude
|
||||
case .cline: return Color(red: 0x23/255.0, green: 0x8A/255.0, blue: 0x7E/255.0)
|
||||
case .codex: return Theme.categoricalCodex
|
||||
case .cursor: return Theme.categoricalCursor
|
||||
case .copilot: return Color(red: 0x6D/255.0, green: 0x8F/255.0, blue: 0xA6/255.0)
|
||||
|
|
|
|||
73
src/providers/cline.ts
Normal file
73
src/providers/cline.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { stat } from 'fs/promises'
|
||||
import { homedir } from 'os'
|
||||
import { basename, join } from 'path'
|
||||
|
||||
import { discoverClineTasks, createClineParser, getVSCodeGlobalStoragePath } from './vscode-cline-parser.js'
|
||||
import type { Provider, SessionSource, SessionParser } from './types.js'
|
||||
|
||||
const EXTENSION_ID = 'saoudrizwan.claude-dev'
|
||||
|
||||
export function getClineDataPath(): string {
|
||||
return join(homedir(), '.cline', 'data')
|
||||
}
|
||||
|
||||
function normalizeOverrideDirs(overrideDirs?: string | string[]): string[] | undefined {
|
||||
if (overrideDirs === undefined) return undefined
|
||||
// Cline has two default roots, so tests and future callers can override one or both.
|
||||
return Array.isArray(overrideDirs) ? overrideDirs : [overrideDirs]
|
||||
}
|
||||
|
||||
async function dedupeTaskSources(sources: SessionSource[]): Promise<SessionSource[]> {
|
||||
const candidates = await Promise.all(sources.map(async source => ({
|
||||
source,
|
||||
mtimeMs: (await stat(join(source.path, 'ui_messages.json')).catch(() => null))?.mtimeMs ?? 0,
|
||||
})))
|
||||
|
||||
const seenTaskIds = new Set<string>()
|
||||
const deduped: SessionSource[] = []
|
||||
|
||||
for (const { source } of candidates.sort((a, b) => b.mtimeMs - a.mtimeMs)) {
|
||||
const taskId = basename(source.path)
|
||||
if (seenTaskIds.has(taskId)) continue
|
||||
seenTaskIds.add(taskId)
|
||||
deduped.push(source)
|
||||
}
|
||||
|
||||
return deduped
|
||||
}
|
||||
|
||||
export function createClineProvider(overrideDirs?: string | string[]): Provider {
|
||||
const configuredDirs = normalizeOverrideDirs(overrideDirs)
|
||||
|
||||
return {
|
||||
name: 'cline',
|
||||
displayName: 'Cline',
|
||||
|
||||
modelDisplayName(model: string): string {
|
||||
return model
|
||||
},
|
||||
|
||||
toolDisplayName(rawTool: string): string {
|
||||
return rawTool
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
const baseDirs = configuredDirs ?? [
|
||||
getVSCodeGlobalStoragePath(EXTENSION_ID),
|
||||
getClineDataPath(),
|
||||
]
|
||||
|
||||
const sources = await Promise.all(
|
||||
baseDirs.map(dir => discoverClineTasks(EXTENSION_ID, 'cline', 'Cline', dir)),
|
||||
)
|
||||
|
||||
return dedupeTaskSources(sources.flat())
|
||||
},
|
||||
|
||||
createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
|
||||
return createClineParser(source, seenKeys, 'cline')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const cline = createClineProvider()
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { claude } from './claude.js'
|
||||
import { cline } from './cline.js'
|
||||
import { codex } from './codex.js'
|
||||
import { copilot } from './copilot.js'
|
||||
import { droid } from './droid.js'
|
||||
|
|
@ -101,7 +102,7 @@ async function loadCrush(): Promise<Provider | null> {
|
|||
}
|
||||
}
|
||||
|
||||
const coreProviders: Provider[] = [claude, codex, copilot, droid, gemini, kiloCode, kiro, openclaw, pi, omp, qwen, rooCode]
|
||||
const coreProviders: Provider[] = [claude, cline, codex, copilot, droid, gemini, kiloCode, kiro, openclaw, pi, omp, qwen, rooCode]
|
||||
|
||||
export async function getAllProviders(): Promise<Provider[]> {
|
||||
const [ag, gs, cursor, opencode, cursorAgent, crush] = await Promise.all([loadAntigravity(), loadGoose(), loadCursor(), loadOpenCode(), loadCursorAgent(), loadCrush()])
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { providers, getAllProviders } from '../src/providers/index.js'
|
|||
|
||||
describe('provider registry', () => {
|
||||
it('has core providers registered synchronously', () => {
|
||||
expect(providers.map(p => p.name)).toEqual(['claude', 'codex', 'copilot', 'droid', 'gemini', 'kilo-code', 'kiro', 'openclaw', 'pi', 'omp', 'qwen', 'roo-code'])
|
||||
expect(providers.map(p => p.name)).toEqual(['claude', 'cline', 'codex', 'copilot', 'droid', 'gemini', 'kilo-code', 'kiro', 'openclaw', 'pi', 'omp', 'qwen', 'roo-code'])
|
||||
})
|
||||
|
||||
it('includes sqlite providers after async load', async () => {
|
||||
|
|
|
|||
139
tests/providers/cline.test.ts
Normal file
139
tests/providers/cline.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtemp, mkdir, writeFile, rm, utimes } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
import { cline, createClineProvider } from '../../src/providers/cline.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
||||
let tmpDir: string
|
||||
|
||||
async function writeTask(baseDir: string, taskId: string, opts?: {
|
||||
tokensIn?: number
|
||||
tokensOut?: number
|
||||
model?: string
|
||||
userMessage?: string
|
||||
cost?: number
|
||||
}): Promise<string> {
|
||||
const taskDir = join(baseDir, 'tasks', taskId)
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
|
||||
const messages: unknown[] = []
|
||||
if (opts?.userMessage) {
|
||||
messages.push({ type: 'say', say: 'user_feedback', text: opts.userMessage, ts: 1700000000000 })
|
||||
}
|
||||
const usage: Record<string, unknown> = {
|
||||
tokensIn: opts?.tokensIn ?? 100,
|
||||
tokensOut: opts?.tokensOut ?? 50,
|
||||
}
|
||||
if (opts?.cost !== undefined) usage.cost = opts.cost
|
||||
messages.push({ type: 'say', say: 'api_req_started', text: JSON.stringify(usage), ts: 1700000001000 })
|
||||
|
||||
const modelTag = opts?.model ? `<model>${opts.model}</model>` : ''
|
||||
const history = [
|
||||
{ role: 'user', content: [{ type: 'text', text: `hello\n<environment_details>\n${modelTag}\n</environment_details>` }] },
|
||||
]
|
||||
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), JSON.stringify(messages))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), JSON.stringify(history))
|
||||
|
||||
return taskDir
|
||||
}
|
||||
|
||||
describe('cline provider - discovery', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'cline-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('discovers Cline tasks from VS Code globalStorage and home data roots', async () => {
|
||||
const vscodeDir = join(tmpDir, 'globalStorage')
|
||||
const homeDataDir = join(tmpDir, 'cline-data')
|
||||
await writeTask(vscodeDir, 'task-vscode')
|
||||
await writeTask(homeDataDir, 'task-home')
|
||||
|
||||
const provider = createClineProvider([vscodeDir, homeDataDir])
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(2)
|
||||
expect(sessions.map(s => s.provider)).toEqual(['cline', 'cline'])
|
||||
expect(sessions.map(s => s.project)).toEqual(['Cline', 'Cline'])
|
||||
expect(sessions.map(s => s.path).sort()).toEqual([
|
||||
join(homeDataDir, 'tasks', 'task-home'),
|
||||
join(vscodeDir, 'tasks', 'task-vscode'),
|
||||
].sort())
|
||||
})
|
||||
|
||||
it('deduplicates the same task id across roots by keeping the newest task directory', async () => {
|
||||
const vscodeDir = join(tmpDir, 'globalStorage')
|
||||
const homeDataDir = join(tmpDir, 'cline-data')
|
||||
const oldTask = await writeTask(vscodeDir, 'task-same')
|
||||
const newTask = await writeTask(homeDataDir, 'task-same')
|
||||
await utimes(join(oldTask, 'ui_messages.json'), new Date('2026-01-01T00:00:00Z'), new Date('2026-01-01T00:00:00Z'))
|
||||
await utimes(join(newTask, 'ui_messages.json'), new Date('2026-02-01T00:00:00Z'), new Date('2026-02-01T00:00:00Z'))
|
||||
|
||||
const provider = createClineProvider([vscodeDir, homeDataDir])
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(1)
|
||||
expect(sessions[0]!.path).toBe(newTask)
|
||||
})
|
||||
|
||||
it('skips task directories without ui_messages.json', async () => {
|
||||
const vscodeDir = join(tmpDir, 'globalStorage')
|
||||
await mkdir(join(vscodeDir, 'tasks', 'task-no-ui'), { recursive: true })
|
||||
|
||||
const provider = createClineProvider(vscodeDir)
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cline provider - parsing', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'cline-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('parses Cline usage with cline provider identity', async () => {
|
||||
const taskDir = await writeTask(tmpDir, 'task-parse', {
|
||||
tokensIn: 200,
|
||||
tokensOut: 100,
|
||||
model: 'anthropic/claude-sonnet-4-5',
|
||||
userMessage: 'build the feature',
|
||||
cost: 0.07,
|
||||
})
|
||||
|
||||
const source = { path: taskDir, project: 'Cline', provider: 'cline' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of cline.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.provider).toBe('cline')
|
||||
expect(calls[0]!.model).toBe('claude-sonnet-4-5')
|
||||
expect(calls[0]!.inputTokens).toBe(200)
|
||||
expect(calls[0]!.outputTokens).toBe(100)
|
||||
expect(calls[0]!.costUSD).toBe(0.07)
|
||||
expect(calls[0]!.userMessage).toBe('build the feature')
|
||||
expect(calls[0]!.deduplicationKey).toMatch(/^cline:task-parse:/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cline provider - metadata', () => {
|
||||
it('has correct name and displayName', () => {
|
||||
expect(cline.name).toBe('cline')
|
||||
expect(cline.displayName).toBe('Cline')
|
||||
})
|
||||
|
||||
it('passes through model and tool display names', () => {
|
||||
expect(cline.modelDisplayName('claude-sonnet-4-5')).toBe('claude-sonnet-4-5')
|
||||
expect(cline.toolDisplayName('read_file')).toBe('read_file')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue