spiracha 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -12
- package/README.md +37 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +17 -10
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
- package/apps/ui/src/components/app-shell.tsx +45 -3
- package/apps/ui/src/components/data-table.tsx +17 -1
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +77 -62
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +4 -1
- package/apps/ui/src/lib/antigravity-server.ts +70 -17
- package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
- package/apps/ui/src/lib/claude-code-server.ts +14 -4
- package/apps/ui/src/lib/codex-queries.ts +3 -3
- package/apps/ui/src/lib/codex-server.ts +3 -3
- package/apps/ui/src/lib/cursor-server.ts +41 -69
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- package/apps/ui/src/lib/grok-server.ts +2 -3
- package/apps/ui/src/lib/kiro-server.ts +14 -2
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +13 -2
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +1 -1
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
- package/apps/ui/src/routeTree.gen.ts +282 -282
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +73 -19
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +29 -16
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
- package/apps/ui/src/routes/grok.index.tsx +2 -2
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
- package/apps/ui/src/routes/opencode.index.tsx +8 -17
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +26 -17
- package/src/client.ts +61 -16
- package/src/lib/antigravity-db.ts +236 -82
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +45 -19
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +2 -7
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +88 -33
- package/src/lib/codex-thread-parser.ts +78 -35
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-renderer.ts +49 -61
- package/src/lib/concurrency.ts +15 -3
- package/src/lib/conversation-api.ts +34 -31
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
- package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
- package/src/lib/conversation-data/codex-adapter.ts +91 -80
- package/src/lib/conversation-data/cursor-adapter.ts +48 -34
- package/src/lib/conversation-data/grok-adapter.ts +19 -6
- package/src/lib/conversation-data/index.ts +29 -10
- package/src/lib/conversation-data/kiro-adapter.ts +18 -5
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +43 -15
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +23 -18
- package/src/lib/conversation-zip-export.ts +20 -2
- package/src/lib/cursor-db.ts +34 -23
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +76 -34
- package/src/lib/grok-transcript-phase.ts +11 -2
- package/src/lib/grok-transcript.ts +15 -2
- package/src/lib/kiro-db.ts +161 -68
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/opencode-db.ts +4 -8
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +51 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +8 -3
- package/src/lib/ui-cache.ts +152 -17
- package/src/lib/ui-export-archive.ts +14 -55
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CodexThreadNotFoundError,
|
|
2
3
|
deleteCodexThread,
|
|
3
4
|
getThreadBrowseData,
|
|
4
5
|
listScopedThreads,
|
|
@@ -9,14 +10,22 @@ import { parseCodexTranscriptFile } from '../codex-thread-parser';
|
|
|
9
10
|
import type { ThreadRow } from '../codex-thread-types';
|
|
10
11
|
import { cleanInlineTitle } from '../shared';
|
|
11
12
|
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
13
|
+
import {
|
|
14
|
+
createConversationUiPath,
|
|
15
|
+
createDeepLinks,
|
|
16
|
+
createTextMessage,
|
|
17
|
+
finalizeMessages,
|
|
18
|
+
isWithinUpdatedWindow,
|
|
19
|
+
normalizeAssistantPhase,
|
|
20
|
+
normalizeRole,
|
|
21
|
+
toDateMs,
|
|
22
|
+
} from './adapter-helpers';
|
|
12
23
|
import { selectConversationMessages } from './message-selector';
|
|
13
24
|
import { getConversationPathMatch } from './path-match';
|
|
14
25
|
import type {
|
|
15
26
|
ConversationAdapter,
|
|
16
27
|
ConversationDetail,
|
|
17
28
|
ConversationMessage,
|
|
18
|
-
ConversationMessagePhase,
|
|
19
|
-
ConversationMessageRole,
|
|
20
29
|
ConversationPathMatch,
|
|
21
30
|
DeleteConversationOptions,
|
|
22
31
|
GetConversationOptions,
|
|
@@ -35,36 +44,6 @@ const toCreatedAtMs = (thread: ThreadRow) => {
|
|
|
35
44
|
return thread.created_at_ms ?? thread.created_at * 1000;
|
|
36
45
|
};
|
|
37
46
|
|
|
38
|
-
const buildCodexDeepLinks = (id: string) => ({
|
|
39
|
-
native: `codex://threads/${id}`,
|
|
40
|
-
spiracha: `spiracha://conversation/codex/${id}`,
|
|
41
|
-
ui: `/threads/${id}`,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const normalizeRole = (role: string): ConversationMessageRole => {
|
|
45
|
-
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
46
|
-
return role;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return 'unknown';
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const normalizeMessagePhase = (event: MessageEvent): ConversationMessagePhase => {
|
|
53
|
-
if (event.role !== 'assistant') {
|
|
54
|
-
return 'unknown';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (event.phase === 'final_answer' || event.phase === 'final') {
|
|
58
|
-
return 'final_answer';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (event.phase === 'commentary' || event.isHiddenByDefault) {
|
|
62
|
-
return 'commentary';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return 'final_answer';
|
|
66
|
-
};
|
|
67
|
-
|
|
68
47
|
const toMessageEventMessage = (event: MessageEvent): ConversationMessage | null => {
|
|
69
48
|
const text = event.text.trim();
|
|
70
49
|
if (!text) {
|
|
@@ -72,14 +51,17 @@ const toMessageEventMessage = (event: MessageEvent): ConversationMessage | null
|
|
|
72
51
|
}
|
|
73
52
|
|
|
74
53
|
return {
|
|
75
|
-
createdAtMs:
|
|
54
|
+
createdAtMs: toDateMs(event.timestamp),
|
|
76
55
|
id: `codex:${event.sequence}`,
|
|
77
56
|
metadata: {
|
|
78
57
|
model: event.model,
|
|
79
58
|
variant: event.variant,
|
|
80
59
|
},
|
|
81
60
|
order: event.sequence,
|
|
82
|
-
phase:
|
|
61
|
+
phase:
|
|
62
|
+
event.role === 'assistant'
|
|
63
|
+
? normalizeAssistantPhase(event.isHiddenByDefault ? 'commentary' : event.phase, 'unknown')
|
|
64
|
+
: 'unknown',
|
|
83
65
|
role: normalizeRole(event.role),
|
|
84
66
|
text,
|
|
85
67
|
};
|
|
@@ -87,37 +69,41 @@ const toMessageEventMessage = (event: MessageEvent): ConversationMessage | null
|
|
|
87
69
|
|
|
88
70
|
const toToolMessage = (event: ThreadEvent): ConversationMessage | null => {
|
|
89
71
|
if (event.kind === 'tool_call') {
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
72
|
+
return (
|
|
73
|
+
createTextMessage({
|
|
74
|
+
createdAtMs: toDateMs(event.timestamp),
|
|
75
|
+
id: `codex:${event.sequence}`,
|
|
76
|
+
metadata: {
|
|
77
|
+
callId: event.callId,
|
|
78
|
+
command: event.command,
|
|
79
|
+
name: event.name,
|
|
80
|
+
workdir: event.workdir,
|
|
81
|
+
},
|
|
82
|
+
order: event.sequence,
|
|
83
|
+
phase: 'tool_call',
|
|
84
|
+
role: 'tool',
|
|
85
|
+
text: event.command || event.name,
|
|
86
|
+
})[0] ?? null
|
|
87
|
+
);
|
|
104
88
|
}
|
|
105
89
|
|
|
106
90
|
if (event.kind === 'tool_output') {
|
|
107
91
|
const text = event.summary || event.outputText;
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
92
|
+
return (
|
|
93
|
+
createTextMessage({
|
|
94
|
+
createdAtMs: toDateMs(event.timestamp),
|
|
95
|
+
id: `codex:${event.sequence}`,
|
|
96
|
+
metadata: {
|
|
97
|
+
callId: event.callId,
|
|
98
|
+
exitCode: event.exitCode,
|
|
99
|
+
wallTime: event.wallTime,
|
|
100
|
+
},
|
|
101
|
+
order: event.sequence,
|
|
102
|
+
phase: 'tool_output',
|
|
103
|
+
role: 'tool',
|
|
104
|
+
text,
|
|
105
|
+
})[0] ?? null
|
|
106
|
+
);
|
|
121
107
|
}
|
|
122
108
|
|
|
123
109
|
return null;
|
|
@@ -125,6 +111,10 @@ const toToolMessage = (event: ThreadEvent): ConversationMessage | null => {
|
|
|
125
111
|
|
|
126
112
|
const toConversationMessage = (event: ThreadEvent): ConversationMessage | null => {
|
|
127
113
|
if (event.kind === 'message') {
|
|
114
|
+
if (event.isHiddenByDefault) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
128
118
|
return toMessageEventMessage(event);
|
|
129
119
|
}
|
|
130
120
|
|
|
@@ -132,7 +122,7 @@ const toConversationMessage = (event: ThreadEvent): ConversationMessage | null =
|
|
|
132
122
|
const text = event.summary.join('\n').trim();
|
|
133
123
|
return text
|
|
134
124
|
? {
|
|
135
|
-
createdAtMs:
|
|
125
|
+
createdAtMs: toDateMs(event.timestamp),
|
|
136
126
|
id: `codex:${event.sequence}`,
|
|
137
127
|
metadata: {
|
|
138
128
|
hasEncryptedContent: event.hasEncryptedContent,
|
|
@@ -149,22 +139,33 @@ const toConversationMessage = (event: ThreadEvent): ConversationMessage | null =
|
|
|
149
139
|
};
|
|
150
140
|
|
|
151
141
|
const readCodexMessages = async (thread: ThreadRow): Promise<ConversationMessage[]> => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
142
|
+
let transcript: Awaited<ReturnType<typeof parseCodexTranscriptFile>>;
|
|
143
|
+
try {
|
|
144
|
+
transcript = await runWithTranscriptLoadLimit(
|
|
145
|
+
() =>
|
|
146
|
+
parseCodexTranscriptFile(thread.rollout_path, {
|
|
147
|
+
includeRaw: false,
|
|
148
|
+
}),
|
|
149
|
+
{
|
|
150
|
+
id: thread.id,
|
|
151
|
+
path: thread.rollout_path,
|
|
152
|
+
source: 'codex-api',
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return finalizeMessages(
|
|
164
|
+
transcript.events.flatMap((event) => {
|
|
165
|
+
const message = toConversationMessage(event);
|
|
166
|
+
return message ? [message] : [];
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
168
169
|
};
|
|
169
170
|
|
|
170
171
|
const buildCodexConversation = async (
|
|
@@ -179,7 +180,12 @@ const buildCodexConversation = async (
|
|
|
179
180
|
|
|
180
181
|
return {
|
|
181
182
|
createdAtMs: toCreatedAtMs(thread),
|
|
182
|
-
deepLinks:
|
|
183
|
+
deepLinks: createDeepLinks(
|
|
184
|
+
'codex',
|
|
185
|
+
thread.id,
|
|
186
|
+
createConversationUiPath('threads', thread.id),
|
|
187
|
+
`codex://threads/${encodeURIComponent(thread.id)}`,
|
|
188
|
+
),
|
|
183
189
|
id: thread.id,
|
|
184
190
|
matches,
|
|
185
191
|
messageCount: options.includeMessages ? allMessages.length : null,
|
|
@@ -221,8 +227,13 @@ const listCodexConversationsForPath = async (
|
|
|
221
227
|
options: ListConversationsForPathOptions,
|
|
222
228
|
): Promise<ConversationDetail[]> => {
|
|
223
229
|
const dbPath = getCodexDbPath(options);
|
|
230
|
+
if (!(await Bun.file(dbPath).exists())) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
224
233
|
const threads = listScopedThreads(dbPath, null);
|
|
225
|
-
const matchedThreads = await filterThreadsForPath(threads, options.cwd)
|
|
234
|
+
const matchedThreads = (await filterThreadsForPath(threads, options.cwd)).filter(({ thread }) =>
|
|
235
|
+
isWithinUpdatedWindow(toTimestampMs(thread), options),
|
|
236
|
+
);
|
|
226
237
|
|
|
227
238
|
return Promise.all(
|
|
228
239
|
matchedThreads.map(({ matches, thread }) =>
|
|
@@ -240,7 +251,7 @@ const getCodexConversation = async (options: GetConversationOptions): Promise<Co
|
|
|
240
251
|
try {
|
|
241
252
|
browseData = getThreadBrowseData(dbPath, options.id);
|
|
242
253
|
} catch (error) {
|
|
243
|
-
if (error instanceof
|
|
254
|
+
if (error instanceof CodexThreadNotFoundError) {
|
|
244
255
|
return null;
|
|
245
256
|
}
|
|
246
257
|
|
|
@@ -12,9 +12,16 @@ import type {
|
|
|
12
12
|
} from '../cursor-exporter-types';
|
|
13
13
|
import { getCursorGlobalDbPath, resolveCursorUserDir } from '../cursor-exporter-types';
|
|
14
14
|
import { collectCursorThreadsForDeletion, isCursorRunning, pruneCursorThreads } from '../cursor-recovery';
|
|
15
|
+
import { getCursorTextBubblePhase, getFinalCursorAssistantTextBubbleIds } from '../cursor-transcript-phase';
|
|
15
16
|
import { cleanInlineTitle } from '../shared';
|
|
16
17
|
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createConversationUiPath,
|
|
20
|
+
createDeepLinks,
|
|
21
|
+
createTextMessage,
|
|
22
|
+
finalizeMessages,
|
|
23
|
+
isWithinUpdatedWindow,
|
|
24
|
+
} from './adapter-helpers';
|
|
18
25
|
import { selectConversationMessages } from './message-selector';
|
|
19
26
|
import { getFirstConversationPathMatch } from './path-match';
|
|
20
27
|
import type {
|
|
@@ -32,21 +39,11 @@ const CURSOR_CONVERSATION_HYDRATION_CONCURRENCY = 4;
|
|
|
32
39
|
const getUserDir = (options: { locations?: { cursorUserDir?: string } }) =>
|
|
33
40
|
options.locations?.cursorUserDir ?? resolveCursorUserDir();
|
|
34
41
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (options.updatedAfterMs !== undefined && updatedAtMs < options.updatedAfterMs) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
if (options.updatedBeforeMs !== undefined && updatedAtMs > options.updatedBeforeMs) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const bubbleToMessages = (bubble: CursorBubble, order: number): ConversationMessage[] => {
|
|
42
|
+
const bubbleToMessages = (
|
|
43
|
+
bubble: CursorBubble,
|
|
44
|
+
finalAssistantTextBubbleIds: Set<string>,
|
|
45
|
+
order: number,
|
|
46
|
+
): ConversationMessage[] => {
|
|
50
47
|
const thinking = createTextMessage({
|
|
51
48
|
createdAtMs: bubble.createdAtMs,
|
|
52
49
|
id: `${bubble.bubbleId}:thinking`,
|
|
@@ -59,27 +56,41 @@ const bubbleToMessages = (bubble: CursorBubble, order: number): ConversationMess
|
|
|
59
56
|
createdAtMs: bubble.createdAtMs,
|
|
60
57
|
id: bubble.bubbleId,
|
|
61
58
|
order,
|
|
62
|
-
phase: bubble
|
|
59
|
+
phase: getCursorTextBubblePhase(bubble, finalAssistantTextBubbleIds) ?? 'unknown',
|
|
63
60
|
role: bubble.kind === 'assistant' ? 'assistant' : bubble.kind === 'user' ? 'user' : 'unknown',
|
|
64
61
|
text: bubble.text,
|
|
65
62
|
});
|
|
66
|
-
const
|
|
63
|
+
const toolCall = bubble.toolCall
|
|
64
|
+
? createTextMessage({
|
|
65
|
+
createdAtMs: bubble.createdAtMs,
|
|
66
|
+
id: `${bubble.bubbleId}:tool_call`,
|
|
67
|
+
metadata: { callId: bubble.toolCall.callId, status: bubble.toolCall.status },
|
|
68
|
+
order,
|
|
69
|
+
phase: 'tool_call',
|
|
70
|
+
role: 'tool',
|
|
71
|
+
text: [bubble.toolCall.name, bubble.toolCall.argumentsText].filter(Boolean).join('\n'),
|
|
72
|
+
})
|
|
73
|
+
: [];
|
|
74
|
+
const toolOutput = bubble.toolCall
|
|
67
75
|
? createTextMessage({
|
|
68
76
|
createdAtMs: bubble.createdAtMs,
|
|
69
|
-
id: `${bubble.bubbleId}:
|
|
77
|
+
id: `${bubble.bubbleId}:tool_output`,
|
|
70
78
|
metadata: { callId: bubble.toolCall.callId, status: bubble.toolCall.status },
|
|
71
79
|
order,
|
|
72
|
-
phase:
|
|
80
|
+
phase: 'tool_output',
|
|
73
81
|
role: 'tool',
|
|
74
|
-
text: bubble.toolCall.resultText
|
|
82
|
+
text: bubble.toolCall.resultText,
|
|
75
83
|
})
|
|
76
84
|
: [];
|
|
77
85
|
|
|
78
|
-
return [...thinking, ...text, ...
|
|
86
|
+
return [...thinking, ...text, ...toolCall, ...toolOutput];
|
|
79
87
|
};
|
|
80
88
|
|
|
81
89
|
const transcriptToMessages = (transcript: CursorThreadTranscript) => {
|
|
82
|
-
|
|
90
|
+
const finalAssistantTextBubbleIds = getFinalCursorAssistantTextBubbleIds(transcript.bubbles);
|
|
91
|
+
return finalizeMessages(
|
|
92
|
+
transcript.bubbles.flatMap((bubble, order) => bubbleToMessages(bubble, finalAssistantTextBubbleIds, order)),
|
|
93
|
+
);
|
|
83
94
|
};
|
|
84
95
|
|
|
85
96
|
const getWorkspacePath = (group: CursorWorkspaceGroup) => {
|
|
@@ -111,7 +122,11 @@ const buildConversation = async (
|
|
|
111
122
|
|
|
112
123
|
return {
|
|
113
124
|
createdAtMs: thread.createdAtMs,
|
|
114
|
-
deepLinks: createDeepLinks(
|
|
125
|
+
deepLinks: createDeepLinks(
|
|
126
|
+
'cursor',
|
|
127
|
+
thread.composerId,
|
|
128
|
+
createConversationUiPath('cursor-threads', thread.composerId),
|
|
129
|
+
),
|
|
115
130
|
id: thread.composerId,
|
|
116
131
|
matches,
|
|
117
132
|
messageCount: options.includeMessages ? allMessages.length : thread.bubbleCount,
|
|
@@ -120,7 +135,6 @@ const buildConversation = async (
|
|
|
120
135
|
bubbleBytes: thread.bubbleBytes,
|
|
121
136
|
bucketId: thread.bucketId,
|
|
122
137
|
mode: thread.mode,
|
|
123
|
-
transcriptDirs: thread.transcriptDirs,
|
|
124
138
|
},
|
|
125
139
|
source: 'cursor',
|
|
126
140
|
title: cleanInlineTitle(thread.name),
|
|
@@ -132,7 +146,7 @@ const buildConversation = async (
|
|
|
132
146
|
|
|
133
147
|
const listCursorConversationsForPath = async (options: ListConversationsForPathOptions) => {
|
|
134
148
|
const userDir = getUserDir(options);
|
|
135
|
-
const groups = await listCursorWorkspaceGroups(userDir
|
|
149
|
+
const groups = await listCursorWorkspaceGroups(userDir);
|
|
136
150
|
const candidates: { group: CursorWorkspaceGroup; match: ConversationPathMatch; thread: CursorThreadSummary }[] = [];
|
|
137
151
|
|
|
138
152
|
for (const group of groups) {
|
|
@@ -142,10 +156,9 @@ const listCursorConversationsForPath = async (options: ListConversationsForPathO
|
|
|
142
156
|
}
|
|
143
157
|
const threads = await listCursorThreadsForGroup(group, userDir, {
|
|
144
158
|
includeTranscriptDirs: false,
|
|
145
|
-
updatedAfterMs: options.updatedAfterMs,
|
|
146
159
|
});
|
|
147
160
|
for (const thread of threads) {
|
|
148
|
-
if (!isWithinUpdatedWindow(thread, options)) {
|
|
161
|
+
if (!isWithinUpdatedWindow(thread.lastUpdatedAtMs, options)) {
|
|
149
162
|
continue;
|
|
150
163
|
}
|
|
151
164
|
|
|
@@ -175,20 +188,21 @@ const getCursorConversation = async (options: GetConversationOptions): Promise<C
|
|
|
175
188
|
return null;
|
|
176
189
|
};
|
|
177
190
|
|
|
178
|
-
const deleteCursorConversation = async (
|
|
191
|
+
export const deleteCursorConversation = async (
|
|
192
|
+
options: DeleteConversationOptions,
|
|
193
|
+
checkCursorRunning: () => Promise<boolean> = isCursorRunning,
|
|
194
|
+
) => {
|
|
179
195
|
const userDir = getUserDir(options);
|
|
180
|
-
if (
|
|
196
|
+
if (await checkCursorRunning()) {
|
|
181
197
|
throw new Error(
|
|
182
198
|
'Quit Cursor before deleting. It rewrites chat history on exit, which can resurrect deleted threads.',
|
|
183
199
|
);
|
|
184
200
|
}
|
|
185
201
|
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
202
|
+
const threads = await collectCursorThreadsForDeletion([options.id], userDir);
|
|
203
|
+
if (threads.length === 0) {
|
|
188
204
|
return { deletedFiles: [], deletedIds: [] };
|
|
189
205
|
}
|
|
190
|
-
|
|
191
|
-
const threads = await collectCursorThreadsForDeletion([options.id], userDir);
|
|
192
206
|
const deletedFiles = threads.flatMap((thread) => thread.transcriptDirs);
|
|
193
207
|
const result = await pruneCursorThreads(threads, true, userDir);
|
|
194
208
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { mapWithConcurrency } from '../concurrency';
|
|
1
2
|
import {
|
|
2
3
|
deleteGrokSession,
|
|
3
|
-
|
|
4
|
+
listGrokSessionTranscriptsForGroup,
|
|
4
5
|
listGrokWorkspaceGroups,
|
|
5
6
|
readGrokSessionTranscript,
|
|
6
7
|
resolveGrokSessionsDir,
|
|
@@ -15,9 +16,11 @@ import { getFinalGrokAssistantTextPartIds, getGrokTextPartPhase } from '../grok-
|
|
|
15
16
|
import { cleanInlineTitle } from '../shared';
|
|
16
17
|
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
17
18
|
import {
|
|
19
|
+
createConversationUiPath,
|
|
18
20
|
createDeepLinks,
|
|
19
21
|
createTextMessage,
|
|
20
22
|
finalizeMessages,
|
|
23
|
+
isWithinUpdatedWindow,
|
|
21
24
|
normalizeAssistantPhase,
|
|
22
25
|
normalizeRole,
|
|
23
26
|
} from './adapter-helpers';
|
|
@@ -33,6 +36,8 @@ import type {
|
|
|
33
36
|
ListConversationsForPathOptions,
|
|
34
37
|
} from './types';
|
|
35
38
|
|
|
39
|
+
const GROK_CONVERSATION_HYDRATION_CONCURRENCY = 4;
|
|
40
|
+
|
|
36
41
|
const getSessionsDir = (options: { locations?: { grokSessionsDir?: string } }) =>
|
|
37
42
|
options.locations?.grokSessionsDir ?? resolveGrokSessionsDir();
|
|
38
43
|
|
|
@@ -129,7 +134,11 @@ const buildConversation = async (
|
|
|
129
134
|
|
|
130
135
|
return {
|
|
131
136
|
createdAtMs: session.createdAtMs,
|
|
132
|
-
deepLinks: createDeepLinks(
|
|
137
|
+
deepLinks: createDeepLinks(
|
|
138
|
+
'grok',
|
|
139
|
+
session.sessionId,
|
|
140
|
+
createConversationUiPath('grok-sessions', session.sessionId),
|
|
141
|
+
),
|
|
133
142
|
id: session.sessionId,
|
|
134
143
|
matches,
|
|
135
144
|
messageCount: options.includeMessages ? allMessages.length : session.messageCount,
|
|
@@ -161,10 +170,14 @@ const listGrokConversationsForPath = async (options: ListConversationsForPathOpt
|
|
|
161
170
|
if (!match) {
|
|
162
171
|
continue;
|
|
163
172
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
const transcripts = (await listGrokSessionTranscriptsForGroup(group.key, sessionsDir)).filter((transcript) =>
|
|
174
|
+
isWithinUpdatedWindow(transcript.session.lastActiveAtMs, options),
|
|
175
|
+
);
|
|
176
|
+
conversations.push(
|
|
177
|
+
...(await mapWithConcurrency(transcripts, GROK_CONVERSATION_HYDRATION_CONCURRENCY, (transcript) =>
|
|
178
|
+
buildConversation(transcript.session, sessionsDir, [match], options, transcript),
|
|
179
|
+
)),
|
|
180
|
+
);
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
return conversations;
|
|
@@ -104,7 +104,7 @@ const getEnabledSources = (sources: ListConversationsForPathOptions['sources']):
|
|
|
104
104
|
return SOURCE_INFOS.map((sourceInfo) => sourceInfo.source);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
return sources;
|
|
107
|
+
return [...new Set(sources)];
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
const isAllSourcesRequest = (sources: ListConversationsForPathOptions['sources']) => !sources || sources === 'all';
|
|
@@ -118,8 +118,16 @@ const decodeCursor = (cursor: string | null | undefined) => {
|
|
|
118
118
|
return 0;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
const
|
|
122
|
-
|
|
121
|
+
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
122
|
+
if (!/^\d+$/u.test(decoded)) {
|
|
123
|
+
throw new Error('Invalid conversation pagination cursor.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parsed = Number(decoded);
|
|
127
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
128
|
+
throw new Error('Invalid conversation pagination cursor.');
|
|
129
|
+
}
|
|
130
|
+
return parsed;
|
|
123
131
|
};
|
|
124
132
|
|
|
125
133
|
const encodeCursor = (offset: number) => Buffer.from(String(offset), 'utf8').toString('base64url');
|
|
@@ -157,7 +165,7 @@ const sortConversations = (conversations: Awaited<ReturnType<ConversationAdapter
|
|
|
157
165
|
);
|
|
158
166
|
};
|
|
159
167
|
|
|
160
|
-
export const listConversationSources = async (): Promise<ConversationSourceInfo[]> => SOURCE_INFOS;
|
|
168
|
+
export const listConversationSources = async (): Promise<ConversationSourceInfo[]> => [...SOURCE_INFOS];
|
|
161
169
|
|
|
162
170
|
const listSourceConversationsForPath = async (
|
|
163
171
|
source: ConversationSource,
|
|
@@ -175,6 +183,10 @@ const listSourceConversationsForPath = async (
|
|
|
175
183
|
if (!ignoreSourceFailures) {
|
|
176
184
|
throw error;
|
|
177
185
|
}
|
|
186
|
+
console.warn(`[spiracha:conversation-data] skipped ${source} during all-source collection`, {
|
|
187
|
+
error: error instanceof Error ? error.message : String(error),
|
|
188
|
+
source,
|
|
189
|
+
});
|
|
178
190
|
return [];
|
|
179
191
|
}
|
|
180
192
|
};
|
|
@@ -314,14 +326,21 @@ const refFromPathSegmentAt = (segments: string[], index: number): ResolvedConver
|
|
|
314
326
|
};
|
|
315
327
|
|
|
316
328
|
const refFromPathSegments = (segments: string[]): ResolvedConversationRef | null => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
329
|
+
if (
|
|
330
|
+
segments[0] === 'api' &&
|
|
331
|
+
segments[1] === 'v1' &&
|
|
332
|
+
segments[2] === 'conversations' &&
|
|
333
|
+
(segments.length === 5 || (segments.length === 6 && segments[5] === 'export'))
|
|
334
|
+
) {
|
|
335
|
+
return refFromPathSegmentAt(segments, 2);
|
|
322
336
|
}
|
|
323
337
|
|
|
324
|
-
|
|
338
|
+
if (segments[0] === 'app') {
|
|
339
|
+
return refFromPathSegments(segments.slice(1));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const expectedLength = segments[0] === 'conversations' ? 3 : 2;
|
|
343
|
+
return segments.length === expectedLength ? refFromPathSegmentAt(segments, 0) : null;
|
|
325
344
|
};
|
|
326
345
|
|
|
327
346
|
const parseUrlRef = (ref: string): ResolvedConversationRef | null => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mapWithConcurrency } from '../concurrency';
|
|
1
2
|
import {
|
|
2
3
|
deleteKiroSession,
|
|
3
4
|
listKiroSessionsForGroup,
|
|
@@ -15,9 +16,11 @@ import { getFinalKiroAssistantMessageEntryIds, getKiroMessagePhase } from '../ki
|
|
|
15
16
|
import { cleanInlineTitle } from '../shared';
|
|
16
17
|
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
17
18
|
import {
|
|
19
|
+
createConversationUiPath,
|
|
18
20
|
createDeepLinks,
|
|
19
21
|
createTextMessage,
|
|
20
22
|
finalizeMessages,
|
|
23
|
+
isWithinUpdatedWindow,
|
|
21
24
|
normalizeAssistantPhase,
|
|
22
25
|
normalizeRole,
|
|
23
26
|
toDateMs,
|
|
@@ -34,6 +37,8 @@ import type {
|
|
|
34
37
|
ListConversationsForPathOptions,
|
|
35
38
|
} from './types';
|
|
36
39
|
|
|
40
|
+
const KIRO_CONVERSATION_HYDRATION_CONCURRENCY = 4;
|
|
41
|
+
|
|
37
42
|
const getSessionsDir = (options: { locations?: { kiroWorkspaceSessionsDir?: string } }) =>
|
|
38
43
|
options.locations?.kiroWorkspaceSessionsDir ?? resolveKiroWorkspaceSessionsDir();
|
|
39
44
|
|
|
@@ -98,7 +103,11 @@ const buildConversation = async (
|
|
|
98
103
|
|
|
99
104
|
return {
|
|
100
105
|
createdAtMs: session.createdAtMs,
|
|
101
|
-
deepLinks: createDeepLinks(
|
|
106
|
+
deepLinks: createDeepLinks(
|
|
107
|
+
'kiro',
|
|
108
|
+
session.sessionId,
|
|
109
|
+
createConversationUiPath('kiro-sessions', session.sessionId),
|
|
110
|
+
),
|
|
102
111
|
id: session.sessionId,
|
|
103
112
|
matches,
|
|
104
113
|
messageCount: options.includeMessages ? allMessages.length : session.messageCount,
|
|
@@ -128,10 +137,14 @@ const listKiroConversationsForPath = async (options: ListConversationsForPathOpt
|
|
|
128
137
|
if (!match) {
|
|
129
138
|
continue;
|
|
130
139
|
}
|
|
131
|
-
const sessions = await listKiroSessionsForGroup(group.key, sessionsDir)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
const sessions = (await listKiroSessionsForGroup(group.key, sessionsDir)).filter((session) =>
|
|
141
|
+
isWithinUpdatedWindow(session.lastActiveAtMs, options),
|
|
142
|
+
);
|
|
143
|
+
conversations.push(
|
|
144
|
+
...(await mapWithConcurrency(sessions, KIRO_CONVERSATION_HYDRATION_CONCURRENCY, (session) =>
|
|
145
|
+
buildConversation(session, sessionsDir, [match], options),
|
|
146
|
+
)),
|
|
147
|
+
);
|
|
135
148
|
}
|
|
136
149
|
|
|
137
150
|
return conversations;
|
|
@@ -14,12 +14,9 @@ const selectLastAssistantMessage = (messages: ConversationMessage[]) => {
|
|
|
14
14
|
return latestByOrder(messages.filter((message) => message.role === 'assistant'));
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// Not every integration can identify final answers. In that case, API list/export defaults still
|
|
18
|
-
// return the latest assistant message instead of silently omitting the conversation.
|
|
19
17
|
const selectLastFinalAnswer = (messages: ConversationMessage[]) => {
|
|
20
|
-
return (
|
|
21
|
-
|
|
22
|
-
selectLastAssistantMessage(messages)
|
|
18
|
+
return latestByOrder(
|
|
19
|
+
messages.filter((message) => message.role === 'assistant' && message.phase === 'final_answer'),
|
|
23
20
|
);
|
|
24
21
|
};
|
|
25
22
|
|