-
Notifications
You must be signed in to change notification settings - Fork 50
add support for Cline CLI and Kilo Code CLI, update README and CONTRI… #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d2be401
4f428b4
219dbd8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| <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> | ||
|
|
@@ -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. | ||
|
|
||
|
|
||
| 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
|
||
| } | ||
| } 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
|
||
| 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
|
||
| content = msg.content; | ||
| } | ||
|
|
||
| if (!content) continue; | ||
|
|
||
| const message = { role, content }; | ||
|
|
||
| if (msg.model) message._model = msg.model; | ||
|
Comment on lines
+94
to
+98
|
||
| 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
|
||
|
|
||
| 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 }; | ||
| 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
|
||
|
|
||
| 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
|
||
| `).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 }; | ||
There was a problem hiding this comment.
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.jsadapter, which reads from the Kiro app's VS Code-likeglobalStoragedirectories (and JSON/.chat files), not~/.kiro/sessions.dbwithsessions/messagestables. Please update this section to reflect the actual adapter behavior and paths so contributors don't implement against the wrong format.