opencode-account-manager 0.4.1
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/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +183 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/accounts.d.ts +19 -0
- package/dist/core/accounts.d.ts.map +1 -0
- package/dist/core/accounts.js +181 -0
- package/dist/core/accounts.js.map +1 -0
- package/dist/core/config-store.d.ts +48 -0
- package/dist/core/config-store.d.ts.map +1 -0
- package/dist/core/config-store.js +206 -0
- package/dist/core/config-store.js.map +1 -0
- package/dist/core/crypto.d.ts +40 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +172 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/importers/amJson.d.ts +17 -0
- package/dist/core/importers/amJson.d.ts.map +1 -0
- package/dist/core/importers/amJson.js +131 -0
- package/dist/core/importers/amJson.js.map +1 -0
- package/dist/core/opencode-config.d.ts +92 -0
- package/dist/core/opencode-config.d.ts.map +1 -0
- package/dist/core/opencode-config.js +148 -0
- package/dist/core/opencode-config.js.map +1 -0
- package/dist/core/paths.d.ts +5 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/types.d.ts +74 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +30 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/utils.d.ts +5 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +35 -0
- package/dist/core/utils.js.map +1 -0
- package/dist/tui/Dashboard.d.ts +7 -0
- package/dist/tui/Dashboard.d.ts.map +1 -0
- package/dist/tui/Dashboard.js +331 -0
- package/dist/tui/Dashboard.js.map +1 -0
- package/dist/tui/components/AccountList.d.ts +18 -0
- package/dist/tui/components/AccountList.d.ts.map +1 -0
- package/dist/tui/components/AccountList.js +92 -0
- package/dist/tui/components/AccountList.js.map +1 -0
- package/dist/tui/components/Box.d.ts +11 -0
- package/dist/tui/components/Box.d.ts.map +1 -0
- package/dist/tui/components/Box.js +15 -0
- package/dist/tui/components/Box.js.map +1 -0
- package/dist/tui/components/ExportModal.d.ts +10 -0
- package/dist/tui/components/ExportModal.d.ts.map +1 -0
- package/dist/tui/components/ExportModal.js +192 -0
- package/dist/tui/components/ExportModal.js.map +1 -0
- package/dist/tui/components/FileBrowser.d.ts +12 -0
- package/dist/tui/components/FileBrowser.d.ts.map +1 -0
- package/dist/tui/components/FileBrowser.js +349 -0
- package/dist/tui/components/FileBrowser.js.map +1 -0
- package/dist/tui/components/Header.d.ts +8 -0
- package/dist/tui/components/Header.d.ts.map +1 -0
- package/dist/tui/components/Header.js +20 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/ImportModal.d.ts +10 -0
- package/dist/tui/components/ImportModal.d.ts.map +1 -0
- package/dist/tui/components/ImportModal.js +215 -0
- package/dist/tui/components/ImportModal.js.map +1 -0
- package/dist/tui/components/McpServerList.d.ts +8 -0
- package/dist/tui/components/McpServerList.d.ts.map +1 -0
- package/dist/tui/components/McpServerList.js +35 -0
- package/dist/tui/components/McpServerList.js.map +1 -0
- package/dist/tui/components/Menu.d.ts +10 -0
- package/dist/tui/components/Menu.d.ts.map +1 -0
- package/dist/tui/components/Menu.js +83 -0
- package/dist/tui/components/Menu.js.map +1 -0
- package/dist/tui/components/PasswordInput.d.ts +12 -0
- package/dist/tui/components/PasswordInput.d.ts.map +1 -0
- package/dist/tui/components/PasswordInput.js +130 -0
- package/dist/tui/components/PasswordInput.js.map +1 -0
- package/dist/tui/components/ProviderList.d.ts +8 -0
- package/dist/tui/components/ProviderList.d.ts.map +1 -0
- package/dist/tui/components/ProviderList.js +37 -0
- package/dist/tui/components/ProviderList.js.map +1 -0
- package/dist/tui/components/SectionBox.d.ts +10 -0
- package/dist/tui/components/SectionBox.d.ts.map +1 -0
- package/dist/tui/components/SectionBox.js +16 -0
- package/dist/tui/components/SectionBox.js.map +1 -0
- package/dist/tui/components/StatsRow.d.ts +13 -0
- package/dist/tui/components/StatsRow.d.ts.map +1 -0
- package/dist/tui/components/StatsRow.js +18 -0
- package/dist/tui/components/StatsRow.js.map +1 -0
- package/dist/tui/components/StatusBadge.d.ts +8 -0
- package/dist/tui/components/StatusBadge.d.ts.map +1 -0
- package/dist/tui/components/StatusBadge.js +30 -0
- package/dist/tui/components/StatusBadge.js.map +1 -0
- package/dist/tui/components/index.d.ts +14 -0
- package/dist/tui/components/index.d.ts.map +1 -0
- package/dist/tui/components/index.js +32 -0
- package/dist/tui/components/index.js.map +1 -0
- package/dist/tui/index.d.ts +5 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +13 -0
- package/dist/tui/index.js.map +1 -0
- package/docs/BLUEPRINT.md +476 -0
- package/docs/ROADMAP.md +74 -0
- package/package.json +38 -0
- package/src/cli.ts +207 -0
- package/src/core/accounts.ts +215 -0
- package/src/core/config-store.ts +212 -0
- package/src/core/crypto.ts +162 -0
- package/src/core/importers/amJson.ts +185 -0
- package/src/core/opencode-config.ts +217 -0
- package/src/core/paths.ts +32 -0
- package/src/core/types.ts +118 -0
- package/src/core/utils.ts +28 -0
- package/src/tui/Dashboard.tsx +431 -0
- package/src/tui/components/AccountList.tsx +155 -0
- package/src/tui/components/Box.tsx +37 -0
- package/src/tui/components/ExportModal.tsx +255 -0
- package/src/tui/components/FileBrowser.tsx +393 -0
- package/src/tui/components/Header.tsx +26 -0
- package/src/tui/components/ImportModal.tsx +288 -0
- package/src/tui/components/McpServerList.tsx +67 -0
- package/src/tui/components/Menu.tsx +103 -0
- package/src/tui/components/PasswordInput.tsx +159 -0
- package/src/tui/components/ProviderList.tsx +61 -0
- package/src/tui/components/SectionBox.tsx +35 -0
- package/src/tui/components/StatsRow.tsx +33 -0
- package/src/tui/components/StatusBadge.tsx +33 -0
- package/src/tui/components/index.ts +13 -0
- package/src/tui/index.tsx +11 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
const ALGORITHM = "aes-256-gcm";
|
|
8
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
9
|
+
const SALT_LENGTH = 32; // 256 bits
|
|
10
|
+
const IV_LENGTH = 12; // 96 bits (recommended for GCM)
|
|
11
|
+
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
12
|
+
|
|
13
|
+
// scrypt parameters (N=16384, r=8, p=1)
|
|
14
|
+
const SCRYPT_N = 16384;
|
|
15
|
+
const SCRYPT_R = 8;
|
|
16
|
+
const SCRYPT_P = 1;
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface EncryptedData {
|
|
23
|
+
salt: string; // hex
|
|
24
|
+
iv: string; // hex
|
|
25
|
+
authTag: string; // hex
|
|
26
|
+
data: string; // hex (encrypted content)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Helper Functions
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate random bytes and return as hex string
|
|
35
|
+
*/
|
|
36
|
+
function randomHex(length: number): string {
|
|
37
|
+
return crypto.randomBytes(length).toString("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Derive encryption key from password using scrypt
|
|
42
|
+
*/
|
|
43
|
+
function deriveKey(password: string, salt: Buffer): Buffer {
|
|
44
|
+
return crypto.scryptSync(password, salt, KEY_LENGTH, {
|
|
45
|
+
N: SCRYPT_N,
|
|
46
|
+
r: SCRYPT_R,
|
|
47
|
+
p: SCRYPT_P,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Public Functions
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a random salt for encryption
|
|
57
|
+
*/
|
|
58
|
+
export function generateSalt(): string {
|
|
59
|
+
return randomHex(SALT_LENGTH);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a random IV for encryption
|
|
64
|
+
*/
|
|
65
|
+
export function generateIV(): string {
|
|
66
|
+
return randomHex(IV_LENGTH);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encrypt data object with password using AES-256-GCM
|
|
71
|
+
*
|
|
72
|
+
* @param data - Object to encrypt (will be JSON stringified)
|
|
73
|
+
* @param password - Password for encryption
|
|
74
|
+
* @returns Encrypted data with salt, iv, authTag, and encrypted content
|
|
75
|
+
*/
|
|
76
|
+
export function encrypt(data: object, password: string): EncryptedData {
|
|
77
|
+
// Generate random salt and IV
|
|
78
|
+
const salt = Buffer.from(generateSalt(), "hex");
|
|
79
|
+
const iv = Buffer.from(generateIV(), "hex");
|
|
80
|
+
|
|
81
|
+
// Derive key from password
|
|
82
|
+
const key = deriveKey(password, salt);
|
|
83
|
+
|
|
84
|
+
// Create cipher
|
|
85
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
86
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Encrypt data
|
|
90
|
+
const plaintext = JSON.stringify(data);
|
|
91
|
+
const encrypted = Buffer.concat([
|
|
92
|
+
cipher.update(plaintext, "utf8"),
|
|
93
|
+
cipher.final(),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// Get auth tag
|
|
97
|
+
const authTag = cipher.getAuthTag();
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
salt: salt.toString("hex"),
|
|
101
|
+
iv: iv.toString("hex"),
|
|
102
|
+
authTag: authTag.toString("hex"),
|
|
103
|
+
data: encrypted.toString("hex"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Decrypt data with password using AES-256-GCM
|
|
109
|
+
*
|
|
110
|
+
* @param encrypted - Encrypted data object
|
|
111
|
+
* @param password - Password for decryption
|
|
112
|
+
* @returns Decrypted object
|
|
113
|
+
* @throws Error if password is wrong or data is corrupted
|
|
114
|
+
*/
|
|
115
|
+
export function decrypt<T = unknown>(encrypted: EncryptedData, password: string): T {
|
|
116
|
+
// Convert hex strings to buffers
|
|
117
|
+
const salt = Buffer.from(encrypted.salt, "hex");
|
|
118
|
+
const iv = Buffer.from(encrypted.iv, "hex");
|
|
119
|
+
const authTag = Buffer.from(encrypted.authTag, "hex");
|
|
120
|
+
const data = Buffer.from(encrypted.data, "hex");
|
|
121
|
+
|
|
122
|
+
// Derive key from password
|
|
123
|
+
const key = deriveKey(password, salt);
|
|
124
|
+
|
|
125
|
+
// Create decipher
|
|
126
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
127
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Set auth tag for verification
|
|
131
|
+
decipher.setAuthTag(authTag);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Decrypt data
|
|
135
|
+
const decrypted = Buffer.concat([
|
|
136
|
+
decipher.update(data),
|
|
137
|
+
decipher.final(),
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
// Parse JSON
|
|
141
|
+
return JSON.parse(decrypted.toString("utf8")) as T;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Auth tag verification failed or JSON parse failed
|
|
144
|
+
throw new Error("Invalid password or corrupted file");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Verify if a password can decrypt the data
|
|
150
|
+
*
|
|
151
|
+
* @param encrypted - Encrypted data object
|
|
152
|
+
* @param password - Password to verify
|
|
153
|
+
* @returns true if password is correct
|
|
154
|
+
*/
|
|
155
|
+
export function verifyPassword(encrypted: EncryptedData, password: string): boolean {
|
|
156
|
+
try {
|
|
157
|
+
decrypt(encrypted, password);
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import accounts from Antigravity Manager JSON files
|
|
3
|
+
*
|
|
4
|
+
* AM Structure:
|
|
5
|
+
* ~/.antigravity_tools/
|
|
6
|
+
* accounts.json - index file with account list
|
|
7
|
+
* accounts/<id>.json - detail files with tokens
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { Account } from "../types";
|
|
13
|
+
import { getAmFolderPath } from "../paths";
|
|
14
|
+
|
|
15
|
+
interface AMIndexEntry {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
name: string;
|
|
19
|
+
disabled: boolean;
|
|
20
|
+
proxy_disabled: boolean;
|
|
21
|
+
created_at?: number;
|
|
22
|
+
last_used?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AMAccountsIndex {
|
|
26
|
+
version: string;
|
|
27
|
+
accounts: AMIndexEntry[];
|
|
28
|
+
current_account_id?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AMToken {
|
|
32
|
+
access_token?: string;
|
|
33
|
+
refresh_token: string;
|
|
34
|
+
expires_in?: number;
|
|
35
|
+
expiry_timestamp?: number;
|
|
36
|
+
token_type?: string;
|
|
37
|
+
email?: string;
|
|
38
|
+
project_id?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface AMAccountDetail {
|
|
42
|
+
id: string;
|
|
43
|
+
email: string;
|
|
44
|
+
name: string;
|
|
45
|
+
token: AMToken;
|
|
46
|
+
device_profile?: Record<string, unknown>;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
proxy_disabled?: boolean;
|
|
49
|
+
created_at?: number;
|
|
50
|
+
last_used?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function generateFingerprint() {
|
|
54
|
+
const randomHex = (len: number) => {
|
|
55
|
+
let result = "";
|
|
56
|
+
for (let i = 0; i < len; i++) {
|
|
57
|
+
result += Math.floor(Math.random() * 16).toString(16);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const platforms = ["win32/x64", "win32/arm64", "darwin/x64", "darwin/arm64"];
|
|
63
|
+
const ides = ["ANDROID_STUDIO", "INTELLIJ", "IDE_UNSPECIFIED"];
|
|
64
|
+
const clients = [
|
|
65
|
+
"google-cloud-sdk android-studio/2024.1",
|
|
66
|
+
"google-cloud-sdk intellij/2024.1",
|
|
67
|
+
"google-cloud-sdk vscode/1.87.0",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const platform = platforms[Math.floor(Math.random() * platforms.length)];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
deviceId: crypto.randomUUID(),
|
|
74
|
+
sessionToken: randomHex(32),
|
|
75
|
+
userAgent: `antigravity/1.15.8 ${platform}`,
|
|
76
|
+
apiClient: clients[Math.floor(Math.random() * clients.length)],
|
|
77
|
+
clientMetadata: {
|
|
78
|
+
ideType: ides[Math.floor(Math.random() * ides.length)],
|
|
79
|
+
platform: platform.startsWith("darwin") ? "MACOS" : "WINDOWS",
|
|
80
|
+
pluginType: "GEMINI",
|
|
81
|
+
osVersion: platform.startsWith("darwin") ? "14.2.1" : "10.0.19042",
|
|
82
|
+
arch: platform.split("/")[1],
|
|
83
|
+
sqmId: `{${crypto.randomUUID().toUpperCase()}}`,
|
|
84
|
+
},
|
|
85
|
+
quotaUser: `device-${randomHex(16)}`,
|
|
86
|
+
createdAt: Date.now(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ImportFromAmResult {
|
|
91
|
+
accounts: Account[];
|
|
92
|
+
skipped: string[];
|
|
93
|
+
errors: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function importFromAmFolder(amPath?: string): ImportFromAmResult {
|
|
97
|
+
const folderPath = amPath || getAmFolderPath();
|
|
98
|
+
const result: ImportFromAmResult = {
|
|
99
|
+
accounts: [],
|
|
100
|
+
skipped: [],
|
|
101
|
+
errors: [],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Check if folder exists
|
|
105
|
+
if (!fs.existsSync(folderPath)) {
|
|
106
|
+
result.errors.push(`AM folder not found: ${folderPath}`);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Read index file
|
|
111
|
+
const indexPath = path.join(folderPath, "accounts.json");
|
|
112
|
+
if (!fs.existsSync(indexPath)) {
|
|
113
|
+
result.errors.push(`AM accounts.json not found: ${indexPath}`);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let index: AMAccountsIndex;
|
|
118
|
+
try {
|
|
119
|
+
const content = fs.readFileSync(indexPath, "utf-8");
|
|
120
|
+
index = JSON.parse(content) as AMAccountsIndex;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
result.errors.push(`Failed to parse accounts.json: ${err}`);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Process each account
|
|
127
|
+
const accountsDir = path.join(folderPath, "accounts");
|
|
128
|
+
|
|
129
|
+
for (const entry of index.accounts) {
|
|
130
|
+
// Skip disabled accounts
|
|
131
|
+
if (entry.disabled || entry.proxy_disabled) {
|
|
132
|
+
result.skipped.push(`${entry.email} (disabled)`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Read detail file
|
|
137
|
+
const detailPath = path.join(accountsDir, `${entry.id}.json`);
|
|
138
|
+
if (!fs.existsSync(detailPath)) {
|
|
139
|
+
result.skipped.push(`${entry.email} (no detail file)`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let detail: AMAccountDetail;
|
|
144
|
+
try {
|
|
145
|
+
const content = fs.readFileSync(detailPath, "utf-8");
|
|
146
|
+
detail = JSON.parse(content) as AMAccountDetail;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
result.skipped.push(`${entry.email} (parse error)`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if has refresh token
|
|
153
|
+
if (!detail.token?.refresh_token) {
|
|
154
|
+
result.skipped.push(`${entry.email} (no refresh token)`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check proxy_disabled in detail file (AM GUI only updates detail file!)
|
|
159
|
+
if (detail.proxy_disabled) {
|
|
160
|
+
result.skipped.push(`${entry.email} (proxy disabled)`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Convert to Plugin account format
|
|
165
|
+
const account: Account = {
|
|
166
|
+
email: detail.email,
|
|
167
|
+
refreshToken: detail.token.refresh_token,
|
|
168
|
+
projectId: detail.token.project_id,
|
|
169
|
+
managedProjectId: detail.token.project_id,
|
|
170
|
+
addedAt: Date.now(),
|
|
171
|
+
lastUsed: Date.now(),
|
|
172
|
+
fingerprint: generateFingerprint(),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
result.accounts.push(account);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function isAmFolder(folderPath: string): boolean {
|
|
182
|
+
const indexPath = path.join(folderPath, "accounts.json");
|
|
183
|
+
const accountsDir = path.join(folderPath, "accounts");
|
|
184
|
+
return fs.existsSync(indexPath) && fs.existsSync(accountsDir);
|
|
185
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Types for opencode.json structure
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export interface ModelLimit {
|
|
10
|
+
context: number;
|
|
11
|
+
output: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ModelModalities {
|
|
15
|
+
input: string[];
|
|
16
|
+
output: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ModelVariant {
|
|
20
|
+
thinkingLevel?: string;
|
|
21
|
+
thinkingConfig?: {
|
|
22
|
+
thinkingBudget?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ModelConfig {
|
|
27
|
+
name: string;
|
|
28
|
+
limit?: ModelLimit;
|
|
29
|
+
modalities?: ModelModalities;
|
|
30
|
+
variants?: Record<string, ModelVariant>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProviderConfig {
|
|
34
|
+
npm?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
options?: {
|
|
37
|
+
baseURL?: string;
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
};
|
|
40
|
+
models: Record<string, ModelConfig>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface McpServerConfig {
|
|
44
|
+
type: "local" | "remote";
|
|
45
|
+
command?: string[];
|
|
46
|
+
url?: string;
|
|
47
|
+
environment?: Record<string, string>;
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface OpencodeConfig {
|
|
52
|
+
$schema?: string;
|
|
53
|
+
plugin?: string[];
|
|
54
|
+
mcp?: Record<string, McpServerConfig>;
|
|
55
|
+
provider?: Record<string, ProviderConfig>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Parsed/Display structures
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
export interface ProviderInfo {
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
modelCount: number;
|
|
66
|
+
models: string[];
|
|
67
|
+
type: "builtin" | "custom";
|
|
68
|
+
baseURL?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface McpServerInfo {
|
|
72
|
+
id: string;
|
|
73
|
+
command: string;
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
hasEnvVars: boolean;
|
|
76
|
+
envVarCount: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PluginInfo {
|
|
80
|
+
name: string;
|
|
81
|
+
version?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface OpencodeInfo {
|
|
85
|
+
configPath: string;
|
|
86
|
+
exists: boolean;
|
|
87
|
+
providers: ProviderInfo[];
|
|
88
|
+
mcpServers: McpServerInfo[];
|
|
89
|
+
plugins: PluginInfo[];
|
|
90
|
+
totalModels: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Functions
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the path to opencode.json
|
|
99
|
+
*/
|
|
100
|
+
export function getOpencodeConfigPath(): string {
|
|
101
|
+
const home = os.homedir();
|
|
102
|
+
// Check .config/opencode first (Unix-style)
|
|
103
|
+
const unixPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
104
|
+
if (fs.existsSync(unixPath)) {
|
|
105
|
+
return unixPath;
|
|
106
|
+
}
|
|
107
|
+
// Fallback to AppData on Windows
|
|
108
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
109
|
+
return path.join(appData, "opencode", "opencode.json");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read and parse opencode.json
|
|
114
|
+
*/
|
|
115
|
+
export function readOpencodeConfig(configPath?: string): OpencodeConfig | null {
|
|
116
|
+
const resolvedPath = configPath || getOpencodeConfigPath();
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
124
|
+
return JSON.parse(content) as OpencodeConfig;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse opencode.json into display-friendly info
|
|
132
|
+
*/
|
|
133
|
+
export function parseOpencodeInfo(configPath?: string): OpencodeInfo {
|
|
134
|
+
const resolvedPath = configPath || getOpencodeConfigPath();
|
|
135
|
+
const config = readOpencodeConfig(resolvedPath);
|
|
136
|
+
|
|
137
|
+
const info: OpencodeInfo = {
|
|
138
|
+
configPath: resolvedPath,
|
|
139
|
+
exists: config !== null,
|
|
140
|
+
providers: [],
|
|
141
|
+
mcpServers: [],
|
|
142
|
+
plugins: [],
|
|
143
|
+
totalModels: 0,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!config) {
|
|
147
|
+
return info;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Parse plugins
|
|
151
|
+
if (config.plugin) {
|
|
152
|
+
info.plugins = config.plugin.map((p) => {
|
|
153
|
+
const match = p.match(/^(.+?)(@(.+))?$/);
|
|
154
|
+
return {
|
|
155
|
+
name: match?.[1] || p,
|
|
156
|
+
version: match?.[3],
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Parse MCP servers
|
|
162
|
+
if (config.mcp) {
|
|
163
|
+
info.mcpServers = Object.entries(config.mcp).map(([id, server]) => {
|
|
164
|
+
const cmd = server.command?.join(" ") || server.url || "N/A";
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
command: cmd,
|
|
168
|
+
enabled: server.enabled !== false,
|
|
169
|
+
hasEnvVars: !!server.environment,
|
|
170
|
+
envVarCount: server.environment ? Object.keys(server.environment).length : 0,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Parse providers
|
|
176
|
+
if (config.provider) {
|
|
177
|
+
info.providers = Object.entries(config.provider).map(([id, provider]) => {
|
|
178
|
+
const modelIds = Object.keys(provider.models);
|
|
179
|
+
const isBuiltin = id === "google" && !provider.npm;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
name: provider.name || id,
|
|
184
|
+
modelCount: modelIds.length,
|
|
185
|
+
models: modelIds,
|
|
186
|
+
type: isBuiltin ? "builtin" : "custom",
|
|
187
|
+
baseURL: provider.options?.baseURL,
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
info.totalModels = info.providers.reduce((sum, p) => sum + p.modelCount, 0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return info;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get summary stats
|
|
199
|
+
*/
|
|
200
|
+
export function getConfigSummary(info: OpencodeInfo): {
|
|
201
|
+
providers: number;
|
|
202
|
+
models: number;
|
|
203
|
+
mcpEnabled: number;
|
|
204
|
+
mcpDisabled: number;
|
|
205
|
+
plugins: number;
|
|
206
|
+
} {
|
|
207
|
+
const mcpEnabled = info.mcpServers.filter((m) => m.enabled).length;
|
|
208
|
+
const mcpDisabled = info.mcpServers.length - mcpEnabled;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
providers: info.providers.length,
|
|
212
|
+
models: info.totalModels,
|
|
213
|
+
mcpEnabled,
|
|
214
|
+
mcpDisabled,
|
|
215
|
+
plugins: info.plugins.length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function getConfigRoot(): string {
|
|
5
|
+
if (process.env.APPDATA) {
|
|
6
|
+
return process.env.APPDATA;
|
|
7
|
+
}
|
|
8
|
+
if (process.platform === "darwin") {
|
|
9
|
+
return path.join(os.homedir(), "Library", "Application Support");
|
|
10
|
+
}
|
|
11
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Plugin ALWAYS uses ~/.config/opencode on ALL platforms (including Windows)
|
|
15
|
+
export function getPluginAccountsPath(customPath?: string): string {
|
|
16
|
+
if (customPath && customPath.trim().length > 0) {
|
|
17
|
+
return path.resolve(customPath);
|
|
18
|
+
}
|
|
19
|
+
return path.join(os.homedir(), ".config", "opencode", "antigravity-accounts.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// AM uses ~/.antigravity_tools on ALL platforms
|
|
23
|
+
export function getAmFolderPath(): string {
|
|
24
|
+
return path.join(os.homedir(), ".antigravity_tools");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAmDbPath(customPath?: string): string {
|
|
28
|
+
if (customPath && customPath.trim().length > 0) {
|
|
29
|
+
return path.resolve(customPath);
|
|
30
|
+
}
|
|
31
|
+
return path.join(getConfigRoot(), "antigravity-manager", "accounts.db");
|
|
32
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type RateLimitResetTimes = Record<string, number>;
|
|
2
|
+
|
|
3
|
+
export interface AccountFingerprint {
|
|
4
|
+
deviceId?: string;
|
|
5
|
+
sessionToken?: string;
|
|
6
|
+
userAgent?: string;
|
|
7
|
+
apiClient?: string;
|
|
8
|
+
clientMetadata?: Record<string, unknown>;
|
|
9
|
+
quotaUser?: string;
|
|
10
|
+
createdAt?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FingerprintHistoryEntry {
|
|
14
|
+
fingerprint: AccountFingerprint;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Account {
|
|
20
|
+
email: string;
|
|
21
|
+
refreshToken?: string;
|
|
22
|
+
projectId?: string;
|
|
23
|
+
managedProjectId?: string;
|
|
24
|
+
addedAt?: number;
|
|
25
|
+
lastUsed?: number;
|
|
26
|
+
rateLimitResetTimes?: RateLimitResetTimes;
|
|
27
|
+
fingerprint?: AccountFingerprint;
|
|
28
|
+
fingerprintHistory?: FingerprintHistoryEntry[];
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PluginAccountsFile {
|
|
33
|
+
version: number;
|
|
34
|
+
accounts: Account[];
|
|
35
|
+
activeIndex?: number;
|
|
36
|
+
activeIndexByFamily?: Record<string, number>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PortableExportFile {
|
|
40
|
+
version: number;
|
|
41
|
+
exportedAt: number;
|
|
42
|
+
exportedFrom: "opencode-account-manager" | "antigravity-sync";
|
|
43
|
+
accounts: Account[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Encrypted Export Types (v0.4.0)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export interface EncryptedExportFile {
|
|
51
|
+
// Header (not encrypted)
|
|
52
|
+
version: 1;
|
|
53
|
+
format: "encrypted";
|
|
54
|
+
algorithm: "aes-256-gcm";
|
|
55
|
+
|
|
56
|
+
// Encryption parameters (hex encoded)
|
|
57
|
+
salt: string;
|
|
58
|
+
iv: string;
|
|
59
|
+
authTag: string;
|
|
60
|
+
|
|
61
|
+
// Encrypted payload (hex encoded JSON)
|
|
62
|
+
data: string;
|
|
63
|
+
|
|
64
|
+
// Metadata (not encrypted, for display)
|
|
65
|
+
exportedAt: number;
|
|
66
|
+
accountCount: number;
|
|
67
|
+
exportedFrom: "opencode-account-manager";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Type guard to check if data is an encrypted export file
|
|
72
|
+
*/
|
|
73
|
+
export function isEncryptedExportFile(data: unknown): data is EncryptedExportFile {
|
|
74
|
+
if (typeof data !== "object" || data === null) return false;
|
|
75
|
+
const obj = data as Record<string, unknown>;
|
|
76
|
+
return (
|
|
77
|
+
obj.format === "encrypted" &&
|
|
78
|
+
obj.algorithm === "aes-256-gcm" &&
|
|
79
|
+
typeof obj.salt === "string" &&
|
|
80
|
+
typeof obj.iv === "string" &&
|
|
81
|
+
typeof obj.authTag === "string" &&
|
|
82
|
+
typeof obj.data === "string"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Type guard to check if data is a portable export file
|
|
88
|
+
*/
|
|
89
|
+
export function isPortableExportFile(data: unknown): data is PortableExportFile {
|
|
90
|
+
if (typeof data !== "object" || data === null) return false;
|
|
91
|
+
const obj = data as Record<string, unknown>;
|
|
92
|
+
return (
|
|
93
|
+
typeof obj.version === "number" &&
|
|
94
|
+
typeof obj.exportedAt === "number" &&
|
|
95
|
+
Array.isArray(obj.accounts)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Export Format Types
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export type ExportFormat = "encrypted" | "plain";
|
|
104
|
+
|
|
105
|
+
export interface ExportOptions {
|
|
106
|
+
format: ExportFormat;
|
|
107
|
+
folder: string;
|
|
108
|
+
password?: string; // Required for encrypted
|
|
109
|
+
accounts: Account[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ImportResult {
|
|
113
|
+
success: boolean;
|
|
114
|
+
accounts: Account[];
|
|
115
|
+
newCount: number;
|
|
116
|
+
overwrittenCount: number;
|
|
117
|
+
error?: string;
|
|
118
|
+
}
|