spiracha 1.2.0 → 1.3.2

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 (149) hide show
  1. package/AGENTS.md +50 -12
  2. package/README.md +117 -64
  3. package/apps/ui/AGENTS.md +16 -8
  4. package/apps/ui/README.md +28 -12
  5. package/apps/ui/dist/client/assets/{analytics-Cv0JMDN2.js → analytics-B_hYz65v.js} +1 -1
  6. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-qiyygB7e.js +1 -0
  7. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-z1SQC2Kg.js +1 -0
  8. package/apps/ui/dist/client/assets/antigravity-keychain-panel-dYuRWtCf.js +1 -0
  9. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CliqUr7o.js +1 -0
  10. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CnoBzyX6.js +1 -0
  11. package/apps/ui/dist/client/assets/antigravity.index-CakfZz_E.js +1 -0
  12. package/apps/ui/dist/client/assets/antigravity.index-DY7M1KhG.js +1 -0
  13. package/apps/ui/dist/client/assets/badge-aHE9ETVe.js +1 -0
  14. package/apps/ui/dist/client/assets/checkbox-DN3XnJaA.js +1 -0
  15. package/apps/ui/dist/client/assets/cursor-threads._composerId-BMQyx8qG.js +1 -0
  16. package/apps/ui/dist/client/assets/cursor-threads._composerId-BTlaA-tV.js +1 -0
  17. package/apps/ui/dist/client/assets/cursor._workspaceKey-CrgrfevV.js +1 -0
  18. package/apps/ui/dist/client/assets/cursor._workspaceKey-bYS2syGL.js +1 -0
  19. package/apps/ui/dist/client/assets/cursor.index-CTqZMPYU.js +1 -0
  20. package/apps/ui/dist/client/assets/cursor.index-Clsz4E_e.js +2 -0
  21. package/apps/ui/dist/client/assets/{data-table-Bgnh7phF.js → data-table-Cj-v-uyB.js} +2 -2
  22. package/apps/ui/dist/client/assets/delete-confirm-dialog-DTpzBiNK.js +11 -0
  23. package/apps/ui/dist/client/assets/dist-BNAn99Pu.js +1 -0
  24. package/apps/ui/dist/client/assets/download-P3Rp23Ad.js +1 -0
  25. package/apps/ui/dist/client/assets/dropdown-menu-3qB5j9nt.js +1 -0
  26. package/apps/ui/dist/client/assets/es2015-Dwm_turD.js +41 -0
  27. package/apps/ui/dist/client/assets/export-dialog-CazdrASq.js +1 -0
  28. package/apps/ui/dist/client/assets/formatters-BdnWuM1z.js +1 -0
  29. package/apps/ui/dist/client/assets/index-BVFnfS78.js +22 -0
  30. package/apps/ui/dist/client/assets/json-panel-DLkS30sQ.js +1 -0
  31. package/apps/ui/dist/client/assets/metadata-section-jnIkB7dB.js +1 -0
  32. package/apps/ui/dist/client/assets/{metric-card-BJX5rkHK.js → metric-card-CBZuWLzQ.js} +1 -1
  33. package/apps/ui/dist/client/assets/page-header-CnD21cPn.js +1 -0
  34. package/apps/ui/dist/client/assets/projects._project-BLszwvYL.js +1 -0
  35. package/apps/ui/dist/client/assets/projects._project-DvLxYbvk.js +1 -0
  36. package/apps/ui/dist/client/assets/projects.index-COn8woBR.js +1 -0
  37. package/apps/ui/dist/client/assets/projects.index-DYs98skV.js +3 -0
  38. package/apps/ui/dist/client/assets/refresh-ccw-BDrYXjtD.js +1 -0
  39. package/apps/ui/dist/client/assets/reload-error-panel-DLAg0AW2.js +1 -0
  40. package/apps/ui/dist/client/assets/routes-BtF5-coe.js +1 -0
  41. package/apps/ui/dist/client/assets/scroll-text-CqaFm9by.js +1 -0
  42. package/apps/ui/dist/client/assets/select-DbnpwqL6.js +1 -0
  43. package/apps/ui/dist/client/assets/settings-CGX3VleN.js +1 -0
  44. package/apps/ui/dist/client/assets/styles-Ch0r3kMZ.css +1 -0
  45. package/apps/ui/dist/client/assets/text-document-panel-DPleOmmq.js +1 -0
  46. package/apps/ui/dist/client/assets/text-filter-7M6wRo-t.js +2 -0
  47. package/apps/ui/dist/client/assets/threads._threadId-D5w76IB-.js +7 -0
  48. package/apps/ui/dist/client/assets/{threads._threadId-CUiCZSwo.js → threads._threadId-Dx85sI9P.js} +1 -1
  49. package/apps/ui/dist/client/assets/useMutation-MZ3Hr9h9.js +1 -0
  50. package/apps/ui/dist/client/assets/useQuery-Cb4V0AT0.js +1 -0
  51. package/apps/ui/dist/client/icon.svg +28 -0
  52. package/apps/ui/dist/client/manifest.json +6 -16
  53. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CBbkUXw6.js +227 -0
  54. package/apps/ui/dist/server/assets/{analytics-2QpLKjlG.js → analytics-CBNOYZwJ.js} +2 -2
  55. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +16 -0
  56. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-B9Rm4EXh.js +212 -0
  57. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BIdYNy68.js +20 -0
  58. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +11 -0
  59. package/apps/ui/dist/server/assets/antigravity-db-D9gW1D8G.js +576 -0
  60. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +126 -0
  61. package/apps/ui/dist/server/assets/antigravity-keychain-panel-DcLyBBwd.js +55 -0
  62. package/apps/ui/dist/server/assets/antigravity-queries-CgQhlQ7J.js +37 -0
  63. package/apps/ui/dist/server/assets/antigravity-server-DFUx4Khk.js +114 -0
  64. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +11 -0
  65. package/apps/ui/dist/server/assets/antigravity._workspaceKey-D42ixtzp.js +210 -0
  66. package/apps/ui/dist/server/assets/antigravity._workspaceKey-DnSlSC-C.js +28 -0
  67. package/apps/ui/dist/server/assets/antigravity.index-DZVT-cac.js +104 -0
  68. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +11 -0
  69. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +26 -0
  70. package/apps/ui/dist/server/assets/{codex-queries-BH4Cb0v3.js → codex-queries-eOJGfHQj.js} +4 -16
  71. package/apps/ui/dist/server/assets/{codex-server-DqzruLmg.js → codex-server-nrETIF--.js} +149 -140
  72. package/apps/ui/dist/server/assets/createServerRpc-BtXIw2iP.js +12 -0
  73. package/apps/ui/dist/server/assets/createSsrRpc-COf5Zuye.js +16 -0
  74. package/apps/ui/dist/server/assets/cursor-db-B7agkAvM.js +643 -0
  75. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +34 -0
  76. package/apps/ui/dist/server/assets/cursor-queries-BMhuJeUO.js +65 -0
  77. package/apps/ui/dist/server/assets/cursor-recovery-9bJLs7vG.js +361 -0
  78. package/apps/ui/dist/server/assets/cursor-server-BgylIFgn.js +184 -0
  79. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +11 -0
  80. package/apps/ui/dist/server/assets/cursor-threads._composerId-BsxFKzoJ.js +218 -0
  81. package/apps/ui/dist/server/assets/cursor-threads._composerId-DXffY_CK.js +18 -0
  82. package/apps/ui/dist/server/assets/cursor-transcript-2iL3KFSK.js +125 -0
  83. package/apps/ui/dist/server/assets/cursor._workspaceKey-BP2J1x_x.js +28 -0
  84. package/apps/ui/dist/server/assets/cursor._workspaceKey-BQd0e-Pd.js +399 -0
  85. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +11 -0
  86. package/apps/ui/dist/server/assets/cursor.index-CQVxtCm8.js +189 -0
  87. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +11 -0
  88. package/apps/ui/dist/server/assets/{delete-confirm-dialog-CWqcTXTF.js → delete-confirm-dialog-PCD7S0_M.js} +5 -4
  89. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +92 -0
  90. package/apps/ui/dist/server/assets/{input-B4tEzctc.js → dropdown-menu-Dy_9t6TN.js} +1 -11
  91. package/apps/ui/dist/server/assets/{download-Drctxary.js → export-dialog-DaPlOGFT.js} +1 -92
  92. package/apps/ui/dist/server/assets/json-panel-RYsxWFae.js +16 -0
  93. package/apps/ui/dist/server/assets/{loading-panel-DbLdvjtR.js → loading-panel-BGFnWseS.js} +1 -1
  94. package/apps/ui/dist/server/assets/metadata-section-D6Lbc7D6.js +54 -0
  95. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +29 -0
  96. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +12 -0
  97. package/apps/ui/dist/server/assets/{projects._project-gT01HBqH.js → projects._project-DUN3iWfg.js} +4 -4
  98. package/apps/ui/dist/server/assets/{projects._project-DreIU5b0.js → projects._project-Dim9Y0kD.js} +54 -26
  99. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +12 -0
  100. package/apps/ui/dist/server/assets/{projects.index-BYmgSGAj.js → projects.index-DjSQK5dm.js} +23 -27
  101. package/apps/ui/dist/server/assets/{projects.index-CaplpeMy.js → reload-error-panel-BJMxY3U1.js} +5 -6
  102. package/apps/ui/dist/server/assets/{router-Qj5Kn7bl.js → router-DrDgc-LD.js} +131 -44
  103. package/apps/ui/dist/server/assets/{routes-_LbCIjtJ.js → routes-B-GlEe2C.js} +54 -39
  104. package/apps/ui/dist/server/assets/{routes-BtcXuK0x.js → routes-CNHAUMwo.js} +2 -2
  105. package/apps/ui/dist/server/assets/{settings-MvWDgc1u.js → settings-OayxIYQQ.js} +1 -1
  106. package/apps/ui/dist/server/assets/shared-CPRNYIql.js +134 -0
  107. package/apps/ui/dist/server/assets/text-document-panel-D8JbQWAn.js +23 -0
  108. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +36 -0
  109. package/apps/ui/dist/server/assets/{threads._threadId-DcbAJkwf.js → threads._threadId-CJzm4KrZ.js} +3 -3
  110. package/apps/ui/dist/server/assets/{threads._threadId-D5m6ypGw.js → threads._threadId-DODTYddm.js} +69 -76
  111. package/apps/ui/dist/server/server.js +77 -13
  112. package/package.json +21 -11
  113. package/src/export-cursor.ts +244 -0
  114. package/src/lib/antigravity-db.ts +936 -0
  115. package/src/lib/antigravity-exporter-types.ts +70 -0
  116. package/src/lib/antigravity-keychain.ts +203 -0
  117. package/src/lib/codex-browser-db.ts +7 -1
  118. package/src/lib/codex-browser-types.ts +22 -1
  119. package/src/lib/codex-thread-recovery.ts +202 -0
  120. package/src/lib/cursor-db.ts +1096 -0
  121. package/src/lib/cursor-exporter-types.ts +190 -0
  122. package/src/lib/cursor-exporter.ts +266 -0
  123. package/src/lib/cursor-recovery.ts +543 -0
  124. package/src/lib/cursor-transcript.ts +183 -0
  125. package/src/spiracha.ts +16 -3
  126. package/src/ui-cli.ts +2 -2
  127. package/apps/ui/dist/client/assets/checkbox-DjHij7DJ.js +0 -1
  128. package/apps/ui/dist/client/assets/delete-confirm-dialog-CIZy_LXD.js +0 -11
  129. package/apps/ui/dist/client/assets/download-DQtfva4z.js +0 -1
  130. package/apps/ui/dist/client/assets/es2015-DsDKdYCE.js +0 -41
  131. package/apps/ui/dist/client/assets/formatters-CWFrMKSn.js +0 -1
  132. package/apps/ui/dist/client/assets/index-C_-e0lDI.js +0 -22
  133. package/apps/ui/dist/client/assets/input-BbgApiqZ.js +0 -1
  134. package/apps/ui/dist/client/assets/page-header-ODLuGLAB.js +0 -1
  135. package/apps/ui/dist/client/assets/projects._project-C2Pys_bB.js +0 -1
  136. package/apps/ui/dist/client/assets/projects._project-CHvAKvlu.js +0 -1
  137. package/apps/ui/dist/client/assets/projects.index-BmwtS1x-.js +0 -1
  138. package/apps/ui/dist/client/assets/projects.index-CuLw73mt.js +0 -1
  139. package/apps/ui/dist/client/assets/routes-CfnaTOlj.js +0 -1
  140. package/apps/ui/dist/client/assets/select-B1kH_5lx.js +0 -1
  141. package/apps/ui/dist/client/assets/settings-mYTB3sso.js +0 -1
  142. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +0 -1
  143. package/apps/ui/dist/client/assets/threads._threadId-C_47okme.js +0 -7
  144. package/apps/ui/dist/client/favicon.ico +0 -0
  145. package/apps/ui/dist/client/logo192.png +0 -0
  146. package/apps/ui/dist/client/logo512.png +0 -0
  147. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-kj_QB_26.js +0 -99
  148. package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +0 -25
  149. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +0 -26
@@ -0,0 +1,70 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export const DEFAULT_ANTIGRAVITY_IDE_DIR = path.join(os.homedir(), '.gemini', 'antigravity-ide');
5
+ export const DEFAULT_ANTIGRAVITY_DIR = path.join(os.homedir(), '.gemini', 'antigravity');
6
+
7
+ export const resolveAntigravityRoots = (): string[] => {
8
+ const configured = process.env.SPIRACHA_ANTIGRAVITY_DIRS?.trim() || process.env.SPIRACHA_ANTIGRAVITY_DIR?.trim();
9
+ if (configured) {
10
+ return configured
11
+ .split(path.delimiter)
12
+ .map((entry) => entry.trim())
13
+ .filter(Boolean);
14
+ }
15
+
16
+ return [DEFAULT_ANTIGRAVITY_IDE_DIR, DEFAULT_ANTIGRAVITY_DIR];
17
+ };
18
+
19
+ export const getAntigravityConversationDir = (root: string): string => path.join(root, 'conversations');
20
+
21
+ export const getAntigravityBrainDir = (root: string): string => path.join(root, 'brain');
22
+
23
+ export const getAntigravitySummaryIndexPath = (root: string): string => path.join(root, 'agyhub_summaries_proto.pb');
24
+
25
+ export type AntigravityArtifact = {
26
+ artifactType: string | null;
27
+ bytes: number;
28
+ name: string;
29
+ path: string;
30
+ sourceRoot: string;
31
+ summary: string | null;
32
+ updatedAtMs: number | null;
33
+ };
34
+
35
+ export type AntigravityTranscriptSource = 'overview' | 'safe-storage' | 'transcript';
36
+
37
+ export type AntigravityConversation = {
38
+ artifactBytes: number;
39
+ artifactCount: number;
40
+ artifacts: AntigravityArtifact[];
41
+ conversationBytes: number;
42
+ conversationId: string;
43
+ conversationMtimeMs: number | null;
44
+ conversationPath: string | null;
45
+ createdAtMs: number | null;
46
+ indexedItemCount: number | null;
47
+ lastUpdatedAtMs: number | null;
48
+ sourceRoot: string | null;
49
+ summaryPath: string | null;
50
+ title: string;
51
+ transcriptBytes: number;
52
+ transcriptEntryCount: number;
53
+ transcriptPath: string | null;
54
+ transcriptSource: AntigravityTranscriptSource | null;
55
+ workspaceFolder: string | null;
56
+ workspaceKey: string;
57
+ workspaceLabel: string;
58
+ workspaceUri: string | null;
59
+ };
60
+
61
+ export type AntigravityWorkspaceGroup = {
62
+ artifactCount: number;
63
+ conversationBytes: number;
64
+ conversationCount: number;
65
+ key: string;
66
+ label: string;
67
+ lastActiveMs: number;
68
+ transcriptCount: number;
69
+ uri: string | null;
70
+ };
@@ -0,0 +1,203 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createDecipheriv, pbkdf2Sync } from 'node:crypto';
3
+ import { promisify } from 'node:util';
4
+
5
+ export const ANTIGRAVITY_KEYCHAIN_SERVICE = 'Antigravity Safe Storage';
6
+ export const ANTIGRAVITY_KEYCHAIN_ACCOUNT = 'Antigravity Key';
7
+
8
+ export type AntigravityDecryptionState = {
9
+ canRequestAccess: boolean;
10
+ error: string | null;
11
+ isUnlocked: boolean;
12
+ keychainAccount: string;
13
+ keychainService: string;
14
+ platform: NodeJS.Platform;
15
+ provider: 'keychain' | 'unsupported';
16
+ status: 'error' | 'locked' | 'unlocked' | 'unsupported';
17
+ };
18
+
19
+ const execFileAsync = promisify(execFile);
20
+ const SAFE_STORAGE_SALT = 'saltysalt';
21
+ const SAFE_STORAGE_ITERATIONS = 1003;
22
+ const SAFE_STORAGE_KEY_LENGTH = 16;
23
+ const SAFE_STORAGE_IV = Buffer.alloc(16, 0x20);
24
+
25
+ let cachedKeychainSecret: string | null = null;
26
+ let lastKeychainError: string | null = null;
27
+
28
+ export const deriveAntigravitySafeStorageKey = (keychainSecret: string | Buffer): Buffer => {
29
+ return pbkdf2Sync(keychainSecret, SAFE_STORAGE_SALT, SAFE_STORAGE_ITERATIONS, SAFE_STORAGE_KEY_LENGTH, 'sha1');
30
+ };
31
+
32
+ const parseBufferJson = (value: unknown): Buffer | null => {
33
+ if (!value || typeof value !== 'object') {
34
+ return null;
35
+ }
36
+
37
+ const data = (value as { data?: unknown }).data;
38
+ if (!Array.isArray(data) || data.some((entry) => typeof entry !== 'number')) {
39
+ return null;
40
+ }
41
+
42
+ return Buffer.from(data);
43
+ };
44
+
45
+ const normalizeEncryptedPayload = (payload: Buffer | Uint8Array | string): Buffer | null => {
46
+ if (payload instanceof Buffer) {
47
+ return payload;
48
+ }
49
+
50
+ if (payload instanceof Uint8Array) {
51
+ return Buffer.from(payload);
52
+ }
53
+
54
+ const trimmed = payload.trim();
55
+ if (trimmed.startsWith('{')) {
56
+ try {
57
+ return parseBufferJson(JSON.parse(trimmed));
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ return Buffer.from(payload, 'binary');
64
+ };
65
+
66
+ const hasSafeStoragePrefix = (payload: Buffer): boolean => {
67
+ const prefix = payload.subarray(0, 3).toString('ascii');
68
+ return prefix === 'v10' || prefix === 'v11';
69
+ };
70
+
71
+ const isReadableUtf8 = (value: string): boolean => {
72
+ if (value.includes('\uFFFD')) {
73
+ return false;
74
+ }
75
+
76
+ const printable = [...value].filter((char) => {
77
+ const code = char.charCodeAt(0);
78
+ return code === 9 || code === 10 || code === 13 || code >= 32;
79
+ }).length;
80
+
81
+ return value.length === 0 || printable / value.length > 0.95;
82
+ };
83
+
84
+ const decryptWithKey = (encrypted: Buffer, key: Buffer): string | null => {
85
+ const ciphertext = hasSafeStoragePrefix(encrypted) ? encrypted.subarray(3) : encrypted;
86
+ if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
87
+ return null;
88
+ }
89
+
90
+ try {
91
+ const decipher = createDecipheriv('aes-128-cbc', key, SAFE_STORAGE_IV);
92
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
93
+ return isReadableUtf8(decrypted) ? decrypted : null;
94
+ } catch {
95
+ return null;
96
+ }
97
+ };
98
+
99
+ export const decryptAntigravitySafeStoragePayload = (
100
+ payload: Buffer | Uint8Array | string,
101
+ keychainSecret: string,
102
+ ): string | null => {
103
+ const encrypted = normalizeEncryptedPayload(payload);
104
+ if (!encrypted) {
105
+ return null;
106
+ }
107
+
108
+ const keyAttempts = [deriveAntigravitySafeStorageKey(keychainSecret)];
109
+ if (/^[A-Za-z0-9+/]+={0,2}$/u.test(keychainSecret)) {
110
+ keyAttempts.push(deriveAntigravitySafeStorageKey(Buffer.from(keychainSecret, 'base64')));
111
+ }
112
+
113
+ for (const key of keyAttempts) {
114
+ const decrypted = decryptWithKey(encrypted, key);
115
+ if (decrypted !== null) {
116
+ return decrypted;
117
+ }
118
+ }
119
+
120
+ return null;
121
+ };
122
+
123
+ export const getAntigravityDecryptionState = ({
124
+ cachedSecret = cachedKeychainSecret,
125
+ lastError = lastKeychainError,
126
+ platform = process.platform,
127
+ }: {
128
+ cachedSecret?: string | null;
129
+ lastError?: string | null;
130
+ platform?: NodeJS.Platform;
131
+ } = {}): AntigravityDecryptionState => {
132
+ if (platform !== 'darwin') {
133
+ return {
134
+ canRequestAccess: false,
135
+ error: null,
136
+ isUnlocked: false,
137
+ keychainAccount: ANTIGRAVITY_KEYCHAIN_ACCOUNT,
138
+ keychainService: ANTIGRAVITY_KEYCHAIN_SERVICE,
139
+ platform,
140
+ provider: 'unsupported',
141
+ status: 'unsupported',
142
+ };
143
+ }
144
+
145
+ if (cachedSecret) {
146
+ return {
147
+ canRequestAccess: true,
148
+ error: null,
149
+ isUnlocked: true,
150
+ keychainAccount: ANTIGRAVITY_KEYCHAIN_ACCOUNT,
151
+ keychainService: ANTIGRAVITY_KEYCHAIN_SERVICE,
152
+ platform,
153
+ provider: 'keychain',
154
+ status: 'unlocked',
155
+ };
156
+ }
157
+
158
+ return {
159
+ canRequestAccess: true,
160
+ error: lastError,
161
+ isUnlocked: false,
162
+ keychainAccount: ANTIGRAVITY_KEYCHAIN_ACCOUNT,
163
+ keychainService: ANTIGRAVITY_KEYCHAIN_SERVICE,
164
+ platform,
165
+ provider: 'keychain',
166
+ status: lastError ? 'error' : 'locked',
167
+ };
168
+ };
169
+
170
+ export const getCachedAntigravityKeychainSecret = (): string | null => cachedKeychainSecret;
171
+
172
+ export const readAntigravityKeychainSecret = async (): Promise<string> => {
173
+ if (process.platform !== 'darwin') {
174
+ throw new Error('Antigravity Keychain access is only available on macOS.');
175
+ }
176
+
177
+ const { stdout } = await execFileAsync('security', [
178
+ 'find-generic-password',
179
+ '-s',
180
+ ANTIGRAVITY_KEYCHAIN_SERVICE,
181
+ '-a',
182
+ ANTIGRAVITY_KEYCHAIN_ACCOUNT,
183
+ '-w',
184
+ ]);
185
+ const secret = stdout.trim();
186
+ if (!secret) {
187
+ throw new Error(`No secret was returned for ${ANTIGRAVITY_KEYCHAIN_SERVICE}.`);
188
+ }
189
+
190
+ return secret;
191
+ };
192
+
193
+ export const unlockAntigravityDecryption = async (): Promise<AntigravityDecryptionState> => {
194
+ try {
195
+ cachedKeychainSecret = await readAntigravityKeychainSecret();
196
+ lastKeychainError = null;
197
+ } catch (error) {
198
+ cachedKeychainSecret = null;
199
+ lastKeychainError = error instanceof Error ? error.message : String(error);
200
+ }
201
+
202
+ return getAntigravityDecryptionState();
203
+ };
@@ -491,7 +491,13 @@ export const getCodexDashboardSummary = (dbPath: string): DashboardSummary => {
491
491
  return {
492
492
  activeThreads: threads.filter((thread) => !thread.archived).length,
493
493
  archivedThreads: threads.filter((thread) => Boolean(thread.archived)).length,
494
- recentThreads: threads.slice(0, 5),
494
+ recentThreads: threads
495
+ .slice(0, 5)
496
+ .filter((thread) => Boolean(getPortablePathBasename(thread.cwd)))
497
+ .map((thread) => ({
498
+ project: getPortablePathBasename(thread.cwd),
499
+ thread: compactThreadListRow(thread),
500
+ })),
495
501
  threadsWithRelations,
496
502
  topProjectsByThreadCount: [...projects]
497
503
  .sort((left, right) => {
@@ -170,10 +170,15 @@ export type ThreadBrowseData = {
170
170
  thread: ThreadRow;
171
171
  };
172
172
 
173
+ export type DashboardRecentThread = {
174
+ project: string;
175
+ thread: ThreadRow;
176
+ };
177
+
173
178
  export type DashboardSummary = {
174
179
  activeThreads: number;
175
180
  archivedThreads: number;
176
- recentThreads: ThreadRow[];
181
+ recentThreads: DashboardRecentThread[];
177
182
  threadsWithRelations: number;
178
183
  topProjectsByThreadCount: ProjectSummary[];
179
184
  topProjectsByTokens: ProjectSummary[];
@@ -191,6 +196,22 @@ export type DeleteProjectResult = DeleteThreadsResult & {
191
196
  projectName: string;
192
197
  };
193
198
 
199
+ export type RecoverProjectThreadsResult = {
200
+ backups: {
201
+ globalState: string;
202
+ sessionIndex: string;
203
+ stateDb: string;
204
+ };
205
+ projectName: string;
206
+ projectRootsAdded: number;
207
+ resolvedCwds: string[];
208
+ rolloutFilesTouched: number;
209
+ savedRootsAdded: number;
210
+ sessionIndexRowsUpdated: number;
211
+ threadDbRowsUpdated: number;
212
+ topLevelThreadsFound: number;
213
+ };
214
+
194
215
  export type ToolUsageSummary = {
195
216
  count: number;
196
217
  name: string;
@@ -0,0 +1,202 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { copyFile, utimes } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import type { RecoverProjectThreadsResult } from './codex-browser-types';
5
+ import { getPortablePathBasename } from './shared';
6
+ import { runWithSqliteRetry } from './sqlite-retry';
7
+
8
+ type RecoveryThreadRow = {
9
+ cwd: string;
10
+ id: string;
11
+ rollout_path: string;
12
+ thread_source: string | null;
13
+ };
14
+
15
+ type GlobalState = {
16
+ 'active-workspace-roots'?: string[];
17
+ 'electron-saved-workspace-roots'?: string[];
18
+ 'project-order'?: string[];
19
+ };
20
+
21
+ const backupFile = async (filePath: string, label: string) => {
22
+ const stamp = new Date()
23
+ .toISOString()
24
+ .replaceAll(':', '')
25
+ .replace(/\.\d{3}Z$/, 'Z')
26
+ .replace('T', '-');
27
+ const backupPath = `${filePath}.bak-${label}-${stamp}`;
28
+ await copyFile(filePath, backupPath);
29
+ return backupPath;
30
+ };
31
+
32
+ const resolveCodexDirFromDbPath = (dbPath: string) => {
33
+ const dbDir = path.dirname(dbPath);
34
+ return path.basename(dbDir) === 'sqlite' ? path.dirname(dbDir) : dbDir;
35
+ };
36
+
37
+ const assertRequiredStatePath = async (filePath: string) => {
38
+ if (!(await Bun.file(filePath).exists())) {
39
+ throw new Error(`Required Codex state file not found: ${filePath}`);
40
+ }
41
+ };
42
+
43
+ const readGlobalState = async (globalStatePath: string) => {
44
+ return (await Bun.file(globalStatePath).json()) as GlobalState;
45
+ };
46
+
47
+ const writeGlobalState = async (globalStatePath: string, state: GlobalState) => {
48
+ await Bun.write(globalStatePath, JSON.stringify(state));
49
+ };
50
+
51
+ const updateGlobalRoots = (state: GlobalState, projectCwds: string[]) => {
52
+ const savedRoots = state['electron-saved-workspace-roots'] ?? [];
53
+ const projectOrder = state['project-order'] ?? [];
54
+ const missingSaved = projectCwds.filter((cwd) => !savedRoots.includes(cwd));
55
+ const missingProjectOrder = projectCwds.filter((cwd) => !projectOrder.includes(cwd));
56
+
57
+ if (missingSaved.length === 0 && missingProjectOrder.length === 0) {
58
+ return {
59
+ projectRootsAdded: 0,
60
+ savedRootsAdded: 0,
61
+ state,
62
+ };
63
+ }
64
+
65
+ state['electron-saved-workspace-roots'] = [...savedRoots, ...missingSaved];
66
+ state['project-order'] = [...projectOrder, ...missingProjectOrder];
67
+
68
+ return {
69
+ projectRootsAdded: missingProjectOrder.length,
70
+ savedRootsAdded: missingSaved.length,
71
+ state,
72
+ };
73
+ };
74
+
75
+ const getProjectTopLevelThreads = (db: Database, projectName: string): RecoveryThreadRow[] => {
76
+ const threads = db
77
+ .query('SELECT id, cwd, rollout_path, thread_source FROM threads WHERE archived = 0')
78
+ .all() as RecoveryThreadRow[];
79
+ return threads.filter((thread) => {
80
+ return getPortablePathBasename(thread.cwd) === projectName && thread.thread_source !== 'subagent';
81
+ });
82
+ };
83
+
84
+ const refreshThreadRows = (db: Database, threadIds: string[]) => {
85
+ if (threadIds.length === 0) {
86
+ return 0;
87
+ }
88
+
89
+ const nowSeconds = Math.floor(Date.now() / 1000);
90
+ const nowMs = Date.now();
91
+ const placeholders = threadIds.map(() => '?').join(', ');
92
+ const statement = db.prepare(`
93
+ UPDATE threads
94
+ SET updated_at = ?1,
95
+ updated_at_ms = ?2,
96
+ has_user_event = 1
97
+ WHERE id IN (${placeholders})
98
+ `);
99
+ const result = statement.run(nowSeconds, nowMs, ...threadIds);
100
+ return Number(result.changes);
101
+ };
102
+
103
+ const refreshSessionIndex = async (sessionIndexPath: string, threadIds: string[]) => {
104
+ if (threadIds.length === 0) {
105
+ return 0;
106
+ }
107
+
108
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
109
+ const threadIdSet = new Set(threadIds);
110
+ const lines = (await Bun.file(sessionIndexPath).text()).split('\n');
111
+ let updated = 0;
112
+ const rewrittenLines: string[] = [];
113
+
114
+ for (const line of lines) {
115
+ if (!line.trim()) {
116
+ continue;
117
+ }
118
+
119
+ const parsed = JSON.parse(line) as { id?: string; updated_at?: string };
120
+ if (parsed.id && threadIdSet.has(parsed.id)) {
121
+ parsed.updated_at = now;
122
+ updated += 1;
123
+ }
124
+
125
+ rewrittenLines.push(JSON.stringify(parsed));
126
+ }
127
+
128
+ await Bun.write(sessionIndexPath, `${rewrittenLines.join('\n')}\n`);
129
+ return updated;
130
+ };
131
+
132
+ const touchRolloutFiles = async (codexDir: string, rolloutPaths: string[]) => {
133
+ const now = new Date();
134
+ let touched = 0;
135
+
136
+ for (const rolloutPath of rolloutPaths) {
137
+ const absolutePath = path.isAbsolute(rolloutPath) ? rolloutPath : path.join(codexDir, rolloutPath);
138
+ if (!(await Bun.file(absolutePath).exists())) {
139
+ continue;
140
+ }
141
+
142
+ await utimes(absolutePath, now, now);
143
+ touched += 1;
144
+ }
145
+
146
+ return touched;
147
+ };
148
+
149
+ export const recoverCodexProjectThreads = async (
150
+ dbPath: string,
151
+ projectName: string,
152
+ ): Promise<RecoverProjectThreadsResult> => {
153
+ const codexDir = resolveCodexDirFromDbPath(dbPath);
154
+ const globalStatePath = path.join(codexDir, '.codex-global-state.json');
155
+ const sessionIndexPath = path.join(codexDir, 'session_index.jsonl');
156
+
157
+ await assertRequiredStatePath(dbPath);
158
+ await assertRequiredStatePath(globalStatePath);
159
+ await assertRequiredStatePath(sessionIndexPath);
160
+
161
+ const backups = {
162
+ globalState: await backupFile(globalStatePath, 'recover-project-roots'),
163
+ sessionIndex: await backupFile(sessionIndexPath, 'recover-project-session-index'),
164
+ stateDb: await backupFile(dbPath, 'recover-project-threads'),
165
+ };
166
+
167
+ const globalState = await readGlobalState(globalStatePath);
168
+ const db = runWithSqliteRetry({
169
+ action: () => {
170
+ const opened = new Database(dbPath);
171
+ opened.exec('PRAGMA busy_timeout = 5000');
172
+ return opened;
173
+ },
174
+ });
175
+
176
+ try {
177
+ const topLevelThreads = getProjectTopLevelThreads(db, projectName);
178
+ const projectCwds = [...new Set(topLevelThreads.map((thread) => thread.cwd))];
179
+ const rootUpdateResult = updateGlobalRoots(globalState, projectCwds);
180
+ await writeGlobalState(globalStatePath, rootUpdateResult.state);
181
+
182
+ const threadIds = topLevelThreads.map((thread) => thread.id);
183
+ const rolloutPaths = topLevelThreads.map((thread) => thread.rollout_path);
184
+ const threadDbRowsUpdated = refreshThreadRows(db, threadIds);
185
+ const sessionIndexRowsUpdated = await refreshSessionIndex(sessionIndexPath, threadIds);
186
+ const rolloutFilesTouched = await touchRolloutFiles(codexDir, rolloutPaths);
187
+
188
+ return {
189
+ backups,
190
+ projectName,
191
+ projectRootsAdded: rootUpdateResult.projectRootsAdded,
192
+ resolvedCwds: projectCwds,
193
+ rolloutFilesTouched,
194
+ savedRootsAdded: rootUpdateResult.savedRootsAdded,
195
+ sessionIndexRowsUpdated,
196
+ threadDbRowsUpdated,
197
+ topLevelThreadsFound: threadIds.length,
198
+ };
199
+ } finally {
200
+ db.close();
201
+ }
202
+ };