clinecli

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package clinecli implements the event-level adapter for the Cline CLI (`cline`, npm package "cline", Apache-2.0). The VS Code extension writes a different, id-less surface and is deliberately NOT read here.

Two surfaces, each authoritative for one thing

USAGE comes from the per-session message document <sessions>/<session id>/<session id>.messages.json. Every assistant message carries its own `metrics` block — inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens — a stable `id` (nanoid, e.g. "msg_BZEHy5S0"), a `ts` in unix milliseconds and a `modelInfo{id, provider}`. One assistant message is one API response, so one message with metrics is one usage event.

DISCOVERY comes from <db>/sessions.db, opened read-only. Its `sessions` table names each session's `messages_path` outright, along with the `cwd` that becomes the event's project. It is an INDEX, never a usage source: the table has no token columns at all. A directory walk runs after it and picks up any message document the index does not name, so a pruned or missing index costs discovery speed and metadata, never events.

Path resolution follows Cline's own, not the third-party parsers

Read out of the shipped CLI bundle (cline 3.0.55), the chain is:

root     = $CLINE_DIR              or <home>/.cline
data     = $CLINE_DATA_DIR         or <root>/data
sessions = $CLINE_SESSION_DATA_DIR or <data>/sessions
db       = $CLINE_DB_DATA_DIR      or <data>/db

Two traps are baked into that. The `data/` level is real — documentation and third-party parsers that spell the path ~/.cline/sessions are describing a layout this CLI does not write. And CLINE_SESSION_DATA_DIR IS the sessions directory, not a parent containing one: appending "sessions" to it (as the harness matrix row does) looks at a directory that never exists, so an adapter that "supports" the variable that way silently collects nothing from exactly the machines that set it.

Traps this adapter is built around

CUMULATIVE-VS-EVENT. The session sidecar <session id>.json carries `metadata.usage` and `metadata.aggregateUsage`, and both are running totals of the very message metrics read here — measured on a live two-turn session, metadata.usage was 9592/37 against per-message metrics of 4770/20 and 4822/17. The same object is copied into sessions.metadata_json in the index database. Reading either alongside the messages would count every token twice, and aggregateUsage additionally folds in subagent sessions that carry their own message documents. Neither is read: the per-message metrics are the events, and the accumulators are ignored everywhere.

CACHE TOKENS ARE NOT UNIFORMLY ADDITIVE. Cline's own cost function prices a message as (inputTokens - cacheReadTokens - cacheWriteTokens) at the input rate plus the two cache buckets at their own rates, i.e. it treats the cache counts as a SUBSET of inputTokens. But its usage normaliser copies each upstream provider's field verbatim — OpenAI's `prompt_tokens` includes cached tokens, Anthropic's `input_tokens` excludes them — so the relationship is a property of the provider behind the request, not of Cline. The subset test is therefore made per event, on the only evidence available: when cacheRead+cacheWrite fits inside inputTokens they are treated as a subset and subtracted out of the input component; when it does not, they cannot be one and are treated as additive. Either way the four components sum to exactly the stored total, so no token is counted twice and none is discarded. The residual error is bounded and one-directional: an Anthropic-backed request whose cache write happens to fit inside its input count is UNDERSTATED by that write, never overstated.

SPLIT IDENTITY, INVERTED. The message document is not append-only — Cline rewrites the whole JSON on every save — so a byte-offset tail read is meaningless and every read is a full parse of the document. The identities survive it: message ids are persisted in the file and reappear unchanged on every rewrite, so the dedup keys collapse re-reads. The gate is the file's size and mtime.

Message ids are minted locally (Io("msg") in the bundle), not handed down by the provider, so they are only unique within their own message stream. The dedup key is scoped accordingly: cline|<session id>|<agent>|<message id>, where the agent is the document's own `agent` field ("lead" for the top-level stream). A collision across two sessions is unrepresentable rather than merely improbable.

Activity

A tool call lives in the SAME assistant message as the metrics it was billed under — one `{"type":"tool_use","id":"call_...","name":"run_commands"}` block in that message's content — so the join is exact and the divisor is the number of tool_use blocks in that one message. Nothing is inferred from adjacency. A call in a message with no metrics is emitted with an empty UsageDedupKey: the call is an observed fact whose cost is unknown, never free.

Only kind=tool is emitted. The CLI's hook log is a separate file ($CLINE_HOOKS_LOG_PATH, default <data>/logs/hooks.jsonl) with no usage join, and a skill invocation is not distinguishable from a tool call in this surface, so neither is invented here.

PRIVACY: names and counts only, by construction. Content blocks are decoded through a three-field allow-list — type, id, name — so a block's `text`, a tool call's `input` and a tool result's `content` have no field to land in and never become values in this process. The document's `system_prompt` and the sidecar's `prompt` are likewise never decoded. The audit payload is re-marshalled from an allow-list struct rather than kept as source bytes.

Not built (yet), and why

The index carries `is_subagent`, `parent_session_id`, `parent_agent_id`, `agent_id` and `conversation_id`, and each message document names its own `agent`. Together those are the handle a future DimensionAgent turn context would hang on — a subagent session's every turn ran under its parent. It is deliberately not built here: no subagent session exists on the verification machine, and attribution guessed from an unexercised schema is exactly the kind of claim this ledger must not make.

CRITICAL: strictly read-only. Message documents are opened O_RDONLY; the index database uses a read-only DSN (mode=ro plus query_only(1)) and never immutable=1, because Cline holds sessions.db open and writes it live with a WAL an immutable reader would not see. Nothing under the agent's directories is created, locked or modified.

Index

Constants

View Source
const (
	// DirEnv moves the Cline root directory (default <home>/.cline), and with it
	// every path below.
	DirEnv = "CLINE_DIR"
	// DataDirEnv moves the Cline data directory (default <root>/data), and with
	// it both the sessions tree and the index database.
	DataDirEnv = "CLINE_DATA_DIR"
	// SessionDataDirEnv moves the sessions directory itself (default
	// <data>/sessions) — the directory that holds <session id>/ subdirectories,
	// NOT a parent containing one.
	SessionDataDirEnv = "CLINE_SESSION_DATA_DIR"
	// DBDataDirEnv moves the database directory (default <data>/db) that holds
	// the sessions.db discovery index.
	DBDataDirEnv = "CLINE_DB_DATA_DIR"
)

Variables

This section is empty.

Functions

func New

func New() adapter.Adapter

New returns a Cline CLI adapter.

Types

type Adapter

type Adapter struct{}

Adapter reads Cline CLI session message documents. Read-only.

func (Adapter) Capabilities

func (Adapter) Capabilities() model.ToolCapability

Capabilities declares what this project can say about Cline.

Cost is COMPUTED: nothing here calls SetCost. Activity is an EXACT join — buildActivity names the usage row that paid for each call, because the tool_use block sits in the SAME message as the metrics it was billed under.

func (Adapter) Collect

func (a Adapter) Collect(ctx context.Context, src adapter.Source) (adapter.Observation, error)

Collect reads one session message document read-only.

func (Adapter) CollectIncremental

func (a Adapter) CollectIncremental(ctx context.Context, src adapter.Source, cp *model.SourceCheckpoint) (adapter.Observation, error)

CollectIncremental gates the document on its size and mtime and otherwise re-parses it in full. There is no tail read to be had: Cline rewrites the whole JSON document on every save, so a byte offset into the previous version points into the middle of a different file. The full re-parse is safe because the message ids it re-derives are the persisted dedup keys, which conflict-skip on insert.

func (Adapter) Discover

func (a Adapter) Discover(ctx context.Context, cfg adapter.DiscoverConfig) ([]adapter.Source, error)

Discover lists one source per session message document: first every document the index database names, then every document under the sessions tree the index did not. Both halves are needed. The index alone would lose a session whose row was pruned or whose database is on a machine that never ran the migration; the walk alone would lose the cwd that becomes the project, and with it a message document the index points at from outside the tree.

func (Adapter) DisplayName

func (Adapter) DisplayName() string

DisplayName returns the human-friendly name.

func (Adapter) ID

func (Adapter) ID() string

ID returns the stable tool identifier.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL