spiracha 2.0.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 +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -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 +13 -47
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- 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/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- 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-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- 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 +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- 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 +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- 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 +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- 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 +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- 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 +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- 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
|
@@ -11,8 +11,17 @@ import type {
|
|
|
11
11
|
CursorWorkspaceGroup,
|
|
12
12
|
} from '../cursor-exporter-types';
|
|
13
13
|
import { getCursorGlobalDbPath, resolveCursorUserDir } from '../cursor-exporter-types';
|
|
14
|
+
import { collectCursorThreadsForDeletion, isCursorRunning, pruneCursorThreads } from '../cursor-recovery';
|
|
15
|
+
import { getCursorTextBubblePhase, getFinalCursorAssistantTextBubbleIds } from '../cursor-transcript-phase';
|
|
14
16
|
import { cleanInlineTitle } from '../shared';
|
|
15
|
-
import {
|
|
17
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
18
|
+
import {
|
|
19
|
+
createConversationUiPath,
|
|
20
|
+
createDeepLinks,
|
|
21
|
+
createTextMessage,
|
|
22
|
+
finalizeMessages,
|
|
23
|
+
isWithinUpdatedWindow,
|
|
24
|
+
} from './adapter-helpers';
|
|
16
25
|
import { selectConversationMessages } from './message-selector';
|
|
17
26
|
import { getFirstConversationPathMatch } from './path-match';
|
|
18
27
|
import type {
|
|
@@ -20,6 +29,7 @@ import type {
|
|
|
20
29
|
ConversationDetail,
|
|
21
30
|
ConversationMessage,
|
|
22
31
|
ConversationPathMatch,
|
|
32
|
+
DeleteConversationOptions,
|
|
23
33
|
GetConversationOptions,
|
|
24
34
|
ListConversationsForPathOptions,
|
|
25
35
|
} from './types';
|
|
@@ -29,21 +39,11 @@ const CURSOR_CONVERSATION_HYDRATION_CONCURRENCY = 4;
|
|
|
29
39
|
const getUserDir = (options: { locations?: { cursorUserDir?: string } }) =>
|
|
30
40
|
options.locations?.cursorUserDir ?? resolveCursorUserDir();
|
|
31
41
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (options.updatedAfterMs !== undefined && updatedAtMs < options.updatedAfterMs) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
if (options.updatedBeforeMs !== undefined && updatedAtMs > options.updatedBeforeMs) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
return true;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const bubbleToMessages = (bubble: CursorBubble, order: number): ConversationMessage[] => {
|
|
42
|
+
const bubbleToMessages = (
|
|
43
|
+
bubble: CursorBubble,
|
|
44
|
+
finalAssistantTextBubbleIds: Set<string>,
|
|
45
|
+
order: number,
|
|
46
|
+
): ConversationMessage[] => {
|
|
47
47
|
const thinking = createTextMessage({
|
|
48
48
|
createdAtMs: bubble.createdAtMs,
|
|
49
49
|
id: `${bubble.bubbleId}:thinking`,
|
|
@@ -56,27 +56,41 @@ const bubbleToMessages = (bubble: CursorBubble, order: number): ConversationMess
|
|
|
56
56
|
createdAtMs: bubble.createdAtMs,
|
|
57
57
|
id: bubble.bubbleId,
|
|
58
58
|
order,
|
|
59
|
-
phase: bubble
|
|
59
|
+
phase: getCursorTextBubblePhase(bubble, finalAssistantTextBubbleIds) ?? 'unknown',
|
|
60
60
|
role: bubble.kind === 'assistant' ? 'assistant' : bubble.kind === 'user' ? 'user' : 'unknown',
|
|
61
61
|
text: bubble.text,
|
|
62
62
|
});
|
|
63
|
-
const
|
|
63
|
+
const toolCall = bubble.toolCall
|
|
64
64
|
? createTextMessage({
|
|
65
65
|
createdAtMs: bubble.createdAtMs,
|
|
66
|
-
id: `${bubble.bubbleId}:
|
|
66
|
+
id: `${bubble.bubbleId}:tool_call`,
|
|
67
67
|
metadata: { callId: bubble.toolCall.callId, status: bubble.toolCall.status },
|
|
68
68
|
order,
|
|
69
|
-
phase:
|
|
69
|
+
phase: 'tool_call',
|
|
70
70
|
role: 'tool',
|
|
71
|
-
text: bubble.toolCall.
|
|
71
|
+
text: [bubble.toolCall.name, bubble.toolCall.argumentsText].filter(Boolean).join('\n'),
|
|
72
|
+
})
|
|
73
|
+
: [];
|
|
74
|
+
const toolOutput = bubble.toolCall
|
|
75
|
+
? createTextMessage({
|
|
76
|
+
createdAtMs: bubble.createdAtMs,
|
|
77
|
+
id: `${bubble.bubbleId}:tool_output`,
|
|
78
|
+
metadata: { callId: bubble.toolCall.callId, status: bubble.toolCall.status },
|
|
79
|
+
order,
|
|
80
|
+
phase: 'tool_output',
|
|
81
|
+
role: 'tool',
|
|
82
|
+
text: bubble.toolCall.resultText,
|
|
72
83
|
})
|
|
73
84
|
: [];
|
|
74
85
|
|
|
75
|
-
return [...thinking, ...text, ...
|
|
86
|
+
return [...thinking, ...text, ...toolCall, ...toolOutput];
|
|
76
87
|
};
|
|
77
88
|
|
|
78
89
|
const transcriptToMessages = (transcript: CursorThreadTranscript) => {
|
|
79
|
-
|
|
90
|
+
const finalAssistantTextBubbleIds = getFinalCursorAssistantTextBubbleIds(transcript.bubbles);
|
|
91
|
+
return finalizeMessages(
|
|
92
|
+
transcript.bubbles.flatMap((bubble, order) => bubbleToMessages(bubble, finalAssistantTextBubbleIds, order)),
|
|
93
|
+
);
|
|
80
94
|
};
|
|
81
95
|
|
|
82
96
|
const getWorkspacePath = (group: CursorWorkspaceGroup) => {
|
|
@@ -90,8 +104,16 @@ const buildConversation = async (
|
|
|
90
104
|
matches: ConversationPathMatch[],
|
|
91
105
|
options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
|
|
92
106
|
): Promise<ConversationDetail> => {
|
|
107
|
+
const globalDbPath = getCursorGlobalDbPath(userDir);
|
|
93
108
|
const transcript = options.includeMessages
|
|
94
|
-
? await
|
|
109
|
+
? await runWithTranscriptLoadLimit(
|
|
110
|
+
() => readCursorThreadTranscriptWithAgentFiles(globalDbPath, thread.composerId, userDir),
|
|
111
|
+
{
|
|
112
|
+
id: thread.composerId,
|
|
113
|
+
path: thread.transcriptDirs[0] ?? globalDbPath,
|
|
114
|
+
source: 'cursor-api',
|
|
115
|
+
},
|
|
116
|
+
)
|
|
95
117
|
: null;
|
|
96
118
|
const allMessages = transcript ? transcriptToMessages(transcript) : [];
|
|
97
119
|
const messages = options.includeMessages
|
|
@@ -100,7 +122,11 @@ const buildConversation = async (
|
|
|
100
122
|
|
|
101
123
|
return {
|
|
102
124
|
createdAtMs: thread.createdAtMs,
|
|
103
|
-
deepLinks: createDeepLinks(
|
|
125
|
+
deepLinks: createDeepLinks(
|
|
126
|
+
'cursor',
|
|
127
|
+
thread.composerId,
|
|
128
|
+
createConversationUiPath('cursor-threads', thread.composerId),
|
|
129
|
+
),
|
|
104
130
|
id: thread.composerId,
|
|
105
131
|
matches,
|
|
106
132
|
messageCount: options.includeMessages ? allMessages.length : thread.bubbleCount,
|
|
@@ -109,7 +135,6 @@ const buildConversation = async (
|
|
|
109
135
|
bubbleBytes: thread.bubbleBytes,
|
|
110
136
|
bucketId: thread.bucketId,
|
|
111
137
|
mode: thread.mode,
|
|
112
|
-
transcriptDirs: thread.transcriptDirs,
|
|
113
138
|
},
|
|
114
139
|
source: 'cursor',
|
|
115
140
|
title: cleanInlineTitle(thread.name),
|
|
@@ -121,7 +146,7 @@ const buildConversation = async (
|
|
|
121
146
|
|
|
122
147
|
const listCursorConversationsForPath = async (options: ListConversationsForPathOptions) => {
|
|
123
148
|
const userDir = getUserDir(options);
|
|
124
|
-
const groups = await listCursorWorkspaceGroups(userDir
|
|
149
|
+
const groups = await listCursorWorkspaceGroups(userDir);
|
|
125
150
|
const candidates: { group: CursorWorkspaceGroup; match: ConversationPathMatch; thread: CursorThreadSummary }[] = [];
|
|
126
151
|
|
|
127
152
|
for (const group of groups) {
|
|
@@ -131,10 +156,9 @@ const listCursorConversationsForPath = async (options: ListConversationsForPathO
|
|
|
131
156
|
}
|
|
132
157
|
const threads = await listCursorThreadsForGroup(group, userDir, {
|
|
133
158
|
includeTranscriptDirs: false,
|
|
134
|
-
updatedAfterMs: options.updatedAfterMs,
|
|
135
159
|
});
|
|
136
160
|
for (const thread of threads) {
|
|
137
|
-
if (!isWithinUpdatedWindow(thread, options)) {
|
|
161
|
+
if (!isWithinUpdatedWindow(thread.lastUpdatedAtMs, options)) {
|
|
138
162
|
continue;
|
|
139
163
|
}
|
|
140
164
|
|
|
@@ -164,7 +188,31 @@ const getCursorConversation = async (options: GetConversationOptions): Promise<C
|
|
|
164
188
|
return null;
|
|
165
189
|
};
|
|
166
190
|
|
|
191
|
+
export const deleteCursorConversation = async (
|
|
192
|
+
options: DeleteConversationOptions,
|
|
193
|
+
checkCursorRunning: () => Promise<boolean> = isCursorRunning,
|
|
194
|
+
) => {
|
|
195
|
+
const userDir = getUserDir(options);
|
|
196
|
+
if (await checkCursorRunning()) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
'Quit Cursor before deleting. It rewrites chat history on exit, which can resurrect deleted threads.',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const threads = await collectCursorThreadsForDeletion([options.id], userDir);
|
|
203
|
+
if (threads.length === 0) {
|
|
204
|
+
return { deletedFiles: [], deletedIds: [] };
|
|
205
|
+
}
|
|
206
|
+
const deletedFiles = threads.flatMap((thread) => thread.transcriptDirs);
|
|
207
|
+
const result = await pruneCursorThreads(threads, true, userDir);
|
|
208
|
+
return {
|
|
209
|
+
deletedFiles,
|
|
210
|
+
deletedIds: result.composerIds,
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
167
214
|
export const cursorConversationAdapter: ConversationAdapter = {
|
|
215
|
+
deleteConversation: deleteCursorConversation,
|
|
168
216
|
getConversation: getCursorConversation,
|
|
169
217
|
listConversationsForPath: listCursorConversationsForPath,
|
|
170
218
|
source: 'cursor',
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { mapWithConcurrency } from '../concurrency';
|
|
2
|
+
import {
|
|
3
|
+
deleteGrokSession,
|
|
4
|
+
listGrokSessionTranscriptsForGroup,
|
|
5
|
+
listGrokWorkspaceGroups,
|
|
6
|
+
readGrokSessionTranscript,
|
|
7
|
+
resolveGrokSessionsDir,
|
|
8
|
+
} from '../grok-db';
|
|
9
|
+
import type {
|
|
10
|
+
GrokSessionSummary,
|
|
11
|
+
GrokSessionTranscript,
|
|
12
|
+
GrokTranscriptEntry,
|
|
13
|
+
GrokTranscriptPart,
|
|
14
|
+
} from '../grok-exporter-types';
|
|
15
|
+
import { getFinalGrokAssistantTextPartIds, getGrokTextPartPhase } from '../grok-transcript-phase';
|
|
16
|
+
import { cleanInlineTitle } from '../shared';
|
|
17
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
18
|
+
import {
|
|
19
|
+
createConversationUiPath,
|
|
20
|
+
createDeepLinks,
|
|
21
|
+
createTextMessage,
|
|
22
|
+
finalizeMessages,
|
|
23
|
+
isWithinUpdatedWindow,
|
|
24
|
+
normalizeAssistantPhase,
|
|
25
|
+
normalizeRole,
|
|
26
|
+
} from './adapter-helpers';
|
|
27
|
+
import { selectConversationMessages } from './message-selector';
|
|
28
|
+
import { getConversationPathMatch } from './path-match';
|
|
29
|
+
import type {
|
|
30
|
+
ConversationAdapter,
|
|
31
|
+
ConversationDetail,
|
|
32
|
+
ConversationMessage,
|
|
33
|
+
ConversationPathMatch,
|
|
34
|
+
DeleteConversationOptions,
|
|
35
|
+
GetConversationOptions,
|
|
36
|
+
ListConversationsForPathOptions,
|
|
37
|
+
} from './types';
|
|
38
|
+
|
|
39
|
+
const GROK_CONVERSATION_HYDRATION_CONCURRENCY = 4;
|
|
40
|
+
|
|
41
|
+
const getSessionsDir = (options: { locations?: { grokSessionsDir?: string } }) =>
|
|
42
|
+
options.locations?.grokSessionsDir ?? resolveGrokSessionsDir();
|
|
43
|
+
|
|
44
|
+
const partToMessages = (
|
|
45
|
+
entry: GrokTranscriptEntry,
|
|
46
|
+
part: GrokTranscriptPart,
|
|
47
|
+
finalTextPartIds: Set<string>,
|
|
48
|
+
order: number,
|
|
49
|
+
): ConversationMessage[] => {
|
|
50
|
+
if (part.type === 'text') {
|
|
51
|
+
return createTextMessage({
|
|
52
|
+
createdAtMs: entry.createdAtMs,
|
|
53
|
+
id: part.partId,
|
|
54
|
+
metadata: { modelFingerprint: entry.modelFingerprint, modelId: entry.modelId },
|
|
55
|
+
order,
|
|
56
|
+
phase: normalizeAssistantPhase(getGrokTextPartPhase(entry, part, finalTextPartIds), 'unknown'),
|
|
57
|
+
role: normalizeRole(entry.role),
|
|
58
|
+
text: part.text,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (part.type === 'reasoning') {
|
|
63
|
+
return createTextMessage({
|
|
64
|
+
createdAtMs: entry.createdAtMs,
|
|
65
|
+
id: part.partId,
|
|
66
|
+
order,
|
|
67
|
+
phase: 'reasoning',
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
text: part.text,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (part.type === 'tool_call') {
|
|
74
|
+
return createTextMessage({
|
|
75
|
+
createdAtMs: entry.createdAtMs,
|
|
76
|
+
id: part.partId,
|
|
77
|
+
metadata: { toolCallId: part.toolCallId, toolName: part.toolName },
|
|
78
|
+
order,
|
|
79
|
+
phase: 'tool_call',
|
|
80
|
+
role: 'tool',
|
|
81
|
+
text: [part.toolName, part.argumentsText].filter(Boolean).join('\n'),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (part.type === 'tool_result') {
|
|
86
|
+
return createTextMessage({
|
|
87
|
+
createdAtMs: entry.createdAtMs,
|
|
88
|
+
id: part.partId,
|
|
89
|
+
metadata: { toolCallId: part.toolCallId },
|
|
90
|
+
order,
|
|
91
|
+
phase: 'tool_output',
|
|
92
|
+
role: 'tool',
|
|
93
|
+
text: part.outputText,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const transcriptToMessages = (transcript: GrokSessionTranscript): ConversationMessage[] => {
|
|
101
|
+
const finalTextPartIds = getFinalGrokAssistantTextPartIds(transcript.entries);
|
|
102
|
+
return finalizeMessages(
|
|
103
|
+
transcript.entries.flatMap((entry, entryIndex) =>
|
|
104
|
+
entry.parts.flatMap((part, partIndex) =>
|
|
105
|
+
partToMessages(entry, part, finalTextPartIds, entryIndex + partIndex),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const buildConversation = async (
|
|
112
|
+
session: GrokSessionSummary,
|
|
113
|
+
sessionsDir: string,
|
|
114
|
+
matches: ConversationPathMatch[],
|
|
115
|
+
options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
|
|
116
|
+
loadedTranscript: GrokSessionTranscript | null = null,
|
|
117
|
+
): Promise<ConversationDetail> => {
|
|
118
|
+
const transcript =
|
|
119
|
+
loadedTranscript ??
|
|
120
|
+
(options.includeMessages
|
|
121
|
+
? await runWithTranscriptLoadLimit(
|
|
122
|
+
() => readGrokSessionTranscript(sessionsDir, session.sessionId, { includeRawPayloads: false }),
|
|
123
|
+
{
|
|
124
|
+
id: session.sessionId,
|
|
125
|
+
path: sessionsDir,
|
|
126
|
+
source: 'grok-api',
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
: null);
|
|
130
|
+
const allMessages = transcript ? transcriptToMessages(transcript) : [];
|
|
131
|
+
const messages = options.includeMessages
|
|
132
|
+
? selectConversationMessages(allMessages, options.messageSelector ?? 'last_final_answer')
|
|
133
|
+
: [];
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
createdAtMs: session.createdAtMs,
|
|
137
|
+
deepLinks: createDeepLinks(
|
|
138
|
+
'grok',
|
|
139
|
+
session.sessionId,
|
|
140
|
+
createConversationUiPath('grok-sessions', session.sessionId),
|
|
141
|
+
),
|
|
142
|
+
id: session.sessionId,
|
|
143
|
+
matches,
|
|
144
|
+
messageCount: options.includeMessages ? allMessages.length : session.messageCount,
|
|
145
|
+
messages,
|
|
146
|
+
metadata: {
|
|
147
|
+
agentName: session.agentName,
|
|
148
|
+
currentModelId: session.currentModelId,
|
|
149
|
+
gitBranch: session.gitBranch,
|
|
150
|
+
headCommit: session.headCommit,
|
|
151
|
+
modelLabel: session.modelLabel,
|
|
152
|
+
renderablePartCount: session.renderablePartCount,
|
|
153
|
+
sandboxProfile: session.sandboxProfile,
|
|
154
|
+
},
|
|
155
|
+
source: 'grok',
|
|
156
|
+
title: cleanInlineTitle(session.title),
|
|
157
|
+
updatedAtMs: session.lastActiveAtMs,
|
|
158
|
+
workspaceKey: session.workspaceKey,
|
|
159
|
+
workspacePath: session.worktree,
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const listGrokConversationsForPath = async (options: ListConversationsForPathOptions) => {
|
|
164
|
+
const sessionsDir = getSessionsDir(options);
|
|
165
|
+
const groups = await listGrokWorkspaceGroups(sessionsDir);
|
|
166
|
+
const conversations: ConversationDetail[] = [];
|
|
167
|
+
|
|
168
|
+
for (const group of groups) {
|
|
169
|
+
const match = await getConversationPathMatch(options.cwd, group.worktree);
|
|
170
|
+
if (!match) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
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
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return conversations;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getGrokConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
|
|
187
|
+
const sessionsDir = getSessionsDir(options);
|
|
188
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
189
|
+
() => readGrokSessionTranscript(sessionsDir, options.id, { includeRawPayloads: false }),
|
|
190
|
+
{
|
|
191
|
+
id: options.id,
|
|
192
|
+
path: sessionsDir,
|
|
193
|
+
source: 'grok-api',
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
return transcript
|
|
197
|
+
? buildConversation(
|
|
198
|
+
transcript.session,
|
|
199
|
+
sessionsDir,
|
|
200
|
+
[],
|
|
201
|
+
{
|
|
202
|
+
includeMessages: true,
|
|
203
|
+
messageSelector: options.messageSelector ?? 'all',
|
|
204
|
+
},
|
|
205
|
+
transcript,
|
|
206
|
+
)
|
|
207
|
+
: null;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const deleteGrokConversation = async (options: DeleteConversationOptions) => {
|
|
211
|
+
const result = await deleteGrokSession(getSessionsDir(options), options.id);
|
|
212
|
+
return {
|
|
213
|
+
deletedFiles: result.deletedFiles,
|
|
214
|
+
deletedIds: result.deletedSessionIds,
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const grokConversationAdapter: ConversationAdapter = {
|
|
219
|
+
deleteConversation: deleteGrokConversation,
|
|
220
|
+
getConversation: getGrokConversation,
|
|
221
|
+
listConversationsForPath: listGrokConversationsForPath,
|
|
222
|
+
source: 'grok',
|
|
223
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { mapWithConcurrency } from '../concurrency';
|
|
1
2
|
import { antigravityConversationAdapter } from './antigravity-adapter';
|
|
2
3
|
import { claudeCodeConversationAdapter } from './claude-code-adapter';
|
|
3
4
|
import { codexConversationAdapter } from './codex-adapter';
|
|
4
5
|
import { cursorConversationAdapter } from './cursor-adapter';
|
|
6
|
+
import { grokConversationAdapter } from './grok-adapter';
|
|
5
7
|
import { kiroConversationAdapter } from './kiro-adapter';
|
|
6
8
|
import { selectConversationMessages } from './message-selector';
|
|
7
9
|
import { opencodeConversationAdapter } from './opencode-adapter';
|
|
@@ -14,6 +16,11 @@ import {
|
|
|
14
16
|
type ConversationPage,
|
|
15
17
|
type ConversationSource,
|
|
16
18
|
type ConversationSourceInfo,
|
|
19
|
+
type DeleteConversationItemResult,
|
|
20
|
+
type DeleteConversationOptions,
|
|
21
|
+
type DeleteConversationResult,
|
|
22
|
+
type DeleteConversationsOptions,
|
|
23
|
+
type DeleteConversationsResult,
|
|
17
24
|
type GetConversationOptions,
|
|
18
25
|
type ListConversationsForPathOptions,
|
|
19
26
|
type ResolvedConversationRef,
|
|
@@ -27,6 +34,7 @@ export {
|
|
|
27
34
|
type ConversationDataLocations,
|
|
28
35
|
type ConversationDeepLinks,
|
|
29
36
|
type ConversationDetail,
|
|
37
|
+
type ConversationIdSetOptions,
|
|
30
38
|
type ConversationMessage,
|
|
31
39
|
type ConversationMessagePhase,
|
|
32
40
|
type ConversationMessageRole,
|
|
@@ -35,6 +43,13 @@ export {
|
|
|
35
43
|
type ConversationPathMatch,
|
|
36
44
|
type ConversationSource,
|
|
37
45
|
type ConversationSourceInfo,
|
|
46
|
+
type ConversationZipDownload,
|
|
47
|
+
type DeleteConversationItemResult,
|
|
48
|
+
type DeleteConversationOptions,
|
|
49
|
+
type DeleteConversationResult,
|
|
50
|
+
type DeleteConversationsOptions,
|
|
51
|
+
type DeleteConversationsResult,
|
|
52
|
+
type ExportConversationsZipOptions,
|
|
38
53
|
type GetConversationOptions,
|
|
39
54
|
type ListConversationsForPathOptions,
|
|
40
55
|
type ResolvedConversationRef,
|
|
@@ -45,6 +60,7 @@ const SOURCE_LABELS: Record<ConversationSource, string> = {
|
|
|
45
60
|
'claude-code': 'Claude Code',
|
|
46
61
|
codex: 'Codex',
|
|
47
62
|
cursor: 'Cursor',
|
|
63
|
+
grok: 'Grok',
|
|
48
64
|
kiro: 'Kiro',
|
|
49
65
|
opencode: 'OpenCode',
|
|
50
66
|
qoder: 'Qoder',
|
|
@@ -64,6 +80,7 @@ const ADAPTERS: Partial<Record<ConversationSource, ConversationAdapter>> = {
|
|
|
64
80
|
'claude-code': claudeCodeConversationAdapter,
|
|
65
81
|
codex: codexConversationAdapter,
|
|
66
82
|
cursor: cursorConversationAdapter,
|
|
83
|
+
grok: grokConversationAdapter,
|
|
67
84
|
kiro: kiroConversationAdapter,
|
|
68
85
|
opencode: opencodeConversationAdapter,
|
|
69
86
|
qoder: qoderConversationAdapter,
|
|
@@ -71,13 +88,23 @@ const ADAPTERS: Partial<Record<ConversationSource, ConversationAdapter>> = {
|
|
|
71
88
|
|
|
72
89
|
const MAX_LIMIT = 200;
|
|
73
90
|
const DEFAULT_LIMIT = 100;
|
|
91
|
+
const DELETE_CONCURRENCY_BY_SOURCE: Record<ConversationSource, number> = {
|
|
92
|
+
antigravity: 1,
|
|
93
|
+
'claude-code': 4,
|
|
94
|
+
codex: 1,
|
|
95
|
+
cursor: 1,
|
|
96
|
+
grok: 1,
|
|
97
|
+
kiro: 1,
|
|
98
|
+
opencode: 2,
|
|
99
|
+
qoder: 1,
|
|
100
|
+
};
|
|
74
101
|
|
|
75
102
|
const getEnabledSources = (sources: ListConversationsForPathOptions['sources']): ConversationSource[] => {
|
|
76
103
|
if (!sources || sources === 'all') {
|
|
77
104
|
return SOURCE_INFOS.map((sourceInfo) => sourceInfo.source);
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
return sources;
|
|
107
|
+
return [...new Set(sources)];
|
|
81
108
|
};
|
|
82
109
|
|
|
83
110
|
const isAllSourcesRequest = (sources: ListConversationsForPathOptions['sources']) => !sources || sources === 'all';
|
|
@@ -91,8 +118,16 @@ const decodeCursor = (cursor: string | null | undefined) => {
|
|
|
91
118
|
return 0;
|
|
92
119
|
}
|
|
93
120
|
|
|
94
|
-
const
|
|
95
|
-
|
|
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;
|
|
96
131
|
};
|
|
97
132
|
|
|
98
133
|
const encodeCursor = (offset: number) => Buffer.from(String(offset), 'utf8').toString('base64url');
|
|
@@ -130,7 +165,7 @@ const sortConversations = (conversations: Awaited<ReturnType<ConversationAdapter
|
|
|
130
165
|
);
|
|
131
166
|
};
|
|
132
167
|
|
|
133
|
-
export const listConversationSources = async (): Promise<ConversationSourceInfo[]> => SOURCE_INFOS;
|
|
168
|
+
export const listConversationSources = async (): Promise<ConversationSourceInfo[]> => [...SOURCE_INFOS];
|
|
134
169
|
|
|
135
170
|
const listSourceConversationsForPath = async (
|
|
136
171
|
source: ConversationSource,
|
|
@@ -148,6 +183,10 @@ const listSourceConversationsForPath = async (
|
|
|
148
183
|
if (!ignoreSourceFailures) {
|
|
149
184
|
throw error;
|
|
150
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
|
+
});
|
|
151
190
|
return [];
|
|
152
191
|
}
|
|
153
192
|
};
|
|
@@ -181,10 +220,56 @@ export const getConversation = async (options: GetConversationOptions) => {
|
|
|
181
220
|
return getAdapter(options.source)?.getConversation(options) ?? null;
|
|
182
221
|
};
|
|
183
222
|
|
|
223
|
+
export const deleteConversation = async (
|
|
224
|
+
options: DeleteConversationOptions,
|
|
225
|
+
): Promise<DeleteConversationResult | null> => {
|
|
226
|
+
return (await getAdapter(options.source)?.deleteConversation?.(options)) ?? null;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const deleteConversations = async (
|
|
230
|
+
options: DeleteConversationsOptions,
|
|
231
|
+
): Promise<DeleteConversationsResult | null> => {
|
|
232
|
+
const adapter = getAdapter(options.source);
|
|
233
|
+
if (!adapter?.deleteConversation) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const deleteAdapterConversation = adapter.deleteConversation;
|
|
237
|
+
|
|
238
|
+
const rawResults = await mapWithConcurrency(
|
|
239
|
+
options.ids,
|
|
240
|
+
DELETE_CONCURRENCY_BY_SOURCE[options.source],
|
|
241
|
+
async (id) => ({
|
|
242
|
+
id,
|
|
243
|
+
result: await deleteAdapterConversation({
|
|
244
|
+
id,
|
|
245
|
+
locations: options.locations,
|
|
246
|
+
source: options.source,
|
|
247
|
+
}),
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
const deletedIdSet = new Set(rawResults.flatMap(({ result }) => result.deletedIds));
|
|
251
|
+
const results: DeleteConversationItemResult[] = rawResults.map(({ id, result }) => ({
|
|
252
|
+
deleted: result.deletedIds.length > 0 || deletedIdSet.has(id),
|
|
253
|
+
deletedFiles: result.deletedFiles,
|
|
254
|
+
deletedIds: result.deletedIds,
|
|
255
|
+
id,
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
deletedFiles: [...new Set(results.flatMap((result) => result.deletedFiles))],
|
|
260
|
+
deletedIds: [...deletedIdSet],
|
|
261
|
+
missingIds: results.filter((result) => !result.deleted).map((result) => result.id),
|
|
262
|
+
results,
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
|
|
184
266
|
const sourceFromSessionRoute = (segment: string): ConversationSource | null => {
|
|
185
267
|
if (segment === 'claude-code-sessions') {
|
|
186
268
|
return 'claude-code';
|
|
187
269
|
}
|
|
270
|
+
if (segment === 'grok-sessions') {
|
|
271
|
+
return 'grok';
|
|
272
|
+
}
|
|
188
273
|
if (segment === 'kiro-sessions') {
|
|
189
274
|
return 'kiro';
|
|
190
275
|
}
|
|
@@ -241,14 +326,21 @@ const refFromPathSegmentAt = (segments: string[], index: number): ResolvedConver
|
|
|
241
326
|
};
|
|
242
327
|
|
|
243
328
|
const refFromPathSegments = (segments: string[]): ResolvedConversationRef | null => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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);
|
|
249
336
|
}
|
|
250
337
|
|
|
251
|
-
|
|
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;
|
|
252
344
|
};
|
|
253
345
|
|
|
254
346
|
const parseUrlRef = (ref: string): ResolvedConversationRef | null => {
|
|
@@ -287,7 +379,9 @@ export const renderConversationMarkdown = (
|
|
|
287
379
|
messages: ConversationMessage[];
|
|
288
380
|
title: string | null;
|
|
289
381
|
},
|
|
290
|
-
options: {
|
|
382
|
+
options: {
|
|
383
|
+
messageSelector?: ConversationMessageSelector;
|
|
384
|
+
} = {},
|
|
291
385
|
) => {
|
|
292
386
|
const selectedMessages = options.messageSelector
|
|
293
387
|
? selectConversationMessages(conversation.messages, options.messageSelector)
|