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
34 changes: 34 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,40 @@ Reads from `~/.local/share/opencode/opencode.db`:
- SQLite database with `session`, `message`, and `project` tables
- Messages queried directly via SQL with full content, model, and token data

### Kiro

Reads from `~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/`:
- `workspace-sessions/` — base64-encoded workspace folder names
- Per-workspace `.chat` files with JSON session metadata and messages
- Messages stored in blob format with role, content, model, and timestamps

Comment on lines +168 to +174
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added Kiro data-source documentation does not match the current editors/kiro.js adapter, which reads from the Kiro app's VS Code-like globalStorage directories (and JSON/.chat files), not ~/.kiro/sessions.db with sessions/messages tables. Please update this section to reflect the actual adapter behavior and paths so contributors don't implement against the wrong format.

Copilot uses AI. Check for mistakes.
### Kilo Code CLI

Reads from platform-specific SQLite database:
- Linux: `~/.local/share/kilo/kilo.db`
- macOS: `~/Library/Application Support/kilo/kilo.db`
- Windows: `%APPDATA%/kilo/kilo.db`

Database tables:
- `session` — session metadata with id, title, directory, timestamps
- `message` — messages with role, content, model, usage
- `part` — message content blocks including tool calls and tool results
- Tool calls extracted from `part` table where `type: 'tool'`
- Token usage from `part` table where `type: 'step-finish'`

### Cline CLI

Reads from platform-specific session directories:
- Linux: `~/.cline/data/sessions/<session-id>/`
- macOS: `~/Library/Application Support/cline/data/sessions/<session-id>/`
- Windows: `%APPDATA%/cline/data/sessions/<session-id>/`

Files:
- `<session-id>.json` — session metadata including cwd, model, started_at
- `<session-id>.messages.json` — full conversation in OpenAI format
- Supports token extraction from `usage` field
- Tool calls extracted from `tool_use` content blocks

---

## Database Schema
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<p align="center">
<a href="https://www.npmjs.com/package/agentlytics"><img src="https://img.shields.io/npm/v/agentlytics?color=6366f1&label=npm" alt="npm"></a>
<a href="#supported-editors"><img src="https://img.shields.io/badge/editors-16-818cf8" alt="editors"></a>
<a href="#supported-editors"><img src="https://img.shields.io/badge/editors-18-818cf8" alt="editors"></a>
<a href="#license"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
Comment on lines 13 to 15
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README now shows 18 supported editors, but the tagline earlier in the file still says "16 editors". Please update that tagline so the count is consistent across the README.

Copilot uses AI. Check for mistakes.
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A520.19%20%7C%20%E2%89%A522.12-brightgreen" alt="node"></a>
<a href="https://deno.land"><img src="https://img.shields.io/badge/deno-%E2%89%A52.0-000?logo=deno" alt="deno"></a>
Expand Down Expand Up @@ -154,6 +154,8 @@ npx agentlytics --collect
| **Command Code** | ✅ | ✅ | ❌ | ❌ |
| **Goose** | ✅ | ✅ | ✅ | ❌ |
| **Kiro** | ✅ | ✅ | ✅ | ❌ |
| **Kilo Code CLI** | ✅ | ✅ | ✅ | ✅ |
| **Cline CLI** | ✅ | ✅ | ✅ | ✅ |

> Windsurf, Windsurf Next, and Antigravity must be running during scan.

Expand Down
133 changes: 133 additions & 0 deletions editors/cline-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const path = require('path');
const fs = require('fs');
const os = require('os');

const name = 'cline-cli';
const sources = ['cline-cli'];

function getClineDir() {
if (process.platform === 'win32') {
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'cline');
} else if (process.platform === 'darwin') {
return path.join(os.homedir(), 'Library', 'Application Support', 'cline');
}
return path.join(os.homedir(), '.cline');
}

const CLINE_DIR = getClineDir();
const CLINE_DATA_DIR = path.join(CLINE_DIR, 'data');
const SESSIONS_DIR = path.join(CLINE_DATA_DIR, 'sessions');

function getChats() {
const chats = [];
if (!fs.existsSync(SESSIONS_DIR)) return chats;

try {
const sessionDirs = fs.readdirSync(SESSIONS_DIR);
for (const sessionDir of sessionDirs) {
const sessionPath = path.join(SESSIONS_DIR, sessionDir);
if (!fs.statSync(sessionPath).isDirectory()) continue;

const sessionJsonPath = path.join(sessionPath, `${sessionDir}.json`);
let sessionData = null;
try {
sessionData = JSON.parse(fs.readFileSync(sessionJsonPath, 'utf-8'));
} catch { continue; }

if (!sessionData) continue;

const startedAt = sessionData.started_at ? new Date(sessionData.started_at).getTime() : null;
const endedAt = sessionData.ended_at ? new Date(sessionData.ended_at).getTime() : startedAt;

chats.push({
source: 'cline-cli',
composerId: sessionData.session_id || sessionDir,
name: sessionData.metadata?.title || sessionData.prompt?.substring(0, 100) || null,
createdAt: startedAt,
lastUpdatedAt: endedAt,
mode: 'cline-cli',
folder: sessionData.cwd || sessionData.workspace_root || null,
encrypted: false,
bubbleCount: 0,
_sessionId: sessionDir,
_messagesPath: sessionData.messages_path,
});
Comment on lines +49 to +54
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_messagesPath is taken directly from sessionData.messages_path and later passed to fs.existsSync/readFileSync. This is brittle if the value is relative (resolved against process CWD) and also allows the session JSON to point outside the session directory. Consider resolving relative paths against sessionPath (and/or defaulting to <session-id>.messages.json when missing) and enforcing that the final resolved path stays within the expected Cline data directory before reading.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +54
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_messagesPath is taken only from sessionData.messages_path, but the documented layout is <session-id>.messages.json inside the session directory. If messages_path is missing/relative, this adapter will return zero messages; consider resolving relative paths against sessionPath and falling back to path.join(sessionPath, ${sessionDir}.messages.json).

Copilot uses AI. Check for mistakes.
}
} catch {}

chats.sort((a, b) => {
const ta = a.lastUpdatedAt || a.createdAt || 0;
const tb = b.lastUpdatedAt || b.createdAt || 0;
return tb - ta;
});
return chats;
}

function getMessages(chat) {
const messages = [];
if (!chat._sessionId || !chat._messagesPath) return messages;

if (!fs.existsSync(chat._messagesPath)) return messages;

try {
const messagesFile = JSON.parse(fs.readFileSync(chat._messagesPath, 'utf-8'));
const messagesData = Array.isArray(messagesFile) ? messagesFile : (messagesFile.messages || []);

for (const msg of messagesData) {
const role = msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : msg.role === 'system' ? 'system' : null;
if (!role) continue;

Comment on lines +76 to +79
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getMessages maps roles to user/assistant/system only, so OpenAI role: 'tool' messages are ignored even though the README/CONTRIBUTING describe OpenAI-format logs. Tool outputs can be lost as a result; consider handling role: 'tool' and preserving its content.

Copilot uses AI. Check for mistakes.
let content = '';
if (Array.isArray(msg.content)) {
content = msg.content.map(c => {
if (typeof c === 'string') return c;
if (c.type === 'text') return c.text || '';
if (c.type === 'tool_use') return `[tool-call: ${c.name}]`;
if (c.type === 'tool_result') return c.content || '';
if (c.type === 'thinking') return c.thinking || '';
return c.text || c.content || '';
}).join('');
} else if (typeof msg.content === 'string') {
Comment on lines +81 to +90
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When msg.content is an array of blocks, the code joins mapped chunks with join(''), which can concatenate adjacent blocks without separators and change meaning/formatting. Join with newlines (or at least a space) to preserve boundaries between content parts.

Copilot uses AI. Check for mistakes.
content = msg.content;
}

if (!content) continue;

const message = { role, content };

if (msg.model) message._model = msg.model;
Comment on lines +94 to +98
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Messages with empty content are dropped (if (!content) continue;) before any tool_calls handling. In OpenAI format, assistant tool-call messages often have empty content with the call details in tool_calls, so this can silently drop tool calls; consider allowing such messages through when msg.tool_calls is present.

Copilot uses AI. Check for mistakes.
if (msg.usage) {
message._inputTokens = msg.usage.prompt_tokens || msg.usage.input_tokens || null;
message._outputTokens = msg.usage.completion_tokens || msg.usage.output_tokens || null;
}

if (msg.tool_calls && msg.tool_calls.length > 0) {
message._toolCalls = msg.tool_calls.map(tc => {
let args = tc.function?.arguments || tc.arguments || {};
if (typeof args === 'string') {
try { args = JSON.parse(args); } catch { args = {}; }
}
return { name: tc.function?.name || tc.name || 'unknown', args };
});
}
Comment on lines +104 to +112
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tool_calls[].function.arguments is often a JSON string (OpenAI format). Currently args is assigned directly, so downstream analytics will store a string instead of an object. Parse JSON when arguments is a string (fall back to {} on parse failure) to match how other adapters normalize tool-call args.

Copilot uses AI. Check for mistakes.

messages.push(message);
}
} catch {}

return messages;
}

function resetCache() {}

function getMCPServers() {
const { parseMcpConfigFile } = require('./base');
const configPath = path.join(CLINE_DATA_DIR, 'settings', 'cline_mcp_settings.json');
return parseMcpConfigFile(configPath, { editor: 'cline-cli', label: 'Cline CLI', scope: 'global' });
}

const labels = {
'cline-cli': 'Cline CLI',
};

module.exports = { name, sources, labels, getChats, getMessages, resetCache, getMCPServers };
4 changes: 3 additions & 1 deletion editors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ const cursorAgent = require('./cursor-agent');
const commandcode = require('./commandcode');
const goose = require('./goose');
const kiro = require('./kiro');
const clineCli = require('./cline-cli');
const kilocodeCli = require('./kilocode-cli');

const editors = [cursor, windsurf, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro];
const editors = [cursor, windsurf, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro, clineCli, kilocodeCli];

// Build a unified source → display-label map from all editor modules
const editorLabels = {};
Expand Down
180 changes: 180 additions & 0 deletions editors/kilocode-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
const path = require('path');
const fs = require('fs');
const os = require('os');

const name = 'kilocode-cli';
const sources = ['kilocode-cli'];

function getKiloDbPath() {
if (process.platform === 'win32') {
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'kilo', 'kilo.db');
} else if (process.platform === 'darwin') {
return path.join(os.homedir(), 'Library', 'Application Support', 'kilo', 'kilo.db');
}
return path.join(os.homedir(), '.local', 'share', 'kilo', 'kilo.db');
}

const KILO_DB_PATH = getKiloDbPath();

function getChats() {
const chats = [];
if (!fs.existsSync(KILO_DB_PATH)) return chats;

let db;
try {
const Database = require('better-sqlite3');
db = new Database(KILO_DB_PATH, { readonly: true });
} catch { return chats; }
Comment on lines +23 to +27
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adapter opens the SQLite DB without readonly: true (and uses the function-call form, which makes it harder to pass options). Other adapters open editor databases read-only to avoid locks/journal writes and reduce the chance of interfering with the running app. Use new Database(KILO_DB_PATH, { readonly: true }) (and ideally reuse a cached Database require like other adapters) for both getChats and getMessages.

Copilot uses AI. Check for mistakes.

try {
const sessions = db.prepare(`
SELECT id, title, directory, time_created, time_updated
FROM session
ORDER BY time_updated DESC
`).all();

for (const session of sessions) {
chats.push({
source: 'kilocode-cli',
composerId: session.id,
name: session.title || null,
createdAt: session.time_created || null,
lastUpdatedAt: session.time_updated || null,
mode: 'kilocode',
folder: session.directory || null,
encrypted: false,
bubbleCount: 0,
_sessionId: session.id,
});
}
} catch {}

try { db.close(); } catch {}
return chats;
}

function getMessages(chat) {
const messages = [];
if (!chat._sessionId) return messages;

if (!fs.existsSync(KILO_DB_PATH)) return messages;

let db;
try {
const Database = require('better-sqlite3');
db = new Database(KILO_DB_PATH, { readonly: true });
} catch { return messages; }

try {
const messagesData = db.prepare(`
SELECT id, data, time_created
FROM message
WHERE session_id = ?
ORDER BY time_created ASC
`).all(chat._sessionId);

const partsData = db.prepare(`
SELECT id, data, time_created
FROM part
WHERE session_id = ?
ORDER BY time_created ASC
Comment on lines +76 to +80
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partsData is queried for the entire session and then appended after the messagesData loop, without associating parts to individual assistant turns. This can collapse multiple assistant responses into one (or split only around tool parts), losing the original turn structure and misattributing tokens. Consider grouping parts per message (e.g., via a message_id FK if available) or merging message + part rows into one time-ordered stream.

Copilot uses AI. Check for mistakes.
`).all(chat._sessionId);

let currentRole = 'user';
let currentContent = '';
let currentModel = null;
let currentTokens = null;

for (const msg of messagesData) {
try {
const data = JSON.parse(msg.data);
if (data.role === 'user') {
if (currentContent || currentRole === 'assistant') {
const m = { role: currentRole, content: currentContent };
if (currentModel) m._model = currentModel;
if (currentTokens) {
if (currentTokens.input) m._inputTokens = currentTokens.input;
if (currentTokens.output) m._outputTokens = currentTokens.output;
}
messages.push(m);
}
currentRole = 'user';
currentContent = data.content || '';
currentModel = data.model?.providerID && data.model?.modelID
? `${data.model.providerID}/${data.model.modelID}`
: data.model?.modelID || null;
currentTokens = null;
} else if (data.role === 'assistant') {
if (currentRole === 'user' && currentContent) {
const m = { role: 'user', content: currentContent };
if (currentModel) m._model = currentModel;
messages.push(m);
}
currentRole = 'assistant';
currentContent = '';

if (data.model?.modelID) {
currentModel = data.model?.providerID && data.model?.modelID
? `${data.model.providerID}/${data.model.modelID}`
: data.model.modelID;
}
if (data.tokens) currentTokens = data.tokens;
}
} catch {}
}

for (const part of partsData) {
try {
const data = JSON.parse(part.data);

if (data.type === 'text' && data.text) {
currentContent += (currentContent ? '\n\n' : '') + data.text;
} else if (data.type === 'tool') {
const toolName = data.tool || 'tool';
const toolInput = data.state?.input || {};
currentContent += (currentContent ? '\n\n' : '') + `[tool-call: ${toolName}]`;
if (toolInput.command) currentContent += ` ${toolInput.command}`;
else if (toolInput.filePath) currentContent += ` ${toolInput.filePath}`;
messages.push({
role: 'assistant',
content: currentContent,
_toolCalls: [{
name: toolName,
args: toolInput,
}],
});
currentContent = '';

if (data.state?.output && !data.state.error) {
currentContent += `[tool-result]\n${data.state.output}`;
}
} else if (data.type === 'file') {
currentContent += (currentContent ? '\n\n' : '') + `[file: ${data.filename || data.url}]`;
} else if (data.type === 'step-finish' && data.tokens) {
currentTokens = data.tokens;
}
} catch {}
}

if (currentContent || currentRole === 'assistant') {
const m = { role: currentRole, content: currentContent };
if (currentModel) m._model = currentModel;
if (currentTokens) {
if (currentTokens.input) m._inputTokens = currentTokens.input;
if (currentTokens.output) m._outputTokens = currentTokens.output;
}
if (m.content || m._toolCalls) messages.push(m);
}
} catch {}

try { db.close(); } catch {}
return messages.filter(m => m.content || m._toolCalls);
}

function resetCache() {}

const labels = {
'kilocode-cli': 'Kilo Code CLI',
};

module.exports = { name, sources, labels, getChats, getMessages, resetCache };
Loading
Loading