spiracha 1.2.0 → 1.3.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 +49 -12
- package/README.md +117 -64
- package/apps/ui/AGENTS.md +16 -8
- package/apps/ui/README.md +28 -12
- package/apps/ui/dist/client/assets/{analytics-Cv0JMDN2.js → analytics-B_hYz65v.js} +1 -1
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-qiyygB7e.js +1 -0
- package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-z1SQC2Kg.js +1 -0
- package/apps/ui/dist/client/assets/antigravity-keychain-panel-dYuRWtCf.js +1 -0
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-CliqUr7o.js +1 -0
- package/apps/ui/dist/client/assets/antigravity._workspaceKey-CnoBzyX6.js +1 -0
- package/apps/ui/dist/client/assets/antigravity.index-CakfZz_E.js +1 -0
- package/apps/ui/dist/client/assets/antigravity.index-DY7M1KhG.js +1 -0
- package/apps/ui/dist/client/assets/badge-aHE9ETVe.js +1 -0
- package/apps/ui/dist/client/assets/checkbox-DN3XnJaA.js +1 -0
- package/apps/ui/dist/client/assets/cursor-threads._composerId-BMQyx8qG.js +1 -0
- package/apps/ui/dist/client/assets/cursor-threads._composerId-BTlaA-tV.js +1 -0
- package/apps/ui/dist/client/assets/cursor._workspaceKey-CrgrfevV.js +1 -0
- package/apps/ui/dist/client/assets/cursor._workspaceKey-bYS2syGL.js +1 -0
- package/apps/ui/dist/client/assets/cursor.index-CTqZMPYU.js +1 -0
- package/apps/ui/dist/client/assets/cursor.index-Clsz4E_e.js +2 -0
- package/apps/ui/dist/client/assets/{data-table-Bgnh7phF.js → data-table-Cj-v-uyB.js} +2 -2
- package/apps/ui/dist/client/assets/delete-confirm-dialog-DTpzBiNK.js +11 -0
- package/apps/ui/dist/client/assets/dist-BNAn99Pu.js +1 -0
- package/apps/ui/dist/client/assets/download-P3Rp23Ad.js +1 -0
- package/apps/ui/dist/client/assets/dropdown-menu-3qB5j9nt.js +1 -0
- package/apps/ui/dist/client/assets/es2015-Dwm_turD.js +41 -0
- package/apps/ui/dist/client/assets/export-dialog-CazdrASq.js +1 -0
- package/apps/ui/dist/client/assets/formatters-BdnWuM1z.js +1 -0
- package/apps/ui/dist/client/assets/index-BVFnfS78.js +22 -0
- package/apps/ui/dist/client/assets/json-panel-DLkS30sQ.js +1 -0
- package/apps/ui/dist/client/assets/metadata-section-jnIkB7dB.js +1 -0
- package/apps/ui/dist/client/assets/{metric-card-BJX5rkHK.js → metric-card-CBZuWLzQ.js} +1 -1
- package/apps/ui/dist/client/assets/page-header-CnD21cPn.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-BLszwvYL.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-DvLxYbvk.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-COn8woBR.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-DYs98skV.js +3 -0
- package/apps/ui/dist/client/assets/refresh-ccw-BDrYXjtD.js +1 -0
- package/apps/ui/dist/client/assets/reload-error-panel-DLAg0AW2.js +1 -0
- package/apps/ui/dist/client/assets/routes-BtF5-coe.js +1 -0
- package/apps/ui/dist/client/assets/scroll-text-CqaFm9by.js +1 -0
- package/apps/ui/dist/client/assets/select-DbnpwqL6.js +1 -0
- package/apps/ui/dist/client/assets/settings-CGX3VleN.js +1 -0
- package/apps/ui/dist/client/assets/styles-Ch0r3kMZ.css +1 -0
- package/apps/ui/dist/client/assets/text-document-panel-DPleOmmq.js +1 -0
- package/apps/ui/dist/client/assets/text-filter-7M6wRo-t.js +2 -0
- package/apps/ui/dist/client/assets/threads._threadId-D5w76IB-.js +7 -0
- package/apps/ui/dist/client/assets/{threads._threadId-CUiCZSwo.js → threads._threadId-Dx85sI9P.js} +1 -1
- package/apps/ui/dist/client/assets/useMutation-MZ3Hr9h9.js +1 -0
- package/apps/ui/dist/client/assets/useQuery-Cb4V0AT0.js +1 -0
- package/apps/ui/dist/client/icon.svg +28 -0
- package/apps/ui/dist/client/manifest.json +6 -16
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CBbkUXw6.js +227 -0
- package/apps/ui/dist/server/assets/{analytics-2QpLKjlG.js → analytics-CBNOYZwJ.js} +2 -2
- package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +16 -0
- package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-B9Rm4EXh.js +212 -0
- package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BIdYNy68.js +20 -0
- package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +11 -0
- package/apps/ui/dist/server/assets/antigravity-db-D9gW1D8G.js +576 -0
- package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +126 -0
- package/apps/ui/dist/server/assets/antigravity-keychain-panel-DcLyBBwd.js +55 -0
- package/apps/ui/dist/server/assets/antigravity-queries-CgQhlQ7J.js +37 -0
- package/apps/ui/dist/server/assets/antigravity-server-DFUx4Khk.js +114 -0
- package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +11 -0
- package/apps/ui/dist/server/assets/antigravity._workspaceKey-D42ixtzp.js +210 -0
- package/apps/ui/dist/server/assets/antigravity._workspaceKey-DnSlSC-C.js +28 -0
- package/apps/ui/dist/server/assets/antigravity.index-DZVT-cac.js +104 -0
- package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +11 -0
- package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +26 -0
- package/apps/ui/dist/server/assets/{codex-queries-BH4Cb0v3.js → codex-queries-eOJGfHQj.js} +4 -16
- package/apps/ui/dist/server/assets/{codex-server-DqzruLmg.js → codex-server-nrETIF--.js} +149 -140
- package/apps/ui/dist/server/assets/createServerRpc-BtXIw2iP.js +12 -0
- package/apps/ui/dist/server/assets/createSsrRpc-COf5Zuye.js +16 -0
- package/apps/ui/dist/server/assets/cursor-db-B7agkAvM.js +643 -0
- package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +34 -0
- package/apps/ui/dist/server/assets/cursor-queries-BMhuJeUO.js +65 -0
- package/apps/ui/dist/server/assets/cursor-recovery-9bJLs7vG.js +361 -0
- package/apps/ui/dist/server/assets/cursor-server-BgylIFgn.js +184 -0
- package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +11 -0
- package/apps/ui/dist/server/assets/cursor-threads._composerId-BsxFKzoJ.js +218 -0
- package/apps/ui/dist/server/assets/cursor-threads._composerId-DXffY_CK.js +18 -0
- package/apps/ui/dist/server/assets/cursor-transcript-2iL3KFSK.js +125 -0
- package/apps/ui/dist/server/assets/cursor._workspaceKey-BP2J1x_x.js +28 -0
- package/apps/ui/dist/server/assets/cursor._workspaceKey-BQd0e-Pd.js +399 -0
- package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +11 -0
- package/apps/ui/dist/server/assets/cursor.index-CQVxtCm8.js +189 -0
- package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +11 -0
- package/apps/ui/dist/server/assets/{delete-confirm-dialog-CWqcTXTF.js → delete-confirm-dialog-PCD7S0_M.js} +5 -4
- package/apps/ui/dist/server/assets/download-DMmiy1xf.js +92 -0
- package/apps/ui/dist/server/assets/{input-B4tEzctc.js → dropdown-menu-Dy_9t6TN.js} +1 -11
- package/apps/ui/dist/server/assets/{download-Drctxary.js → export-dialog-DaPlOGFT.js} +1 -92
- package/apps/ui/dist/server/assets/json-panel-RYsxWFae.js +16 -0
- package/apps/ui/dist/server/assets/{loading-panel-DbLdvjtR.js → loading-panel-BGFnWseS.js} +1 -1
- package/apps/ui/dist/server/assets/metadata-section-D6Lbc7D6.js +54 -0
- package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +29 -0
- package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +12 -0
- package/apps/ui/dist/server/assets/{projects._project-gT01HBqH.js → projects._project-DUN3iWfg.js} +4 -4
- package/apps/ui/dist/server/assets/{projects._project-DreIU5b0.js → projects._project-Dim9Y0kD.js} +54 -26
- package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +12 -0
- package/apps/ui/dist/server/assets/{projects.index-BYmgSGAj.js → projects.index-DjSQK5dm.js} +23 -27
- package/apps/ui/dist/server/assets/{projects.index-CaplpeMy.js → reload-error-panel-BJMxY3U1.js} +5 -6
- package/apps/ui/dist/server/assets/{router-Qj5Kn7bl.js → router-DrDgc-LD.js} +131 -44
- package/apps/ui/dist/server/assets/{routes-_LbCIjtJ.js → routes-B-GlEe2C.js} +54 -39
- package/apps/ui/dist/server/assets/{routes-BtcXuK0x.js → routes-CNHAUMwo.js} +2 -2
- package/apps/ui/dist/server/assets/{settings-MvWDgc1u.js → settings-OayxIYQQ.js} +1 -1
- package/apps/ui/dist/server/assets/shared-CPRNYIql.js +134 -0
- package/apps/ui/dist/server/assets/text-document-panel-D8JbQWAn.js +23 -0
- package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +36 -0
- package/apps/ui/dist/server/assets/{threads._threadId-DcbAJkwf.js → threads._threadId-CJzm4KrZ.js} +3 -3
- package/apps/ui/dist/server/assets/{threads._threadId-D5m6ypGw.js → threads._threadId-DODTYddm.js} +69 -76
- package/apps/ui/dist/server/server.js +77 -13
- package/package.json +19 -9
- package/src/export-cursor.ts +244 -0
- package/src/lib/antigravity-db.ts +936 -0
- package/src/lib/antigravity-exporter-types.ts +70 -0
- package/src/lib/antigravity-keychain.ts +203 -0
- package/src/lib/codex-browser-db.ts +7 -1
- package/src/lib/codex-browser-types.ts +22 -1
- package/src/lib/codex-thread-recovery.ts +202 -0
- package/src/lib/cursor-db.ts +1096 -0
- package/src/lib/cursor-exporter-types.ts +190 -0
- package/src/lib/cursor-exporter.ts +266 -0
- package/src/lib/cursor-recovery.ts +543 -0
- package/src/lib/cursor-transcript.ts +183 -0
- package/src/spiracha.ts +16 -3
- package/src/ui-cli.ts +2 -2
- package/apps/ui/dist/client/assets/checkbox-DjHij7DJ.js +0 -1
- package/apps/ui/dist/client/assets/delete-confirm-dialog-CIZy_LXD.js +0 -11
- package/apps/ui/dist/client/assets/download-DQtfva4z.js +0 -1
- package/apps/ui/dist/client/assets/es2015-DsDKdYCE.js +0 -41
- package/apps/ui/dist/client/assets/formatters-CWFrMKSn.js +0 -1
- package/apps/ui/dist/client/assets/index-C_-e0lDI.js +0 -22
- package/apps/ui/dist/client/assets/input-BbgApiqZ.js +0 -1
- package/apps/ui/dist/client/assets/page-header-ODLuGLAB.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-C2Pys_bB.js +0 -1
- package/apps/ui/dist/client/assets/projects._project-CHvAKvlu.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-BmwtS1x-.js +0 -1
- package/apps/ui/dist/client/assets/projects.index-CuLw73mt.js +0 -1
- package/apps/ui/dist/client/assets/routes-CfnaTOlj.js +0 -1
- package/apps/ui/dist/client/assets/select-B1kH_5lx.js +0 -1
- package/apps/ui/dist/client/assets/settings-mYTB3sso.js +0 -1
- package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +0 -1
- package/apps/ui/dist/client/assets/threads._threadId-C_47okme.js +0 -7
- package/apps/ui/dist/client/favicon.ico +0 -0
- package/apps/ui/dist/client/logo192.png +0 -0
- package/apps/ui/dist/client/logo512.png +0 -0
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-kj_QB_26.js +0 -99
- package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +0 -25
- 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
|
|
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:
|
|
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
|
+
};
|