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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +266 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +183 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/core/accounts.d.ts +19 -0
  8. package/dist/core/accounts.d.ts.map +1 -0
  9. package/dist/core/accounts.js +181 -0
  10. package/dist/core/accounts.js.map +1 -0
  11. package/dist/core/config-store.d.ts +48 -0
  12. package/dist/core/config-store.d.ts.map +1 -0
  13. package/dist/core/config-store.js +206 -0
  14. package/dist/core/config-store.js.map +1 -0
  15. package/dist/core/crypto.d.ts +40 -0
  16. package/dist/core/crypto.d.ts.map +1 -0
  17. package/dist/core/crypto.js +172 -0
  18. package/dist/core/crypto.js.map +1 -0
  19. package/dist/core/importers/amJson.d.ts +17 -0
  20. package/dist/core/importers/amJson.d.ts.map +1 -0
  21. package/dist/core/importers/amJson.js +131 -0
  22. package/dist/core/importers/amJson.js.map +1 -0
  23. package/dist/core/opencode-config.d.ts +92 -0
  24. package/dist/core/opencode-config.d.ts.map +1 -0
  25. package/dist/core/opencode-config.js +148 -0
  26. package/dist/core/opencode-config.js.map +1 -0
  27. package/dist/core/paths.d.ts +5 -0
  28. package/dist/core/paths.d.ts.map +1 -0
  29. package/dist/core/paths.js +38 -0
  30. package/dist/core/paths.js.map +1 -0
  31. package/dist/core/types.d.ts +74 -0
  32. package/dist/core/types.d.ts.map +1 -0
  33. package/dist/core/types.js +30 -0
  34. package/dist/core/types.js.map +1 -0
  35. package/dist/core/utils.d.ts +5 -0
  36. package/dist/core/utils.d.ts.map +1 -0
  37. package/dist/core/utils.js +35 -0
  38. package/dist/core/utils.js.map +1 -0
  39. package/dist/tui/Dashboard.d.ts +7 -0
  40. package/dist/tui/Dashboard.d.ts.map +1 -0
  41. package/dist/tui/Dashboard.js +331 -0
  42. package/dist/tui/Dashboard.js.map +1 -0
  43. package/dist/tui/components/AccountList.d.ts +18 -0
  44. package/dist/tui/components/AccountList.d.ts.map +1 -0
  45. package/dist/tui/components/AccountList.js +92 -0
  46. package/dist/tui/components/AccountList.js.map +1 -0
  47. package/dist/tui/components/Box.d.ts +11 -0
  48. package/dist/tui/components/Box.d.ts.map +1 -0
  49. package/dist/tui/components/Box.js +15 -0
  50. package/dist/tui/components/Box.js.map +1 -0
  51. package/dist/tui/components/ExportModal.d.ts +10 -0
  52. package/dist/tui/components/ExportModal.d.ts.map +1 -0
  53. package/dist/tui/components/ExportModal.js +192 -0
  54. package/dist/tui/components/ExportModal.js.map +1 -0
  55. package/dist/tui/components/FileBrowser.d.ts +12 -0
  56. package/dist/tui/components/FileBrowser.d.ts.map +1 -0
  57. package/dist/tui/components/FileBrowser.js +349 -0
  58. package/dist/tui/components/FileBrowser.js.map +1 -0
  59. package/dist/tui/components/Header.d.ts +8 -0
  60. package/dist/tui/components/Header.d.ts.map +1 -0
  61. package/dist/tui/components/Header.js +20 -0
  62. package/dist/tui/components/Header.js.map +1 -0
  63. package/dist/tui/components/ImportModal.d.ts +10 -0
  64. package/dist/tui/components/ImportModal.d.ts.map +1 -0
  65. package/dist/tui/components/ImportModal.js +215 -0
  66. package/dist/tui/components/ImportModal.js.map +1 -0
  67. package/dist/tui/components/McpServerList.d.ts +8 -0
  68. package/dist/tui/components/McpServerList.d.ts.map +1 -0
  69. package/dist/tui/components/McpServerList.js +35 -0
  70. package/dist/tui/components/McpServerList.js.map +1 -0
  71. package/dist/tui/components/Menu.d.ts +10 -0
  72. package/dist/tui/components/Menu.d.ts.map +1 -0
  73. package/dist/tui/components/Menu.js +83 -0
  74. package/dist/tui/components/Menu.js.map +1 -0
  75. package/dist/tui/components/PasswordInput.d.ts +12 -0
  76. package/dist/tui/components/PasswordInput.d.ts.map +1 -0
  77. package/dist/tui/components/PasswordInput.js +130 -0
  78. package/dist/tui/components/PasswordInput.js.map +1 -0
  79. package/dist/tui/components/ProviderList.d.ts +8 -0
  80. package/dist/tui/components/ProviderList.d.ts.map +1 -0
  81. package/dist/tui/components/ProviderList.js +37 -0
  82. package/dist/tui/components/ProviderList.js.map +1 -0
  83. package/dist/tui/components/SectionBox.d.ts +10 -0
  84. package/dist/tui/components/SectionBox.d.ts.map +1 -0
  85. package/dist/tui/components/SectionBox.js +16 -0
  86. package/dist/tui/components/SectionBox.js.map +1 -0
  87. package/dist/tui/components/StatsRow.d.ts +13 -0
  88. package/dist/tui/components/StatsRow.d.ts.map +1 -0
  89. package/dist/tui/components/StatsRow.js +18 -0
  90. package/dist/tui/components/StatsRow.js.map +1 -0
  91. package/dist/tui/components/StatusBadge.d.ts +8 -0
  92. package/dist/tui/components/StatusBadge.d.ts.map +1 -0
  93. package/dist/tui/components/StatusBadge.js +30 -0
  94. package/dist/tui/components/StatusBadge.js.map +1 -0
  95. package/dist/tui/components/index.d.ts +14 -0
  96. package/dist/tui/components/index.d.ts.map +1 -0
  97. package/dist/tui/components/index.js +32 -0
  98. package/dist/tui/components/index.js.map +1 -0
  99. package/dist/tui/index.d.ts +5 -0
  100. package/dist/tui/index.d.ts.map +1 -0
  101. package/dist/tui/index.js +13 -0
  102. package/dist/tui/index.js.map +1 -0
  103. package/docs/BLUEPRINT.md +476 -0
  104. package/docs/ROADMAP.md +74 -0
  105. package/package.json +38 -0
  106. package/src/cli.ts +207 -0
  107. package/src/core/accounts.ts +215 -0
  108. package/src/core/config-store.ts +212 -0
  109. package/src/core/crypto.ts +162 -0
  110. package/src/core/importers/amJson.ts +185 -0
  111. package/src/core/opencode-config.ts +217 -0
  112. package/src/core/paths.ts +32 -0
  113. package/src/core/types.ts +118 -0
  114. package/src/core/utils.ts +28 -0
  115. package/src/tui/Dashboard.tsx +431 -0
  116. package/src/tui/components/AccountList.tsx +155 -0
  117. package/src/tui/components/Box.tsx +37 -0
  118. package/src/tui/components/ExportModal.tsx +255 -0
  119. package/src/tui/components/FileBrowser.tsx +393 -0
  120. package/src/tui/components/Header.tsx +26 -0
  121. package/src/tui/components/ImportModal.tsx +288 -0
  122. package/src/tui/components/McpServerList.tsx +67 -0
  123. package/src/tui/components/Menu.tsx +103 -0
  124. package/src/tui/components/PasswordInput.tsx +159 -0
  125. package/src/tui/components/ProviderList.tsx +61 -0
  126. package/src/tui/components/SectionBox.tsx +35 -0
  127. package/src/tui/components/StatsRow.tsx +33 -0
  128. package/src/tui/components/StatusBadge.tsx +33 -0
  129. package/src/tui/components/index.ts +13 -0
  130. package/src/tui/index.tsx +11 -0
  131. 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
+ }