Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Arrow keys switch between Today, 7 Days, 30 Days, Month, and 6 Months (use `--fr
| <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) |
| <img src="assets/providers/devin.png" width="28" /> | Devin | Yes | [devin.md](docs/providers/devin.md) |
| <img src="assets/providers/forge.png" width="28" /> | Forge | Yes | [forge.md](docs/providers/forge.md) |
| <img src="assets/providers/gemini.png" width="28" /> | Gemini CLI | Yes | [gemini.md](docs/providers/gemini.md) |
| <img src="assets/providers/mistral-vibe.svg" width="28" /> | Mistral Vibe | Yes | [mistral-vibe.md](docs/providers/mistral-vibe.md) |
Expand Down
Binary file added assets/providers/devin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ For the architectural picture, see `../architecture.md`.
| [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` |
| [Devin](devin.md) | JSON + SQLite enrichment | `src/providers/devin.ts` | `tests/providers/devin.test.ts` |
| [Droid](droid.md) | JSONL | `src/providers/droid.ts` | `tests/providers/droid.test.ts` |
| [Gemini](gemini.md) | JSON / JSONL | `src/providers/gemini.ts` | none |
| [IBM Bob](ibm-bob.md) | JSON | `src/providers/ibm-bob.ts` | `tests/providers/ibm-bob.test.ts` |
Expand Down
175 changes: 175 additions & 0 deletions docs/providers/devin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Devin

Cognition Devin CLI local usage tracking.

- **Source:** `src/providers/devin.ts`
- **Loading:** eager (`src/providers/index.ts`)
- **Test:** `tests/providers/devin.test.ts` (336 lines)

## Where it reads from

Devin CLI data lives under:

```text
~/.local/share/devin/cli/
```

The MVP usage source is transcript JSON:

```text
~/.local/share/devin/cli/transcripts/*.json
```

The provider also reads:

```text
~/.local/share/devin/cli/sessions.db
```

`sessions.db` is enrichment only. It supplies project path/name, model fallback,
timestamp fallback, and hidden-session filtering. It is not the source of usage
or billing.

## Configuration

Devin reports spend in ACUs. CodeBurn reports provider cost through `costUSD`,
so Devin stays disabled until a positive finite ACU-to-USD rate is configured:

```json
{
"devin": {
"acuUsdRate": 2.25
}
}
```

The config file is:

```text
~/.config/codeburn/config.json
```

The macOS Settings window writes this value from the Devin tab. There is no
environment-variable override and no default rate. Do not hardcode a universal
ACU price; Devin ACU pricing is account/contract dependent.

When the rate is missing or invalid, `discoverSessions()` returns `[]` and the
parser yields no calls. Devin remains registered as a provider, but it does not
appear in CLI/UI results until configured.

## Storage format

Transcript root is a JSON object following the [ATIF-v1.4 trajectory schema][atif],
with Devin-specific additions such as per-step `metadata`. The parser does not
validate `schema_version`; it only requires a parseable object with `steps[]`.

Core fields include `session_id`, `agent.model_name`, and `steps[]`.

Each counted step can provide:

- `step_id`
- `metadata.committed_acu_cost`
- `metadata.metrics.input_tokens`
- `metadata.metrics.output_tokens`
- `metadata.metrics.cache_creation_tokens`
- `metadata.metrics.cache_read_tokens`
- `metadata.created_at`
- `metadata.generation_model`
- `metadata.request_id`
- `tool_calls[].function_name`

User-input steps (`metadata.is_user_input === true`) are skipped. Non-user
steps are included only if they have positive ACU usage or positive token usage.

## Pricing

`metadata.committed_acu_cost` is per step, not cumulative. The provider converts
each step with:

```text
costUSD = committed_acu_cost * devin.acuUsdRate
```

Token-only steps are still included when they have positive token metrics, but
their `costUSD` is `0` if `committed_acu_cost` is absent.

`src/parser.ts` preserves Devin's provider-supplied `costUSD` instead of
re-pricing it through LiteLLM.

## sessions.db enrichment

The provider currently reads these columns from `sessions`:

| Column | Use |
| ------------------- | ----------------------------------------------------------------------------------------------------------- |
| `id` | join key with transcript `session_id` during parsing; discovery uses the transcript filename before `.json` |
| `working_directory` | `projectPath` and derived project name |
| `model` | model fallback |
| `title` | project name fallback |
| `created_at` | timestamp fallback |
| `last_activity_at` | preferred session timestamp fallback |
| `hidden` | skip hidden sessions |

`message_nodes`, `prompt_history`, and `tool_call_state` are not parsed yet.

## Timestamps

Step timestamps come from `metadata.created_at`, falling back to
`sessions.last_activity_at`, then `sessions.created_at`.

Transcript step timestamps are passed through as ATIF string timestamps.
Numeric normalization is only applied to `sessions.db` timestamps:

- less than `10_000_000_000`: seconds
- otherwise: milliseconds

## Model Resolution

Model names resolve in this order:

1. `step.metadata.generation_model`
2. `step.model_name`
3. `transcript.agent.model_name`
4. `sessions.model`
5. `devin`

## Caching

No provider-level cache.

The normal session cache stores parsed provider calls, but Devin is always
reparsed by `src/parser.ts` because `sessions.db` can change without the
transcript JSON fingerprint changing.

## Deduplication

`devin:<sessionId>:<step.step_id>`

The provider name is part of the key via the `devin:` prefix.

## Quirks

- The transcript directory has usage; `sessions.db` is enrichment only.
- `committed_acu_cost` is per-generation/per-step ACU usage. Never treat it as cumulative.
- There is no default ACU-to-USD rate. Missing config intentionally hides Devin.
- Hidden sessions from `sessions.db` are skipped in discovery and parsing.
- Tool names come directly from `tool_calls[].function_name`; the provider assumes valid ATIF tool-call records.
- If SQLite is unavailable or `sessions.db` cannot be opened, the provider still parses transcripts without enrichment.

## When fixing a bug here

1. First check whether `~/.config/codeburn/config.json` contains a valid
`devin.acuUsdRate`. Without it, no Devin sessions should appear.
2. For usage total bugs, compare against:

```bash
jq '[.steps[] | select(.metadata.committed_acu_cost != null) | .metadata.committed_acu_cost] | add' ~/.local/share/devin/cli/transcripts/<session>.json
```

3. If project/model/timestamp metadata is wrong, inspect `sessions.db`, not the transcript.
4. If a hidden session appears, check the `hidden` column. Discovery can only
hide sessions whose transcript filename matches `sessions.id`; parsing uses
the transcript `session_id` when present.
5. Run `tests/providers/devin.test.ts` after parser changes. It covers ACU conversion, disabled-until-configured behavior, timestamp parsing, deduplication, hidden sessions, and `sessions.db` enrichment.

[atif]: https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md
2 changes: 2 additions & 0 deletions gnome/indicator.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const PROVIDERS = [
{ id: 'codex', label: 'Codex' },
{ id: 'cursor', label: 'Cursor' },
{ id: 'copilot', label: 'Copilot' },
{ id: 'devin', label: 'Devin' },
{ id: 'opencode', label: 'OpenCode' },
{ id: 'pi', label: 'Pi' },
{ id: 'droid', label: 'Droid' },
Expand Down Expand Up @@ -70,6 +71,7 @@ const PROVIDER_PATHS = {
codex: '.codex/sessions',
cursor: '.config/Cursor/User/globalStorage/state.vscdb',
copilot: '.copilot/session-state',
devin: '.local/share/devin/cli',
kimi: '.kimi/sessions',
pi: '.pi/agent/sessions',
};
Expand Down
1 change: 1 addition & 0 deletions gnome/prefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const PROVIDERS = [
{ id: 'codex', label: 'Codex' },
{ id: 'copilot', label: 'Copilot' },
{ id: 'cursor', label: 'Cursor' },
{ id: 'devin', label: 'Devin' },
{ id: 'droid', label: 'Droid' },
{ id: 'gemini', label: 'Gemini' },
{ id: 'goose', label: 'Goose' },
Expand Down
2 changes: 2 additions & 0 deletions mac/Sources/CodeBurnMenubar/AppStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
case cursor = "Cursor"
case cursorAgent = "Cursor Agent"
case copilot = "Copilot"
case devin = "Devin"
case droid = "Droid"
case gemini = "Gemini"
case ibmBob = "IBM Bob"
Expand Down Expand Up @@ -1067,6 +1068,7 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
case .cursor: "cursor"
case .cursorAgent: "cursor-agent"
case .copilot: "copilot"
case .devin: "devin"
case .droid: "droid"
case .gemini: "gemini"
case .ibmBob: "ibm-bob"
Expand Down
71 changes: 71 additions & 0 deletions mac/Sources/CodeBurnMenubar/CurrencyState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,74 @@ enum CLICurrencyConfig {
}
}
}

struct CodeburnCLIConfigStore {
let homeDirectory: String

init(homeDirectory: String = NSHomeDirectory()) {
self.homeDirectory = homeDirectory
}

private var configDir: String {
(homeDirectory as NSString).appendingPathComponent(".config/codeburn")
}
private var configPath: String {
(configDir as NSString).appendingPathComponent("config.json")
}
private var lockPath: String {
(configDir as NSString).appendingPathComponent(".config.lock")
}

func loadDevinAcuUsdRate() -> Double? {
guard
let data = try? SafeFile.read(from: configPath),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let devin = json["devin"] as? [String: Any],
let rate = devin["acuUsdRate"] as? Double,
rate.isFinite,
rate > 0
else {
return nil
}
return rate
}

func persistDevinAcuUsdRate(_ rate: Double) {
guard rate.isFinite, rate > 0 else { return }
do {
try SafeFile.withExclusiveLock(at: lockPath) {
var existing: [String: Any] = [:]
if let data = try? SafeFile.read(from: configPath),
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
existing = parsed
}

var devin = existing["devin"] as? [String: Any] ?? [:]
devin["acuUsdRate"] = rate
existing["devin"] = devin

guard let data = try? JSONSerialization.data(
withJSONObject: existing,
options: [.prettyPrinted, .sortedKeys]
) else {
return
}
try SafeFile.write(data, to: configPath, mode: 0o600)
}
} catch {
NSLog("CodeBurn: failed to persist Devin ACU config: \(error)")
}
}
}

enum CLIDevinConfig {
private static let store = CodeburnCLIConfigStore()

static func loadAcuUsdRate() -> Double? {
store.loadDevinAcuUsdRate()
}

static func persistAcuUsdRate(_ rate: Double) {
store.persistDevinAcuUsdRate(rate)
}
}
1 change: 1 addition & 0 deletions mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ extension ProviderFilter {
case .cursor: return Theme.categoricalCursor
case .cursorAgent: return Color(red: 0x4E/255.0, green: 0xC9/255.0, blue: 0xB0/255.0)
case .copilot: return Color(red: 0x6D/255.0, green: 0x8F/255.0, blue: 0xA6/255.0)
case .devin: return Color(red: 0x25/255.0, green: 0xA0/255.0, blue: 0x8D/255.0)
case .droid: return Color(red: 0x7C/255.0, green: 0x3A/255.0, blue: 0xED/255.0)
case .gemini: return Color(red: 0x44/255.0, green: 0x85/255.0, blue: 0xF4/255.0)
case .ibmBob: return Color(red: 0x0F/255.0, green: 0x62/255.0, blue: 0xFE/255.0)
Expand Down
Loading