spiracha 2.0.0 → 2.1.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/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- 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 +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- 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/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- 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 +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- 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 +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- 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 +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- 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-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- package/src/lib/ui-export-archive.ts +60 -0
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import { resolveQoderGlobalStateDb, resolveQoderWorkspaceStorageDir } from '../qoder-exporter-types';
|
|
15
15
|
import { getFinalQoderAssistantMessageEntryIds, getQoderMessagePhase } from '../qoder-transcript-phase';
|
|
16
16
|
import { cleanInlineTitle } from '../shared';
|
|
17
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
17
18
|
import {
|
|
18
19
|
createDeepLinks,
|
|
19
20
|
createTextMessage,
|
|
@@ -117,18 +118,28 @@ const buildConversation = async (
|
|
|
117
118
|
locations: ReturnType<typeof getQoderLocations>,
|
|
118
119
|
matches: ConversationPathMatch[],
|
|
119
120
|
options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
|
|
121
|
+
loadedTranscript: QoderSessionTranscript | null = null,
|
|
120
122
|
): Promise<ConversationDetail> => {
|
|
121
123
|
const transcript = options.includeMessages
|
|
122
|
-
?
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
? (loadedTranscript ??
|
|
125
|
+
(await runWithTranscriptLoadLimit(
|
|
126
|
+
() =>
|
|
127
|
+
readQoderSessionTranscript(
|
|
128
|
+
locations.globalStateDb,
|
|
129
|
+
locations.workspaceStorageDir,
|
|
130
|
+
session.sessionId,
|
|
131
|
+
locations.cliProjectsDir,
|
|
132
|
+
{
|
|
133
|
+
acpSocketPath: locations.acpSocketPath,
|
|
134
|
+
enableAcp: locations.acpSocketPath ? true : undefined,
|
|
135
|
+
},
|
|
136
|
+
),
|
|
127
137
|
{
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
id: session.sessionId,
|
|
139
|
+
path: session.sourceStatePath ?? locations.globalStateDb,
|
|
140
|
+
source: 'qoder-api',
|
|
130
141
|
},
|
|
131
|
-
)
|
|
142
|
+
)))
|
|
132
143
|
: null;
|
|
133
144
|
const allMessages = transcript ? transcriptToMessages(transcript) : [];
|
|
134
145
|
const messages = options.includeMessages
|
|
@@ -205,21 +216,35 @@ const listQoderConversationsForPath = async (options: ListConversationsForPathOp
|
|
|
205
216
|
|
|
206
217
|
const getQoderConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
|
|
207
218
|
const locations = getQoderLocations(options);
|
|
208
|
-
const transcript = await
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
220
|
+
() =>
|
|
221
|
+
readQoderSessionTranscript(
|
|
222
|
+
locations.globalStateDb,
|
|
223
|
+
locations.workspaceStorageDir,
|
|
224
|
+
options.id,
|
|
225
|
+
locations.cliProjectsDir,
|
|
226
|
+
{
|
|
227
|
+
acpSocketPath: locations.acpSocketPath,
|
|
228
|
+
enableAcp: locations.acpSocketPath ? true : undefined,
|
|
229
|
+
},
|
|
230
|
+
),
|
|
213
231
|
{
|
|
214
|
-
|
|
215
|
-
|
|
232
|
+
id: options.id,
|
|
233
|
+
path: locations.globalStateDb,
|
|
234
|
+
source: 'qoder-api',
|
|
216
235
|
},
|
|
217
236
|
);
|
|
218
237
|
return transcript
|
|
219
|
-
? buildConversation(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
? buildConversation(
|
|
239
|
+
transcript.session,
|
|
240
|
+
locations,
|
|
241
|
+
[],
|
|
242
|
+
{
|
|
243
|
+
includeMessages: true,
|
|
244
|
+
messageSelector: options.messageSelector ?? 'all',
|
|
245
|
+
},
|
|
246
|
+
transcript,
|
|
247
|
+
)
|
|
223
248
|
: null;
|
|
224
249
|
};
|
|
225
250
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const CONVERSATION_SOURCES = [
|
|
2
2
|
'codex',
|
|
3
3
|
'claude-code',
|
|
4
|
+
'grok',
|
|
4
5
|
'kiro',
|
|
5
6
|
'qoder',
|
|
6
7
|
'cursor',
|
|
@@ -77,6 +78,7 @@ export type ConversationDataLocations = {
|
|
|
77
78
|
claudeCodeProjectsDir?: string;
|
|
78
79
|
codexDbPath?: string;
|
|
79
80
|
cursorUserDir?: string;
|
|
81
|
+
grokSessionsDir?: string;
|
|
80
82
|
kiroWorkspaceSessionsDir?: string;
|
|
81
83
|
opencodeDbPath?: string;
|
|
82
84
|
qoderAcpSocketPath?: string;
|
|
@@ -104,12 +106,53 @@ export type GetConversationOptions = {
|
|
|
104
106
|
source: ConversationSource;
|
|
105
107
|
};
|
|
106
108
|
|
|
109
|
+
export type DeleteConversationOptions = {
|
|
110
|
+
id: string;
|
|
111
|
+
locations?: ConversationDataLocations;
|
|
112
|
+
source: ConversationSource;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type DeleteConversationResult = {
|
|
116
|
+
deletedFiles: string[];
|
|
117
|
+
deletedIds: string[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type ConversationIdSetOptions = {
|
|
121
|
+
ids: string[];
|
|
122
|
+
locations?: ConversationDataLocations;
|
|
123
|
+
source: ConversationSource;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type DeleteConversationsOptions = ConversationIdSetOptions;
|
|
127
|
+
|
|
128
|
+
export type DeleteConversationItemResult = DeleteConversationResult & {
|
|
129
|
+
deleted: boolean;
|
|
130
|
+
id: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type DeleteConversationsResult = DeleteConversationResult & {
|
|
134
|
+
missingIds: string[];
|
|
135
|
+
results: DeleteConversationItemResult[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type ExportConversationsZipOptions = ConversationIdSetOptions & {
|
|
139
|
+
messageSelector?: ConversationMessageSelector;
|
|
140
|
+
outputFormat?: 'md';
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export type ConversationZipDownload = {
|
|
144
|
+
blob: Blob;
|
|
145
|
+
fileName: string;
|
|
146
|
+
mimeType: 'application/zip';
|
|
147
|
+
};
|
|
148
|
+
|
|
107
149
|
export type ResolvedConversationRef = {
|
|
108
150
|
id: string;
|
|
109
151
|
source: ConversationSource;
|
|
110
152
|
};
|
|
111
153
|
|
|
112
154
|
export type ConversationAdapter = {
|
|
155
|
+
deleteConversation?: (options: DeleteConversationOptions) => Promise<DeleteConversationResult>;
|
|
113
156
|
getConversation: (options: GetConversationOptions) => Promise<ConversationDetail | null>;
|
|
114
157
|
listConversationsForPath: (options: ListConversationsForPathOptions) => Promise<ConversationDetail[]>;
|
|
115
158
|
source: ConversationSource;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { resolveUniqueExportFileBaseName, sanitizeExportFileName, zipExportDirectory } from './ui-export-archive';
|
|
6
|
+
|
|
7
|
+
type ConversationMarkdownZipEntry = {
|
|
8
|
+
fallbackBaseName: string;
|
|
9
|
+
markdown: string;
|
|
10
|
+
title: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ConversationMarkdownZipOptions = {
|
|
14
|
+
entries: ConversationMarkdownZipEntry[];
|
|
15
|
+
fileBaseName: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ConversationMarkdownZip = {
|
|
19
|
+
blob: Blob;
|
|
20
|
+
fileName: string;
|
|
21
|
+
mimeType: 'application/zip';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const toSafeFileBaseName = (value: string | null, fallback: string) => {
|
|
25
|
+
return sanitizeExportFileName(value?.trim() || '') || sanitizeExportFileName(fallback) || 'conversation';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createConversationMarkdownZip = async ({
|
|
29
|
+
entries,
|
|
30
|
+
fileBaseName,
|
|
31
|
+
}: ConversationMarkdownZipOptions): Promise<ConversationMarkdownZip> => {
|
|
32
|
+
if (entries.length === 0) {
|
|
33
|
+
throw new Error('No conversations selected for export');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const safeBaseName = toSafeFileBaseName(fileBaseName, 'conversations');
|
|
37
|
+
const workspaceDir = await mkdtemp(path.join(os.tmpdir(), `${safeBaseName}-`));
|
|
38
|
+
const zipPath = path.join(os.tmpdir(), `${safeBaseName}-${randomUUID()}.zip`);
|
|
39
|
+
const usedBaseNames = new Map<string, number>();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const entryBaseName = toSafeFileBaseName(entry.title, entry.fallbackBaseName);
|
|
44
|
+
const fileBaseNameForEntry = resolveUniqueExportFileBaseName(entryBaseName, usedBaseNames);
|
|
45
|
+
await Bun.write(path.join(workspaceDir, `${fileBaseNameForEntry}.md`), entry.markdown);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await zipExportDirectory(workspaceDir, zipPath);
|
|
49
|
+
return {
|
|
50
|
+
blob: new Blob([await Bun.file(zipPath).arrayBuffer()], { type: 'application/zip' }),
|
|
51
|
+
fileName: `${safeBaseName}.zip`,
|
|
52
|
+
mimeType: 'application/zip',
|
|
53
|
+
};
|
|
54
|
+
} finally {
|
|
55
|
+
await Promise.all([rm(workspaceDir, { force: true, recursive: true }), rm(zipPath, { force: true })]);
|
|
56
|
+
}
|
|
57
|
+
};
|
package/src/lib/cursor-db.ts
CHANGED
|
@@ -126,6 +126,11 @@ const parseCodeWorkspaceJson = (text: string): { folders?: Array<{ path?: string
|
|
|
126
126
|
}
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
const isMissingCodeWorkspaceFileError = (error: unknown): boolean => {
|
|
130
|
+
const code = (error as { code?: unknown }).code;
|
|
131
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
132
|
+
};
|
|
133
|
+
|
|
129
134
|
const parseCodeWorkspaceFolders = async (workspaceFilePath: string): Promise<string[]> => {
|
|
130
135
|
if (!workspaceFilePath.endsWith('.code-workspace')) {
|
|
131
136
|
return [];
|
|
@@ -149,6 +154,10 @@ const parseCodeWorkspaceFolders = async (workspaceFilePath: string): Promise<str
|
|
|
149
154
|
|
|
150
155
|
return folders;
|
|
151
156
|
} catch (error) {
|
|
157
|
+
if (isMissingCodeWorkspaceFileError(error)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
152
161
|
warnCursorDataIssue('invalid_code_workspace_json', {
|
|
153
162
|
error: error instanceof Error ? error.message : String(error),
|
|
154
163
|
workspaceFilePath,
|
|
@@ -679,22 +688,32 @@ const readAllHeads = (db: Database, options: CursorDiscoveryOptions = {}): Map<s
|
|
|
679
688
|
WHERE key LIKE 'composerData:%'
|
|
680
689
|
AND COALESCE(json_extract(value, '$.lastUpdatedAt'), 0) >= ?`,
|
|
681
690
|
)
|
|
682
|
-
.all(options.updatedAfterMs) as Array<{ id: string; value: string }>;
|
|
691
|
+
.all(options.updatedAfterMs) as Array<{ id: string; value: string | null }>;
|
|
683
692
|
|
|
684
693
|
return new Map(rows.map((row) => [row.id, parseGlobalHead(row.value)]));
|
|
685
694
|
}
|
|
686
695
|
|
|
687
696
|
const rows = db
|
|
688
697
|
.query(`SELECT substr(key, 14) AS id, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'`)
|
|
689
|
-
.all() as Array<{ id: string; value: string }>;
|
|
698
|
+
.all() as Array<{ id: string; value: string | null }>;
|
|
690
699
|
|
|
691
700
|
return new Map(rows.map((row) => [row.id, parseGlobalHead(row.value)]));
|
|
692
701
|
};
|
|
693
702
|
|
|
694
|
-
const parseGlobalHead = (value: string): GlobalHead => {
|
|
695
|
-
let parsed: Record<string, JsonValue> = {};
|
|
703
|
+
const parseGlobalHead = (value: string | null): GlobalHead => {
|
|
704
|
+
let parsed: Record<string, JsonValue> | null = {};
|
|
705
|
+
if (value === null) {
|
|
706
|
+
return {
|
|
707
|
+
createdAtMs: null,
|
|
708
|
+
lastUpdatedAtMs: null,
|
|
709
|
+
mode: null,
|
|
710
|
+
name: null,
|
|
711
|
+
pathHint: null,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
696
715
|
try {
|
|
697
|
-
parsed = JSON.parse(value) as
|
|
716
|
+
parsed = asObject(JSON.parse(value) as JsonValue);
|
|
698
717
|
} catch {
|
|
699
718
|
return {
|
|
700
719
|
createdAtMs: null,
|
|
@@ -705,6 +724,16 @@ const parseGlobalHead = (value: string): GlobalHead => {
|
|
|
705
724
|
};
|
|
706
725
|
}
|
|
707
726
|
|
|
727
|
+
if (!parsed) {
|
|
728
|
+
return {
|
|
729
|
+
createdAtMs: null,
|
|
730
|
+
lastUpdatedAtMs: null,
|
|
731
|
+
mode: null,
|
|
732
|
+
name: null,
|
|
733
|
+
pathHint: inferFolderFromBlob(value),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
708
737
|
return {
|
|
709
738
|
createdAtMs: asNumber(parsed.createdAt ?? null),
|
|
710
739
|
lastUpdatedAtMs: asNumber(parsed.lastUpdatedAt ?? null),
|