From 8ec98058ae52748e0dc874f0f958349be1ace89c Mon Sep 17 00:00:00 2001 From: Joseph Guenther Date: Tue, 30 Jun 2026 11:43:51 -0400 Subject: [PATCH] Add pi support --- README.md | 6 +- docs/index.html | 4 +- editors/index.js | 4 +- editors/pi.js | 296 ++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- ui/src/lib/constants.js | 2 + 6 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 editors/pi.js diff --git a/README.md b/README.md index d0def53..3fc50c1 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@

Your Cursor, Devin, Claude Code sessions — analyzed, unified, tracked.
- One command to turn scattered AI conversations from 17 editors into a unified analytics dashboard.
Sessions, costs, models, tools — finally in one place. 100% local.
+ One command to turn scattered AI conversations from 18 editors into a unified analytics dashboard.
Sessions, costs, models, tools — finally in one place. 100% local.

npm - editors + editors license node

@@ -49,6 +49,7 @@ bunx agentlytics Opens at **http://localhost:4637**. Requires Node.js ≥ 20.19 or ≥ 22.12, macOS. No data ever leaves your machine. + ### Node.js ``` @@ -111,6 +112,7 @@ npx agentlytics --collect | **Goose** | ✅ | ✅ | ✅ | ❌ | | **Kiro** | ✅ | ✅ | ✅ | ❌ | | **Codebuff** | ✅ | ✅ | ⚠️ | ⚠️ | +| **Pi Agent** | ✅ | ✅ | ✅ | ✅ | > Devin, Devin Next, and Antigravity must be running during scan. diff --git a/docs/index.html b/docs/index.html index 72e9d6f..8d25b67 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,9 +4,9 @@ Agentlytics — Unified analytics for your AI coding agents - + - + diff --git a/editors/index.js b/editors/index.js index c122a18..8a8e2a8 100644 --- a/editors/index.js +++ b/editors/index.js @@ -14,8 +14,9 @@ const commandcode = require('./commandcode'); const goose = require('./goose'); const kiro = require('./kiro'); const codebuff = require('./codebuff'); +const pi = require('./pi'); -const editors = [cursor, devin, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, copilotJetbrains, cursorAgent, commandcode, goose, kiro, codebuff]; +const editors = [cursor, devin, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, copilotJetbrains, cursorAgent, commandcode, goose, kiro, codebuff, pi]; // Build a unified source → display-label map from all editor modules const editorLabels = {}; @@ -150,6 +151,7 @@ function getAllMCPServers(projectFolders = []) { { file: '.vscode/mcp.json', editor: 'vscode', label: 'VS Code' }, { file: '.gemini/settings.json', editor: 'gemini-cli', label: 'Gemini CLI' }, { file: '.kiro/settings/mcp.json', editor: 'kiro', label: 'Kiro' }, + { file: '.pi/settings.json', editor: 'pi', label: 'Pi Agent' }, ]; const seenProjects = new Set(); diff --git a/editors/pi.js b/editors/pi.js new file mode 100644 index 0000000..64213d4 --- /dev/null +++ b/editors/pi.js @@ -0,0 +1,296 @@ +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const { scanArtifacts, parseMcpConfigFile } = require('./base'); + +const name = 'pi'; +const labels = { pi: 'Pi Agent' }; +const PI_HOME = process.env.PI_CODING_AGENT_DIR && process.env.PI_CODING_AGENT_DIR.trim() + ? path.resolve(process.env.PI_CODING_AGENT_DIR.trim()) + : (process.env.PI_HOME && process.env.PI_HOME.trim() + ? path.resolve(process.env.PI_HOME.trim()) + : path.join(os.homedir(), '.pi', 'agent')); +const SESSIONS_DIR = process.env.PI_CODING_AGENT_SESSION_DIR && process.env.PI_CODING_AGENT_SESSION_DIR.trim() + ? path.resolve(process.env.PI_CODING_AGENT_SESSION_DIR.trim()) + : path.join(PI_HOME, 'sessions'); +const MAX_TOOL_RESULT_PREVIEW = 500; + +function getChats() { + const chats = []; + if (!fs.existsSync(SESSIONS_DIR)) return chats; + + for (const filePath of walkJsonlFiles(SESSIONS_DIR)) { + const chat = readChatMetadata(filePath); + if (chat) chats.push(chat); + } + + return chats; +} + +function getMessages(chat) { + const filePath = chat && chat._filePath; + if (!filePath || !fs.existsSync(filePath)) return []; + return parseSessionMessages(filePath); +} + +function walkJsonlFiles(dir) { + const results = []; + const stack = [dir]; + + while (stack.length > 0) { + const current = stack.pop(); + let entries; + try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } + + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) stack.push(fullPath); + else if (entry.isFile() && entry.name.endsWith('.jsonl')) results.push(fullPath); + } + } + + return results.sort(); +} + +function readChatMetadata(filePath) { + const lines = readLines(filePath); + if (lines.length === 0) return null; + + const header = safeParseJson(lines[0]); + if (!header || header.type !== 'session') return null; + + let firstPrompt = null; + let messageCount = 0; + let lastTimestamp = toTimestamp(header.timestamp); + + for (let i = 1; i < lines.length; i++) { + const entry = safeParseJson(lines[i]); + if (!entry) continue; + const ts = toTimestamp(entry.timestamp || entry.message?.timestamp); + if (ts && (!lastTimestamp || ts > lastTimestamp)) lastTimestamp = ts; + + if (entry.type === 'custom_message') { + if (entry.display !== false) messageCount++; + continue; + } + + if (entry.type !== 'message' || !entry.message) continue; + const msg = entry.message; + if (isVisibleRole(msg.role)) messageCount++; + if (!firstPrompt && msg.role === 'user') { + const text = extractTextContent(msg.content); + if (text) firstPrompt = cleanPrompt(text); + } + } + + let stat = null; + try { stat = fs.statSync(filePath); } catch {} + + const sessionId = header.id || path.basename(filePath, '.jsonl').split('_').pop(); + return { + source: 'pi', + composerId: sessionId || path.basename(filePath, '.jsonl'), + name: firstPrompt, + createdAt: toTimestamp(header.timestamp) || (stat ? stat.birthtimeMs : null), + lastUpdatedAt: lastTimestamp || (stat ? stat.mtimeMs : null), + mode: 'pi', + folder: header.cwd || decodeFolderFromPath(filePath), + encrypted: false, + bubbleCount: messageCount, + _filePath: filePath, + _version: header.version || null, + _parentSession: header.parentSession || null, + }; +} + +function parseSessionMessages(filePath) { + const messages = []; + + for (const line of readLines(filePath)) { + const entry = safeParseJson(line); + if (!entry) continue; + + if (entry.type === 'model_change') { + const model = [entry.provider, entry.modelId].filter(Boolean).join('/') || entry.modelId || null; + if (model) messages.push({ role: 'system', content: `[model changed to ${model}]`, _model: model }); + continue; + } + + if (entry.type === 'compaction' && entry.summary) { + messages.push({ role: 'system', content: `[compaction] ${entry.summary}` }); + continue; + } + + if (entry.type === 'branch_summary' && entry.summary) { + messages.push({ role: 'system', content: `[branch summary] ${entry.summary}` }); + continue; + } + + if (entry.type === 'custom_message') { + if (entry.display !== false) { + const content = extractTextContent(entry.content); + if (content) messages.push({ role: 'system', content: `[${entry.customType || 'custom'}] ${content}` }); + } + continue; + } + + if (entry.type !== 'message' || !entry.message) continue; + const msg = entry.message; + + if (msg.role === 'user') { + const content = extractTextContent(msg.content); + if (content) messages.push({ role: 'user', content }); + continue; + } + + if (msg.role === 'assistant') { + const { text, toolCalls } = extractAssistantContent(msg.content); + const usage = normalizeUsage(msg.usage); + if (text || toolCalls.length > 0 || usage) { + messages.push({ + role: 'assistant', + content: text || toolCalls.map((tc) => `[tool-call: ${tc.name}]`).join('\n'), + _model: msg.model || null, + _provider: msg.provider || null, + _inputTokens: usage?.input, + _outputTokens: usage?.output, + _cacheRead: usage?.cacheRead, + _cacheWrite: usage?.cacheWrite, + _toolCalls: toolCalls, + }); + } + continue; + } + + if (msg.role === 'toolResult') { + const content = extractTextContent(msg.content).substring(0, MAX_TOOL_RESULT_PREVIEW); + messages.push({ + role: 'tool', + content: `[tool-result: ${msg.toolName || 'tool'}${msg.isError ? ' error' : ''}]${content ? ` ${content}` : ''}`, + }); + continue; + } + + if (msg.role === 'bashExecution') { + const content = [`$ ${msg.command || ''}`, msg.output || ''].filter(Boolean).join('\n').substring(0, MAX_TOOL_RESULT_PREVIEW); + messages.push({ role: 'tool', content: `[bash-execution] ${content}` }); + continue; + } + + if (msg.role === 'custom' && msg.display !== false) { + const content = extractTextContent(msg.content); + if (content) messages.push({ role: 'system', content: `[${msg.customType || 'custom'}] ${content}` }); + continue; + } + + if (msg.role === 'branchSummary' && msg.summary) { + messages.push({ role: 'system', content: `[branch summary] ${msg.summary}` }); + } else if (msg.role === 'compactionSummary' && msg.summary) { + messages.push({ role: 'system', content: `[compaction] ${msg.summary}` }); + } + } + + return messages; +} + +function extractAssistantContent(content) { + const parts = []; + const toolCalls = []; + const blocks = Array.isArray(content) ? content : [{ type: 'text', text: String(content || '') }]; + + for (const block of blocks) { + if (!block) continue; + if (block.type === 'text' && block.text) { + parts.push(block.text); + } else if (block.type === 'thinking' && block.thinking) { + parts.push(`[thinking] ${block.thinking}`); + } else if (block.type === 'toolCall') { + const toolName = block.name || 'tool'; + const args = block.arguments || {}; + toolCalls.push({ name: toolName, args }); + const argKeys = args && typeof args === 'object' ? Object.keys(args).join(', ') : ''; + parts.push(`[tool-call: ${toolName}(${argKeys})]`); + } + } + + return { text: parts.join('\n'), toolCalls }; +} + +function extractTextContent(content) { + if (typeof content === 'string') return content; + if (!Array.isArray(content)) return ''; + const parts = []; + for (const block of content) { + if (!block) continue; + if (block.type === 'text' && block.text) parts.push(block.text); + else if (block.type === 'image') parts.push(`[image: ${block.mimeType || 'image'}]`); + else if (block.type === 'thinking' && block.thinking) parts.push(`[thinking] ${block.thinking}`); + } + return parts.join('\n'); +} + +function normalizeUsage(usage) { + if (!usage || typeof usage !== 'object') return null; + return { + input: numberOrNull(usage.input), + output: numberOrNull(usage.output), + cacheRead: numberOrNull(usage.cacheRead), + cacheWrite: numberOrNull(usage.cacheWrite), + }; +} + +function getArtifacts(folder) { + return scanArtifacts(folder, { + editor: 'pi', + label: 'Pi Agent', + files: ['AGENTS.md', 'CLAUDE.md', '.pi/settings.json', '.pi/SYSTEM.md', '.pi/APPEND_SYSTEM.md'], + dirs: ['.pi/prompts', '.pi/skills', '.pi/extensions', '.pi/themes'], + }); +} + +function getMCPServers() { + const servers = []; + servers.push(...parseMcpConfigFile(path.join(PI_HOME, 'settings.json'), { editor: 'pi', label: 'Pi Agent', scope: 'global' })); + return servers; +} + +function readLines(filePath) { + try { return fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean); } catch { return []; } +} + +function safeParseJson(line) { + try { return JSON.parse(line); } catch { return null; } +} + +function toTimestamp(value) { + if (!value) return null; + if (typeof value === 'number') return value; + const ts = new Date(value).getTime(); + return Number.isFinite(ts) ? ts : null; +} + +function numberOrNull(value) { + return typeof value === 'number' && Number.isFinite(value) ? value : null; +} + +function isVisibleRole(role) { + return role === 'user' || role === 'assistant' || role === 'toolResult' || role === 'bashExecution' || role === 'custom'; +} + +function cleanPrompt(text) { + return String(text || '').replace(/\s+/g, ' ').trim().substring(0, 120); +} + +function decodeFolderFromPath(filePath) { + const folderName = path.basename(path.dirname(filePath)); + if (!folderName.startsWith('--') || !folderName.endsWith('--')) return null; + const inner = folderName.slice(2, -2); + if (!inner) return null; + if (process.platform === 'win32') { + const parts = inner.split('--').filter(Boolean); + if (parts.length > 1 && /^[A-Za-z]$/.test(parts[0])) return `${parts[0]}:\\${parts.slice(1).join('\\')}`; + } + return inner.replace(/--/g, path.sep); +} + +module.exports = { name, labels, getChats, getMessages, getArtifacts, getMCPServers }; diff --git a/package.json b/package.json index 064b9f3..87b2fa6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "agentlytics", "version": "0.2.13", - "description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Devin, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code", + "description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Devin, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code, Pi Agent", "main": "index.js", "bin": { "agentlytics": "./index.js" @@ -44,7 +44,8 @@ "codex", "analytics", "ai", - "agent" + "agent", + "pi" ], "author": "fkadev", "license": "ISC", diff --git a/ui/src/lib/constants.js b/ui/src/lib/constants.js index 389d96b..4cc7ea5 100644 --- a/ui/src/lib/constants.js +++ b/ui/src/lib/constants.js @@ -18,6 +18,7 @@ export const EDITOR_COLORS = { 'goose': '#333333', 'kiro': '#9046FF', 'codebuff': '#44ff00', + 'pi': '#7c3aed', }; export const EDITOR_LABELS = { @@ -40,6 +41,7 @@ export const EDITOR_LABELS = { 'goose': 'Goose', 'kiro': 'Kiro', 'codebuff': 'Codebuff', + 'pi': 'Pi Agent', }; export function editorColor(src) {