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
@@ -0,0 +1,110 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import type { ExportFormat, JsonValue } from './shared';
4
+
5
+ export const getDefaultGrokHome = (_env: NodeJS.ProcessEnv = process.env, homeDir = os.homedir()): string => {
6
+ return path.join(homeDir, '.grok');
7
+ };
8
+
9
+ export const DEFAULT_GROK_HOME = getDefaultGrokHome();
10
+
11
+ export const resolveGrokHome = (): string => {
12
+ const configured = process.env.SPIRACHA_GROK_HOME?.trim() || process.env.SPIRACHA_GROK_DIR?.trim();
13
+ return configured ? configured : DEFAULT_GROK_HOME;
14
+ };
15
+
16
+ export const resolveGrokSessionsDir = (): string => {
17
+ const configured = process.env.SPIRACHA_GROK_SESSIONS_DIR?.trim();
18
+ return configured ? configured : path.join(resolveGrokHome(), 'sessions');
19
+ };
20
+
21
+ export type GrokWorkspaceGroup = {
22
+ assistantMessageCount: number;
23
+ chatMessageCount: number;
24
+ directoryName: string;
25
+ key: string;
26
+ label: string;
27
+ lastActiveAtIso: string | null;
28
+ lastActiveAtMs: number | null;
29
+ messageCount: number;
30
+ reasoningCount: number;
31
+ sessionCount: number;
32
+ toolCallCount: number;
33
+ toolResultCount: number;
34
+ uri: string;
35
+ userMessageCount: number;
36
+ worktree: string;
37
+ };
38
+
39
+ export type GrokSessionSummary = {
40
+ agentName: string | null;
41
+ assistantMessageCount: number;
42
+ chatHistoryPath: string;
43
+ chatMessageCount: number;
44
+ createdAtIso: string | null;
45
+ createdAtMs: number | null;
46
+ currentModelId: string | null;
47
+ cwd: string | null;
48
+ gitBranch: string | null;
49
+ gitRemotes: string[];
50
+ gitRootDir: string | null;
51
+ headCommit: string | null;
52
+ lastActiveAtIso: string | null;
53
+ lastActiveAtMs: number | null;
54
+ messageCount: number;
55
+ modelLabel: string | null;
56
+ reasoningCount: number;
57
+ renderablePartCount: number;
58
+ sandboxProfile: string | null;
59
+ sessionDir: string;
60
+ sessionId: string;
61
+ summaryPath: string;
62
+ title: string;
63
+ toolCallCount: number;
64
+ toolResultCount: number;
65
+ updatesPath: string | null;
66
+ userMessageCount: number;
67
+ workspaceKey: string;
68
+ workspaceLabel: string;
69
+ worktree: string;
70
+ };
71
+
72
+ export type GrokPartType = 'reasoning' | 'text' | 'tool_call' | 'tool_result' | 'unknown';
73
+
74
+ export type GrokTranscriptPart = {
75
+ argumentsText?: string | null;
76
+ outputText?: string | null;
77
+ partId: string;
78
+ raw: Record<string, JsonValue>;
79
+ text?: string;
80
+ toolCallId?: string | null;
81
+ toolName?: string | null;
82
+ type: GrokPartType;
83
+ };
84
+
85
+ export type GrokTranscriptEntry = {
86
+ createdAtMs: number | null;
87
+ entryId: string;
88
+ modelFingerprint?: string | null;
89
+ modelId?: string | null;
90
+ parts: GrokTranscriptPart[];
91
+ raw: Record<string, JsonValue>;
92
+ role: string;
93
+ timestamp: string | null;
94
+ type: string;
95
+ };
96
+
97
+ export type GrokSessionTranscript = {
98
+ entries: GrokTranscriptEntry[];
99
+ rawEvents: Record<string, JsonValue>[];
100
+ rawPayloadsOmitted?: boolean;
101
+ renderablePartCount: number;
102
+ session: GrokSessionSummary;
103
+ };
104
+
105
+ export type GrokExportOptions = {
106
+ includeCommentary: boolean;
107
+ includeMetadata: boolean;
108
+ includeTools: boolean;
109
+ outputFormat: ExportFormat;
110
+ };
@@ -0,0 +1,52 @@
1
+ import type { GrokTranscriptEntry, GrokTranscriptPart } from './grok-exporter-types';
2
+
3
+ export type GrokAssistantMessagePhase = 'commentary' | 'final_answer' | 'unknown';
4
+
5
+ export const getFinalGrokAssistantTextPartIds = (entries: GrokTranscriptEntry[]): Set<string> => {
6
+ const finalPartIds = new Set<string>();
7
+ let latestAssistantTextPartId: string | null = null;
8
+
9
+ const flushAssistantRun = () => {
10
+ if (latestAssistantTextPartId) {
11
+ finalPartIds.add(latestAssistantTextPartId);
12
+ latestAssistantTextPartId = null;
13
+ }
14
+ };
15
+
16
+ for (const entry of entries) {
17
+ if (entry.role === 'user') {
18
+ flushAssistantRun();
19
+ continue;
20
+ }
21
+
22
+ if (entry.role !== 'assistant') {
23
+ continue;
24
+ }
25
+
26
+ for (const part of entry.parts) {
27
+ if (part.type === 'tool_call') {
28
+ latestAssistantTextPartId = null;
29
+ continue;
30
+ }
31
+
32
+ if (part.type === 'text' && part.text?.trim()) {
33
+ latestAssistantTextPartId = part.partId;
34
+ }
35
+ }
36
+ }
37
+
38
+ flushAssistantRun();
39
+ return finalPartIds;
40
+ };
41
+
42
+ export const getGrokTextPartPhase = (
43
+ entry: GrokTranscriptEntry,
44
+ part: GrokTranscriptPart,
45
+ finalTextPartIds: Set<string>,
46
+ ): GrokAssistantMessagePhase => {
47
+ if (entry.role !== 'assistant') {
48
+ return 'unknown';
49
+ }
50
+
51
+ return finalTextPartIds.has(part.partId) ? 'final_answer' : 'commentary';
52
+ };
@@ -0,0 +1,154 @@
1
+ import type {
2
+ GrokExportOptions,
3
+ GrokSessionSummary,
4
+ GrokSessionTranscript,
5
+ GrokTranscriptEntry,
6
+ GrokTranscriptPart,
7
+ } from './grok-exporter-types';
8
+ import {
9
+ cleanExtractedText,
10
+ cleanInlineTitle,
11
+ formatInlineLiteral,
12
+ type MetadataEntry,
13
+ renderCodeBlock,
14
+ renderDocumentTitle,
15
+ renderMetadataBlock,
16
+ renderSection,
17
+ } from './shared';
18
+
19
+ const TOOL_OUTPUT_PREVIEW_LIMIT = 4000;
20
+
21
+ const getSessionTitle = (session: GrokSessionSummary): string => {
22
+ return cleanInlineTitle(session.title || session.sessionId);
23
+ };
24
+
25
+ const buildMetadataEntries = (session: GrokSessionSummary): MetadataEntry[] => [
26
+ { key: 'exported_from', value: 'grok_local_session' },
27
+ { key: 'session_id', value: session.sessionId },
28
+ { key: 'title', value: session.title },
29
+ { key: 'source_transcript_path', value: session.chatHistoryPath },
30
+ { key: 'workspace_key', value: session.workspaceKey },
31
+ { key: 'worktree', value: session.worktree },
32
+ { key: 'cwd', value: session.cwd },
33
+ { key: 'model', value: session.currentModelId },
34
+ { key: 'model_label', value: session.modelLabel },
35
+ { key: 'agent_name', value: session.agentName },
36
+ { key: 'git_branch', value: session.gitBranch },
37
+ { key: 'created_at_iso', value: session.createdAtIso },
38
+ { key: 'last_active_at_iso', value: session.lastActiveAtIso },
39
+ { key: 'message_count', value: session.messageCount },
40
+ { key: 'tool_call_count', value: session.toolCallCount },
41
+ { key: 'tool_result_count', value: session.toolResultCount },
42
+ ];
43
+
44
+ const roleTitle = (role: string): string => {
45
+ if (role === 'assistant') {
46
+ return 'Assistant';
47
+ }
48
+
49
+ if (role === 'user') {
50
+ return 'User';
51
+ }
52
+
53
+ if (role === 'system') {
54
+ return 'System';
55
+ }
56
+
57
+ if (role === 'tool') {
58
+ return 'Tool';
59
+ }
60
+
61
+ return role ? cleanInlineTitle(role) : 'Message';
62
+ };
63
+
64
+ const truncateOutput = (text: string): string => {
65
+ if (text.length <= TOOL_OUTPUT_PREVIEW_LIMIT) {
66
+ return text;
67
+ }
68
+
69
+ return `${text.slice(0, TOOL_OUTPUT_PREVIEW_LIMIT)}\n... (truncated)`;
70
+ };
71
+
72
+ const renderTextPart = (entry: GrokTranscriptEntry, part: GrokTranscriptPart, options: GrokExportOptions): string => {
73
+ const text = cleanExtractedText(part.text ?? '').trim();
74
+ return text ? renderSection(roleTitle(entry.role), text, options.outputFormat) : '';
75
+ };
76
+
77
+ const renderReasoningPart = (part: GrokTranscriptPart, options: GrokExportOptions): string => {
78
+ if (!options.includeCommentary) {
79
+ return '';
80
+ }
81
+
82
+ const text = cleanExtractedText(part.text ?? '').trim();
83
+ return text ? renderSection('Reasoning', text, options.outputFormat) : '';
84
+ };
85
+
86
+ const renderToolCallPart = (part: GrokTranscriptPart, options: GrokExportOptions): string => {
87
+ if (!options.includeTools) {
88
+ return '';
89
+ }
90
+
91
+ const toolName = part.toolName ?? 'unknown';
92
+ const lines = [`Tool: ${formatInlineLiteral(toolName, options.outputFormat)}`];
93
+ if (part.toolCallId) {
94
+ lines.push(`Call ID: ${part.toolCallId}`);
95
+ }
96
+ if (part.argumentsText?.trim()) {
97
+ lines.push('', 'Input:', '', renderCodeBlock(part.argumentsText.trim(), options.outputFormat));
98
+ }
99
+
100
+ return renderSection('Tool Call', lines.join('\n'), options.outputFormat);
101
+ };
102
+
103
+ const renderToolResultPart = (part: GrokTranscriptPart, options: GrokExportOptions): string => {
104
+ if (!options.includeTools) {
105
+ return '';
106
+ }
107
+
108
+ const outputText = part.outputText?.trim();
109
+ if (!outputText) {
110
+ return '';
111
+ }
112
+
113
+ const lines: string[] = [];
114
+ if (part.toolCallId) {
115
+ lines.push(`Call ID: ${part.toolCallId}`, '');
116
+ }
117
+ lines.push(renderCodeBlock(truncateOutput(outputText), options.outputFormat));
118
+ return renderSection('Tool Output', lines.join('\n'), options.outputFormat);
119
+ };
120
+
121
+ const renderPart = (entry: GrokTranscriptEntry, part: GrokTranscriptPart, options: GrokExportOptions): string => {
122
+ switch (part.type) {
123
+ case 'text':
124
+ return renderTextPart(entry, part, options);
125
+ case 'reasoning':
126
+ return renderReasoningPart(part, options);
127
+ case 'tool_call':
128
+ return renderToolCallPart(part, options);
129
+ case 'tool_result':
130
+ return renderToolResultPart(part, options);
131
+ case 'unknown':
132
+ return '';
133
+ }
134
+ };
135
+
136
+ export const renderGrokTranscript = (transcript: GrokSessionTranscript, options: GrokExportOptions): string | null => {
137
+ const sections = transcript.entries.flatMap((entry) =>
138
+ entry.parts.map((part) => renderPart(entry, part, options)).filter(Boolean),
139
+ );
140
+ if (sections.length === 0) {
141
+ return null;
142
+ }
143
+
144
+ const parts = [
145
+ renderDocumentTitle(getSessionTitle(transcript.session), options.outputFormat),
146
+ '',
147
+ options.includeMetadata
148
+ ? renderMetadataBlock(buildMetadataEntries(transcript.session), options.outputFormat)
149
+ : '',
150
+ ...sections,
151
+ ].filter(Boolean);
152
+
153
+ return `${parts.join('\n').trimEnd()}\n`;
154
+ };
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { readdir, stat } from 'node:fs/promises';
2
+ import { readdir, rm, stat } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { mapWithConcurrency } from './concurrency';
5
5
  import {
@@ -45,6 +45,11 @@ type KiroExecutionFile = {
45
45
  raw: Record<string, JsonValue>;
46
46
  };
47
47
 
48
+ export type DeleteKiroSessionResult = {
49
+ deletedFiles: string[];
50
+ deletedSessionIds: string[];
51
+ };
52
+
48
53
  type ReadSessionFileOptions = {
49
54
  includeExecutions?: boolean;
50
55
  sessionsDir: string;
@@ -932,6 +937,23 @@ const locateSessionFile = async (sessionsDir: string, sessionId: string): Promis
932
937
  return files.find((file) => path.basename(file.filePath, '.json') === sessionId) ?? null;
933
938
  };
934
939
 
940
+ const removeKiroSessionIndexEntry = async (workspaceDir: string, sessionId: string): Promise<void> => {
941
+ const indexPath = path.join(workspaceDir, 'sessions.json');
942
+ const value = (await Bun.file(indexPath)
943
+ .json()
944
+ .catch(() => null)) as JsonValue | null;
945
+ if (!Array.isArray(value)) {
946
+ return;
947
+ }
948
+
949
+ const next = value.filter((item) => asString(asObject(item)?.sessionId ?? null) !== sessionId);
950
+ if (next.length === value.length) {
951
+ return;
952
+ }
953
+
954
+ await Bun.write(indexPath, JSON.stringify(next, null, 2));
955
+ };
956
+
935
957
  export const readKiroSessionTranscript = async (
936
958
  sessionsDir: string,
937
959
  sessionId: string,
@@ -947,3 +969,32 @@ export const readKiroSessionTranscript = async (
947
969
 
948
970
  return readSessionFile(file, { includeExecutions: true, sessionsDir });
949
971
  };
972
+
973
+ export const deleteKiroSession = async (sessionsDir: string, sessionId: string): Promise<DeleteKiroSessionResult> => {
974
+ if (!(await pathExists(sessionsDir))) {
975
+ return { deletedFiles: [], deletedSessionIds: [] };
976
+ }
977
+
978
+ const file = await locateSessionFile(sessionsDir, sessionId);
979
+ if (!file) {
980
+ return { deletedFiles: [], deletedSessionIds: [] };
981
+ }
982
+
983
+ const transcript = await readSessionFile(file, { includeExecutions: false, sessionsDir });
984
+ const executionFiles = transcript
985
+ ? await listExecutionFilesForSession(
986
+ getKiroDataDirFromSessionsDir(sessionsDir),
987
+ sessionId,
988
+ transcript.session.worktree,
989
+ )
990
+ : [];
991
+ const deletedFiles = [file.filePath, ...executionFiles.map((execution) => execution.filePath)];
992
+
993
+ await Promise.all(deletedFiles.map((filePath) => rm(filePath, { force: true })));
994
+ await removeKiroSessionIndexEntry(path.dirname(file.filePath), sessionId);
995
+
996
+ return {
997
+ deletedFiles,
998
+ deletedSessionIds: [sessionId],
999
+ };
1000
+ };
@@ -3,9 +3,17 @@ export const formatModelLabel = (value: string | null | undefined): string => {
3
3
  return 'Assistant';
4
4
  }
5
5
 
6
- return value
7
- .split(/[-_\s]+/u)
8
- .filter(Boolean)
6
+ const parts = value.split(/[-_\s]+/u).filter(Boolean);
7
+ if (parts[0]?.toLowerCase() === 'claude') {
8
+ const majorIndex = parts.findIndex(
9
+ (part, index) => /^\d{1,2}$/u.test(part) && /^\d{1,2}$/u.test(parts[index + 1] ?? ''),
10
+ );
11
+ if (majorIndex >= 0) {
12
+ parts.splice(majorIndex, 2, `${parts[majorIndex]}.${parts[majorIndex + 1]}`);
13
+ }
14
+ }
15
+
16
+ return parts
9
17
  .map((part) => {
10
18
  const lower = part.toLowerCase();
11
19
  if (lower === 'gpt') {