Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
365 changes: 41 additions & 324 deletions packages/cli/src/providers/codebuff.ts

Large diffs are not rendered by default.

226 changes: 39 additions & 187 deletions packages/cli/src/providers/openclaw.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,13 @@
import { readdir, readFile } from 'fs/promises'
import { basename, join } from 'path'
import { join } from 'path'
import { homedir } from 'os'

import { decodeOpenClaw, openclawToolNameMap } from '@codeburn/core/providers/openclaw'
import type { OpenClawDecodedCall } from '@codeburn/core/providers/openclaw'
import { readSessionFile } from '../fs-utils.js'
import { extractBashCommands } from '../bash-utils.js'
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'

const toolNameMap: Record<string, string> = {
bash: 'Bash',
exec: 'Bash',
read: 'Read',
edit: 'Edit',
write: 'Write',
glob: 'Glob',
grep: 'Grep',
task: 'Agent',
dispatch_agent: 'Agent',
fetch: 'WebFetch',
search: 'WebSearch',
todo: 'TodoWrite',
patch: 'Patch',
}

type OpenClawUsage = {
input: number
output: number
cacheRead: number
cacheWrite: number
totalTokens?: number
cost?: {
total?: number
}
}

type OpenClawEntry = {
type: string
customType?: string
id?: string
timestamp?: string
provider?: string
modelId?: string
data?: {
provider?: string
modelId?: string
}
message?: {
role?: string
content?: Array<{ type?: string; text?: string; name?: string; arguments?: Record<string, unknown> }>
model?: string
provider?: string
usage?: OpenClawUsage
}
}
import { createBridgedProvider } from './bridge.js'
import type { Provider, SessionSource, ParsedProviderCall } from './types.js'

type SessionIndex = Record<string, {
sessionId: string
Expand All @@ -68,139 +24,6 @@ function getOpenClawDirs(): string[] {
]
}

function extractTools(content: Array<{ type?: string; name?: string; arguments?: Record<string, unknown> }> | undefined): { tools: string[]; bashCommands: string[] } {
const tools: string[] = []
const bashCommands: string[] = []
if (!content) return { tools, bashCommands }

for (const block of content) {
if ((block.type === 'tool_use' || block.type === 'toolCall') && block.name) {
const mapped = toolNameMap[block.name] ?? block.name
tools.push(mapped)
if (mapped === 'Bash' && block.arguments && typeof block.arguments.command === 'string') {
bashCommands.push(...extractBashCommands(block.arguments.command))
}
}
}
return { tools, bashCommands }
}

function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
return {
async *parse(): AsyncGenerator<ParsedProviderCall> {
const raw = await readSessionFile(source.path)
if (raw === null) return

const lines = raw.split('\n').filter(l => l.trim())
let sessionId = ''
let sessionTimestamp = ''
let currentModel = ''

const calls: {
model: string
usage: OpenClawUsage
tools: string[]
bashCommands: string[]
timestamp: string
userMessage: string
dedupId: string
}[] = []

let pendingUserMessage = ''

for (const line of lines) {
let entry: OpenClawEntry
try {
entry = JSON.parse(line)
} catch {
continue
}

if (entry.type === 'session') {
sessionId = entry.id ?? basename(source.path, '.jsonl')
sessionTimestamp = entry.timestamp ?? ''
continue
}

if (entry.type === 'model_change') {
currentModel = entry.modelId ?? currentModel
continue
}

if (entry.type === 'custom' && entry.customType === 'model-snapshot') {
currentModel = entry.data?.modelId ?? currentModel
continue
}

if (entry.type !== 'message' || !entry.message) continue

const msg = entry.message
if (msg.role === 'user') {
if (!pendingUserMessage && Array.isArray(msg.content)) {
const textBlock = msg.content.find(c => c.type === 'text' && c.text)
pendingUserMessage = (textBlock?.text ?? '').slice(0, 500)
}
continue
}

if (msg.role !== 'assistant') continue

const model = msg.model ?? currentModel
if (msg.usage) {
const { tools, bashCommands } = extractTools(msg.content)
calls.push({
model,
usage: msg.usage,
tools,
bashCommands,
timestamp: entry.timestamp ?? sessionTimestamp,
userMessage: pendingUserMessage,
dedupId: entry.id ?? '',
})
pendingUserMessage = ''
}
}

if (!sessionId) sessionId = basename(source.path, '.jsonl')

for (let i = 0; i < calls.length; i++) {
const call = calls[i]
const dedupKey = `openclaw:${sessionId}:${call.dedupId || i}`
if (seenKeys.has(dedupKey)) continue
seenKeys.add(dedupKey)

const u = call.usage
const costFromProvider = u.cost?.total ?? 0

const ts = new Date(call.timestamp)
if (isNaN(ts.getTime()) || ts.getTime() < 1_000_000_000_000) continue

yield {
provider: 'openclaw',
model: call.model || 'openclaw-auto',
inputTokens: u.input,
outputTokens: u.output,
cacheCreationInputTokens: u.cacheWrite,
cacheReadInputTokens: u.cacheRead,
cachedInputTokens: u.cacheRead,
reasoningTokens: 0,
webSearchRequests: 0,
...(costFromProvider > 0
? { costUSD: costFromProvider, costBasis: 'measured' as const }
: { costBasis: 'estimated' as const }),
tools: [...new Set(call.tools)],
bashCommands: [...new Set(call.bashCommands)],
timestamp: ts.toISOString(),
speed: 'standard',
deduplicationKey: dedupKey,
userMessage: call.userMessage,
sessionId,
}
}
},
}
}

async function discoverInDir(agentsDir: string): Promise<SessionSource[]> {
const sources: SessionSource[] = []

Expand Down Expand Up @@ -248,8 +71,32 @@ async function discoverInDir(agentsDir: string): Promise<SessionSource[]> {
return sources
}

export function createOpenClawProvider(overrideDir?: string): Provider {
function toProviderCall(rich: OpenClawDecodedCall): ParsedProviderCall {
return {
provider: 'openclaw',
model: rich.model,
inputTokens: rich.inputTokens,
outputTokens: rich.outputTokens,
cacheCreationInputTokens: rich.cacheCreationInputTokens,
cacheReadInputTokens: rich.cacheReadInputTokens,
cachedInputTokens: rich.cachedInputTokens,
reasoningTokens: rich.reasoningTokens,
webSearchRequests: rich.webSearchRequests,
...(rich.costBasis === 'measured'
? { costUSD: rich.costUSD, costBasis: 'measured' as const }
: { costBasis: 'estimated' as const }),
tools: [...new Set(rich.tools)],
bashCommands: [...new Set(rich.rawBashCommands.flatMap(c => extractBashCommands(c)))],
timestamp: rich.timestamp,
speed: rich.speed,
deduplicationKey: rich.deduplicationKey,
userMessage: rich.userMessage,
sessionId: rich.sessionId,
}
}

export function createOpenClawProvider(overrideDir?: string): Provider {
return createBridgedProvider<OpenClawDecodedCall>({
name: 'openclaw',
displayName: 'OpenClaw',

Expand All @@ -258,7 +105,7 @@ export function createOpenClawProvider(overrideDir?: string): Provider {
},

toolDisplayName(rawTool: string): string {
return toolNameMap[rawTool] ?? rawTool
return openclawToolNameMap[rawTool] ?? rawTool
},

async discoverSessions(): Promise<SessionSource[]> {
Expand All @@ -271,10 +118,15 @@ export function createOpenClawProvider(overrideDir?: string): Provider {
return all
},

createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
return createParser(source, seenKeys)
async readRecords(source: SessionSource): Promise<unknown[] | null> {
const raw = await readSessionFile(source.path)
if (raw === null) return null
return raw.split('\n').filter(l => l.trim())
},
}

decode: decodeOpenClaw,
toProviderCall,
})
}

export const openclaw = createOpenClawProvider()
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
[
{
"id": "msg-user-1",
"variant": "user",
"content": "implement the feature",
"timestamp": "2026-04-14T10:00:10.000Z"
},
{
"id": "a1",
"variant": "ai",
"content": "",
"timestamp": "2026-04-14T10:00:30.000Z",
"credits": 42,
"metadata": {
"runState": {
"sessionState": {
"mainAgentState": {
"agentType": "base2"
}
}
}
},
"blocks": [
{ "type": "tool", "toolName": "read_files", "input": {} },
{ "type": "tool", "toolName": "str_replace", "input": {} },
{ "type": "tool", "toolName": "run_terminal_command", "input": { "command": "npm test" } },
{ "type": "tool", "toolName": "suggest_followups", "input": {} },
{ "type": "tool", "toolName": "read_files", "input": {} },
{ "type": "tool", "toolName": "run_terminal_command", "input": { "command": "npm run build" } }
]
},
{
"id": "msg-user-2",
"variant": "user",
"content": "fix the bug",
"timestamp": "2026-04-14T10:01:00.000Z"
},
{
"id": "a2",
"variant": "ai",
"content": "",
"timestamp": "2026-04-14T10:01:30.000Z",
"credits": 10,
"metadata": {
"model": "claude-haiku-4-5-20251001",
"usage": {
"inputTokens": 5000,
"outputTokens": 2000,
"cacheCreationInputTokens": 1000,
"cacheReadInputTokens": 500
}
}
},
{
"id": "a3",
"variant": "ai",
"content": "",
"timestamp": "2026-04-14T10:02:00.000Z",
"credits": 7,
"metadata": {
"runState": {
"sessionState": {
"mainAgentState": {
"messageHistory": [
{ "role": "user" },
{
"role": "assistant",
"providerOptions": {
"codebuff": {
"model": "openai/gpt-4o",
"usage": {
"prompt_tokens": 2000,
"completion_tokens": 800,
"prompt_tokens_details": { "cached_tokens": 400 }
}
}
}
}
]
}
}
}
}
},
{
"id": "a1",
"variant": "ai",
"content": "",
"timestamp": "2026-04-14T10:02:30.000Z",
"credits": 5
},
{
"id": "a4",
"variant": "ai",
"content": "mode-divider",
"timestamp": "2026-04-14T10:03:00.000Z"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"type":"session","timestamp":"2026-04-20T10:00:00.000Z"}
{"type":"model_change","modelId":"claude-x","timestamp":"2026-04-20T10:00:01.000Z"}
{"type":"message","id":"u1","timestamp":"2026-04-20T10:00:02.000Z","message":{"role":"user","content":[{"type":"text","text":"do work"}]}}
{"type":"message","id":"a1","timestamp":"2026-04-20T10:00:03.000Z","message":{"role":"assistant","model":"claude-x","content":[{"type":"toolCall","name":"exec","arguments":{"command":"echo hi"}}],"usage":{"input":5,"output":5,"cacheRead":0,"cacheWrite":0}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{"type":"session","version":3,"id":"test-sess-1","timestamp":"2026-04-20T10:00:00.000Z","cwd":"/tmp"}
{"type":"model_change","id":"mc1","timestamp":"2026-04-20T10:00:01.000Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
{"type":"message","id":"u1","timestamp":"2026-04-20T10:00:02.000Z","message":{"role":"user","content":[{"type":"text","text":"hello world"}]}}
{"type":"message","id":"a1","timestamp":"2026-04-20T10:00:03.000Z","message":{"role":"assistant","model":"claude-sonnet-4-6","content":[{"type":"text","text":"Hi!"}],"usage":{"input":500,"output":100,"cacheRead":200,"cacheWrite":50,"totalTokens":850}}}
{"type":"message","id":"a2","timestamp":"2026-04-20T10:00:05.000Z","message":{"role":"assistant","model":"claude-sonnet-4-6","content":[{"type":"text","text":"Running command"},{"type":"toolCall","name":"exec","arguments":{"command":"ls -la"}},{"type":"tool_use","name":"write","arguments":{"path":"/tmp/y"}},{"type":"toolCall","name":"exec","arguments":{"command":"ls -la"}}],"usage":{"input":600,"output":200,"cacheRead":100,"cacheWrite":0,"totalTokens":900,"cost":{"total":0.05}}}}
{"type":"message","id":"a1","timestamp":"2026-04-20T10:00:06.000Z","message":{"role":"assistant","model":"claude-sonnet-4-6","content":[{"type":"text","text":"Duplicate"}],"usage":{"input":10,"output":10,"cacheRead":0,"cacheWrite":0,"totalTokens":20}}}
Loading