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.
Files changed (104) hide show
  1. package/README.md +6 -1
  2. package/apps/ui/README.md +20 -2
  3. package/apps/ui/package.json +2 -2
  4. package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
  5. package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
  6. package/apps/ui/src/components/app-shell.tsx +1 -0
  7. package/apps/ui/src/components/breadcrumbs.tsx +21 -3
  8. package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
  9. package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
  10. package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
  11. package/apps/ui/src/components/data-table.tsx +7 -5
  12. package/apps/ui/src/components/export-dialog.tsx +2 -12
  13. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  14. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  15. package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
  16. package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
  17. package/apps/ui/src/components/page-header.tsx +15 -9
  18. package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
  19. package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
  20. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  21. package/apps/ui/src/components/threads-table.tsx +9 -46
  22. package/apps/ui/src/components/transcript-view.tsx +148 -52
  23. package/apps/ui/src/components/ui/select.tsx +1 -1
  24. package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
  25. package/apps/ui/src/lib/antigravity-server.ts +152 -7
  26. package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
  27. package/apps/ui/src/lib/claude-code-queries.ts +8 -0
  28. package/apps/ui/src/lib/claude-code-server.ts +175 -7
  29. package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
  30. package/apps/ui/src/lib/codex-queries.ts +19 -3
  31. package/apps/ui/src/lib/codex-server.ts +135 -53
  32. package/apps/ui/src/lib/cursor-server.ts +48 -5
  33. package/apps/ui/src/lib/formatters.ts +3 -5
  34. package/apps/ui/src/lib/grok-queries.ts +22 -0
  35. package/apps/ui/src/lib/grok-server.ts +169 -0
  36. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  37. package/apps/ui/src/lib/kiro-server.ts +85 -7
  38. package/apps/ui/src/lib/opencode-server.ts +85 -7
  39. package/apps/ui/src/lib/qoder-server.ts +67 -11
  40. package/apps/ui/src/lib/route-search.ts +114 -0
  41. package/apps/ui/src/lib/source-session-export-server.ts +86 -3
  42. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  43. package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
  44. package/apps/ui/src/routeTree.gen.ts +107 -0
  45. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
  46. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
  47. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
  48. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  49. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  50. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +143 -17
  51. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
  52. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
  53. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
  54. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
  55. package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
  56. package/apps/ui/src/routes/grok.index.tsx +48 -0
  57. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
  58. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
  59. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
  60. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
  61. package/apps/ui/src/routes/opencode.index.tsx +16 -3
  62. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
  63. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
  64. package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
  65. package/package.json +19 -18
  66. package/src/client.ts +134 -1
  67. package/src/lib/antigravity-db.ts +208 -32
  68. package/src/lib/antigravity-exporter-types.ts +3 -0
  69. package/src/lib/antigravity-projects.ts +201 -0
  70. package/src/lib/claude-code-db.ts +498 -32
  71. package/src/lib/claude-code-exporter-types.ts +5 -0
  72. package/src/lib/claude-code-transcript-phase.ts +60 -1
  73. package/src/lib/claude-code-transcript.ts +8 -5
  74. package/src/lib/codex-browser-export.ts +23 -27
  75. package/src/lib/codex-thread-cache.ts +41 -13
  76. package/src/lib/codex-thread-parser.ts +86 -35
  77. package/src/lib/codex-transcript-filter.ts +31 -0
  78. package/src/lib/codex-transcript-renderer.ts +39 -19
  79. package/src/lib/concurrency.ts +41 -0
  80. package/src/lib/conversation-api.ts +399 -19
  81. package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
  82. package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
  83. package/src/lib/conversation-data/codex-adapter.ts +28 -4
  84. package/src/lib/conversation-data/cursor-adapter.ts +35 -1
  85. package/src/lib/conversation-data/grok-adapter.ts +210 -0
  86. package/src/lib/conversation-data/index.ts +76 -1
  87. package/src/lib/conversation-data/kiro-adapter.ts +41 -7
  88. package/src/lib/conversation-data/opencode-adapter.ts +24 -2
  89. package/src/lib/conversation-data/qoder-adapter.ts +44 -19
  90. package/src/lib/conversation-data/types.ts +43 -0
  91. package/src/lib/conversation-zip-export.ts +57 -0
  92. package/src/lib/cursor-db.ts +34 -5
  93. package/src/lib/grok-db.ts +1026 -0
  94. package/src/lib/grok-exporter-types.ts +110 -0
  95. package/src/lib/grok-transcript-phase.ts +52 -0
  96. package/src/lib/grok-transcript.ts +154 -0
  97. package/src/lib/kiro-db.ts +52 -1
  98. package/src/lib/model-label.ts +11 -3
  99. package/src/lib/opencode-db.ts +598 -55
  100. package/src/lib/opencode-transcript.ts +8 -5
  101. package/src/lib/shared.ts +12 -0
  102. package/src/lib/transcript-load-limiter.ts +82 -0
  103. package/src/lib/ui-cache.ts +43 -17
  104. 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
- ? await readQoderSessionTranscript(
123
- locations.globalStateDb,
124
- locations.workspaceStorageDir,
125
- session.sessionId,
126
- locations.cliProjectsDir,
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
- acpSocketPath: locations.acpSocketPath,
129
- enableAcp: locations.acpSocketPath ? true : undefined,
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 readQoderSessionTranscript(
209
- locations.globalStateDb,
210
- locations.workspaceStorageDir,
211
- options.id,
212
- locations.cliProjectsDir,
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
- acpSocketPath: locations.acpSocketPath,
215
- enableAcp: locations.acpSocketPath ? true : undefined,
232
+ id: options.id,
233
+ path: locations.globalStateDb,
234
+ source: 'qoder-api',
216
235
  },
217
236
  );
218
237
  return transcript
219
- ? buildConversation(transcript.session, locations, [], {
220
- includeMessages: true,
221
- messageSelector: options.messageSelector ?? 'all',
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
+ };
@@ -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 Record<string, JsonValue>;
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),