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
152 changes: 152 additions & 0 deletions src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ const OMP_AGENT_DIR = process.env.OMP_CODING_AGENT_DIR || path.join(ALL_HOMES[0]
const PI_SESSIONS_DIR = path.join(PI_AGENT_DIR, 'sessions');
const OMP_SESSIONS_DIR = path.join(OMP_AGENT_DIR, 'sessions');
const KIRO_DB = path.join(ALL_HOMES[0], 'Library', 'Application Support', 'kiro-cli', 'data.sqlite3');
const KIRO_SESSIONS_DIR = path.join(ALL_HOMES[0], '.kiro', 'sessions', 'cli');
const COPILOT_SESSION_DIR = path.join(ALL_HOMES[0], '.copilot', 'session-state');
const COPILOT_JB_DIR = path.join(ALL_HOMES[0], '.copilot', 'jb');
const KILO_DB = path.join(ALL_HOMES[0], '.local', 'share', 'kilo', 'kilo.db');
Expand Down Expand Up @@ -1837,6 +1838,102 @@ function loadKiroDetail(conversationId) {
}
}

// ── Kiro CLI (new format: ~/.kiro/sessions/cli/, since ~May 2026) ─────────────

function scanKiroCliSessions() {
const sessions = [];
if (!fs.existsSync(KIRO_SESSIONS_DIR)) return sessions;

let files;
try { files = fs.readdirSync(KIRO_SESSIONS_DIR, { withFileTypes: true }); } catch { return sessions; }

for (const entry of files) {
const f = entry.name;
// Reject symlinks so a crafted <uuid>.json link can't leak an out-of-tree
// file through the dashboard (matches the Claude reader's symlink guard).
if (entry.isSymbolicLink() || !entry.isFile()) continue;
if (!f.endsWith('.json')) continue;
const sessionId = f.slice(0, -5);
// skip if not a strict UUID name
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) continue;

try {
const meta = JSON.parse(fs.readFileSync(path.join(KIRO_SESSIONS_DIR, f), 'utf8'));
const createdMs = meta.created_at ? new Date(meta.created_at).getTime() : 0;
const updatedMs = meta.updated_at ? new Date(meta.updated_at).getTime() : 0;
const jsonlPath = path.join(KIRO_SESSIONS_DIR, sessionId + '.jsonl');
// Only trust a regular (non-symlink) events file; a symlinked .jsonl
// would otherwise be followed by loadKiroCliDetail's readFileSync.
let jsonlStat = null;
try { jsonlStat = fs.lstatSync(jsonlPath); } catch {}
const hasDetail = !!(jsonlStat && jsonlStat.isFile());
const fileSize = hasDetail ? jsonlStat.size : 0;

sessions.push({
id: sessionId,
tool: 'kiro',
format: 'kiro-cli',
project: meta.cwd || '',
project_short: (meta.cwd || '').replace(os.homedir(), '~'),
first_ts: createdMs || Date.now(),
last_ts: updatedMs || Date.now(),
messages: fileSize > 0 ? Math.max(2, Math.floor(fileSize / 3000)) : 0,
first_message: meta.title || '',
has_detail: hasDetail,
file_size: fileSize,
detail_messages: 0,
});
} catch {}
}

return sessions;
}

function loadKiroCliDetail(sessionId) {
// sessionId is untrusted here (resolved from a request param) — require a
// strict UUID before building the path to close a path-traversal vector.
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
return { messages: [] };
}
const jsonlPath = path.join(KIRO_SESSIONS_DIR, sessionId + '.jsonl');
// Reject symlinks (and non-regular files) before reading so a crafted link
// named <uuid>.jsonl can't leak an out-of-tree file through the dashboard.
let jsonlStat;
try { jsonlStat = fs.lstatSync(jsonlPath); } catch { return { messages: [] }; }
if (!jsonlStat.isFile()) return { messages: [] };

const messages = [];
try {
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\n');
for (const line of lines) {
if (!line.trim()) continue;
let entry;
try { entry = JSON.parse(line); } catch { continue; }

const { kind, data } = entry;
if (!data) continue;

if (kind === 'Prompt') {
// data.content is array of {kind, data} blocks
const text = (data.content || [])
.filter(b => b.kind === 'text')
.map(b => b.data || '')
.join('').trim();
if (text) messages.push({ role: 'user', content: text.slice(0, 2000), uuid: data.message_id || '' });

} else if (kind === 'AssistantMessage') {
const text = (data.content || [])
.filter(b => b.kind === 'text')
.map(b => b.data || '')
.join('').trim();
if (text) messages.push({ role: 'assistant', content: text.slice(0, 2000), uuid: data.message_id || '' });
}
}
} catch {}

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

// ── Copilot Chat (VS Code extension) ─────────────────────────

// Build workspace-hash -> project path mapping for VS Code workspaceStorage
Expand Down Expand Up @@ -3468,6 +3565,14 @@ function loadSessions() {
}
} catch {}

// Load Kiro CLI sessions (new format: ~/.kiro/sessions/cli/, since ~May 2026)
try {
const kiroCliSessions = scanKiroCliSessions();
for (const ks of kiroCliSessions) {
sessions[ks.id] = ks;
}
} catch {}

// Load Copilot CLI sessions
try {
const copilotSessions = scanCopilotCliSessions();
Expand Down Expand Up @@ -3737,6 +3842,9 @@ function loadSessionDetail(sessionId, project) {
if (found.format === 'kiro') {
return loadKiroDetail(sessionId);
}
if (found.format === 'kiro-cli') {
return loadKiroCliDetail(sessionId);
}

// Copilot CLI uses JSONL events
if (found.format === 'copilot') {
Expand Down Expand Up @@ -3972,6 +4080,7 @@ function exportSessionMarkdown(sessionId, project) {
found.format === 'cursor' ? loadCursorDetail(sessionId) :
found.format === 'opencode' ? loadOpenCodeDetail(sessionId) :
found.format === 'kiro' ? loadKiroDetail(sessionId) :
found.format === 'kiro-cli' ? loadKiroCliDetail(sessionId) :
found.format === 'kilo' ? loadKiloCliDetail(sessionId) :
found.format === 'qwen' ? loadQwenDetail(sessionId, found.file) :
found.format === 'pi' ? loadPiDetail(sessionId, found.file) :
Expand Down Expand Up @@ -4117,6 +4226,22 @@ function _buildSessionFileIndex() {
} catch {}
}

// Index Kiro CLI file-based sessions (~/.kiro/sessions/cli/, since ~May 2026)
if (fs.existsSync(KIRO_SESSIONS_DIR)) {
try {
for (const entry of fs.readdirSync(KIRO_SESSIONS_DIR, { withFileTypes: true })) {
if (entry.isSymbolicLink() || !entry.isFile()) continue;
const f = entry.name;
if (!f.endsWith('.jsonl')) continue;
const sid = f.slice(0, -6);
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sid)) continue;
if (!_sessionFileIndex[sid]) {
_sessionFileIndex[sid] = { file: path.join(KIRO_SESSIONS_DIR, f), format: 'kiro-cli', sessionId: sid };
}
}
} catch {}
}

_sessionFileIndexTs = now;
}

Expand Down Expand Up @@ -4621,6 +4746,12 @@ function getSessionPreview(sessionId, project, limit) {
return { role: m.role, content: m.content.slice(0, 300) };
});
}
if (found.format === 'kiro-cli') {
var detail = loadKiroCliDetail(sessionId);
return detail.messages.slice(0, limit).map(function(m) {
return { role: m.role, content: m.content.slice(0, 300) };
});
}

// OpenCode: use loadOpenCodeDetail and slice
if (found.format === 'opencode') {
Expand Down Expand Up @@ -4753,6 +4884,13 @@ function buildSearchIndex(sessions) {
texts.push({ role: msg.role, content: msg.content.slice(0, 500) });
}
}
} else if (found.format === 'kiro-cli') {
const detail = loadKiroCliDetail(s.id);
for (const msg of detail.messages) {
if (msg.content && !isSystemMessage(msg.content)) {
texts.push({ role: msg.role, content: msg.content.slice(0, 500) });
}
}
} else if (found.format === 'cursor') {
const detail = loadCursorDetail(s.id);
for (const msg of detail.messages) {
Expand Down Expand Up @@ -4909,6 +5047,18 @@ function getSessionReplay(sessionId, project) {
});
}
}
} else if (found.format === 'kiro-cli') {
const detail = loadKiroCliDetail(sessionId);
for (const msg of detail.messages) {
if (msg.content && !isSystemMessage(msg.content)) {
messages.push({
role: msg.role,
content: msg.content.slice(0, 3000),
timestamp: 0,
ms: 0,
});
}
}
} else if (found.format === 'cursor') {
const detail = loadCursorDetail(sessionId);
for (const msg of detail.messages) {
Expand Down Expand Up @@ -6393,5 +6543,7 @@ module.exports = {
findPiSessionByResumeTarget,
_sessionsNeedRescan,
_updateScanMarkers,
scanKiroCliSessions,
loadKiroCliDetail,
},
};
Loading