| @@ -2,6 +2,8 @@ import { | ||
| newSession, | ||
| finalizeSession, | ||
| pushTurn, | ||
| + | addAction, | |
| + | addThinking, | |
| flattenParts, | ||
| looksSynthetic, | ||
| readJsonl, | ||
| @@ -27,6 +29,7 @@ export function parseCodex(text, path, sessionId) { | ||
| const session = newSession(path, sessionId); | ||
| const records = readJsonl(text); | ||
| let turn = 0; | ||
| + | let currentModel = null; | |
| for (const rec of records) { | ||
| const ts = rec.timestamp || null; | ||
| @@ -55,6 +58,17 @@ export function parseCodex(text, path, sessionId) { | ||
| session.stats.toolUses++; | ||
| const file = filePathFromArgs(payload.arguments); | ||
| if (file) session.stats.filesTouched.add(file); | ||
| + | addAction(session, { | |
| + | tool: payload.name || null, | |
| + | file: file || null, | |
| + | command: commandFromArgs(payload.name, payload.arguments), | |
| + | model: currentModel, | |
| + | }); | |
| + | continue; | |
| + | } | |
| + | ||
| + | if (rec.type === 'response_item' && payload.type === 'reasoning') { | |
| + | addThinking(session); | |
| continue; | ||
| } | ||
| @@ -69,12 +83,27 @@ export function parseCodex(text, path, sessionId) { | ||
| if (rec.type === 'turn_context' && payload.model) { | ||
| session.stats.models.add(payload.model); | ||
| + | currentModel = payload.model; | |
| } | ||
| } | ||
| return finalizeSession(session); | ||
| } | ||
| + | function commandFromArgs(name, args) { | |
| + | if (!/exec|shell|bash|run|terminal|command/i.test(name || '')) return null; | |
| + | if (!args || typeof args !== 'string') return null; | |
| + | let parsed; | |
| + | try { | |
| + | parsed = JSON.parse(args); | |
| + | } catch { | |
| + | return null; | |
| + | } | |
| + | const cmd = parsed.command || parsed.cmd; | |
| + | if (Array.isArray(cmd)) return cmd.join(' '); | |
| + | return typeof cmd === 'string' ? cmd : null; | |
| + | } | |
| + | ||
| function filePathFromArgs(args) { | ||
| if (!args || typeof args !== 'string') return null; | ||
| let parsed; |
| @@ -53,7 +53,7 @@ export function pushTurn(session, idx, text, ts, { hasImage = false, hadToolResu | ||
| session.leafUuid = uuid; | ||
| session._lastUserUuid = uuid; | ||
| session.stats.userLines++; | ||
| - | session.prompts.push({ | |
| + | const prompt = { | |
| uuid, | ||
| parentUuid, | ||
| ts: ts || null, | ||
| @@ -61,11 +61,23 @@ export function pushTurn(session, idx, text, ts, { hasImage = false, hadToolResu | ||
| hasImage, | ||
| hadToolResultContext, | ||
| afterInterruption: false, | ||
| - | }); | |
| + | actions: [], | |
| + | thinking: 0, | |
| + | }; | |
| + | session.prompts.push(prompt); | |
| + | session._currentPrompt = prompt; | |
| noteTimestamp(session, ts); | ||
| return uuid; | ||
| } | ||
| + | export function addAction(session, action) { | |
| + | if (session._currentPrompt && action) session._currentPrompt.actions.push(action); | |
| + | } | |
| + | ||
| + | export function addThinking(session, n = 1) { | |
| + | if (session._currentPrompt) session._currentPrompt.thinking += n; | |
| + | } | |
| + | ||
| export function flattenParts(parts) { | ||
| if (typeof parts === 'string') return parts; | ||
| if (!Array.isArray(parts)) { |
| @@ -7,6 +7,7 @@ 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'; | ||
| + | import { analyzeTree } from '../src/analyze.js'; | |
| const DIR = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'adapters'); | ||
| const fx = (name) => join(DIR, name); | ||
| @@ -135,3 +136,29 @@ test('adaptFrom rejects an unknown tool name', () => { | ||
| assert.throws(() => adaptFrom('notatool', '{}', 'x.json'), /unknown/); | ||
| assert.ok(TOOLS.includes('codex') && TOOLS.includes('cursor')); | ||
| }); | ||
| + | ||
| + | test('codex import emits actions that drive a verified security signal and model attribution', () => { | |
| + | const jsonl = [ | |
| + | { type: 'session_meta', timestamp: '2026-06-12T10:00:00Z', payload: { id: 'cdx1', originator: 'codex_cli_rs', cwd: '/repo', cli_version: '0.139.0' } }, | |
| + | { type: 'turn_context', timestamp: '2026-06-12T10:00:01Z', payload: { model: 'gpt-5.5', cwd: '/repo' } }, | |
| + | { type: 'response_item', timestamp: '2026-06-12T10:00:02Z', payload: { type: 'message', role: 'user', content: [{ type: 'text', text: 'Add rate limiting to the checkout endpoint' }] } }, | |
| + | { type: 'response_item', timestamp: '2026-06-12T10:00:03Z', payload: { type: 'reasoning', summary: [] } }, | |
| + | { type: 'response_item', timestamp: '2026-06-12T10:00:05Z', payload: { type: 'function_call', name: 'apply_patch', arguments: JSON.stringify({ path: 'src/auth/session.ts' }), call_id: 'c1' } }, | |
| + | { type: 'response_item', timestamp: '2026-06-12T10:00:06Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'Edited session.ts' }] } }, | |
| + | ].map((r) => JSON.stringify(r)).join('\n'); | |
| + | ||
| + | const sessions = adaptFrom('codex', jsonl, fx('codex-synth.jsonl')); | |
| + | const s = sessions[0]; | |
| + | assert.equal(s.prompts[0].actions.length, 1); | |
| + | assert.equal(s.prompts[0].actions[0].file, 'src/auth/session.ts'); | |
| + | assert.equal(s.prompts[0].actions[0].model, 'gpt-5.5'); | |
| + | assert.equal(s.prompts[0].thinking, 1); | |
| + | ||
| + | const { tree } = pipeline(sessions); | |
| + | const analysis = analyzeTree(tree); | |
| + | const sec = analysis.failures.find((f) => f.type === 'security_or_privacy_risk' && f.tier === 'verified'); | |
| + | assert.ok(sec, 'a codex import should now produce a verified security signal'); | |
| + | assert.equal(sec.model, 'gpt-5.5'); | |
| + | assert.deepEqual(analysis.summary.models, ['gpt-5.5']); | |
| + | assert.ok(analysis.summary.thinkingBlocks >= 1); | |
| + | }); |