Skip to content
Merged
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
214 changes: 210 additions & 4 deletions src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ function readLines(filePath) {
return fs.readFileSync(filePath, 'utf8').split('\n').map(l => l.replace(/\r$/, '')).filter(Boolean);
}

// OpenCode built-in tools that should NOT be treated as MCP servers
const OPENCODE_BUILTIN_TOOLS = new Set([
'read', 'write', 'edit', 'bash', 'glob', 'grep', 'task', 'todowrite',
'delegate_task', 'apply_patch', 'webfetch', 'websearch', 'slashcommand',
'question', 'background_task', 'background_output', 'background_cancel',
'lsp_diagnostics', 'ast_grep_search', 'ast_grep_replace', 'session_read',
'skill', 'skill_mcp', 'call_omo_agent',
]);

// OpenCode tool names like "chrome-devtools_take_screenshot" → server "chrome-devtools"
// Returns null if it's a built-in tool, otherwise the server name (first segment).
function parseOpenCodeMcpServer(toolName) {
if (!toolName || OPENCODE_BUILTIN_TOOLS.has(toolName)) return null;
// Match server_tool or server-with-dashes_tool
const idx = toolName.indexOf('_');
if (idx <= 0) return null;
return toolName.slice(0, idx);
}

function parseClaudeSessionFile(sessionFile) {
if (!fs.existsSync(sessionFile)) return null;

Expand All @@ -91,6 +110,8 @@ function parseClaudeSessionFile(sessionFile) {
let lastTs = stat.mtimeMs;
let entrypointFound = false;
let worktreeOriginalCwd = '';
const mcpSet = new Set();
const skillSet = new Set();

for (const line of lines) {
try {
Expand Down Expand Up @@ -120,6 +141,23 @@ function parseClaudeSessionFile(sessionFile) {
const content = extractContent(entry.message.content).trim();
if (content) firstMsg = content.slice(0, 200);
}
// MCP/Skill extraction from assistant tool_use blocks
if (entry.type === 'assistant') {
const aContent = (entry.message || {}).content;
if (Array.isArray(aContent)) {
for (const block of aContent) {
if (!block || block.type !== 'tool_use') continue;
const name = block.name || '';
if (name.startsWith('mcp__')) {
const parts = name.split('__');
if (parts.length >= 3) mcpSet.add(parts[1]);
} else if (name === 'Skill') {
const sk = (block.input || {}).skill;
if (sk) skillSet.add(sk.includes(':') ? sk.split(':')[0] : sk);
}
}
}
}
} catch {}
}

Expand All @@ -133,6 +171,8 @@ function parseClaudeSessionFile(sessionFile) {
lastTs,
fileSize: stat.size,
worktreeOriginalCwd,
mcpServers: Array.from(mcpSet),
skills: Array.from(skillSet),
};
}

Expand All @@ -144,6 +184,8 @@ function mergeClaudeSessionDetail(session, summary, sessionFile) {
session.file_size = summary.fileSize;
session.detail_messages = summary.msgCount;
session._session_file = sessionFile;
session.mcp_servers = summary.mcpServers || [];
session.skills = summary.skills || [];

if (!session.project && summary.projectPath) {
session.project = summary.projectPath;
Expand Down Expand Up @@ -229,6 +271,43 @@ function scanOpenCodeSessions() {

if (!rows) return sessions;

// Get MCP/Skills usage per session in one query
const sessionMcp = {};
const sessionSkills = {};
try {
const toolRows = execSync(
`sqlite3 -separator $'\\t' "${OPENCODE_DB}" "SELECT session_id, json_extract(data, '\\$.tool'), json_extract(data, '\\$.state.input.name') FROM part WHERE json_extract(data, '\\$.type') = 'tool'"`,
{ encoding: 'utf8', timeout: 10000, maxBuffer: 50 * 1024 * 1024 }
).trim();
if (toolRows) {
for (const tr of toolRows.split('\n')) {
const cols = tr.split('\t');
if (cols.length < 2) continue;
const sid = cols[0];
const toolName = cols[1];
const skillName = cols[2];
if (!sid || !toolName) continue;
// Skill tool: collect skill name
if (toolName === 'skill' || toolName === 'skill_mcp') {
if (skillName) {
if (!sessionSkills[sid]) sessionSkills[sid] = new Set();
// Plugin prefix: "superpowers:writing-plans" -> "superpowers"
// For OpenCode keep full name (e.g. "openspec-propose", "chrome-devtools")
const sk = skillName.includes(':') ? skillName.split(':')[0] : skillName;
sessionSkills[sid].add(sk);
}
continue;
}
// MCP tool: extract server name
const server = parseOpenCodeMcpServer(toolName);
if (server) {
if (!sessionMcp[sid]) sessionMcp[sid] = new Set();
sessionMcp[sid].add(server);
}
}
}
} catch {}

for (const row of rows.split('\n')) {
const parts = row.split('\t');
if (parts.length < 6) continue;
Expand All @@ -246,6 +325,8 @@ function scanOpenCodeSessions() {
has_detail: true,
file_size: 0,
detail_messages: parseInt(msgCount) || 0,
mcp_servers: sessionMcp[id] ? Array.from(sessionMcp[id]) : [],
skills: sessionSkills[id] ? Array.from(sessionSkills[id]) : [],
});
}
} catch {}
Expand Down Expand Up @@ -291,14 +372,39 @@ function loadOpenCodeDetail(sessionId) {
const role = msgData.role;
if (role !== 'user' && role !== 'assistant') continue;

// Extract text from parts
// Extract text + tools from parts
let content = '';
const tools = [];
const toolSeen = new Set();
if (partsRaw) {
for (const partStr of partsRaw.split('|||')) {
try {
const part = JSON.parse(partStr);
if (part.type === 'text' && part.text) {
content += part.text + '\n';
} else if (part.type === 'tool' && part.tool) {
const toolName = part.tool;
if (toolName === 'skill' || toolName === 'skill_mcp') {
const skillRaw = part.state && part.state.input && part.state.input.name;
if (skillRaw) {
const sk = skillRaw.includes(':') ? skillRaw.split(':')[0] : skillRaw;
const key = 'skill:' + sk;
if (!toolSeen.has(key)) {
toolSeen.add(key);
tools.push({ type: 'skill', skill: sk });
}
}
} else {
const server = parseOpenCodeMcpServer(toolName);
if (server) {
const tool = toolName.slice(server.length + 1);
const key = 'mcp:' + server + ':' + tool;
if (!toolSeen.has(key)) {
toolSeen.add(key);
tools.push({ type: 'mcp', server: server, tool: tool });
}
}
}
}
} catch {}
}
Expand All @@ -309,13 +415,15 @@ function loadOpenCodeDetail(sessionId) {

const tokens = msgData.tokens || {};

messages.push({
const msg = {
role: role,
content: content.slice(0, 2000),
uuid: '',
model: msgData.modelID || msgData.model?.modelID || '',
tokens: tokens,
});
};
if (tools.length > 0) msg.tools = tools;
messages.push(msg);
}

return { messages: messages.slice(0, 200) };
Expand Down Expand Up @@ -646,6 +754,7 @@ function parseCodexSessionFile(sessionFile) {
let firstMsg = '';
let firstTs = stat.mtimeMs;
let lastTs = stat.mtimeMs;
const mcpSet = new Set();

for (const line of lines) {
try {
Expand All @@ -662,6 +771,17 @@ function parseCodexSessionFile(sessionFile) {
}

if (entry.type !== 'response_item' || !entry.payload) continue;

// MCP function_call extraction
if (entry.payload.type === 'function_call') {
const name = entry.payload.name || '';
if (name.startsWith('mcp__')) {
const parts = name.split('__');
if (parts.length >= 3) mcpSet.add(parts[1]);
}
continue;
}

const role = entry.payload.role;
if (role !== 'user' && role !== 'assistant') continue;

Expand All @@ -680,6 +800,7 @@ function parseCodexSessionFile(sessionFile) {
firstTs,
lastTs,
fileSize: stat.size,
mcpServers: Array.from(mcpSet),
};
}

Expand Down Expand Up @@ -756,6 +877,9 @@ function scanCodexSessions() {
}
existing.first_ts = Math.min(existing.first_ts, summary.firstTs);
existing.last_ts = Math.max(existing.last_ts, summary.lastTs);
if (summary.mcpServers && summary.mcpServers.length > 0) {
existing.mcp_servers = summary.mcpServers;
}
} else {
sessions.push({
id: sid,
Expand All @@ -769,6 +893,8 @@ function scanCodexSessions() {
has_detail: true,
file_size: summary.fileSize,
detail_messages: summary.msgCount,
mcp_servers: summary.mcpServers || [],
skills: [],
});
}
}
Expand Down Expand Up @@ -1028,6 +1154,8 @@ function loadSessions() {
s.has_detail = false;
s.file_size = 0;
s.detail_messages = 0;
s.mcp_servers = [];
s.skills = [];
}
}

Expand Down Expand Up @@ -1060,6 +1188,8 @@ function loadSessions() {
has_detail: true,
file_size: summary.fileSize,
detail_messages: summary.msgCount,
mcp_servers: summary.mcpServers,
skills: summary.skills,
_claude_dir: CLAUDE_DIR,
_session_file: filePath,
worktree_original_cwd: summary.worktreeOriginalCwd || '',
Expand All @@ -1069,6 +1199,12 @@ function loadSessions() {
} catch {}
}

// Ensure all sessions have mcp_servers/skills (defaults for non-Claude)
for (const s of Object.values(sessions)) {
if (!s.mcp_servers) s.mcp_servers = [];
if (!s.skills) s.skills = [];
}

const result = Object.values(sessions).sort((a, b) => b.last_ts - a.last_ts);

// Collect unique project paths and resolve git roots in one pass
Expand Down Expand Up @@ -1119,23 +1255,58 @@ function loadSessionDetail(sessionId, project) {
if (entry.type === 'user' || entry.type === 'assistant') {
const content = extractContent((entry.message || {}).content);
if (content) {
messages.push({ role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' });
const msg = { role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' };
if (entry.type === 'assistant') {
const rawContent = (entry.message || {}).content;
if (Array.isArray(rawContent)) {
const tools = extractTools(rawContent);
if (tools.length > 0) msg.tools = tools;
}
}
messages.push(msg);
}
}
} else {
// Codex format: response_item with payload
if (entry.type === 'response_item' && entry.payload) {
const pType = entry.payload.type;
const role = entry.payload.role;
if (role === 'user' || role === 'assistant') {
const content = extractContent(entry.payload.content);
if (content && !isSystemMessage(content)) {
messages.push({ role: role, content: content.slice(0, 2000), uuid: '' });
}
}
// Codex function_call → attach as tool to last assistant message
if (pType === 'function_call') {
const name = entry.payload.name || '';
if (name.startsWith('mcp__')) {
const parts = name.split('__');
if (parts.length >= 3) {
const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null;
if (lastMsg && lastMsg.role === 'assistant') {
if (!lastMsg.tools) lastMsg.tools = [];
if (!lastMsg._toolSeen) lastMsg._toolSeen = new Set();
const tool = parts.slice(2).join('__');
const key = 'mcp:' + parts[1] + ':' + tool;
if (!lastMsg._toolSeen.has(key)) {
lastMsg._toolSeen.add(key);
lastMsg.tools.push({ type: 'mcp', server: parts[1], tool: tool });
}
}
}
}
}
}
}
} catch {}
}

// Clean up internal markers from Codex
for (const m of messages) {
if (m._toolSeen) delete m._toolSeen;
}

return { messages: messages.slice(0, 200) };
}

Expand Down Expand Up @@ -1362,6 +1533,41 @@ function extractContent(raw) {
return String(raw);
}

// Extract MCP/Skill tool_use blocks from a Claude assistant message content array.
// Returns deduplicated array of { type, server, tool } or { type, skill }.
function extractTools(contentBlocks) {
if (!Array.isArray(contentBlocks)) return [];
const tools = [];
const seen = new Set();
for (const block of contentBlocks) {
if (!block || block.type !== 'tool_use') continue;
const name = block.name || '';
if (name.startsWith('mcp__')) {
const parts = name.split('__');
if (parts.length >= 3) {
const tool = parts.slice(2).join('__');
const key = 'mcp:' + parts[1] + ':' + tool;
if (!seen.has(key)) {
seen.add(key);
tools.push({ type: 'mcp', server: parts[1], tool: tool });
}
}
} else if (name === 'Skill') {
const skillRaw = (block.input || {}).skill;
if (skillRaw) {
// Use plugin name only (e.g. "superpowers:writing-plans" -> "superpowers")
const skill = skillRaw.includes(':') ? skillRaw.split(':')[0] : skillRaw;
const key = 'skill:' + skill;
if (!seen.has(key)) {
seen.add(key);
tools.push({ type: 'skill', skill: skill });
}
}
}
}
return tools;
}

function getSessionPreview(sessionId, project, limit) {
limit = limit || 10;
const found = findSessionFile(sessionId, project);
Expand Down
Loading
Loading