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
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {
|
|
4
5
|
type AntigravityArtifact,
|
|
@@ -12,7 +13,17 @@ import {
|
|
|
12
13
|
} from './antigravity-exporter-types';
|
|
13
14
|
import { decryptAntigravitySafeStoragePayload } from './antigravity-keychain';
|
|
14
15
|
import { resolveAntigravityProjectNames } from './antigravity-projects';
|
|
16
|
+
import { getAntigravityAssistantPhase, getFinalAntigravityAssistantSequences } from './antigravity-transcript-phase';
|
|
15
17
|
import { mapWithConcurrency } from './concurrency';
|
|
18
|
+
import {
|
|
19
|
+
type ExportFormat,
|
|
20
|
+
formatInlineLiteral,
|
|
21
|
+
type MetadataEntry,
|
|
22
|
+
renderCodeBlock,
|
|
23
|
+
renderDocumentTitle,
|
|
24
|
+
renderMetadataBlock,
|
|
25
|
+
renderSection,
|
|
26
|
+
} from './shared';
|
|
16
27
|
|
|
17
28
|
type ProtoField = {
|
|
18
29
|
bytes?: Uint8Array;
|
|
@@ -66,11 +77,19 @@ export type AntigravityConversationMessage = {
|
|
|
66
77
|
createdAtMs: number | null;
|
|
67
78
|
metadata: Record<string, unknown>;
|
|
68
79
|
order: number;
|
|
69
|
-
phase: 'final_answer' | 'reasoning' | 'tool_call' | 'tool_output' | 'unknown';
|
|
80
|
+
phase: 'commentary' | 'final_answer' | 'reasoning' | 'tool_call' | 'tool_output' | 'unknown';
|
|
70
81
|
role: 'assistant' | 'system' | 'tool' | 'unknown' | 'user';
|
|
71
82
|
text: string;
|
|
72
83
|
};
|
|
73
84
|
|
|
85
|
+
export type AntigravityConversationRenderOptions = {
|
|
86
|
+
includeCommentary?: boolean;
|
|
87
|
+
includeMetadata?: boolean;
|
|
88
|
+
includeTools?: boolean;
|
|
89
|
+
keychainSecret?: string | null;
|
|
90
|
+
outputFormat?: ExportFormat;
|
|
91
|
+
};
|
|
92
|
+
|
|
74
93
|
type WorkspaceInfo = Pick<SummaryEntry, 'workspaceFolder' | 'workspaceKey' | 'workspaceLabel' | 'workspaceUri'>;
|
|
75
94
|
|
|
76
95
|
const UNKNOWN_WORKSPACE: WorkspaceInfo = {
|
|
@@ -400,7 +419,15 @@ const removeConversationFromSummaryIndex = async (summaryPath: string, conversat
|
|
|
400
419
|
return false;
|
|
401
420
|
}
|
|
402
421
|
|
|
403
|
-
|
|
422
|
+
const tempPath = `${summaryPath}.${randomUUID()}.tmp`;
|
|
423
|
+
const mode = (await stat(summaryPath)).mode & 0o777;
|
|
424
|
+
try {
|
|
425
|
+
await Bun.write(tempPath, Buffer.concat(retained));
|
|
426
|
+
await chmod(tempPath, mode);
|
|
427
|
+
await rename(tempPath, summaryPath);
|
|
428
|
+
} finally {
|
|
429
|
+
await rm(tempPath, { force: true });
|
|
430
|
+
}
|
|
404
431
|
return true;
|
|
405
432
|
};
|
|
406
433
|
|
|
@@ -577,15 +604,6 @@ const mergeArtifactMaps = async (roots: string[]): Promise<Map<string, Antigravi
|
|
|
577
604
|
return merged;
|
|
578
605
|
};
|
|
579
606
|
|
|
580
|
-
const countJsonlEntries = async (filePath: string): Promise<number> => {
|
|
581
|
-
try {
|
|
582
|
-
const text = await Bun.file(filePath).text();
|
|
583
|
-
return text.split(/\r?\n/u).filter((line) => line.trim().length > 0).length;
|
|
584
|
-
} catch {
|
|
585
|
-
return 0;
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
|
|
589
607
|
const stripModelQualifier = (value: string): string => value.replace(/\s*\([^)]*\)\s*$/u, '').trim();
|
|
590
608
|
|
|
591
609
|
const extractModelSelection = (content: string): string | null => {
|
|
@@ -610,18 +628,31 @@ const extractModelSelection = (content: string): string | null => {
|
|
|
610
628
|
return rawModel ? stripModelQualifier(rawModel) : null;
|
|
611
629
|
};
|
|
612
630
|
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
631
|
+
const transcriptAnalysisCache = new Map<string, { entryCount: number; model: string | null }>();
|
|
632
|
+
|
|
633
|
+
const analyzeTranscriptFile = async (filePath: string, size: number, mtimeMs: number) => {
|
|
634
|
+
const cacheKey = `${filePath}:${size}:${mtimeMs}`;
|
|
635
|
+
const cached = transcriptAnalysisCache.get(cacheKey);
|
|
636
|
+
if (cached) {
|
|
637
|
+
return cached;
|
|
638
|
+
}
|
|
639
|
+
const text = await Bun.file(filePath).text();
|
|
640
|
+
let model: string | null = null;
|
|
617
641
|
for (const entry of parseLogEntries(text)) {
|
|
618
|
-
|
|
619
|
-
if (model) {
|
|
620
|
-
|
|
642
|
+
model = extractModelSelection(getString(entry.content) ?? '') ?? model;
|
|
643
|
+
if (model !== null) {
|
|
644
|
+
break;
|
|
621
645
|
}
|
|
622
646
|
}
|
|
623
|
-
|
|
624
|
-
|
|
647
|
+
const analysis = {
|
|
648
|
+
entryCount: text.split(/\r?\n/u).filter((line) => line.trim().length > 0).length,
|
|
649
|
+
model,
|
|
650
|
+
};
|
|
651
|
+
if (transcriptAnalysisCache.size >= 256) {
|
|
652
|
+
transcriptAnalysisCache.delete(transcriptAnalysisCache.keys().next().value ?? '');
|
|
653
|
+
}
|
|
654
|
+
transcriptAnalysisCache.set(cacheKey, analysis);
|
|
655
|
+
return analysis;
|
|
625
656
|
};
|
|
626
657
|
|
|
627
658
|
const preferTranscriptFile = (current: TranscriptFile | undefined, candidate: TranscriptFile): TranscriptFile => {
|
|
@@ -658,11 +689,12 @@ const readTranscriptFileCandidate = async (
|
|
|
658
689
|
if (!info.isFile()) {
|
|
659
690
|
return null;
|
|
660
691
|
}
|
|
692
|
+
const analysis = await analyzeTranscriptFile(transcriptPath, info.size, info.mtimeMs);
|
|
661
693
|
|
|
662
694
|
return {
|
|
663
695
|
bytes: info.size,
|
|
664
|
-
entryCount:
|
|
665
|
-
model:
|
|
696
|
+
entryCount: analysis.entryCount,
|
|
697
|
+
model: analysis.model,
|
|
666
698
|
mtimeMs: info.mtimeMs,
|
|
667
699
|
path: transcriptPath,
|
|
668
700
|
root,
|
|
@@ -1043,48 +1075,99 @@ const logEntryHeading = (entry: AntigravityLogEntry): string => {
|
|
|
1043
1075
|
return type ? `Tool: ${type}` : 'Event';
|
|
1044
1076
|
};
|
|
1045
1077
|
|
|
1046
|
-
|
|
1078
|
+
type ResolvedAntigravityConversationRenderOptions = {
|
|
1079
|
+
includeCommentary: boolean;
|
|
1080
|
+
includeMetadata: boolean;
|
|
1081
|
+
includeTools: boolean;
|
|
1082
|
+
keychainSecret: string | null;
|
|
1083
|
+
outputFormat: ExportFormat;
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
const resolveAntigravityRenderOptions = (
|
|
1087
|
+
options: AntigravityConversationRenderOptions,
|
|
1088
|
+
): ResolvedAntigravityConversationRenderOptions => ({
|
|
1089
|
+
includeCommentary: options.includeCommentary ?? true,
|
|
1090
|
+
includeMetadata: options.includeMetadata ?? true,
|
|
1091
|
+
includeTools: options.includeTools ?? true,
|
|
1092
|
+
keychainSecret: options.keychainSecret ?? null,
|
|
1093
|
+
outputFormat: options.outputFormat ?? 'md',
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
const renderNestedHeading = (title: string, outputFormat: ExportFormat) => {
|
|
1097
|
+
return outputFormat === 'md' ? `### ${title}` : `${title}\n${'~'.repeat(Math.max(title.length, 3))}`;
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const renderToolCalls = (toolCalls: unknown, outputFormat: ExportFormat): string => {
|
|
1047
1101
|
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
1048
|
-
return
|
|
1102
|
+
return '';
|
|
1049
1103
|
}
|
|
1050
1104
|
|
|
1051
|
-
const parts = [
|
|
1105
|
+
const parts: string[] = [];
|
|
1052
1106
|
for (const call of toolCalls) {
|
|
1053
1107
|
if (!call || typeof call !== 'object') {
|
|
1054
1108
|
continue;
|
|
1055
1109
|
}
|
|
1056
1110
|
|
|
1057
1111
|
const { args, name } = call as { args?: unknown; name?: unknown };
|
|
1058
|
-
parts.push(`-
|
|
1112
|
+
parts.push(`- ${formatInlineLiteral(typeof name === 'string' ? name : 'unknown', outputFormat)}`);
|
|
1059
1113
|
if (args !== undefined) {
|
|
1060
|
-
parts.push('', '
|
|
1114
|
+
parts.push('', 'Input:', '', renderCodeBlock(JSON.stringify(args, null, 2), outputFormat), '');
|
|
1061
1115
|
}
|
|
1062
1116
|
}
|
|
1063
1117
|
|
|
1064
|
-
return parts;
|
|
1118
|
+
return parts.length > 0 ? [renderNestedHeading('Tool Calls', outputFormat), '', ...parts].join('\n').trimEnd() : '';
|
|
1065
1119
|
};
|
|
1066
1120
|
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1121
|
+
const renderLogEntryBodyParts = (
|
|
1122
|
+
entry: AntigravityLogEntry,
|
|
1123
|
+
role: AntigravityConversationMessage['role'],
|
|
1124
|
+
sequence: number,
|
|
1125
|
+
finalAssistantSequences: Set<number>,
|
|
1126
|
+
options: ResolvedAntigravityConversationRenderOptions,
|
|
1127
|
+
): string[] => {
|
|
1128
|
+
const bodyParts: string[] = [];
|
|
1071
1129
|
const thinking = getString(entry.thinking) ?? '';
|
|
1072
|
-
|
|
1130
|
+
if (options.includeCommentary && thinking) {
|
|
1131
|
+
bodyParts.push(renderNestedHeading('Thinking', options.outputFormat), '', thinking.trim(), '');
|
|
1132
|
+
}
|
|
1073
1133
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1134
|
+
const content = cleanLogContent(entry);
|
|
1135
|
+
const phase = role === 'assistant' ? getAntigravityAssistantPhase(sequence, finalAssistantSequences) : 'unknown';
|
|
1136
|
+
if (content && (role !== 'assistant' || phase !== 'commentary' || options.includeCommentary)) {
|
|
1137
|
+
bodyParts.push(content, '');
|
|
1076
1138
|
}
|
|
1077
1139
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1140
|
+
const toolCalls = options.includeTools ? renderToolCalls(entry.tool_calls, options.outputFormat) : '';
|
|
1141
|
+
if (toolCalls) {
|
|
1142
|
+
bodyParts.push(toolCalls);
|
|
1080
1143
|
}
|
|
1081
1144
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1145
|
+
return bodyParts;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
const renderLogEntry = (
|
|
1149
|
+
entry: AntigravityLogEntry,
|
|
1150
|
+
sequence: number,
|
|
1151
|
+
finalAssistantSequences: Set<number>,
|
|
1152
|
+
options: ResolvedAntigravityConversationRenderOptions,
|
|
1153
|
+
): string => {
|
|
1154
|
+
const heading = logEntryHeading(entry);
|
|
1155
|
+
const role = logEntryRole(entry);
|
|
1156
|
+
if (!options.includeTools && (role === 'tool' || heading.startsWith('Tool: '))) {
|
|
1157
|
+
return '';
|
|
1084
1158
|
}
|
|
1085
1159
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1160
|
+
const timestamp = getString(entry.created_at);
|
|
1161
|
+
const bodyParts = renderLogEntryBodyParts(entry, role, sequence, finalAssistantSequences, options);
|
|
1162
|
+
|
|
1163
|
+
if (bodyParts.length === 0) {
|
|
1164
|
+
return '';
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const parts = timestamp
|
|
1168
|
+
? [options.outputFormat === 'md' ? `_Timestamp: ${timestamp}_` : `Timestamp: ${timestamp}`, '', ...bodyParts]
|
|
1169
|
+
: bodyParts;
|
|
1170
|
+
return renderSection(heading, parts.join('\n').trimEnd(), options.outputFormat);
|
|
1088
1171
|
};
|
|
1089
1172
|
|
|
1090
1173
|
const logEntryCreatedAtMs = (entry: AntigravityLogEntry): number | null => {
|
|
@@ -1122,10 +1205,14 @@ const logEntryRole = (entry: AntigravityLogEntry): AntigravityConversationMessag
|
|
|
1122
1205
|
return 'unknown';
|
|
1123
1206
|
};
|
|
1124
1207
|
|
|
1125
|
-
const logEntryPhase = (
|
|
1208
|
+
const logEntryPhase = (
|
|
1209
|
+
entry: AntigravityLogEntry,
|
|
1210
|
+
sequence: number,
|
|
1211
|
+
finalAssistantSequences: Set<number>,
|
|
1212
|
+
): AntigravityConversationMessage['phase'] => {
|
|
1126
1213
|
const role = logEntryRole(entry);
|
|
1127
1214
|
if (role === 'assistant') {
|
|
1128
|
-
return
|
|
1215
|
+
return getAntigravityAssistantPhase(sequence, finalAssistantSequences);
|
|
1129
1216
|
}
|
|
1130
1217
|
if (role === 'tool') {
|
|
1131
1218
|
return 'tool_output';
|
|
@@ -1155,11 +1242,27 @@ const toolCallsText = (toolCalls: unknown): string => {
|
|
|
1155
1242
|
.join('\n');
|
|
1156
1243
|
};
|
|
1157
1244
|
|
|
1158
|
-
const
|
|
1245
|
+
const getAntigravityPhaseItems = (entries: AntigravityLogEntry[]) => {
|
|
1246
|
+
return entries.map((entry, sequence) => {
|
|
1247
|
+
const role = logEntryRole(entry);
|
|
1248
|
+
return {
|
|
1249
|
+
hasContent: Boolean(cleanLogContent(entry)),
|
|
1250
|
+
hasToolCalls: Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0,
|
|
1251
|
+
role: role === 'assistant' || role === 'user' ? role : ('other' as const),
|
|
1252
|
+
sequence,
|
|
1253
|
+
};
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const logEntryToMessages = (
|
|
1258
|
+
entry: AntigravityLogEntry,
|
|
1259
|
+
index: number,
|
|
1260
|
+
finalAssistantSequences: Set<number>,
|
|
1261
|
+
): AntigravityConversationMessage[] => {
|
|
1159
1262
|
const order = logEntryOrder(entry, index);
|
|
1160
1263
|
const createdAtMs = logEntryCreatedAtMs(entry);
|
|
1161
1264
|
const role = logEntryRole(entry);
|
|
1162
|
-
const phase = logEntryPhase(entry);
|
|
1265
|
+
const phase = logEntryPhase(entry, index, finalAssistantSequences);
|
|
1163
1266
|
const metadata = logEntryMetadata(entry);
|
|
1164
1267
|
const messages: AntigravityConversationMessage[] = [];
|
|
1165
1268
|
const thinking = getString(entry.thinking)?.trim();
|
|
@@ -1208,15 +1311,39 @@ export const readAntigravityConversationMessages = async (
|
|
|
1208
1311
|
return [];
|
|
1209
1312
|
}
|
|
1210
1313
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1314
|
+
const content = await Bun.file(conversation.transcriptPath).text();
|
|
1315
|
+
const entries = parseLogEntries(content);
|
|
1316
|
+
if (content.trim() && entries.length === 0) {
|
|
1317
|
+
throw new Error(`Antigravity transcript is corrupt: ${conversation.transcriptPath}`);
|
|
1318
|
+
}
|
|
1319
|
+
const finalAssistantSequences = getFinalAntigravityAssistantSequences(getAntigravityPhaseItems(entries));
|
|
1320
|
+
return entries.flatMap((entry, index) => logEntryToMessages(entry, index, finalAssistantSequences));
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const renderAntigravityMetadata = (entries: MetadataEntry[], outputFormat: ExportFormat): string => {
|
|
1324
|
+
if (outputFormat === 'txt') {
|
|
1325
|
+
return renderMetadataBlock(entries, outputFormat);
|
|
1216
1326
|
}
|
|
1327
|
+
|
|
1328
|
+
return entries
|
|
1329
|
+
.filter((entry) => entry.value !== null && entry.value !== undefined && entry.value !== '')
|
|
1330
|
+
.map((entry) => `- ${entry.key}: ${formatInlineLiteral(String(entry.value), outputFormat)}`)
|
|
1331
|
+
.join('\n');
|
|
1217
1332
|
};
|
|
1218
1333
|
|
|
1219
|
-
const
|
|
1334
|
+
const buildAntigravityIdentityMetadata = (
|
|
1335
|
+
conversation: AntigravityConversation,
|
|
1336
|
+
exportedFrom: string,
|
|
1337
|
+
): MetadataEntry[] => [
|
|
1338
|
+
{ key: 'exported_from', value: exportedFrom },
|
|
1339
|
+
{ key: 'conversation_id', value: conversation.conversationId },
|
|
1340
|
+
{ key: 'workspace', value: conversation.workspaceUri },
|
|
1341
|
+
];
|
|
1342
|
+
|
|
1343
|
+
const renderAntigravityTranscript = async (
|
|
1344
|
+
conversation: AntigravityConversation,
|
|
1345
|
+
options: ResolvedAntigravityConversationRenderOptions,
|
|
1346
|
+
): Promise<string | null> => {
|
|
1220
1347
|
if (!conversation.transcriptPath || !conversation.transcriptSource) {
|
|
1221
1348
|
return null;
|
|
1222
1349
|
}
|
|
@@ -1230,33 +1357,44 @@ const renderAntigravityTranscriptMarkdown = async (conversation: AntigravityConv
|
|
|
1230
1357
|
conversation.transcriptSource === 'overview'
|
|
1231
1358
|
? 'antigravity_overview_transcript'
|
|
1232
1359
|
: 'antigravity_jsonl_transcript';
|
|
1233
|
-
const
|
|
1360
|
+
const finalAssistantSequences = getFinalAntigravityAssistantSequences(getAntigravityPhaseItems(entries));
|
|
1361
|
+
const sections = entries
|
|
1362
|
+
.map((entry, sequence) => renderLogEntry(entry, sequence, finalAssistantSequences, options))
|
|
1363
|
+
.filter(Boolean);
|
|
1234
1364
|
const parts = [
|
|
1235
|
-
|
|
1236
|
-
'',
|
|
1237
|
-
`- exported_from: \`${exportedFrom}\``,
|
|
1238
|
-
`- conversation_id: \`${conversation.conversationId}\``,
|
|
1239
|
-
conversation.workspaceUri ? `- workspace: \`${conversation.workspaceUri}\`` : '',
|
|
1365
|
+
renderDocumentTitle(conversation.title, options.outputFormat),
|
|
1240
1366
|
'',
|
|
1367
|
+
options.includeMetadata
|
|
1368
|
+
? renderAntigravityMetadata(
|
|
1369
|
+
buildAntigravityIdentityMetadata(conversation, exportedFrom),
|
|
1370
|
+
options.outputFormat,
|
|
1371
|
+
)
|
|
1372
|
+
: '',
|
|
1241
1373
|
...sections,
|
|
1242
1374
|
].filter(Boolean);
|
|
1243
1375
|
|
|
1244
1376
|
return `${parts.join('\n').trimEnd()}\n`;
|
|
1245
1377
|
};
|
|
1246
1378
|
|
|
1247
|
-
const
|
|
1379
|
+
const renderDecryptedSafeStorage = (
|
|
1380
|
+
conversation: AntigravityConversation,
|
|
1381
|
+
content: string,
|
|
1382
|
+
options: ResolvedAntigravityConversationRenderOptions,
|
|
1383
|
+
): string | null => {
|
|
1248
1384
|
const trimmed = content.trim();
|
|
1249
1385
|
if (!trimmed) {
|
|
1250
1386
|
return null;
|
|
1251
1387
|
}
|
|
1252
1388
|
|
|
1253
1389
|
return [
|
|
1254
|
-
|
|
1255
|
-
'',
|
|
1256
|
-
'- exported_from: `antigravity_safe_storage_payload`',
|
|
1257
|
-
`- conversation_id: \`${conversation.conversationId}\``,
|
|
1258
|
-
conversation.workspaceUri ? `- workspace: \`${conversation.workspaceUri}\`` : '',
|
|
1390
|
+
renderDocumentTitle(conversation.title, options.outputFormat),
|
|
1259
1391
|
'',
|
|
1392
|
+
options.includeMetadata
|
|
1393
|
+
? renderAntigravityMetadata(
|
|
1394
|
+
buildAntigravityIdentityMetadata(conversation, 'antigravity_safe_storage_payload'),
|
|
1395
|
+
options.outputFormat,
|
|
1396
|
+
)
|
|
1397
|
+
: '',
|
|
1260
1398
|
trimmed,
|
|
1261
1399
|
'',
|
|
1262
1400
|
]
|
|
@@ -1264,7 +1402,10 @@ const renderDecryptedSafeStorageMarkdown = (conversation: AntigravityConversatio
|
|
|
1264
1402
|
.join('\n');
|
|
1265
1403
|
};
|
|
1266
1404
|
|
|
1267
|
-
const
|
|
1405
|
+
const renderAntigravitySummary = (
|
|
1406
|
+
conversation: AntigravityConversation,
|
|
1407
|
+
options: ResolvedAntigravityConversationRenderOptions,
|
|
1408
|
+
): string | null => {
|
|
1268
1409
|
if (conversation.artifactCount > 0) {
|
|
1269
1410
|
return null;
|
|
1270
1411
|
}
|
|
@@ -1274,16 +1415,30 @@ const renderAntigravitySummaryMarkdown = (conversation: AntigravityConversation)
|
|
|
1274
1415
|
}
|
|
1275
1416
|
|
|
1276
1417
|
return [
|
|
1277
|
-
|
|
1278
|
-
'',
|
|
1279
|
-
'- exported_from: `antigravity_summary_index`',
|
|
1280
|
-
`- conversation_id: \`${conversation.conversationId}\``,
|
|
1281
|
-
conversation.workspaceUri ? `- workspace: \`${conversation.workspaceUri}\`` : '',
|
|
1282
|
-
conversation.indexedItemCount !== null ? `- indexed_items: \`${conversation.indexedItemCount}\`` : '',
|
|
1283
|
-
conversation.createdAtMs ? `- created_at: \`${new Date(conversation.createdAtMs).toISOString()}\`` : '',
|
|
1284
|
-
conversation.lastUpdatedAtMs ? `- updated_at: \`${new Date(conversation.lastUpdatedAtMs).toISOString()}\`` : '',
|
|
1418
|
+
renderDocumentTitle(conversation.title, options.outputFormat),
|
|
1285
1419
|
'',
|
|
1286
|
-
|
|
1420
|
+
options.includeMetadata
|
|
1421
|
+
? renderAntigravityMetadata(
|
|
1422
|
+
[
|
|
1423
|
+
...buildAntigravityIdentityMetadata(conversation, 'antigravity_summary_index'),
|
|
1424
|
+
{ key: 'indexed_items', value: conversation.indexedItemCount },
|
|
1425
|
+
{
|
|
1426
|
+
key: 'created_at',
|
|
1427
|
+
value: conversation.createdAtMs ? new Date(conversation.createdAtMs).toISOString() : null,
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
key: 'updated_at',
|
|
1431
|
+
value: conversation.lastUpdatedAtMs
|
|
1432
|
+
? new Date(conversation.lastUpdatedAtMs).toISOString()
|
|
1433
|
+
: null,
|
|
1434
|
+
},
|
|
1435
|
+
],
|
|
1436
|
+
options.outputFormat,
|
|
1437
|
+
)
|
|
1438
|
+
: '',
|
|
1439
|
+
options.includeMetadata
|
|
1440
|
+
? 'No local transcript log was found for this Antigravity conversation. The export contains the summary index metadata that Antigravity retained.'
|
|
1441
|
+
: 'No local transcript log was found for this Antigravity conversation.',
|
|
1287
1442
|
'',
|
|
1288
1443
|
]
|
|
1289
1444
|
.filter(Boolean)
|
|
@@ -1292,22 +1447,21 @@ const renderAntigravitySummaryMarkdown = (conversation: AntigravityConversation)
|
|
|
1292
1447
|
|
|
1293
1448
|
export const renderAntigravityConversationMarkdown = async (
|
|
1294
1449
|
conversation: AntigravityConversation,
|
|
1295
|
-
options: {
|
|
1296
|
-
keychainSecret?: string | null;
|
|
1297
|
-
} = {},
|
|
1450
|
+
options: AntigravityConversationRenderOptions = {},
|
|
1298
1451
|
): Promise<string | null> => {
|
|
1299
|
-
const
|
|
1452
|
+
const resolvedOptions = resolveAntigravityRenderOptions(options);
|
|
1453
|
+
const transcript = await renderAntigravityTranscript(conversation, resolvedOptions);
|
|
1300
1454
|
if (transcript) {
|
|
1301
1455
|
return transcript;
|
|
1302
1456
|
}
|
|
1303
1457
|
|
|
1304
|
-
if (
|
|
1458
|
+
if (resolvedOptions.keychainSecret && conversation.conversationPath) {
|
|
1305
1459
|
const encrypted = Buffer.from(await Bun.file(conversation.conversationPath).arrayBuffer());
|
|
1306
|
-
const decrypted = decryptAntigravitySafeStoragePayload(encrypted,
|
|
1460
|
+
const decrypted = decryptAntigravitySafeStoragePayload(encrypted, resolvedOptions.keychainSecret);
|
|
1307
1461
|
if (decrypted) {
|
|
1308
|
-
return
|
|
1462
|
+
return renderDecryptedSafeStorage(conversation, decrypted, resolvedOptions);
|
|
1309
1463
|
}
|
|
1310
1464
|
}
|
|
1311
1465
|
|
|
1312
|
-
return
|
|
1466
|
+
return renderAntigravitySummary(conversation, resolvedOptions);
|
|
1313
1467
|
};
|
|
@@ -169,19 +169,18 @@ export const getAntigravityDecryptionState = ({
|
|
|
169
169
|
|
|
170
170
|
export const getCachedAntigravityKeychainSecret = (): string | null => cachedKeychainSecret;
|
|
171
171
|
|
|
172
|
+
export const getAntigravityKeychainExecOptions = () => ({ timeout: 10_000 });
|
|
173
|
+
|
|
172
174
|
export const readAntigravityKeychainSecret = async (): Promise<string> => {
|
|
173
175
|
if (process.platform !== 'darwin') {
|
|
174
176
|
throw new Error('Antigravity Keychain access is only available on macOS.');
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
const { stdout } = await execFileAsync(
|
|
178
|
-
'
|
|
179
|
-
'-s',
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ANTIGRAVITY_KEYCHAIN_ACCOUNT,
|
|
183
|
-
'-w',
|
|
184
|
-
]);
|
|
179
|
+
const { stdout } = await execFileAsync(
|
|
180
|
+
'security',
|
|
181
|
+
['find-generic-password', '-s', ANTIGRAVITY_KEYCHAIN_SERVICE, '-a', ANTIGRAVITY_KEYCHAIN_ACCOUNT, '-w'],
|
|
182
|
+
getAntigravityKeychainExecOptions(),
|
|
183
|
+
);
|
|
185
184
|
const secret = stdout.trim();
|
|
186
185
|
if (!secret) {
|
|
187
186
|
throw new Error(`No secret was returned for ${ANTIGRAVITY_KEYCHAIN_SERVICE}.`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type AntigravityTranscriptPhaseItem = {
|
|
2
|
+
hasContent: boolean;
|
|
3
|
+
hasToolCalls: boolean;
|
|
4
|
+
role: 'assistant' | 'other' | 'user';
|
|
5
|
+
sequence: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type AntigravityAssistantMessagePhase = 'commentary' | 'final_answer';
|
|
9
|
+
|
|
10
|
+
export const getFinalAntigravityAssistantSequences = (items: AntigravityTranscriptPhaseItem[]): Set<number> => {
|
|
11
|
+
const finalSequences = new Set<number>();
|
|
12
|
+
let latestAssistantContentSequence: number | null = null;
|
|
13
|
+
|
|
14
|
+
const flushAssistantRun = () => {
|
|
15
|
+
if (latestAssistantContentSequence !== null) {
|
|
16
|
+
finalSequences.add(latestAssistantContentSequence);
|
|
17
|
+
}
|
|
18
|
+
latestAssistantContentSequence = null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
if (item.role === 'user') {
|
|
23
|
+
flushAssistantRun();
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (item.role !== 'assistant') {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (item.hasToolCalls) {
|
|
32
|
+
latestAssistantContentSequence = null;
|
|
33
|
+
} else if (item.hasContent) {
|
|
34
|
+
latestAssistantContentSequence = item.sequence;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
flushAssistantRun();
|
|
39
|
+
return finalSequences;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const getAntigravityAssistantPhase = (
|
|
43
|
+
sequence: number,
|
|
44
|
+
finalAssistantSequences: Set<number>,
|
|
45
|
+
): AntigravityAssistantMessagePhase => {
|
|
46
|
+
return finalAssistantSequences.has(sequence) ? 'final_answer' : 'commentary';
|
|
47
|
+
};
|