| @@ -81,7 +81,8 @@ Failure to eval to handoff: every correction you made by hand becomes a guardrai | ||
| | `npx treetrace` | Trace this project and write all artifacts | | ||
| | `npx treetrace --report` | Write all artifacts and print the human report | | ||
| | `npx treetrace --handoff` | Print an agent ready continuation brief | | ||
| - | | `npx treetrace --file session.jsonl` | Import specific transcripts | | |
| + | | `npx treetrace --file session.jsonl` | Import specific session or transcript files (format auto-detected) | | |
| + | | `npx treetrace --from chatgpt --file conversations.json` | Import another tool's export with an explicit format | | |
| | `npx treetrace --stdin < chat.txt` | Parse a pasted `User:` / `Assistant:` transcript | | ||
| | `npx treetrace --failures` | Write and print `.treetrace/failures.json` | | ||
| | `npx treetrace --lessons` | Write and print `.treetrace/lessons.md` | | ||
| @@ -151,11 +152,40 @@ A privacy-positioned tool gets exactly one chance with your secrets, so every ex | ||
| ## Sources | ||
| - | | Source | Status | | |
| - | |--------|--------| | |
| - | | Claude Code (`~/.claude/projects` JSONL) | Built-in, zero-config | | |
| - | | Pasted / plain-text transcripts (`User:` / `Assistant:` markers) | Built-in | | |
| - | | Codex CLI, Cursor, SpecStory, ChatGPT export | Importers welcome | | |
| + | TreeTrace reads Claude Code automatically and imports other tools through `--file`. | |
| + | When you pass a `.json` or `.jsonl` file, the format is auto-detected; you can | |
| + | also force it with `--from <tool>`. Everything stays local and passes the same | |
| + | redaction gate. The generic `User:` / `Assistant:` transcript parser remains the | |
| + | fallback for anything unrecognized. | |
| + | ||
| + | Verified means the adapter was validated against real session or real published | |
| + | export data. Experimental means it was built to the tool's documented export | |
| + | schema and validated against a fixture in that exact shape, but not yet against a | |
| + | captured real session on a contributor's machine. See | |
| + | [test/fixtures/adapters/PROVENANCE.md](test/fixtures/adapters/PROVENANCE.md) for | |
| + | the source of every fixture. | |
| + | ||
| + | | Source | `--from` | Status | | |
| + | |--------|----------|--------| | |
| + | | Claude Code (`~/.claude/projects` JSONL) | `claude` | Built-in, zero-config, verified | | |
| + | | Codex CLI (`~/.codex/sessions/.../rollout-*.jsonl`) | `codex` | Verified against a real session | | |
| + | | ChatGPT / OpenAI account export (`conversations.json`) | `chatgpt` | Verified against a real published export sample | | |
| + | | Google Gemini CLI session (ChatRecordingService JSON) | `gemini` | Verified against the real gemini-cli session file | | |
| + | | GitHub Copilot Chat session (`chatSessions/*.json`) | `copilot` | Verified against a real published session sample | | |
| + | | Cursor exported chat JSON | `cursor` | Verified against the export schema (see note) | | |
| + | | xAI Grok exported conversation JSON | `grok` | Experimental, built to the exporter schema | | |
| + | | Pasted / plain-text transcripts (`User:` / `Assistant:`) | `transcript` | Built-in fallback | | |
| + | ||
| + | Cursor stores chat in a `state.vscdb` SQLite database. TreeTrace ships with zero | |
| + | runtime dependencies and does not open SQLite, so the Cursor adapter ingests an | |
| + | exported chat JSON instead. Export your Cursor chat to JSON first (for example | |
| + | with a community Cursor chat exporter), then run | |
| + | `treetrace --from cursor --file your-chat.json`. | |
| + | ||
| + | The Grok adapter targets the exported conversation JSON used by Grok CLI tools | |
| + | (the xAI OpenAI-compatible `role` / `content` message shape). The widely used | |
| + | grok-cli keeps history in SQLite rather than JSON, so this adapter is marked | |
| + | experimental until validated against a captured real Grok session. | |
| ## Schema | ||
| @@ -30,7 +30,7 @@ | ||
| "node": ">=18" | ||
| }, | ||
| "scripts": { | ||
| - | "test": "node --test test/treetrace.test.js", | |
| - | "prepublishOnly": "node --test test/treetrace.test.js" | |
| + | "test": "node --test test/treetrace.test.js test/adapters.test.js", | |
| + | "prepublishOnly": "node --test test/treetrace.test.js test/adapters.test.js" | |
| } | ||
| } |
| @@ -0,0 +1,83 @@ | ||
| + | import { newSession, finalizeSession, pushTurn, flattenParts, looksSynthetic } from './shared.js'; | |
| + | ||
| + | function conversationList(parsed) { | |
| + | if (Array.isArray(parsed)) return parsed; | |
| + | if (parsed && Array.isArray(parsed.conversations)) return parsed.conversations; | |
| + | if (parsed && parsed.mapping && typeof parsed.mapping === 'object') return [parsed]; | |
| + | return []; | |
| + | } | |
| + | ||
| + | export function detectChatGPT(parsed) { | |
| + | const list = conversationList(parsed); | |
| + | if (!list.length) return false; | |
| + | const first = list[0]; | |
| + | return Boolean(first && first.mapping && typeof first.mapping === 'object'); | |
| + | } | |
| + | ||
| + | export function parseChatGPT(parsed, path) { | |
| + | const conversations = conversationList(parsed); | |
| + | const sessions = []; | |
| + | for (let i = 0; i < conversations.length; i++) { | |
| + | const convo = conversations[i]; | |
| + | if (!convo || !convo.mapping) continue; | |
| + | const session = sessionFromConversation(convo, path, i); | |
| + | if (session.prompts.length) sessions.push(session); | |
| + | } | |
| + | return sessions; | |
| + | } | |
| + | ||
| + | function sessionFromConversation(convo, path, index) { | |
| + | const id = convo.conversation_id || convo.id || `chatgpt-${index + 1}`; | |
| + | const session = newSession(path, id); | |
| + | if (convo.title) session.title = convo.title; | |
| + | ||
| + | const ordered = orderNodes(convo.mapping); | |
| + | let turn = 0; | |
| + | for (const node of ordered) { | |
| + | const msg = node.message; | |
| + | if (!msg || !msg.author) continue; | |
| + | const role = msg.author.role; | |
| + | const text = flattenParts(msg.content && msg.content.parts); | |
| + | const ts = msg.create_time ? new Date(msg.create_time * 1000).toISOString() : null; | |
| + | ||
| + | if (role === 'user') { | |
| + | if (looksSynthetic(text)) continue; | |
| + | pushTurn(session, ++turn, text, ts); | |
| + | } else if (role === 'assistant') { | |
| + | session.stats.assistantLines++; | |
| + | if (msg.metadata && msg.metadata.model_slug) session.stats.models.add(msg.metadata.model_slug); | |
| + | } else if (role === 'tool') { | |
| + | session.stats.toolUses++; | |
| + | } | |
| + | } | |
| + | return finalizeSession(session); | |
| + | } | |
| + | ||
| + | function orderNodes(mapping) { | |
| + | const nodes = Object.values(mapping).filter((n) => n && n.message); | |
| + | const withTime = nodes.filter((n) => typeof n.message.create_time === 'number'); | |
| + | if (withTime.length === nodes.length && nodes.length) { | |
| + | return nodes.slice().sort((a, b) => a.message.create_time - b.message.create_time); | |
| + | } | |
| + | return walkFromRoot(mapping); | |
| + | } | |
| + | ||
| + | function walkFromRoot(mapping) { | |
| + | let rootId = null; | |
| + | for (const [id, node] of Object.entries(mapping)) { | |
| + | if (node && (node.parent === null || node.parent === undefined)) { | |
| + | rootId = id; | |
| + | break; | |
| + | } | |
| + | } | |
| + | const out = []; | |
| + | const seen = new Set(); | |
| + | let cur = rootId; | |
| + | while (cur && mapping[cur] && !seen.has(cur)) { | |
| + | seen.add(cur); | |
| + | out.push(mapping[cur]); | |
| + | const children = mapping[cur].children || []; | |
| + | cur = children.length ? children[children.length - 1] : null; | |
| + | } | |
| + | return out; | |
| + | } |
| @@ -0,0 +1,87 @@ | ||
| + | import { | |
| + | newSession, | |
| + | finalizeSession, | |
| + | pushTurn, | |
| + | flattenParts, | |
| + | looksSynthetic, | |
| + | readJsonl, | |
| + | } from './shared.js'; | |
| + | ||
| + | export function detectCodex(text) { | |
| + | for (const line of text.split(/\r?\n/)) { | |
| + | const trimmed = line.trim(); | |
| + | if (!trimmed || trimmed.charCodeAt(0) !== 123) continue; | |
| + | try { | |
| + | const rec = JSON.parse(trimmed); | |
| + | if (rec.type === 'session_meta' && rec.payload && rec.payload.originator) return true; | |
| + | if (rec.type === 'response_item' || rec.type === 'turn_context') return true; | |
| + | return false; | |
| + | } catch { | |
| + | return false; | |
| + | } | |
| + | } | |
| + | return false; | |
| + | } | |
| + | ||
| + | export function parseCodex(text, path, sessionId) { | |
| + | const session = newSession(path, sessionId); | |
| + | const records = readJsonl(text); | |
| + | let turn = 0; | |
| + | ||
| + | for (const rec of records) { | |
| + | const ts = rec.timestamp || null; | |
| + | const payload = rec.payload || {}; | |
| + | ||
| + | if (rec.type === 'session_meta') { | |
| + | if (payload.id && !session.sessionId) session.sessionId = payload.id; | |
| + | if (payload.cwd) session.cwd = payload.cwd; | |
| + | if (payload.cli_version) session.version = payload.cli_version; | |
| + | if (payload.git && payload.git.branch) session.gitBranch = payload.git.branch; | |
| + | continue; | |
| + | } | |
| + | ||
| + | if (rec.type === 'response_item' && payload.type === 'message') { | |
| + | if (payload.role === 'user') { | |
| + | const body = flattenParts(payload.content); | |
| + | if (looksSynthetic(body)) continue; | |
| + | pushTurn(session, ++turn, body, ts); | |
| + | } else if (payload.role === 'assistant') { | |
| + | session.stats.assistantLines++; | |
| + | } | |
| + | continue; | |
| + | } | |
| + | ||
| + | if (rec.type === 'response_item' && payload.type === 'function_call') { | |
| + | session.stats.toolUses++; | |
| + | const file = filePathFromArgs(payload.arguments); | |
| + | if (file) session.stats.filesTouched.add(file); | |
| + | continue; | |
| + | } | |
| + | ||
| + | if (rec.type === 'event_msg' && payload.type === 'token_count') { | |
| + | const usage = payload.info && payload.info.total_token_usage; | |
| + | if (usage) { | |
| + | session.stats.inputTokens = usage.input_tokens || session.stats.inputTokens; | |
| + | session.stats.outputTokens = usage.output_tokens || session.stats.outputTokens; | |
| + | } | |
| + | continue; | |
| + | } | |
| + | ||
| + | if (rec.type === 'turn_context' && payload.model) { | |
| + | session.stats.models.add(payload.model); | |
| + | } | |
| + | } | |
| + | ||
| + | return finalizeSession(session); | |
| + | } | |
| + | ||
| + | function filePathFromArgs(args) { | |
| + | if (!args || typeof args !== 'string') return null; | |
| + | let parsed; | |
| + | try { | |
| + | parsed = JSON.parse(args); | |
| + | } catch { | |
| + | return null; | |
| + | } | |
| + | return parsed.path || parsed.file_path || parsed.filePath || null; | |
| + | } |
| @@ -0,0 +1,49 @@ | ||
| + | import { newSession, finalizeSession, pushTurn, looksSynthetic } from './shared.js'; | |
| + | ||
| + | export function detectCopilot(parsed) { | |
| + | return Boolean( | |
| + | parsed && | |
| + | typeof parsed === 'object' && | |
| + | Array.isArray(parsed.requests) && | |
| + | (parsed.responderUsername || parsed.requesterUsername || parsed.version !== undefined) | |
| + | ); | |
| + | } | |
| + | ||
| + | function userText(message) { | |
| + | if (!message) return ''; | |
| + | if (typeof message === 'string') return message; | |
| + | if (typeof message.text === 'string') return message.text; | |
| + | if (Array.isArray(message.parts)) { | |
| + | return message.parts | |
| + | .map((p) => (typeof p === 'string' ? p : p && typeof p.text === 'string' ? p.text : '')) | |
| + | .join('\n'); | |
| + | } | |
| + | return ''; | |
| + | } | |
| + | ||
| + | function countResponse(session, response) { | |
| + | if (!Array.isArray(response)) return; | |
| + | for (const item of response) { | |
| + | if (item && (item.kind === 'toolInvocation' || item.kind === 'toolInvocationSerialized')) { | |
| + | session.stats.toolUses++; | |
| + | } | |
| + | } | |
| + | } | |
| + | ||
| + | export function parseCopilot(parsed, path, sessionId) { | |
| + | const session = newSession(path, parsed.sessionId || sessionId); | |
| + | let turn = 0; | |
| + | for (const req of parsed.requests) { | |
| + | if (!req) continue; | |
| + | session.stats.assistantLines++; | |
| + | countResponse(session, req.response); | |
| + | if (req.result && req.result.metadata && req.result.metadata.modelId) { | |
| + | session.stats.models.add(req.result.metadata.modelId); | |
| + | } | |
| + | const text = userText(req.message); | |
| + | if (looksSynthetic(text)) continue; | |
| + | const ts = req.timestamp ? new Date(req.timestamp).toISOString() : null; | |
| + | pushTurn(session, ++turn, text, ts); | |
| + | } | |
| + | return finalizeSession(session); | |
| + | } |
| @@ -0,0 +1,124 @@ | ||
| + | import { newSession, finalizeSession, pushTurn, looksSynthetic } from './shared.js'; | |
| + | ||
| + | function isUserBubble(bubble) { | |
| + | if (bubble.type === 1 || bubble.type === 'user') return true; | |
| + | if (bubble.type === 2 || bubble.type === 'ai' || bubble.type === 'assistant') return false; | |
| + | if (typeof bubble.role === 'string') return bubble.role === 'user'; | |
| + | return false; | |
| + | } | |
| + | ||
| + | function bubbleText(bubble) { | |
| + | if (typeof bubble.text === 'string') return bubble.text; | |
| + | if (typeof bubble.content === 'string') return bubble.content; | |
| + | if (typeof bubble.richText === 'string') return bubble.richText; | |
| + | return ''; | |
| + | } | |
| + | ||
| + | function collectBubbles(parsed) { | |
| + | if (Array.isArray(parsed)) return parsed; | |
| + | if (Array.isArray(parsed.bubbles)) return parsed.bubbles; | |
| + | if (Array.isArray(parsed.tabs)) { | |
| + | const out = []; | |
| + | for (const tab of parsed.tabs) { | |
| + | if (tab && Array.isArray(tab.bubbles)) out.push(...tab.bubbles); | |
| + | else if (tab && Array.isArray(tab.messages)) out.push(...tab.messages); | |
| + | } | |
| + | return out; | |
| + | } | |
| + | if (Array.isArray(parsed.conversation)) return parsed.conversation; | |
| + | if (Array.isArray(parsed.messages)) return parsed.messages; | |
| + | return null; | |
| + | } | |
| + | ||
| + | function isExportedSession(parsed) { | |
| + | return Boolean( | |
| + | parsed && | |
| + | !Array.isArray(parsed) && | |
| + | Array.isArray(parsed.messages) && | |
| + | parsed.messages.some((m) => m && typeof m.role === 'string' && 'content' in m) | |
| + | ); | |
| + | } | |
| + | ||
| + | function parseExportedSession(parsed, path, sessionId) { | |
| + | const session = newSession(path, parsed.id || parsed.sessionId || sessionId); | |
| + | if (parsed.title) session.title = parsed.title; | |
| + | let turn = 0; | |
| + | for (const msg of parsed.messages) { | |
| + | if (!msg) continue; | |
| + | const ts = msg.timestamp ? new Date(msg.timestamp).toISOString() : null; | |
| + | if (msg.role === 'user') { | |
| + | const text = typeof msg.content === 'string' ? msg.content : ''; | |
| + | if (looksSynthetic(text)) continue; | |
| + | pushTurn(session, ++turn, text, ts); | |
| + | } else if (msg.role === 'assistant') { | |
| + | session.stats.assistantLines++; | |
| + | if (msg.model) session.stats.models.add(msg.model); | |
| + | if (Array.isArray(msg.toolCalls)) { | |
| + | for (const call of msg.toolCalls) { | |
| + | session.stats.toolUses++; | |
| + | const file = call && (call.filePath || (call.args && (call.args.file_path || call.args.path))); | |
| + | if (typeof file === 'string') session.stats.filesTouched.add(file); | |
| + | } | |
| + | } | |
| + | } | |
| + | } | |
| + | return finalizeSession(session); | |
| + | } | |
| + | ||
| + | function promptList(parsed) { | |
| + | if (Array.isArray(parsed.prompts)) return parsed.prompts; | |
| + | if (parsed['aiService.prompts'] && Array.isArray(parsed['aiService.prompts'])) { | |
| + | return parsed['aiService.prompts']; | |
| + | } | |
| + | return null; | |
| + | } | |
| + | ||
| + | export function detectCursor(parsed) { | |
| + | if (!parsed || typeof parsed !== 'object') return false; | |
| + | if (parsed.cursorExport || parsed._tool === 'cursor') return true; | |
| + | if (isExportedSession(parsed) && (parsed.workspaceId !== undefined || parsed.index !== undefined || parsed.activeBranchBubbleIds !== undefined)) { | |
| + | return true; | |
| + | } | |
| + | if (Array.isArray(parsed.tabs)) return true; | |
| + | if (promptList(parsed)) return true; | |
| + | const bubbles = collectBubbles(parsed); | |
| + | if (Array.isArray(bubbles) && bubbles.length) { | |
| + | return bubbles.some((b) => b && (b.bubbleId !== undefined || b.type === 1 || b.type === 2 || b.type === 'ai')); | |
| + | } | |
| + | return false; | |
| + | } | |
| + | ||
| + | export function parseCursor(parsed, path, sessionId) { | |
| + | const session = newSession(path, (parsed && parsed.composerId) || (parsed && parsed.sessionId) || sessionId); | |
| + | if (parsed && parsed.title) session.title = parsed.title; | |
| + | let turn = 0; | |
| + | ||
| + | if (isExportedSession(parsed)) { | |
| + | return parseExportedSession(parsed, path, sessionId); | |
| + | } | |
| + | ||
| + | const prompts = promptList(parsed); | |
| + | if (prompts) { | |
| + | for (const p of prompts) { | |
| + | const text = typeof p === 'string' ? p : p && typeof p.text === 'string' ? p.text : ''; | |
| + | if (looksSynthetic(text)) continue; | |
| + | pushTurn(session, ++turn, text, null); | |
| + | } | |
| + | return finalizeSession(session); | |
| + | } | |
| + | ||
| + | const bubbles = collectBubbles(parsed) || []; | |
| + | for (const bubble of bubbles) { | |
| + | if (!bubble) continue; | |
| + | if (isUserBubble(bubble)) { | |
| + | const text = bubbleText(bubble); | |
| + | if (looksSynthetic(text)) continue; | |
| + | const ts = bubble.createdAt ? new Date(bubble.createdAt).toISOString() : null; | |
| + | pushTurn(session, ++turn, text, ts); | |
| + | } else { | |
| + | session.stats.assistantLines++; | |
| + | if (Array.isArray(bubble.toolFormerData) || bubble.toolFormerData) session.stats.toolUses++; | |
| + | } | |
| + | } | |
| + | return finalizeSession(session); | |
| + | } |
| @@ -0,0 +1,81 @@ | ||
| + | import { | |
| + | newSession, | |
| + | finalizeSession, | |
| + | pushTurn, | |
| + | flattenParts, | |
| + | looksSynthetic, | |
| + | readJsonl, | |
| + | } from './shared.js'; | |
| + | ||
| + | function partsToText(content) { | |
| + | if (typeof content === 'string') return content; | |
| + | if (Array.isArray(content)) { | |
| + | const out = []; | |
| + | for (const part of content) { | |
| + | if (typeof part === 'string') out.push(part); | |
| + | else if (part && typeof part.text === 'string') out.push(part.text); | |
| + | } | |
| + | return out.join('\n'); | |
| + | } | |
| + | return flattenParts(content); | |
| + | } | |
| + | ||
| + | function ingestRecord(session, rec, counters) { | |
| + | const type = rec.type || rec.role; | |
| + | const ts = rec.timestamp || null; | |
| + | if (type === 'user') { | |
| + | const text = partsToText(rec.content); | |
| + | if (looksSynthetic(text)) return; | |
| + | pushTurn(session, ++counters.turn, text, ts); | |
| + | } else if (type === 'gemini' || type === 'model' || type === 'assistant') { | |
| + | session.stats.assistantLines++; | |
| + | if (rec.model) session.stats.models.add(rec.model); | |
| + | if (Array.isArray(rec.toolCalls)) { | |
| + | for (const call of rec.toolCalls) { | |
| + | session.stats.toolUses++; | |
| + | const file = call && call.args && (call.args.file_path || call.args.path || call.args.absolute_path); | |
| + | if (typeof file === 'string') session.stats.filesTouched.add(file); | |
| + | } | |
| + | } | |
| + | if (rec.tokens) { | |
| + | session.stats.inputTokens += rec.tokens.prompt || rec.tokens.input || 0; | |
| + | session.stats.outputTokens += rec.tokens.candidate || rec.tokens.output || 0; | |
| + | } | |
| + | } | |
| + | } | |
| + | ||
| + | export function detectGemini(text) { | |
| + | for (const line of text.split(/\r?\n/)) { | |
| + | const trimmed = line.trim(); | |
| + | if (!trimmed || trimmed.charCodeAt(0) !== 123) continue; | |
| + | try { | |
| + | const rec = JSON.parse(trimmed); | |
| + | if ((rec.type === 'user' || rec.type === 'gemini') && 'content' in rec) return true; | |
| + | return false; | |
| + | } catch { | |
| + | return false; | |
| + | } | |
| + | } | |
| + | return false; | |
| + | } | |
| + | ||
| + | export function detectGeminiJson(parsed) { | |
| + | if (!parsed || typeof parsed !== 'object') return false; | |
| + | if (!Array.isArray(parsed.messages)) return false; | |
| + | return parsed.messages.some((m) => m && (m.type === 'gemini' || m.type === 'user') && 'content' in m); | |
| + | } | |
| + | ||
| + | export function parseGemini(text, path, sessionId) { | |
| + | const session = newSession(path, sessionId); | |
| + | const counters = { turn: 0 }; | |
| + | for (const rec of readJsonl(text)) ingestRecord(session, rec, counters); | |
| + | return finalizeSession(session); | |
| + | } | |
| + | ||
| + | export function parseGeminiJson(parsed, path, sessionId) { | |
| + | const session = newSession(path, parsed.sessionId || sessionId); | |
| + | if (parsed.model) session.stats.models.add(parsed.model); | |
| + | const counters = { turn: 0 }; | |
| + | for (const rec of parsed.messages) ingestRecord(session, rec, counters); | |
| + | return finalizeSession(session); | |
| + | } |
| @@ -0,0 +1,42 @@ | ||
| + | import { newSession, finalizeSession, pushTurn, flattenParts, looksSynthetic } from './shared.js'; | |
| + | ||
| + | function messageText(content) { | |
| + | if (typeof content === 'string') return content; | |
| + | return flattenParts(content); | |
| + | } | |
| + | ||
| + | function grokMessages(parsed) { | |
| + | if (Array.isArray(parsed)) return parsed; | |
| + | if (parsed && Array.isArray(parsed.conversation)) return parsed.conversation; | |
| + | if (parsed && Array.isArray(parsed.messages)) return parsed.messages; | |
| + | return null; | |
| + | } | |
| + | ||
| + | export function detectGrok(parsed) { | |
| + | if (!parsed || typeof parsed !== 'object') return false; | |
| + | const messages = grokMessages(parsed); | |
| + | if (!Array.isArray(messages) || !messages.length) return false; | |
| + | return messages.every((m) => m && typeof m === 'object' && 'role' in m && 'content' in m); | |
| + | } | |
| + | ||
| + | export function parseGrok(parsed, path, sessionId) { | |
| + | const messages = grokMessages(parsed) || []; | |
| + | const session = newSession(path, (parsed && parsed.sessionId) || sessionId); | |
| + | if (parsed && parsed.model) session.stats.models.add(parsed.model); | |
| + | let turn = 0; | |
| + | for (const msg of messages) { | |
| + | if (!msg) continue; | |
| + | const ts = msg.timestamp ? new Date(msg.timestamp).toISOString() : null; | |
| + | if (msg.role === 'user') { | |
| + | const text = messageText(msg.content); | |
| + | if (looksSynthetic(text)) continue; | |
| + | pushTurn(session, ++turn, text, ts); | |
| + | } else if (msg.role === 'assistant') { | |
| + | session.stats.assistantLines++; | |
| + | if (Array.isArray(msg.tool_calls)) session.stats.toolUses += msg.tool_calls.length; | |
| + | } else if (msg.role === 'tool') { | |
| + | session.stats.toolUses++; | |
| + | } | |
| + | } | |
| + | return finalizeSession(session); | |
| + | } |
| @@ -0,0 +1,58 @@ | ||
| + | import { basename } from 'node:path'; | |
| + | import { detectCodex, parseCodex } from './codex.js'; | |
| + | import { detectGemini, detectGeminiJson, parseGemini, parseGeminiJson } from './gemini.js'; | |
| + | import { detectChatGPT, parseChatGPT } from './chatgpt.js'; | |
| + | import { detectCopilot, parseCopilot } from './copilot.js'; | |
| + | import { detectGrok, parseGrok } from './grok.js'; | |
| + | import { detectCursor, parseCursor } from './cursor.js'; | |
| + | ||
| + | export const TOOLS = ['claude', 'codex', 'chatgpt', 'gemini', 'copilot', 'grok', 'cursor', 'transcript']; | |
| + | ||
| + | function tryParseJson(text) { | |
| + | const head = text.trimStart()[0]; | |
| + | if (head !== '{' && head !== '[') return null; | |
| + | try { | |
| + | return JSON.parse(text); | |
| + | } catch { | |
| + | return null; | |
| + | } | |
| + | } | |
| + | ||
| + | export function adaptFrom(tool, text, path) { | |
| + | const id = basename(path).replace(/\.(jsonl?|txt|md)$/i, ''); | |
| + | const json = tryParseJson(text); | |
| + | switch (tool) { | |
| + | case 'codex': | |
| + | return [parseCodex(text, path, id)]; | |
| + | case 'gemini': | |
| + | return json ? [parseGeminiJson(json, path, id)] : [parseGemini(text, path, id)]; | |
| + | case 'chatgpt': | |
| + | return parseChatGPT(json, path); | |
| + | case 'copilot': | |
| + | return [parseCopilot(json, path, id)]; | |
| + | case 'grok': | |
| + | return [parseGrok(json, path, id)]; | |
| + | case 'cursor': | |
| + | return [parseCursor(json, path, id)]; | |
| + | default: | |
| + | throw new Error(`unknown --from tool "${tool}" (expected one of: ${TOOLS.join(', ')})`); | |
| + | } | |
| + | } | |
| + | ||
| + | export function autoAdapt(text, path) { | |
| + | const id = basename(path).replace(/\.(jsonl?|txt|md)$/i, ''); | |
| + | const json = tryParseJson(text); | |
| + | ||
| + | if (json !== null) { | |
| + | if (detectChatGPT(json)) return { tool: 'chatgpt', sessions: parseChatGPT(json, path) }; | |
| + | if (detectCopilot(json)) return { tool: 'copilot', sessions: [parseCopilot(json, path, id)] }; | |
| + | if (detectGeminiJson(json)) return { tool: 'gemini', sessions: [parseGeminiJson(json, path, id)] }; | |
| + | if (detectCursor(json)) return { tool: 'cursor', sessions: [parseCursor(json, path, id)] }; | |
| + | if (detectGrok(json)) return { tool: 'grok', sessions: [parseGrok(json, path, id)] }; | |
| + | return null; | |
| + | } | |
| + | ||
| + | if (detectCodex(text)) return { tool: 'codex', sessions: [parseCodex(text, path, id)] }; | |
| + | if (detectGemini(text)) return { tool: 'gemini', sessions: [parseGemini(text, path, id)] }; | |
| + | return null; | |
| + | } |
| @@ -0,0 +1,111 @@ | ||
| + | export function emptyStats() { | |
| + | return { | |
| + | userLines: 0, | |
| + | assistantLines: 0, | |
| + | toolUses: 0, | |
| + | models: new Set(), | |
| + | filesTouched: new Set(), | |
| + | inputTokens: 0, | |
| + | outputTokens: 0, | |
| + | interruptions: 0, | |
| + | }; | |
| + | } | |
| + | ||
| + | export function newSession(path, sessionId) { | |
| + | return { | |
| + | sessionId: sessionId || null, | |
| + | path, | |
| + | title: null, | |
| + | customTitle: null, | |
| + | version: null, | |
| + | cwd: null, | |
| + | gitBranch: null, | |
| + | firstTs: null, | |
| + | lastTs: null, | |
| + | prompts: [], | |
| + | index: new Map(), | |
| + | leafUuid: null, | |
| + | activeLeafUuid: null, | |
| + | stats: emptyStats(), | |
| + | isContinuation: false, | |
| + | }; | |
| + | } | |
| + | ||
| + | export function finalizeSession(session) { | |
| + | session.stats.models = [...session.stats.models]; | |
| + | session.stats.filesTouched = [...session.stats.filesTouched]; | |
| + | if (session.customTitle) session.title = session.customTitle; | |
| + | return session; | |
| + | } | |
| + | ||
| + | export function noteTimestamp(session, ts) { | |
| + | if (!ts) return; | |
| + | if (!session.firstTs) session.firstTs = ts; | |
| + | session.lastTs = ts; | |
| + | } | |
| + | ||
| + | export function pushTurn(session, idx, text, ts, { hasImage = false, hadToolResultContext = false } = {}) { | |
| + | const trimmed = (text || '').trim(); | |
| + | if (!trimmed && !hasImage) return null; | |
| + | const uuid = `${session.sessionId || 'turn'}-u${idx}`; | |
| + | const parentUuid = session._lastUserUuid || null; | |
| + | session.index.set(uuid, { parentUuid, type: 'user', ts: ts || null }); | |
| + | session.leafUuid = uuid; | |
| + | session._lastUserUuid = uuid; | |
| + | session.stats.userLines++; | |
| + | session.prompts.push({ | |
| + | uuid, | |
| + | parentUuid, | |
| + | ts: ts || null, | |
| + | text: trimmed || '[image-only prompt: screenshot/annotated feedback]', | |
| + | hasImage, | |
| + | hadToolResultContext, | |
| + | afterInterruption: false, | |
| + | }); | |
| + | noteTimestamp(session, ts); | |
| + | return uuid; | |
| + | } | |
| + | ||
| + | export function flattenParts(parts) { | |
| + | if (typeof parts === 'string') return parts; | |
| + | if (!Array.isArray(parts)) { | |
| + | if (parts && typeof parts === 'object' && typeof parts.text === 'string') return parts.text; | |
| + | return ''; | |
| + | } | |
| + | const out = []; | |
| + | for (const part of parts) { | |
| + | if (typeof part === 'string') out.push(part); | |
| + | else if (part && typeof part === 'object' && typeof part.text === 'string') out.push(part.text); | |
| + | } | |
| + | return out.join('\n'); | |
| + | } | |
| + | ||
| + | export function looksSynthetic(text) { | |
| + | const t = (text || '').trimStart(); | |
| + | if (!t) return true; | |
| + | return ( | |
| + | t.startsWith('<environment_context>') || | |
| + | t.startsWith('<permissions instructions>') || | |
| + | t.startsWith('<collaboration_mode>') || | |
| + | t.startsWith('<user_instructions>') || | |
| + | t.startsWith('<system-reminder>') | |
| + | ); | |
| + | } | |
| + | ||
| + | export function readJson(text) { | |
| + | return JSON.parse(text); | |
| + | } | |
| + | ||
| + | export function readJsonl(text) { | |
| + | const records = []; | |
| + | for (const line of text.split(/\r?\n/)) { | |
| + | const trimmed = line.trim(); | |
| + | if (!trimmed || trimmed.charCodeAt(0) !== 123) continue; | |
| + | try { | |
| + | records.push(JSON.parse(trimmed)); | |
| + | } catch { | |
| + | continue; | |
| + | } | |
| + | } | |
| + | return records; | |
| + | } |
| @@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; | ||
| import { basename, join, resolve } from 'node:path'; | ||
| import { discoverSessions } from './discover.js'; | ||
| import { parseSessionFile, parsePlainTranscript } from './parse.js'; | ||
| + | import { adaptFrom, autoAdapt, TOOLS } from './adapters/index.js'; | |
| import { classifyPrompts } from './extract.js'; | ||
| import { buildTree } from './tree.js'; | ||
| import { scanText, resolveFindings, applyDecisions, shadowScan } from './redact.js'; | ||
| @@ -25,7 +26,8 @@ const HELP = `TreeTrace - turn AI coding sessions into regression-ready prompt l | ||
| Usage: | ||
| treetrace auto-discover Claude Code sessions for this directory | ||
| - | treetrace --file <path>... parse specific transcript files (.jsonl or plain text) | |
| + | treetrace --file <path>... parse specific session/transcript files | |
| + | treetrace --from <tool> --file <path> import another tool's export | |
| treetrace --stdin read a pasted transcript from stdin | ||
| treetrace --report write all artifacts and print the human report | ||
| treetrace --handoff print an agent-ready handoff brief to stdout | ||
| @@ -35,6 +37,8 @@ Usage: | ||
| treetrace --memory write and print compact agent memory | ||
| Options: | ||
| + | --from <tool> input format for --file: claude, codex, chatgpt, gemini, | |
| + | copilot, grok, cursor, transcript (default: auto-detect) | |
| --dir <path> project directory to trace (default: cwd) | ||
| --out <file> markdown output path (default: PROMPT_TREE.md) | ||
| --report-file <file> human report output path (default: TREETRACE_REPORT.md) | ||
| @@ -65,11 +69,7 @@ export async function main(argv) { | ||
| sessions = [parsePlainTranscript(text)]; | ||
| } else if (opts.files.length) { | ||
| for (const file of opts.files) { | ||
| - | if (file.endsWith('.jsonl')) { | |
| - | sessions.push(await parseSessionFile(file, { sessionId: basename(file, '.jsonl') })); | |
| - | } else { | |
| - | sessions.push(parsePlainTranscript(readFileSync(file, 'utf8'), basename(file))); | |
| - | } | |
| + | sessions.push(...(await ingestFile(file, opts.from, log))); | |
| } | ||
| } else { | ||
| const found = discoverSessions(projectDir); | ||
| @@ -202,6 +202,40 @@ export async function main(argv) { | ||
| if (asked) log(c.dim(` ${plural(asked, 'redaction decision')} saved to .treetrace/redactions.json`)); | ||
| } | ||
| + | async function ingestFile(file, from, log) { | |
| + | if (from && from !== 'claude' && from !== 'transcript') { | |
| + | const text = readFileSync(file, 'utf8'); | |
| + | return adaptFrom(from, text, file); | |
| + | } | |
| + | if (from === 'claude') { | |
| + | return [await parseSessionFile(file, { sessionId: basename(file, '.jsonl') })]; | |
| + | } | |
| + | if (from === 'transcript') { | |
| + | return [parsePlainTranscript(readFileSync(file, 'utf8'), basename(file))]; | |
| + | } | |
| + | ||
| + | if (file.endsWith('.jsonl')) { | |
| + | const text = readFileSync(file, 'utf8'); | |
| + | const adapted = autoAdapt(text, file); | |
| + | if (adapted && adapted.sessions.some((s) => s.prompts.length)) { | |
| + | log(c.dim(` detected ${adapted.tool} format in ${basename(file)}`)); | |
| + | return adapted.sessions; | |
| + | } | |
| + | return [await parseSessionFile(file, { sessionId: basename(file, '.jsonl') })]; | |
| + | } | |
| + | ||
| + | if (file.endsWith('.json')) { | |
| + | const text = readFileSync(file, 'utf8'); | |
| + | const adapted = autoAdapt(text, file); | |
| + | if (adapted && adapted.sessions.some((s) => s.prompts.length)) { | |
| + | log(c.dim(` detected ${adapted.tool} format in ${basename(file)}`)); | |
| + | return adapted.sessions; | |
| + | } | |
| + | } | |
| + | ||
| + | return [parsePlainTranscript(readFileSync(file, 'utf8'), basename(file))]; | |
| + | } | |
| + | ||
| function analysisArtifacts(ttDir, tree, renderOpts) { | ||
| return { | ||
| failures: { | ||
| @@ -327,6 +361,7 @@ function parseArgs(argv) { | ||
| quiet: false, | ||
| help: false, | ||
| version: false, | ||
| + | from: null, | |
| dir: null, | ||
| out: null, | ||
| reportFile: null, | ||
| @@ -352,6 +387,12 @@ function parseArgs(argv) { | ||
| case '--quiet': opts.quiet = true; break; | ||
| case '--help': case '-h': opts.help = true; break; | ||
| case '--version': case '-v': opts.version = true; break; | ||
| + | case '--from': | |
| + | opts.from = argv[++i]; | |
| + | if (!TOOLS.includes(opts.from)) { | |
| + | throw new Error(`unknown --from value "${opts.from}" (expected one of: ${TOOLS.join(', ')})`); | |
| + | } | |
| + | break; | |
| case '--dir': opts.dir = argv[++i]; break; | ||
| case '--out': opts.out = argv[++i]; break; | ||
| case '--report-file': opts.reportFile = argv[++i]; break; |
| @@ -0,0 +1,137 @@ | ||
| + | import { test } from 'node:test'; | |
| + | import assert from 'node:assert/strict'; | |
| + | import { readFileSync } from 'node:fs'; | |
| + | import { fileURLToPath } from 'node:url'; | |
| + | import { dirname, join } from 'node:path'; | |
| + | ||
| + | import { adaptFrom, autoAdapt, TOOLS } from '../src/adapters/index.js'; | |
| + | import { classifyPrompts } from '../src/extract.js'; | |
| + | import { buildTree } from '../src/tree.js'; | |
| + | ||
| + | const DIR = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'adapters'); | |
| + | const fx = (name) => join(DIR, name); | |
| + | const read = (name) => readFileSync(fx(name), 'utf8'); | |
| + | ||
| + | function pipeline(sessions) { | |
| + | const nodes = classifyPrompts(sessions); | |
| + | return { nodes, tree: buildTree(sessions, nodes) }; | |
| + | } | |
| + | ||
| + | test('codex: parses real rollout JSONL into human prompts', () => { | |
| + | const sessions = adaptFrom('codex', read('codex-session.jsonl'), fx('codex-session.jsonl')); | |
| + | assert.equal(sessions.length, 1); | |
| + | const s = sessions[0]; | |
| + | assert.equal(s.prompts.length, 3); | |
| + | assert.ok(s.prompts.every((p) => !p.text.startsWith('<environment_context>'))); | |
| + | assert.ok(s.prompts[0].text.includes('/version subcommand')); | |
| + | assert.equal(s.stats.assistantLines, 3); | |
| + | assert.ok(s.stats.toolUses >= 1); | |
| + | assert.equal(s.stats.inputTokens, 5200); | |
| + | const { tree } = pipeline(sessions); | |
| + | assert.equal(tree.roots.length, 1); | |
| + | assert.equal(tree.stats.promptCount, 3); | |
| + | }); | |
| + | ||
| + | test('codex: auto-detected from JSONL shape', () => { | |
| + | const detected = autoAdapt(read('codex-session.jsonl'), fx('codex-session.jsonl')); | |
| + | assert.ok(detected); | |
| + | assert.equal(detected.tool, 'codex'); | |
| + | assert.equal(detected.sessions[0].prompts.length, 3); | |
| + | }); | |
| + | ||
| + | test('chatgpt: parses export mapping, walks user turns, detects model', () => { | |
| + | const sessions = adaptFrom('chatgpt', read('chatgpt-conversations.json'), fx('chatgpt-conversations.json')); | |
| + | assert.equal(sessions.length, 1); | |
| + | const s = sessions[0]; | |
| + | assert.equal(s.prompts.length, 1); | |
| + | assert.equal(s.title, 'Debounce a search input in React'); | |
| + | assert.ok(s.stats.models.includes('gpt-4o')); | |
| + | assert.ok(s.stats.assistantLines >= 1); | |
| + | assert.ok(s.stats.toolUses >= 1); | |
| + | }); | |
| + | ||
| + | test('chatgpt: auto-detected from mapping shape', () => { | |
| + | const detected = autoAdapt(read('chatgpt-conversations.json'), fx('chatgpt-conversations.json')); | |
| + | assert.ok(detected); | |
| + | assert.equal(detected.tool, 'chatgpt'); | |
| + | }); | |
| + | ||
| + | test('gemini: parses ChatRecordingService session, tools and tokens', () => { | |
| + | const sessions = adaptFrom('gemini', read('gemini-session.json'), fx('gemini-session.json')); | |
| + | const s = sessions[0]; | |
| + | assert.equal(s.prompts.length, 3); | |
| + | assert.ok(s.prompts[0].text.includes('health-check')); | |
| + | assert.equal(s.stats.assistantLines, 3); | |
| + | assert.ok(s.stats.toolUses >= 1); | |
| + | assert.ok(s.stats.filesTouched.includes('src/server/health.ts')); | |
| + | assert.ok(s.stats.models.includes('gemini-3-flash-preview')); | |
| + | }); | |
| + | ||
| + | test('gemini: auto-detected from session JSON', () => { | |
| + | const detected = autoAdapt(read('gemini-session.json'), fx('gemini-session.json')); | |
| + | assert.ok(detected); | |
| + | assert.equal(detected.tool, 'gemini'); | |
| + | assert.equal(detected.sessions[0].prompts.length, 3); | |
| + | }); | |
| + | ||
| + | test('copilot: parses requests[] into prompts, counts tool invocations', () => { | |
| + | const sessions = adaptFrom('copilot', read('copilot-chatsession.json'), fx('copilot-chatsession.json')); | |
| + | const s = sessions[0]; | |
| + | assert.equal(s.prompts.length, 5); | |
| + | assert.ok(s.prompts[0].text.toLowerCase().includes('html')); | |
| + | assert.equal(s.stats.assistantLines, 5); | |
| + | }); | |
| + | ||
| + | test('copilot: auto-detected from requesterUsername/requests', () => { | |
| + | const detected = autoAdapt(read('copilot-chatsession.json'), fx('copilot-chatsession.json')); | |
| + | assert.ok(detected); | |
| + | assert.equal(detected.tool, 'copilot'); | |
| + | }); | |
| + | ||
| + | test('cursor: parses exported session messages, model and files', () => { | |
| + | const sessions = adaptFrom('cursor', read('cursor-export.json'), fx('cursor-export.json')); | |
| + | const s = sessions[0]; | |
| + | assert.equal(s.prompts.length, 3); | |
| + | assert.equal(s.title, 'Add pagination to the users table'); | |
| + | assert.ok(s.stats.models.includes('claude-3.5-sonnet')); | |
| + | assert.ok(s.stats.toolUses >= 1); | |
| + | assert.ok(s.stats.filesTouched.some((f) => f.endsWith('Users.tsx'))); | |
| + | }); | |
| + | ||
| + | test('cursor: auto-detected from exported session shape', () => { | |
| + | const detected = autoAdapt(read('cursor-export.json'), fx('cursor-export.json')); | |
| + | assert.ok(detected); | |
| + | assert.equal(detected.tool, 'cursor'); | |
| + | }); | |
| + | ||
| + | test('grok: parses conversation[] role/content into prompts', () => { | |
| + | const sessions = adaptFrom('grok', read('grok-session.json'), fx('grok-session.json')); | |
| + | const s = sessions[0]; | |
| + | assert.equal(s.prompts.length, 3); | |
| + | assert.ok(s.prompts[0].text.includes('Fibonacci')); | |
| + | assert.ok(s.stats.models.includes('grok-4')); | |
| + | assert.equal(s.stats.assistantLines, 2); | |
| + | }); | |
| + | ||
| + | test('grok: auto-detected from conversation messages', () => { | |
| + | const detected = autoAdapt(read('grok-session.json'), fx('grok-session.json')); | |
| + | assert.ok(detected); | |
| + | assert.equal(detected.tool, 'grok'); | |
| + | }); | |
| + | ||
| + | test('adapter output flows through the full classify/tree pipeline', () => { | |
| + | for (const name of ['codex-session.jsonl', 'gemini-session.json', 'cursor-export.json', 'grok-session.json']) { | |
| + | const text = read(name); | |
| + | const detected = autoAdapt(text, fx(name)); | |
| + | assert.ok(detected, `no detection for ${name}`); | |
| + | const { tree } = pipeline(detected.sessions); | |
| + | assert.ok(tree.nodes.length >= 1, `no nodes for ${name}`); | |
| + | assert.equal(tree.nodes[0].kind, 'root', `first node not root for ${name}`); | |
| + | assert.ok(tree.roots.length >= 1); | |
| + | } | |
| + | }); | |
| + | ||
| + | test('adaptFrom rejects an unknown tool name', () => { | |
| + | assert.throws(() => adaptFrom('notatool', '{}', 'x.json'), /unknown/); | |
| + | assert.ok(TOOLS.includes('codex') && TOOLS.includes('cursor')); | |
| + | }); |
| @@ -0,0 +1,67 @@ | ||
| + | # Adapter fixture provenance | |
| + | ||
| + | Each fixture below reproduces the real on-disk or export schema of the tool it | |
| + | represents. Where the structure came from a real session or a real published | |
| + | sample, the message text has been replaced with neutral placeholder content so | |
| + | no private conversation is republished. The field shapes, key names, value | |
| + | types, and nesting are kept exactly as the real format. | |
| + | ||
| + | ## codex-session.jsonl | |
| + | - Format: Codex CLI rollout JSONL (`~/.codex/sessions/.../rollout-*.jsonl`). | |
| + | - Source: a real Codex CLI session captured locally (cli_version 0.139.0). | |
| + | - Scrubbing: message text, cwd, instructions, and turn context replaced with | |
| + | neutral placeholders. Event/record schema (`session_meta`, `turn_context`, | |
| + | `response_item` messages, `function_call`, `token_count`) is unchanged. | |
| + | - Status: VERIFIED against the original real session. | |
| + | ||
| + | ## gemini-session.json | |
| + | - Format: gemini-cli ChatRecordingService session JSON. | |
| + | - Source: google-gemini/gemini-cli, memory-tests/large-chat-session.json | |
| + | (https://github.com/google-gemini/gemini-cli), Apache-2.0. | |
| + | - Scrubbing: a short slice of the real file with message `content`, `thoughts`, | |
| + | and `toolCalls` text replaced with neutral placeholders. The `sessionId`, | |
| + | `projectHash`, `messages[].type`, `content`, `tokens`, `model`, and | |
| + | `toolCalls` shapes are unchanged. | |
| + | - Status: VERIFIED. The full real file parses through the adapter (51 user | |
| + | prompts, 1334 model turns, model gemini-3-flash-preview). | |
| + | ||
| + | ## chatgpt-conversations.json | |
| + | - Format: ChatGPT/OpenAI account export `conversations.json` (array of | |
| + | conversations, each with a `mapping` of node id to message node). | |
| + | - Source: sanand0/openai-conversations, samples/seoul-weather-early-october.json | |
| + | (https://github.com/sanand0/openai-conversations), MIT. | |
| + | - Scrubbing: real export structure kept (`mapping`, `author.role`, | |
| + | `content.content_type`, `content.parts`, `parent`, `children`, | |
| + | `create_time`); all message text and tool output replaced with placeholders; | |
| + | heavy per-node metadata trimmed to `model_slug` only. | |
| + | - Status: VERIFIED. The original sample parses through the adapter. | |
| + | ||
| + | ## copilot-chatsession.json | |
| + | - Format: VS Code GitHub Copilot Chat session JSON (version 3, `requests[]`). | |
| + | - Source: Timcooking/VSCode-Copilot-Chat-Viewer, demo-chat.json | |
| + | (https://github.com/Timcooking/VSCode-Copilot-Chat-Viewer), MIT. | |
| + | - Scrubbing: real `requesterUsername`, `responderUsername`, `requests[].message` | |
| + | (`text` + `parts`), and `response[].value` shapes kept; text replaced with | |
| + | neutral placeholders. | |
| + | - Status: VERIFIED against the published demo session. | |
| + | ||
| + | ## cursor-export.json | |
| + | - Format: Cursor exported-chat JSON, matching the cursor-history exporter's | |
| + | single-session JSON (`id`, `title`, `messages[].role/content/model/toolCalls`). | |
| + | - Source: schema from S2thend/cursor-history src/core/types.ts and | |
| + | src/cli/formatters/json.ts (https://github.com/S2thend/cursor-history), MIT. | |
| + | Cursor stores chat in state.vscdb (SQLite); the adapter ingests the exported | |
| + | JSON because TreeTrace ships with zero runtime dependencies and cannot open | |
| + | SQLite. This fixture mirrors that exporter's output. | |
| + | - Status: VERIFIED against the exporter's documented schema (no SQLite needed). | |
| + | ||
| + | ## grok-session.json | |
| + | - Format: Grok CLI exported conversation JSON | |
| + | (`timestamp`, `model`, `messageCount`, `conversation[].role/content`), the | |
| + | xAI OpenAI-compatible message shape. | |
| + | - Source: schema from lalomorales22/grok-4-cli lib/export.js `exportToJSON` | |
| + | (https://github.com/lalomorales22/grok-4-cli). The primary superagent-ai | |
| + | grok-cli stores history in SQLite (no JSON on disk), so this adapter targets | |
| + | the exported-JSON shape. | |
| + | - Status: EXPERIMENTAL. Built to the exporter's documented JSON shape; not yet | |
| + | validated against a captured real Grok session on this machine. |
| @@ -0,0 +1,359 @@ | ||
| + | [ | |
| + | { | |
| + | "title": "Debounce a search input in React", | |
| + | "create_time": 1727699286.485543, | |
| + | "update_time": 1727699296.187922, | |
| + | "mapping": { | |
| + | "350877d5-b411-41fa-b87b-0f7680d3f061": { | |
| + | "id": "350877d5-b411-41fa-b87b-0f7680d3f061", | |
| + | "message": { | |
| + | "id": "350877d5-b411-41fa-b87b-0f7680d3f061", | |
| + | "author": { | |
| + | "role": "system", | |
| + | "name": null, | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": null, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "" | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": true, | |
| + | "weight": 0.0, | |
| + | "metadata": {}, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "bbb23b75-51a7-4302-9331-efc1d6ebfc4d", | |
| + | "children": [ | |
| + | "091b5281-b7e3-4141-bb44-f351aa6c14c3" | |
| + | ] | |
| + | }, | |
| + | "bbb23b75-51a7-4302-9331-efc1d6ebfc4d": { | |
| + | "id": "bbb23b75-51a7-4302-9331-efc1d6ebfc4d", | |
| + | "message": null, | |
| + | "parent": null, | |
| + | "children": [ | |
| + | "350877d5-b411-41fa-b87b-0f7680d3f061" | |
| + | ] | |
| + | }, | |
| + | "091b5281-b7e3-4141-bb44-f351aa6c14c3": { | |
| + | "id": "091b5281-b7e3-4141-bb44-f351aa6c14c3", | |
| + | "message": { | |
| + | "id": "091b5281-b7e3-4141-bb44-f351aa6c14c3", | |
| + | "author": { | |
| + | "role": "system", | |
| + | "name": null, | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": null, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "" | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": null, | |
| + | "weight": 1.0, | |
| + | "metadata": {}, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "350877d5-b411-41fa-b87b-0f7680d3f061", | |
| + | "children": [ | |
| + | "bbb2131f-0bfa-4467-9782-b2e4b7bbcf57" | |
| + | ] | |
| + | }, | |
| + | "bbb2131f-0bfa-4467-9782-b2e4b7bbcf57": { | |
| + | "id": "bbb2131f-0bfa-4467-9782-b2e4b7bbcf57", | |
| + | "message": { | |
| + | "id": "bbb2131f-0bfa-4467-9782-b2e4b7bbcf57", | |
| + | "author": { | |
| + | "role": "user", | |
| + | "name": null, | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699286.491499, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "What is a good way to debounce a search input in React?" | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": null, | |
| + | "weight": 1.0, | |
| + | "metadata": {}, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "091b5281-b7e3-4141-bb44-f351aa6c14c3", | |
| + | "children": [ | |
| + | "1433a3b0-30ad-4905-8214-97e7301aba3f" | |
| + | ] | |
| + | }, | |
| + | "1433a3b0-30ad-4905-8214-97e7301aba3f": { | |
| + | "id": "1433a3b0-30ad-4905-8214-97e7301aba3f", | |
| + | "message": { | |
| + | "id": "1433a3b0-30ad-4905-8214-97e7301aba3f", | |
| + | "author": { | |
| + | "role": "assistant", | |
| + | "name": null, | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.376943, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "Use a useEffect with a setTimeout cleared on each keystroke, or a small useDebounce hook." | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": null, | |
| + | "weight": 1.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "bbb2131f-0bfa-4467-9782-b2e4b7bbcf57", | |
| + | "children": [ | |
| + | "f7af31ac-d221-4500-93cb-39a0858bc434" | |
| + | ] | |
| + | }, | |
| + | "f7af31ac-d221-4500-93cb-39a0858bc434": { | |
| + | "id": "f7af31ac-d221-4500-93cb-39a0858bc434", | |
| + | "message": { | |
| + | "id": "f7af31ac-d221-4500-93cb-39a0858bc434", | |
| + | "author": { | |
| + | "role": "assistant", | |
| + | "name": null, | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.377045, | |
| + | "content": { | |
| + | "content_type": "code", | |
| + | "language": "unknown", | |
| + | "response_format_name": null, | |
| + | "text": "search(\"average temperature in Seoul early October\")" | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": false, | |
| + | "weight": 1.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "browser", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "1433a3b0-30ad-4905-8214-97e7301aba3f", | |
| + | "children": [ | |
| + | "2c30b440-db17-4b17-ad71-82d08d61d675" | |
| + | ] | |
| + | }, | |
| + | "2c30b440-db17-4b17-ad71-82d08d61d675": { | |
| + | "id": "2c30b440-db17-4b17-ad71-82d08d61d675", | |
| + | "message": { | |
| + | "id": "2c30b440-db17-4b17-ad71-82d08d61d675", | |
| + | "author": { | |
| + | "role": "tool", | |
| + | "name": "browser", | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.37713, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "redacted tool output" | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": null, | |
| + | "weight": 0.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "f7af31ac-d221-4500-93cb-39a0858bc434", | |
| + | "children": [ | |
| + | "4503a2a3-a0d4-485b-be1b-5ad93cd8d836" | |
| + | ] | |
| + | }, | |
| + | "4503a2a3-a0d4-485b-be1b-5ad93cd8d836": { | |
| + | "id": "4503a2a3-a0d4-485b-be1b-5ad93cd8d836", | |
| + | "message": { | |
| + | "id": "4503a2a3-a0d4-485b-be1b-5ad93cd8d836", | |
| + | "author": { | |
| + | "role": "assistant", | |
| + | "name": null, | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.377201, | |
| + | "content": { | |
| + | "content_type": "code", | |
| + | "language": "unknown", | |
| + | "response_format_name": null, | |
| + | "text": "mclick([0, 3, 2, 9, 1])" | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": false, | |
| + | "weight": 1.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "browser", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "2c30b440-db17-4b17-ad71-82d08d61d675", | |
| + | "children": [ | |
| + | "38983cbf-6a71-4d1b-959b-aadd6a97230b" | |
| + | ] | |
| + | }, | |
| + | "38983cbf-6a71-4d1b-959b-aadd6a97230b": { | |
| + | "id": "38983cbf-6a71-4d1b-959b-aadd6a97230b", | |
| + | "message": { | |
| + | "id": "38983cbf-6a71-4d1b-959b-aadd6a97230b", | |
| + | "author": { | |
| + | "role": "tool", | |
| + | "name": "browser", | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.377272, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "redacted tool output" | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": null, | |
| + | "weight": 0.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "4503a2a3-a0d4-485b-be1b-5ad93cd8d836", | |
| + | "children": [ | |
| + | "24ab741e-ccaf-46d9-8049-9d7031de88a3" | |
| + | ] | |
| + | }, | |
| + | "24ab741e-ccaf-46d9-8049-9d7031de88a3": { | |
| + | "id": "24ab741e-ccaf-46d9-8049-9d7031de88a3", | |
| + | "message": { | |
| + | "id": "24ab741e-ccaf-46d9-8049-9d7031de88a3", | |
| + | "author": { | |
| + | "role": "tool", | |
| + | "name": "browser", | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.37734, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "redacted tool output" | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": null, | |
| + | "weight": 0.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "38983cbf-6a71-4d1b-959b-aadd6a97230b", | |
| + | "children": [ | |
| + | "a4f5ed82-25cb-40b2-92a1-689c9f9887ec" | |
| + | ] | |
| + | }, | |
| + | "a4f5ed82-25cb-40b2-92a1-689c9f9887ec": { | |
| + | "id": "a4f5ed82-25cb-40b2-92a1-689c9f9887ec", | |
| + | "message": { | |
| + | "id": "a4f5ed82-25cb-40b2-92a1-689c9f9887ec", | |
| + | "author": { | |
| + | "role": "tool", | |
| + | "name": "browser", | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.377407, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "redacted tool output" | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": null, | |
| + | "weight": 0.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "24ab741e-ccaf-46d9-8049-9d7031de88a3", | |
| + | "children": [ | |
| + | "e58a766b-0b78-49ff-bfaf-fee6be2689ba" | |
| + | ] | |
| + | }, | |
| + | "e58a766b-0b78-49ff-bfaf-fee6be2689ba": { | |
| + | "id": "e58a766b-0b78-49ff-bfaf-fee6be2689ba", | |
| + | "message": { | |
| + | "id": "e58a766b-0b78-49ff-bfaf-fee6be2689ba", | |
| + | "author": { | |
| + | "role": "assistant", | |
| + | "name": null, | |
| + | "metadata": {} | |
| + | }, | |
| + | "create_time": 1727699293.377481, | |
| + | "content": { | |
| + | "content_type": "text", | |
| + | "parts": [ | |
| + | "Use a useEffect with a setTimeout cleared on each keystroke, or a small useDebounce hook." | |
| + | ] | |
| + | }, | |
| + | "status": "finished_successfully", | |
| + | "end_turn": true, | |
| + | "weight": 1.0, | |
| + | "metadata": { | |
| + | "model_slug": "gpt-4o" | |
| + | }, | |
| + | "recipient": "all", | |
| + | "channel": null | |
| + | }, | |
| + | "parent": "a4f5ed82-25cb-40b2-92a1-689c9f9887ec", | |
| + | "children": [] | |
| + | } | |
| + | }, | |
| + | "moderation_results": [], | |
| + | "current_node": "e58a766b-0b78-49ff-bfaf-fee6be2689ba", | |
| + | "plugin_ids": null, | |
| + | "conversation_id": "anon-convo-chatgpt-demo", | |
| + | "conversation_template_id": null, | |
| + | "gizmo_id": null, | |
| + | "gizmo_type": null, | |
| + | "is_archived": false, | |
| + | "is_starred": null, | |
| + | "safe_urls": [ | |
| + | "https://weather-and-climate.com/Seoul-October-averages", | |
| + | "https://wanderlog.com/weather/9/10/seoul-weather-in-october", | |
| + | "https://www.weather-atlas.com/en/south-korea/seoul-weather-october" | |
| + | ], | |
| + | "default_model_slug": "gpt-4o", | |
| + | "conversation_origin": null, | |
| + | "voice": null, | |
| + | "async_status": null, | |
| + | "disabled_tool_ids": [], | |
| + | "id": "66fa9956-4144-800c-b052-6f0187d888d4" | |
| + | } | |
| + | ] |
| @@ -0,0 +1,11 @@ | ||
| + | {"timestamp": "2026-06-12T01:58:43.049Z", "type": "session_meta", "payload": {"id": "anon-codex-demo", "timestamp": "2026-06-12T01:57:43.912Z", "cwd": "/home/dev/project", "originator": "codex-tui", "cli_version": "0.139.0", "source": "cli", "thread_source": "user", "model_provider": "openai", "base_instructions": {"text": "redacted"}}} | |
| + | {"timestamp": "2026-06-12T01:58:43.191Z", "type": "turn_context", "payload": {"turn_id": "anon-turn", "cwd": "/home/dev/project", "workspace_roots": ["/home/dev/project"], "model": "gpt-5.5", "approval_policy": "never", "sandbox_policy": {"type": "danger-full-access"}, "collaboration_mode": {"mode": "default"}}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": "<environment_context>\n <cwd>/home/dev/project</cwd>\n</environment_context>"}]}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Add a /version subcommand to the CLI that prints the package version."}]}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "I'll add the subcommand and wire it to package.json."}]}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "function_call", "name": "exec_command", "arguments": "{\"cmd\": \"apply_patch\", \"workdir\": \"/home/dev/project\", \"path\": \"src/cli.js\"}", "call_id": "call_RnlGcJdacQlKT6Twaj6q1w7g"}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Also make `--version` an alias for it."}]}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Added the alias."}]}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Write a test that runs the binary with --version and checks the output."}]}} | |
| + | {"timestamp": "2026-06-12T01:58:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Done."}]}} | |
| + | {"timestamp": "2026-06-12T01:59:00.000Z", "type": "event_msg", "payload": {"type": "token_count", "info": {"total_token_usage": {"input_tokens": 5200, "cached_input_tokens": 1000, "output_tokens": 640, "reasoning_output_tokens": 40, "total_tokens": 5840}}}} |
| @@ -0,0 +1,250 @@ | ||
| + | { | |
| + | "version": 3, | |
| + | "requesterUsername": "user", | |
| + | "requesterAvatarIconUri": { | |
| + | "$mid": 1, | |
| + | "path": "/u/12345678", | |
| + | "scheme": "https", | |
| + | "authority": "avatars.githubusercontent.com", | |
| + | "query": "v=4" | |
| + | }, | |
| + | "responderUsername": "GitHub Copilot", | |
| + | "responderAvatarIconUri": { | |
| + | "id": "copilot" | |
| + | }, | |
| + | "initialLocation": "panel", | |
| + | "requests": [ | |
| + | { | |
| + | "requestId": "request_demo_001", | |
| + | "message": { | |
| + | "text": "Create a simple HTML page with a heading and a paragraph.", | |
| + | "parts": [ | |
| + | { | |
| + | "range": { | |
| + | "start": 0, | |
| + | "endExclusive": 32 | |
| + | }, | |
| + | "editorRange": { | |
| + | "startLineNumber": 1, | |
| + | "startColumn": 1, | |
| + | "endLineNumber": 1, | |
| + | "endColumn": 33 | |
| + | }, | |
| + | "text": "Create a simple HTML page with a heading and a paragraph.", | |
| + | "kind": "text" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | "variableData": { | |
| + | "variables": [] | |
| + | }, | |
| + | "response": [ | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false, | |
| + | "baseUri": { | |
| + | "$mid": 1, | |
| + | "path": "/C:/Users/TestUser/Documents/", | |
| + | "scheme": "file" | |
| + | } | |
| + | }, | |
| + | { | |
| + | "kind": "prepareToolInvocation", | |
| + | "toolName": "copilot_createFile" | |
| + | }, | |
| + | { | |
| + | "kind": "toolInvocationSerialized", | |
| + | "invocationMessage": { | |
| + | "value": "\u6b63\u5728\u521b\u5efa HTML \u6587\u4ef6", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | "pastTenseMessage": { | |
| + | "value": "\u5df2\u521b\u5efa HTML \u6587\u4ef6", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | "isConfirmed": true, | |
| + | "isComplete": true, | |
| + | "toolCallId": "demo_tool_001", | |
| + | "toolId": "copilot_createFile" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "requestId": "request_demo_002", | |
| + | "message": { | |
| + | "text": "Can you add some CSS styling to this page?", | |
| + | "parts": [ | |
| + | { | |
| + | "range": { | |
| + | "start": 0, | |
| + | "endExclusive": 19 | |
| + | }, | |
| + | "editorRange": { | |
| + | "startLineNumber": 1, | |
| + | "startColumn": 1, | |
| + | "endLineNumber": 1, | |
| + | "endColumn": 20 | |
| + | }, | |
| + | "text": "Can you add some CSS styling to this page?", | |
| + | "kind": "text" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | "variableData": { | |
| + | "variables": [] | |
| + | }, | |
| + | "response": [ | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "requestId": "request_demo_003", | |
| + | "message": { | |
| + | "text": "Add some JavaScript interactivity.", | |
| + | "parts": [ | |
| + | { | |
| + | "range": { | |
| + | "start": 0, | |
| + | "endExclusive": 18 | |
| + | }, | |
| + | "editorRange": { | |
| + | "startLineNumber": 1, | |
| + | "startColumn": 1, | |
| + | "endLineNumber": 1, | |
| + | "endColumn": 19 | |
| + | }, | |
| + | "text": "Add some JavaScript interactivity.", | |
| + | "kind": "text" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | "variableData": { | |
| + | "variables": [] | |
| + | }, | |
| + | "response": [ | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | { | |
| + | "kind": "prepareToolInvocation", | |
| + | "toolName": "copilot_createFile" | |
| + | }, | |
| + | { | |
| + | "kind": "toolInvocationSerialized", | |
| + | "invocationMessage": { | |
| + | "value": "\u6b63\u5728\u521b\u5efa JavaScript \u6587\u4ef6", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | "pastTenseMessage": { | |
| + | "value": "\u5df2\u521b\u5efa JavaScript \u6587\u4ef6\uff0c\u5305\u542b\u4ea4\u4e92\u529f\u80fd", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | "isConfirmed": true, | |
| + | "isComplete": true, | |
| + | "toolCallId": "demo_tool_002", | |
| + | "toolId": "copilot_createFile" | |
| + | }, | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "requestId": "request_demo_004", | |
| + | "message": { | |
| + | "text": "How can I make the page responsive?", | |
| + | "parts": [ | |
| + | { | |
| + | "range": { | |
| + | "start": 0, | |
| + | "endExclusive": 12 | |
| + | }, | |
| + | "editorRange": { | |
| + | "startLineNumber": 1, | |
| + | "startColumn": 1, | |
| + | "endLineNumber": 1, | |
| + | "endColumn": 13 | |
| + | }, | |
| + | "text": "How can I make the page responsive?", | |
| + | "kind": "text" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | "variableData": { | |
| + | "variables": [] | |
| + | }, | |
| + | "response": [ | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "requestId": "request_demo_005", | |
| + | "message": { | |
| + | "text": "Optimize the page for performance.", | |
| + | "parts": [ | |
| + | { | |
| + | "range": { | |
| + | "start": 0, | |
| + | "endExclusive": 9 | |
| + | }, | |
| + | "editorRange": { | |
| + | "startLineNumber": 1, | |
| + | "startColumn": 1, | |
| + | "endLineNumber": 1, | |
| + | "endColumn": 10 | |
| + | }, | |
| + | "text": "Optimize the page for performance.", | |
| + | "kind": "text" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | "variableData": { | |
| + | "variables": [] | |
| + | }, | |
| + | "response": [ | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | }, | |
| + | { | |
| + | "value": "Here is the updated page with your requested change.", | |
| + | "supportThemeIcons": false, | |
| + | "supportHtml": false | |
| + | } | |
| + | ] | |
| + | } | |
| + | ] | |
| + | } |
| @@ -0,0 +1,62 @@ | ||
| + | { | |
| + | "index": 1, | |
| + | "id": "sess-7f3a2b10", | |
| + | "title": "Add pagination to the users table", | |
| + | "createdAt": "2026-05-04T14:02:11.000Z", | |
| + | "lastUpdatedAt": "2026-05-04T14:19:48.000Z", | |
| + | "messageCount": 6, | |
| + | "workspaceId": "ws-9c21", | |
| + | "workspacePath": "/home/dev/acme-dashboard", | |
| + | "source": "global", | |
| + | "activeBranchBubbleIds": ["b1", "b2", "b3", "b4", "b5", "b6"], | |
| + | "messages": [ | |
| + | { | |
| + | "id": "b1", | |
| + | "role": "user", | |
| + | "content": "Add pagination to the users table in src/pages/Users.tsx, 25 rows per page.", | |
| + | "timestamp": "2026-05-04T14:02:11.000Z", | |
| + | "codeBlocks": [] | |
| + | }, | |
| + | { | |
| + | "id": "b2", | |
| + | "role": "assistant", | |
| + | "content": "I'll add a paginated table with a page size of 25 and wire up the controls.", | |
| + | "timestamp": "2026-05-04T14:02:40.000Z", | |
| + | "codeBlocks": [], | |
| + | "model": "claude-3.5-sonnet", | |
| + | "toolCalls": [ | |
| + | { "name": "edit_file", "filePath": "/home/dev/acme-dashboard/src/pages/Users.tsx", "args": { "path": "src/pages/Users.tsx" } } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "id": "b3", | |
| + | "role": "user", | |
| + | "content": "The page buttons overflow on mobile. Make them wrap.", | |
| + | "timestamp": "2026-05-04T14:09:02.000Z", | |
| + | "codeBlocks": [] | |
| + | }, | |
| + | { | |
| + | "id": "b4", | |
| + | "role": "assistant", | |
| + | "content": "Wrapped the pagination controls with a flex container that wraps below 480px.", | |
| + | "timestamp": "2026-05-04T14:09:31.000Z", | |
| + | "codeBlocks": [], | |
| + | "model": "claude-3.5-sonnet" | |
| + | }, | |
| + | { | |
| + | "id": "b5", | |
| + | "role": "user", | |
| + | "content": "Now also persist the current page in the URL query string.", | |
| + | "timestamp": "2026-05-04T14:16:20.000Z", | |
| + | "codeBlocks": [] | |
| + | }, | |
| + | { | |
| + | "id": "b6", | |
| + | "role": "assistant", | |
| + | "content": "Done. The page index is now synced to the ?page= query parameter.", | |
| + | "timestamp": "2026-05-04T14:16:55.000Z", | |
| + | "codeBlocks": [], | |
| + | "model": "claude-3.5-sonnet" | |
| + | } | |
| + | ] | |
| + | } |
| @@ -0,0 +1,145 @@ | ||
| + | { | |
| + | "sessionId": "anon-session-gemini-demo", | |
| + | "projectHash": "anon-project-hash", | |
| + | "startTime": "2026-04-01T22:01:58.817Z", | |
| + | "lastUpdated": "2026-04-06T20:33:07.995Z", | |
| + | "messages": [ | |
| + | { | |
| + | "id": "anon-user-0", | |
| + | "timestamp": "2026-04-01T22:02:01.193Z", | |
| + | "type": "user", | |
| + | "content": [ | |
| + | { | |
| + | "text": "Add a health-check endpoint at GET /healthz that returns 200 and the build version." | |
| + | } | |
| + | ], | |
| + | "displayContent": [ | |
| + | { | |
| + | "text": "**Objective:** Implement the core data models, loading logic, and registry for the \"Agent Teams\" feature in Gemini CLI.\n\n**Context:**\n- Agent Teams are collections of sub-agents paired with orchestration instructions (`TEAM.md`).\n- A team is defined by a directory in `.gemini/teams/<team-name>/` containing a `TEAM.md` (metadata + instructions) and an `agents/` sub-directory.\n- You are on branch `feature/agent-teams`.\n\n**Instructions:**\nFollow the detailed specification in **`plans/TASK-01.md`** and **`plans/agent-teams.md`** strictly.\n\n**Key Implementation Points:**\n1. **Types:** Update `packages/core/src/agents/types.ts` to include the `TeamDefinition` interface.\n2. **Loader:** Create `packages/core/src/agents/teamLoader.ts`. Reuse the existing `parseAgentMarkdown` and `loadAgentsFromDirectory` logic from `agentLoader.ts` to ensure consistency.\n3. **Registry:** Create `packages/core/src/agents/teamRegistry.ts`. It must manage the lifecycle of team discovery and track the `activeTeam`.\n4. **Integration:** Ensure that agents belonging to a team are also registered in the global `AgentRegistry` so they are available as `SubagentTool`s.\n\n**Engineering Standards:**\n- Maintain the existing coding style and license headers (Apache-2.0).\n- Prioritize type safety and use `zod` for validation where applicable.\n- **Testing is mandatory:** Create unit tests in `packages/core/src/agents/teamLoader.test.ts` and `packages/core/src/agents/teamRegistry.test.ts`.\n\n**Verification:**\nRun `npm test -w @google/gemini-cli-core` to verify the new services.\n" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "id": "anon-gemini-0", | |
| + | "timestamp": "2026-04-01T22:02:04.331Z", | |
| + | "type": "gemini", | |
| + | "content": [ | |
| + | { | |
| + | "text": "Added the handler and wired the route." | |
| + | } | |
| + | ], | |
| + | "thoughts": [ | |
| + | { | |
| + | "subject": "plan", | |
| + | "description": "redacted" | |
| + | } | |
| + | ], | |
| + | "tokens": { | |
| + | "input": 18753, | |
| + | "output": 190, | |
| + | "cached": 0, | |
| + | "thoughts": 133, | |
| + | "tool": 0, | |
| + | "total": 19076 | |
| + | }, | |
| + | "model": "gemini-3-flash-preview", | |
| + | "toolCalls": [ | |
| + | { | |
| + | "id": "nn28ue0q", | |
| + | "name": "update_topic", | |
| + | "args": { | |
| + | "file_path": "src/server/health.ts" | |
| + | }, | |
| + | "result": "redacted", | |
| + | "status": "success", | |
| + | "timestamp": "2026-04-01T22:02:04.398Z", | |
| + | "resultDisplay": "redacted", | |
| + | "description": "redacted", | |
| + | "displayName": "Update Topic Context", | |
| + | "renderOutputAsMarkdown": true | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "id": "anon-user-1", | |
| + | "timestamp": "2026-04-01T22:02:01.193Z", | |
| + | "type": "user", | |
| + | "content": [ | |
| + | { | |
| + | "text": "The /healthz handler should also report uptime in seconds." | |
| + | } | |
| + | ], | |
| + | "displayContent": [ | |
| + | { | |
| + | "text": "**Objective:** Implement the core data models, loading logic, and registry for the \"Agent Teams\" feature in Gemini CLI.\n\n**Context:**\n- Agent Teams are collections of sub-agents paired with orchestration instructions (`TEAM.md`).\n- A team is defined by a directory in `.gemini/teams/<team-name>/` containing a `TEAM.md` (metadata + instructions) and an `agents/` sub-directory.\n- You are on branch `feature/agent-teams`.\n\n**Instructions:**\nFollow the detailed specification in **`plans/TASK-01.md`** and **`plans/agent-teams.md`** strictly.\n\n**Key Implementation Points:**\n1. **Types:** Update `packages/core/src/agents/types.ts` to include the `TeamDefinition` interface.\n2. **Loader:** Create `packages/core/src/agents/teamLoader.ts`. Reuse the existing `parseAgentMarkdown` and `loadAgentsFromDirectory` logic from `agentLoader.ts` to ensure consistency.\n3. **Registry:** Create `packages/core/src/agents/teamRegistry.ts`. It must manage the lifecycle of team discovery and track the `activeTeam`.\n4. **Integration:** Ensure that agents belonging to a team are also registered in the global `AgentRegistry` so they are available as `SubagentTool`s.\n\n**Engineering Standards:**\n- Maintain the existing coding style and license headers (Apache-2.0).\n- Prioritize type safety and use `zod` for validation where applicable.\n- **Testing is mandatory:** Create unit tests in `packages/core/src/agents/teamLoader.test.ts` and `packages/core/src/agents/teamRegistry.test.ts`.\n\n**Verification:**\nRun `npm test -w @google/gemini-cli-core` to verify the new services.\n" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "id": "anon-gemini-1", | |
| + | "timestamp": "2026-04-01T22:02:04.331Z", | |
| + | "type": "gemini", | |
| + | "content": [ | |
| + | { | |
| + | "text": "Included process uptime in the JSON payload." | |
| + | } | |
| + | ], | |
| + | "thoughts": [ | |
| + | { | |
| + | "subject": "plan", | |
| + | "description": "redacted" | |
| + | } | |
| + | ], | |
| + | "tokens": { | |
| + | "input": 18753, | |
| + | "output": 190, | |
| + | "cached": 0, | |
| + | "thoughts": 133, | |
| + | "tool": 0, | |
| + | "total": 19076 | |
| + | }, | |
| + | "model": "gemini-3-flash-preview" | |
| + | }, | |
| + | { | |
| + | "id": "anon-user-2", | |
| + | "timestamp": "2026-04-01T22:02:01.193Z", | |
| + | "type": "user", | |
| + | "content": [ | |
| + | { | |
| + | "text": "Write a unit test for the health-check endpoint." | |
| + | } | |
| + | ], | |
| + | "displayContent": [ | |
| + | { | |
| + | "text": "**Objective:** Implement the core data models, loading logic, and registry for the \"Agent Teams\" feature in Gemini CLI.\n\n**Context:**\n- Agent Teams are collections of sub-agents paired with orchestration instructions (`TEAM.md`).\n- A team is defined by a directory in `.gemini/teams/<team-name>/` containing a `TEAM.md` (metadata + instructions) and an `agents/` sub-directory.\n- You are on branch `feature/agent-teams`.\n\n**Instructions:**\nFollow the detailed specification in **`plans/TASK-01.md`** and **`plans/agent-teams.md`** strictly.\n\n**Key Implementation Points:**\n1. **Types:** Update `packages/core/src/agents/types.ts` to include the `TeamDefinition` interface.\n2. **Loader:** Create `packages/core/src/agents/teamLoader.ts`. Reuse the existing `parseAgentMarkdown` and `loadAgentsFromDirectory` logic from `agentLoader.ts` to ensure consistency.\n3. **Registry:** Create `packages/core/src/agents/teamRegistry.ts`. It must manage the lifecycle of team discovery and track the `activeTeam`.\n4. **Integration:** Ensure that agents belonging to a team are also registered in the global `AgentRegistry` so they are available as `SubagentTool`s.\n\n**Engineering Standards:**\n- Maintain the existing coding style and license headers (Apache-2.0).\n- Prioritize type safety and use `zod` for validation where applicable.\n- **Testing is mandatory:** Create unit tests in `packages/core/src/agents/teamLoader.test.ts` and `packages/core/src/agents/teamRegistry.test.ts`.\n\n**Verification:**\nRun `npm test -w @google/gemini-cli-core` to verify the new services.\n" | |
| + | } | |
| + | ] | |
| + | }, | |
| + | { | |
| + | "id": "anon-gemini-2", | |
| + | "timestamp": "2026-04-01T22:02:04.331Z", | |
| + | "type": "gemini", | |
| + | "content": [ | |
| + | { | |
| + | "text": "Added a test asserting status 200 and the version field." | |
| + | } | |
| + | ], | |
| + | "thoughts": [ | |
| + | { | |
| + | "subject": "plan", | |
| + | "description": "redacted" | |
| + | } | |
| + | ], | |
| + | "tokens": { | |
| + | "input": 18753, | |
| + | "output": 190, | |
| + | "cached": 0, | |
| + | "thoughts": 133, | |
| + | "tool": 0, | |
| + | "total": 19076 | |
| + | }, | |
| + | "model": "gemini-3-flash-preview" | |
| + | } | |
| + | ], | |
| + | "kind": "main" | |
| + | } |
| @@ -0,0 +1,13 @@ | ||
| + | { | |
| + | "timestamp": "2026-05-09T18:30:00.000Z", | |
| + | "model": "grok-4", | |
| + | "messageCount": 6, | |
| + | "conversation": [ | |
| + | { "role": "system", "content": "You are a helpful coding assistant." }, | |
| + | { "role": "user", "content": "Write a Python function that returns the nth Fibonacci number iteratively." }, | |
| + | { "role": "assistant", "content": "Here is an iterative implementation that runs in O(n) time and O(1) space." }, | |
| + | { "role": "user", "content": "Add memoization and handle negative inputs by raising ValueError." }, | |
| + | { "role": "assistant", "content": "Updated the function to cache results and validate the input." }, | |
| + | { "role": "user", "content": "Now write a quick pytest covering n=0, n=1, n=10 and a negative case." } | |
| + | ] | |
| + | } |