Zion Boggan
repos/TreeTrace/src/adapters/cursor.js
zionboggan.com ↗
170 lines · javascript
History for this file →
1
import { newSession, finalizeSession, pushTurn, addAction, looksSynthetic, noteAssistantRefusal } from './shared.js';
2
 
3
function parseCursorParams(tfd) {
4
  const raw = tfd && (tfd.params || tfd.rawArgs);
5
  if (!raw) return null;
6
  if (typeof raw === 'object') return raw;
7
  if (typeof raw === 'string') {
8
    try {
9
      return JSON.parse(raw);
10
    } catch {
11
      return null;
12
    }
13
  }
14
  return null;
15
}
16
 
17
function cursorToolFile(tfd) {
18
  const p = parseCursorParams(tfd);
19
  return (p && (p.file_path || p.path || p.target_file || p.relativePath)) || null;
20
}
21
 
22
function cursorToolCommand(tfd) {
23
  const p = parseCursorParams(tfd);
24
  return p && typeof p.command === 'string' ? p.command : null;
25
}
26
 
27
function isUserBubble(bubble) {
28
  if (bubble.type === 1 || bubble.type === 'user') return true;
29
  if (bubble.type === 2 || bubble.type === 'ai' || bubble.type === 'assistant') return false;
30
  if (typeof bubble.role === 'string') return bubble.role === 'user';
31
  return false;
32
}
33
 
34
function bubbleText(bubble) {
35
  if (typeof bubble.text === 'string') return bubble.text;
36
  if (typeof bubble.content === 'string') return bubble.content;
37
  if (typeof bubble.richText === 'string') return bubble.richText;
38
  return '';
39
}
40
 
41
function collectBubbles(parsed) {
42
  if (Array.isArray(parsed)) return parsed;
43
  if (Array.isArray(parsed.bubbles)) return parsed.bubbles;
44
  if (Array.isArray(parsed.tabs)) {
45
    const out = [];
46
    for (const tab of parsed.tabs) {
47
      if (tab && Array.isArray(tab.bubbles)) out.push(...tab.bubbles);
48
      else if (tab && Array.isArray(tab.messages)) out.push(...tab.messages);
49
    }
50
    return out;
51
  }
52
  if (Array.isArray(parsed.conversation)) return parsed.conversation;
53
  if (Array.isArray(parsed.messages)) return parsed.messages;
54
  return null;
55
}
56
 
57
function isExportedSession(parsed) {
58
  return Boolean(
59
    parsed &&
60
      !Array.isArray(parsed) &&
61
      Array.isArray(parsed.messages) &&
62
      parsed.messages.some((m) => m && typeof m.role === 'string' && 'content' in m)
63
  );
64
}
65
 
66
function parseExportedSession(parsed, path, sessionId) {
67
  const session = newSession(path, parsed.id || parsed.sessionId || sessionId);
68
  if (parsed.title) session.title = parsed.title;
69
  let turn = 0;
70
  for (const msg of parsed.messages) {
71
    if (!msg) continue;
72
    const ts = msg.timestamp ? new Date(msg.timestamp).toISOString() : null;
73
    if (msg.role === 'user') {
74
      const text = typeof msg.content === 'string' ? msg.content : '';
75
      if (looksSynthetic(text)) continue;
76
      pushTurn(session, ++turn, text, ts);
77
    } else if (msg.role === 'assistant') {
78
      session.stats.assistantLines++;
79
      if (msg.model) session.stats.models.add(msg.model);
80
      if (typeof msg.content === 'string') noteAssistantRefusal(session, msg.content);
81
      if (Array.isArray(msg.toolCalls)) {
82
        for (const call of msg.toolCalls) {
83
          session.stats.toolUses++;
84
          const file = call && (call.filePath || (call.args && (call.args.file_path || call.args.path)));
85
          if (typeof file === 'string') session.stats.filesTouched.add(file);
86
          addAction(session, {
87
            tool: (call && call.name) || null,
88
            file: typeof file === 'string' ? file : null,
89
            command: call && call.args && typeof call.args.command === 'string' ? call.args.command : null,
90
            model: msg.model || null,
91
          });
92
        }
93
      }
94
    }
95
  }
96
  return finalizeSession(session);
97
}
98
 
99
function promptList(parsed) {
100
  if (Array.isArray(parsed.prompts)) return parsed.prompts;
101
  if (parsed['aiService.prompts'] && Array.isArray(parsed['aiService.prompts'])) {
102
    return parsed['aiService.prompts'];
103
  }
104
  return null;
105
}
106
 
107
export function detectCursor(parsed) {
108
  if (!parsed || typeof parsed !== 'object') return false;
109
  if (parsed.cursorExport || parsed._tool === 'cursor') return true;
110
  if (isExportedSession(parsed) && (parsed.workspaceId !== undefined || parsed.index !== undefined || parsed.activeBranchBubbleIds !== undefined)) {
111
    return true;
112
  }
113
  if (Array.isArray(parsed.tabs)) return true;
114
  if (promptList(parsed)) return true;
115
  const bubbles = collectBubbles(parsed);
116
  if (Array.isArray(bubbles) && bubbles.length) {
117
    return bubbles.some((b) => b && (b.bubbleId !== undefined || b.type === 1 || b.type === 2 || b.type === 'ai'));
118
  }
119
  return false;
120
}
121
 
122
export function parseCursor(parsed, path, sessionId) {
123
  if (!parsed || typeof parsed !== 'object') {
124
    return finalizeSession(newSession(path, sessionId));
125
  }
126
  const session = newSession(path, (parsed && parsed.composerId) || (parsed && parsed.sessionId) || sessionId);
127
  if (parsed && parsed.title) session.title = parsed.title;
128
  let turn = 0;
129
 
130
  if (isExportedSession(parsed)) {
131
    return parseExportedSession(parsed, path, sessionId);
132
  }
133
 
134
  const prompts = promptList(parsed);
135
  if (prompts) {
136
    for (const p of prompts) {
137
      const text = typeof p === 'string' ? p : p && typeof p.text === 'string' ? p.text : '';
138
      if (looksSynthetic(text)) continue;
139
      pushTurn(session, ++turn, text, null);
140
    }
141
    return finalizeSession(session);
142
  }
143
 
144
  const bubbles = collectBubbles(parsed) || [];
145
  for (const bubble of bubbles) {
146
    if (!bubble) continue;
147
    if (isUserBubble(bubble)) {
148
      const text = bubbleText(bubble);
149
      if (looksSynthetic(text)) continue;
150
      const ts = bubble.createdAt ? new Date(bubble.createdAt).toISOString() : null;
151
      pushTurn(session, ++turn, text, ts);
152
    } else {
153
      session.stats.assistantLines++;
154
      noteAssistantRefusal(session, bubbleText(bubble));
155
      if (bubble.toolFormerData) {
156
        session.stats.toolUses++;
157
        const tfd = bubble.toolFormerData;
158
        const file = cursorToolFile(tfd);
159
        if (typeof file === 'string') session.stats.filesTouched.add(file);
160
        addAction(session, {
161
          tool: tfd.name || null,
162
          file: file || null,
163
          command: cursorToolCommand(tfd),
164
          model: bubble.model || null,
165
        });
166
      }
167
    }
168
  }
169
  return finalizeSession(session);
170
}