ai-agent-config 2.5.9 → 2.6.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/README.md CHANGED
@@ -33,14 +33,31 @@ ai-agent update
33
33
  | Command | Description |
34
34
  |---------|-------------|
35
35
  | `init --repo <url>` | Initialize config and clone repo |
36
- | `push` / `pull` | Git push/pull with your skills repo |
37
- | `update` | Pull -> sync external skills -> push |
36
+ | `push [--message "msg"]` | Git push to your skills repo |
37
+ | `pull` | Git pull from repo + auto-install |
38
+ | `update` | Pull → sync external skills → push |
38
39
  | `install` | Copy skills to platform directories |
39
40
  | `list` | List installed skills |
40
41
  | `platforms` | Show detected platforms |
41
- | `source add/remove/list` | Manage skill sources |
42
- | `config get/set/edit` | Manage configuration |
43
42
  | `uninstall` | Remove installed skills |
43
+ | `source add <url>` | Add custom skill source |
44
+ | `source remove <name>` | Remove skill source |
45
+ | `source list` | List all sources |
46
+ | `source enable <name>` | Enable a source |
47
+ | `source disable <name>` | Disable a source |
48
+ | `source info <name>` | View source details |
49
+ | `config get <key>` | Get config value |
50
+ | `config set <key> <value>` | Set config value |
51
+ | `config edit` | Open config in $EDITOR |
52
+ | `config validate` | Validate configuration |
53
+ | `config export [file]` | Export configuration |
54
+ | `config import [file]` | Import configuration |
55
+ | `config reset --yes` | Reset to defaults |
56
+ | `secrets sync` | Sync MCP secrets from Bitwarden vault |
57
+ | `sync-external` | Alias for `update` |
58
+ | `list-external` | List available external skills |
59
+ | `version` | Show version |
60
+ | `help` | Show help |
44
61
 
45
62
  ## Supported Platforms
46
63
 
@@ -53,22 +70,44 @@ ai-agent update
53
70
  | Codex CLI | `~/.codex/skills/` |
54
71
  | GitHub Copilot | `~/.github/copilot-instructions.md` |
55
72
 
73
+ ## Secret Management
74
+
75
+ Securely sync MCP secrets from Bitwarden vault to your shell profile:
76
+
77
+ ```bash
78
+ ai-agent secrets sync
79
+ ```
80
+
81
+ **How it works:**
82
+ - Discovers required secrets from MCP config files (e.g., `${GITHUB_TOKEN}`)
83
+ - Fetches secrets from Bitwarden vault folder "MCP Secrets"
84
+ - Writes to `~/.zshrc` for persistence across sessions
85
+ - Never stores Bitwarden master password
86
+
87
+ **Setup:** See [Bitwarden MCP Setup Guide](./mcp-servers/bitwarden/README.md)
88
+
89
+ **Auto-configuration:** Package automatically configures Bitwarden MCP server in Antigravity on install
90
+
56
91
  ## Configuration
57
92
 
58
93
  User config at `~/.ai-agent/config.json`:
59
94
 
60
95
  ```json
61
96
  {
62
- "version": "2.3",
97
+ "version": "2.5",
63
98
  "repository": {
64
99
  "url": "https://github.com/youruser/my-ai-skills.git",
65
100
  "branch": "main",
66
- "local": "~/.ai-agent/sync-repo"
101
+ "local": "/Users/you/.ai-agent/sync-repo"
67
102
  },
68
- "sources": {
69
- "official": [],
70
- "custom": []
71
- }
103
+ "sources": [
104
+ {
105
+ "name": "vercel-labs",
106
+ "url": "https://github.com/vercel-labs/agent-skills.git",
107
+ "enabled": true
108
+ }
109
+ ],
110
+ "lastSync": "2026-02-13T12:00:00.000Z"
72
111
  }
73
112
  ```
74
113
 
package/bin/cli.js CHANGED
@@ -199,6 +199,21 @@ function listSkills() {
199
199
  });
200
200
  }
201
201
 
202
+ // MCP Servers
203
+ const mcpInstaller = require("../scripts/mcp-installer");
204
+ const mcpServers = mcpInstaller.getAvailableMcpServers();
205
+
206
+ console.log("\nMCP Servers:");
207
+ if (mcpServers.length === 0) {
208
+ console.log(" (no MCP servers found)");
209
+ } else {
210
+ mcpServers.forEach((server) => {
211
+ const status = server.enabled === false ? " (disabled)" : "";
212
+ const desc = server.description ? ` - ${server.description}` : "";
213
+ console.log(` • ${server.name}${desc}${status}`);
214
+ });
215
+ }
216
+
202
217
  console.log(`\nSource: ${installer.CACHE_DIR}`);
203
218
  console.log("");
204
219
  }
@@ -668,6 +683,13 @@ function install(args) {
668
683
  if (result.skillsCount > 0) parts.push(`${result.skillsCount} skill(s)`);
669
684
  if (result.workflowsCount > 0) parts.push(`${result.workflowsCount} workflow(s)`);
670
685
 
686
+ // Count MCP servers across all platforms
687
+ let mcpTotal = 0;
688
+ result.details.forEach((d) => {
689
+ if (d.mcpServers) mcpTotal += d.mcpServers.added + d.mcpServers.skipped;
690
+ });
691
+ if (mcpTotal > 0) parts.push(`${mcpTotal} MCP server(s)`);
692
+
671
693
  console.log(`\n✓ Installed ${parts.join(", ")} to ${result.platformsCount} platform(s)\n`);
672
694
  result.details.forEach((d) => {
673
695
  console.log(` ${d.platform}:`);
@@ -685,7 +707,18 @@ function install(args) {
685
707
  console.log(` • ${w.name} ${status}`);
686
708
  });
687
709
  }
710
+ if (d.mcpServers && d.mcpServers.servers.length > 0) {
711
+ console.log(` MCP Servers: ${d.mcpServers.added} added, ${d.mcpServers.skipped} skipped`);
712
+ d.mcpServers.servers.forEach((name) => {
713
+ console.log(` • ${name}`);
714
+ });
715
+ }
688
716
  });
717
+
718
+ // Hint about secrets sync if MCP servers were installed
719
+ if (mcpTotal > 0) {
720
+ console.log("\n💡 Run 'ai-agent secrets sync' to resolve Bitwarden secrets");
721
+ }
689
722
  } else {
690
723
  console.log("\n⚠️ No skills or workflows installed.");
691
724
  }
@@ -830,7 +863,7 @@ function pull(args) {
830
863
  const noInstall = args.includes("--no-install");
831
864
 
832
865
  if (!noInstall) {
833
- console.log("📥 Auto-installing skills...\n");
866
+ console.log("📥 Auto-installing skills + MCP servers...\n");
834
867
  install(["--force"]); // Force install to ensure latest
835
868
  }
836
869
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agent-config",
3
- "version": "2.5.9",
3
+ "version": "2.6.1",
4
4
  "description": "Universal skill & workflow manager for AI coding assistants with bi-directional GitHub sync",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,6 +8,8 @@ const path = require("path");
8
8
  const { execSync } = require("child_process");
9
9
  const platforms = require("./platforms");
10
10
 
11
+ const mcpInstaller = require("./mcp-installer");
12
+
11
13
  const REPO_URL = "https://github.com/dongitran/ai-agent-config.git";
12
14
  const CACHE_DIR = path.join(platforms.HOME, ".ai-agent-config-cache");
13
15
  const REPO_SKILLS_DIR = path.join(CACHE_DIR, ".agent", "skills");
@@ -178,6 +180,7 @@ function installToPlatform(platform, options = {}) {
178
180
  workflowsPath: workflowsPath,
179
181
  skills: [],
180
182
  workflows: [],
183
+ mcpServers: null,
181
184
  };
182
185
 
183
186
  // Install skills
@@ -230,6 +233,15 @@ function installToPlatform(platform, options = {}) {
230
233
  }
231
234
  }
232
235
 
236
+ // Install MCP servers (Antigravity only for now)
237
+ if (platform.name === "antigravity") {
238
+ try {
239
+ results.mcpServers = mcpInstaller.installMcpServers({ force });
240
+ } catch (error) {
241
+ console.warn(` ⚠️ MCP install failed: ${error.message}`);
242
+ }
243
+ }
244
+
233
245
  return results;
234
246
  }
235
247
 
@@ -0,0 +1,280 @@
1
+ /**
2
+ * MCP Installer Module
3
+ * Handles discovering, validating, and installing MCP servers from repo to platforms
4
+ */
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const platforms = require("./platforms");
9
+ const configManager = require("./config-manager");
10
+
11
+ const SKIP_FOLDERS = ["bitwarden"];
12
+
13
+ /**
14
+ * Get the MCP servers directory from the user's sync repo
15
+ * @returns {string|null} Path to .agent/mcp-servers/ or null
16
+ */
17
+ function getMcpServersDir() {
18
+ const config = configManager.loadConfig();
19
+ const repoLocal = config.repository && config.repository.local;
20
+ if (!repoLocal) return null;
21
+
22
+ const expanded = repoLocal.replace(/^~/, process.env.HOME || process.env.USERPROFILE);
23
+ const mcpDir = path.join(expanded, ".agent", "mcp-servers");
24
+ return fs.existsSync(mcpDir) ? mcpDir : null;
25
+ }
26
+
27
+ /**
28
+ * Validate an MCP server config
29
+ * @param {Object} config - Parsed config.json
30
+ * @returns {{ valid: boolean, errors: string[] }}
31
+ */
32
+ function validateMcpConfig(config) {
33
+ const errors = [];
34
+
35
+ if (!config.name || typeof config.name !== "string") {
36
+ errors.push("Missing or invalid 'name' field");
37
+ }
38
+ if (!config.command || typeof config.command !== "string") {
39
+ errors.push("Missing or invalid 'command' field");
40
+ }
41
+ if (!Array.isArray(config.args)) {
42
+ errors.push("Missing or invalid 'args' field (must be array)");
43
+ }
44
+ if (config.bitwardenEnv && typeof config.bitwardenEnv !== "object") {
45
+ errors.push("'bitwardenEnv' must be an object");
46
+ }
47
+ if (config.disabledTools && !Array.isArray(config.disabledTools)) {
48
+ errors.push("'disabledTools' must be an array");
49
+ }
50
+
51
+ return { valid: errors.length === 0, errors };
52
+ }
53
+
54
+ /**
55
+ * Get all available MCP servers from the repo
56
+ * @returns {Array<Object>} Array of parsed and validated MCP server configs
57
+ */
58
+ function getAvailableMcpServers() {
59
+ const mcpDir = getMcpServersDir();
60
+ if (!mcpDir) return [];
61
+
62
+ const servers = [];
63
+
64
+ const entries = fs.readdirSync(mcpDir, { withFileTypes: true });
65
+ for (const entry of entries) {
66
+ if (!entry.isDirectory()) continue;
67
+ if (SKIP_FOLDERS.includes(entry.name)) continue;
68
+
69
+ const configPath = path.join(mcpDir, entry.name, "config.json");
70
+ if (!fs.existsSync(configPath)) continue;
71
+
72
+ try {
73
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
74
+ const validation = validateMcpConfig(config);
75
+ if (!validation.valid) {
76
+ console.warn(` ⚠️ Invalid MCP config in ${entry.name}/: ${validation.errors.join(", ")}`);
77
+ continue;
78
+ }
79
+ servers.push(config);
80
+ } catch (error) {
81
+ console.warn(` ⚠️ Failed to parse ${entry.name}/config.json: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ return servers;
86
+ }
87
+
88
+ /**
89
+ * Collect all bitwardenEnv entries from MCP server configs
90
+ * @returns {Array<{ serverName: string, envVar: string, bitwardenItem: string }>}
91
+ */
92
+ function collectBitwardenEnvs() {
93
+ const servers = getAvailableMcpServers();
94
+ const envs = [];
95
+
96
+ for (const server of servers) {
97
+ if (server.enabled === false) continue;
98
+ if (!server.bitwardenEnv) continue;
99
+
100
+ for (const [envVar, bitwardenItem] of Object.entries(server.bitwardenEnv)) {
101
+ envs.push({
102
+ serverName: server.name,
103
+ envVar,
104
+ bitwardenItem,
105
+ });
106
+ }
107
+ }
108
+
109
+ return envs;
110
+ }
111
+
112
+ /**
113
+ * Install MCP servers to Antigravity's mcp_config.json
114
+ * This is the "pull" flow - installs structure without resolved secrets
115
+ * @param {Object} options - { force: boolean }
116
+ * @returns {{ added: number, skipped: number, servers: string[] }}
117
+ */
118
+ function installMcpServers(options = {}) {
119
+ const { force = false } = options;
120
+ const antigravity = platforms.getByName("antigravity");
121
+
122
+ if (!antigravity || !antigravity.mcpConfigPath) {
123
+ return { added: 0, skipped: 0, servers: [] };
124
+ }
125
+
126
+ const servers = getAvailableMcpServers().filter((s) => s.enabled !== false);
127
+ if (servers.length === 0) {
128
+ return { added: 0, skipped: 0, servers: [] };
129
+ }
130
+
131
+ // Read existing mcp_config.json
132
+ let mcpConfig = { mcpServers: {} };
133
+ if (fs.existsSync(antigravity.mcpConfigPath)) {
134
+ try {
135
+ mcpConfig = JSON.parse(fs.readFileSync(antigravity.mcpConfigPath, "utf-8"));
136
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
137
+ } catch {
138
+ mcpConfig = { mcpServers: {} };
139
+ }
140
+ }
141
+
142
+ let added = 0;
143
+ let skipped = 0;
144
+ const serverNames = [];
145
+
146
+ for (const server of servers) {
147
+ const existing = mcpConfig.mcpServers[server.name];
148
+
149
+ if (existing && !force) {
150
+ skipped++;
151
+ serverNames.push(server.name);
152
+ continue;
153
+ }
154
+
155
+ // Build server entry for Antigravity format
156
+ const entry = {
157
+ command: server.command,
158
+ args: server.args,
159
+ };
160
+
161
+ // Preserve existing env if not forcing
162
+ if (existing && existing.env) {
163
+ entry.env = existing.env;
164
+ }
165
+
166
+ if (server.disabledTools && server.disabledTools.length > 0) {
167
+ entry.disabledTools = server.disabledTools;
168
+ }
169
+
170
+ mcpConfig.mcpServers[server.name] = entry;
171
+ added++;
172
+ serverNames.push(server.name);
173
+ }
174
+
175
+ // Write back
176
+ if (added > 0) {
177
+ const configDir = path.dirname(antigravity.mcpConfigPath);
178
+ if (!fs.existsSync(configDir)) {
179
+ fs.mkdirSync(configDir, { recursive: true });
180
+ }
181
+ fs.writeFileSync(antigravity.mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
182
+ }
183
+
184
+ return { added, skipped, servers: serverNames };
185
+ }
186
+
187
+ /**
188
+ * Install MCP servers with resolved secrets to Antigravity
189
+ * This is the "secrets sync" flow - updates env with real values
190
+ * @param {Object} resolvedSecrets - Map of envVar → resolvedValue
191
+ * @returns {{ installed: number, servers: Array<{ name: string, secretsCount: number }> }}
192
+ */
193
+ function installMcpServersWithSecrets(resolvedSecrets) {
194
+ const antigravity = platforms.getByName("antigravity");
195
+
196
+ if (!antigravity || !antigravity.mcpConfigPath) {
197
+ return { installed: 0, servers: [] };
198
+ }
199
+
200
+ const allServers = getAvailableMcpServers().filter((s) => s.enabled !== false);
201
+ if (allServers.length === 0) {
202
+ return { installed: 0, servers: [] };
203
+ }
204
+
205
+ // Read existing mcp_config.json
206
+ let mcpConfig = { mcpServers: {} };
207
+ if (fs.existsSync(antigravity.mcpConfigPath)) {
208
+ try {
209
+ mcpConfig = JSON.parse(fs.readFileSync(antigravity.mcpConfigPath, "utf-8"));
210
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
211
+ } catch {
212
+ mcpConfig = { mcpServers: {} };
213
+ }
214
+ }
215
+
216
+ let installed = 0;
217
+ const serverResults = [];
218
+
219
+ for (const server of allServers) {
220
+ const existing = mcpConfig.mcpServers[server.name] || {};
221
+
222
+ // Build entry: keep existing custom fields (disabledTools user may have customized)
223
+ const entry = {
224
+ command: server.command,
225
+ args: server.args,
226
+ };
227
+
228
+ // Build env from resolved secrets
229
+ if (server.bitwardenEnv) {
230
+ const env = {};
231
+ let secretsCount = 0;
232
+
233
+ for (const [envVar, bitwardenItem] of Object.entries(server.bitwardenEnv)) {
234
+ if (resolvedSecrets[bitwardenItem]) {
235
+ env[envVar] = resolvedSecrets[bitwardenItem];
236
+ secretsCount++;
237
+ }
238
+ }
239
+
240
+ if (Object.keys(env).length > 0) {
241
+ entry.env = { ...(existing.env || {}), ...env };
242
+ }
243
+
244
+ serverResults.push({ name: server.name, secretsCount });
245
+ } else {
246
+ serverResults.push({ name: server.name, secretsCount: 0 });
247
+ }
248
+
249
+ // Preserve user-customized disabledTools from existing config
250
+ if (existing.disabledTools) {
251
+ entry.disabledTools = existing.disabledTools;
252
+ } else if (server.disabledTools && server.disabledTools.length > 0) {
253
+ entry.disabledTools = server.disabledTools;
254
+ }
255
+
256
+ mcpConfig.mcpServers[server.name] = entry;
257
+ installed++;
258
+ }
259
+
260
+ // Write back
261
+ if (installed > 0) {
262
+ const configDir = path.dirname(antigravity.mcpConfigPath);
263
+ if (!fs.existsSync(configDir)) {
264
+ fs.mkdirSync(configDir, { recursive: true });
265
+ }
266
+ fs.writeFileSync(antigravity.mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
267
+ }
268
+
269
+ return { installed, servers: serverResults };
270
+ }
271
+
272
+ module.exports = {
273
+ getMcpServersDir,
274
+ validateMcpConfig,
275
+ getAvailableMcpServers,
276
+ collectBitwardenEnvs,
277
+ installMcpServers,
278
+ installMcpServersWithSecrets,
279
+ SKIP_FOLDERS,
280
+ };
@@ -42,6 +42,7 @@ const SUPPORTED = [
42
42
  configDir: ".gemini/antigravity",
43
43
  skillsDir: "skills",
44
44
  workflowsDir: "workflows",
45
+ mcpConfigFile: "mcp_config.json",
45
46
  get configPath() {
46
47
  return path.join(HOME, this.configDir);
47
48
  },
@@ -51,6 +52,9 @@ const SUPPORTED = [
51
52
  get workflowsPath() {
52
53
  return path.join(HOME, this.configDir, this.workflowsDir);
53
54
  },
55
+ get mcpConfigPath() {
56
+ return path.join(HOME, this.configDir, this.mcpConfigFile);
57
+ },
54
58
  detect() {
55
59
  // Check for .gemini directory or Antigravity app
56
60
  return (
@@ -9,7 +9,6 @@ const { execSync, spawnSync } = require("child_process");
9
9
  const os = require("os");
10
10
 
11
11
  const HOME = os.homedir();
12
- const BITWARDEN_FOLDER = "MCP Secrets";
13
12
 
14
13
  /**
15
14
  * Validate that Bitwarden CLI is installed
@@ -153,40 +152,73 @@ function unlockBitwarden(password) {
153
152
  }
154
153
 
155
154
  /**
156
- * Discover required secrets from MCP configs
157
- * Scans ~/.gemini/antigravity/mcp_config.json for ${VAR} patterns
155
+ * Try to reuse BW_SESSION from Antigravity MCP config
156
+ * This prevents creating a new session that would invalidate the MCP server's session
157
+ * @returns {{ success: boolean, sessionKey?: string, reason?: string }}
158
158
  */
159
- function discoverRequiredSecrets() {
159
+ function tryReuseAntigravitySession() {
160
160
  const platforms = require("./platforms");
161
161
  const antigravity = platforms.getByName("antigravity");
162
162
 
163
- if (!antigravity) {
164
- return { found: false, secrets: [], reason: "Antigravity platform not detected" };
163
+ if (!antigravity || !antigravity.mcpConfigPath) {
164
+ return { success: false, reason: "Antigravity not configured" };
165
165
  }
166
166
 
167
- const mcpConfigPath = path.join(antigravity.configPath, "mcp_config.json");
168
-
169
- if (!fs.existsSync(mcpConfigPath)) {
170
- return { found: false, secrets: [], reason: "MCP config not found" };
167
+ if (!fs.existsSync(antigravity.mcpConfigPath)) {
168
+ return { success: false, reason: "mcp_config.json not found" };
171
169
  }
172
170
 
173
171
  try {
174
- const content = fs.readFileSync(mcpConfigPath, "utf-8");
172
+ const mcpConfig = JSON.parse(fs.readFileSync(antigravity.mcpConfigPath, "utf-8"));
173
+ const bitwardenServer = mcpConfig.mcpServers?.bitwarden;
175
174
 
176
- // Extract all ${VAR_NAME} patterns (supports mixed/lowercase)
177
- const regex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
178
- const matches = [...content.matchAll(regex)];
179
- const secretNames = [...new Set(matches.map((m) => m[1]))];
175
+ if (!bitwardenServer || !bitwardenServer.env || !bitwardenServer.env.BW_SESSION) {
176
+ return { success: false, reason: "No BW_SESSION in bitwarden MCP config" };
177
+ }
180
178
 
181
- return { found: true, secrets: secretNames };
179
+ const sessionKey = bitwardenServer.env.BW_SESSION;
180
+
181
+ // Validate session by trying to list folders
182
+ const testResult = spawnSync("bw", ["list", "folders", "--session", sessionKey], {
183
+ encoding: "utf-8",
184
+ stdio: ["pipe", "pipe", "pipe"],
185
+ });
186
+
187
+ if (testResult.status === 0) {
188
+ return { success: true, sessionKey };
189
+ } else {
190
+ return { success: false, reason: "Session expired or invalid" };
191
+ }
182
192
  } catch (error) {
183
- return { found: false, secrets: [], reason: error.message };
193
+ return { success: false, reason: `Failed to read config: ${error.message}` };
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Discover required secrets from MCP server configs in the repo
199
+ * Scans .agent/mcp-servers/{name}/config.json for bitwardenEnv fields
200
+ */
201
+ function discoverRequiredSecrets() {
202
+ const mcpInstaller = require("./mcp-installer");
203
+ const envs = mcpInstaller.collectBitwardenEnvs();
204
+
205
+ if (envs.length === 0) {
206
+ const mcpDir = mcpInstaller.getMcpServersDir();
207
+ if (!mcpDir) {
208
+ return { found: false, secrets: [], reason: "No repository configured or no MCP servers found" };
209
+ }
210
+ return { found: true, secrets: [] };
184
211
  }
212
+
213
+ // Collect unique Bitwarden item names to fetch
214
+ const secretNames = [...new Set(envs.map((e) => e.bitwardenItem))];
215
+
216
+ return { found: true, secrets: secretNames, envs };
185
217
  }
186
218
 
187
219
  /**
188
220
  * Fetch secrets from Bitwarden vault
189
- * Only searches in "MCP Secrets" folder
221
+ * Searches entire vault (all folders) for matching items
190
222
  */
191
223
  function fetchSecretsFromBitwarden(sessionKey, secretNames) {
192
224
  const results = {
@@ -195,30 +227,14 @@ function fetchSecretsFromBitwarden(sessionKey, secretNames) {
195
227
  };
196
228
 
197
229
  try {
198
- // Step 1: Get folder ID for "MCP Secrets"
199
- const foldersJson = execSync(`bw list folders --session ${sessionKey}`, {
200
- encoding: "utf-8",
201
- stdio: ["pipe", "pipe", "pipe"],
202
- });
203
- const folders = JSON.parse(foldersJson);
204
- const mcpFolder = folders.find((f) => f.name === BITWARDEN_FOLDER);
205
-
206
- if (!mcpFolder) {
207
- console.warn(`\n⚠️ Folder "${BITWARDEN_FOLDER}" not found in Bitwarden vault`);
208
- console.warn(` Create folder "${BITWARDEN_FOLDER}" and add your secrets there\n`);
209
- // All secrets are missing since folder doesn't exist
210
- secretNames.forEach((name) => results.missing.push(name));
211
- return results;
212
- }
213
-
214
- // Step 2: List all items in "MCP Secrets" folder
215
- const itemsJson = execSync(`bw list items --folderid ${mcpFolder.id} --session ${sessionKey}`, {
230
+ // List all items in the vault (across all folders)
231
+ const itemsJson = execSync(`bw list items --session ${sessionKey}`, {
216
232
  encoding: "utf-8",
217
233
  stdio: ["pipe", "pipe", "pipe"],
218
234
  });
219
235
  const items = JSON.parse(itemsJson);
220
236
 
221
- // Step 3: Match secrets by name
237
+ // Match secrets by name
222
238
  for (const secretName of secretNames) {
223
239
  const item = items.find((i) => i.name === secretName);
224
240
 
@@ -301,6 +317,7 @@ function writeToShellProfile(secrets) {
301
317
  */
302
318
  async function syncSecrets() {
303
319
  let sessionKey = null;
320
+ let sessionSource = null; // Track where session came from: "reused" or "new"
304
321
 
305
322
  try {
306
323
  console.log("\n🔐 Bitwarden Secret Sync\n");
@@ -319,82 +336,107 @@ async function syncSecrets() {
319
336
  process.exit(1);
320
337
  }
321
338
 
322
- // 3. Prompt for password
323
- const password = await promptPassword();
339
+ // 3. Try to reuse existing session from Antigravity MCP
340
+ console.log("🔄 Checking for existing Bitwarden session...");
341
+ const reuseResult = tryReuseAntigravitySession();
324
342
 
325
- // 4. Unlock vault
326
- console.log("\n🔓 Unlocking vault...");
327
- const unlockResult = unlockBitwarden(password);
343
+ if (reuseResult.success) {
344
+ console.log("✓ Reusing session from Antigravity MCP config\n");
345
+ sessionKey = reuseResult.sessionKey;
346
+ sessionSource = "reused";
347
+ } else {
348
+ console.log(` ⊗ ${reuseResult.reason}`);
349
+ console.log(" → Creating new session\n");
328
350
 
329
- if (!unlockResult.success) {
330
- console.error(`❌ ${unlockResult.message}\n`);
331
- process.exit(1);
332
- }
351
+ // 4. Fallback: Prompt for password
352
+ const password = await promptPassword();
353
+
354
+ // 5. Unlock vault
355
+ console.log("\n🔓 Unlocking vault...");
356
+ const unlockResult = unlockBitwarden(password);
357
+
358
+ if (!unlockResult.success) {
359
+ console.error(`❌ ${unlockResult.message}\n`);
360
+ process.exit(1);
361
+ }
333
362
 
334
- console.log("✓ Vault unlocked\n");
335
- sessionKey = unlockResult.sessionKey;
363
+ console.log("✓ Vault unlocked\n");
364
+ sessionKey = unlockResult.sessionKey;
365
+ sessionSource = "new";
366
+ }
336
367
 
337
- // 5. Discover required secrets
338
- console.log("🔍 Scanning MCP configs for required secrets...");
368
+ // 6. Discover required secrets from repo's bitwardenEnv
369
+ console.log("🔍 Scanning MCP server configs for required secrets...");
339
370
  const discovery = discoverRequiredSecrets();
340
371
 
341
372
  if (!discovery.found) {
342
373
  console.log(`⚠️ ${discovery.reason}`);
343
- console.log("\n💡 No secrets to sync. MCP configs will be available after implementing Plan 01.\n");
374
+ console.log("\n💡 Configure a repository first: ai-agent init --repo <url>\n");
344
375
  return;
345
376
  }
346
377
 
347
378
  if (discovery.secrets.length === 0) {
348
- console.log("No environment variables found in MCP configs.\n");
379
+ console.log("No bitwardenEnv entries found in MCP server configs.\n");
349
380
  return;
350
381
  }
351
382
 
352
- console.log(`Found ${discovery.secrets.length} secret(s):`);
383
+ console.log(`Found ${discovery.secrets.length} secret(s) to fetch:`);
353
384
  discovery.secrets.forEach((name) => {
354
385
  console.log(` • ${name}`);
355
386
  });
356
387
 
357
- // 6. Fetch secrets from Bitwarden
358
- console.log(`\n🔐 Fetching from Bitwarden (folder: ${BITWARDEN_FOLDER})...`);
388
+ // 7. Fetch secrets from Bitwarden
389
+ console.log(`\n🔐 Fetching from Bitwarden vault...`);
359
390
  const fetchResults = fetchSecretsFromBitwarden(sessionKey, discovery.secrets);
360
391
 
361
392
  fetchResults.found.forEach((secret) => {
362
- console.log(`✓ ${secret.name} (found)`);
393
+ console.log(` ✓ ${secret.name}`);
363
394
  });
364
395
 
365
396
  fetchResults.missing.forEach((name) => {
366
- console.log(`⚠ ${name} (not found in vault)`);
397
+ console.log(` ⚠ ${name} (not found in vault)`);
367
398
  });
368
399
 
369
- // 7. Write to shell profile
400
+ // 7. Install MCP servers with resolved secrets to Antigravity
370
401
  if (fetchResults.found.length > 0) {
371
- console.log("\n💾 Writing secrets to shell profile...");
372
- const writeResult = writeToShellProfile(fetchResults.found);
373
- console.log(`✓ Added ${writeResult.count} environment variable(s) to ${writeResult.path}`);
402
+ const mcpInstaller = require("./mcp-installer");
403
+
404
+ // Build resolvedSecrets map: bitwardenItemName value
405
+ const resolvedSecrets = {};
406
+ fetchResults.found.forEach((s) => {
407
+ resolvedSecrets[s.name] = s.value;
408
+ });
409
+
410
+ console.log("\n📦 Installing MCP servers to Antigravity...");
411
+ const installResult = mcpInstaller.installMcpServersWithSecrets(resolvedSecrets);
412
+
413
+ installResult.servers.forEach((s) => {
414
+ if (s.secretsCount > 0) {
415
+ console.log(` ✓ ${s.name}: ${s.secretsCount} secret(s) resolved`);
416
+ } else {
417
+ console.log(` ✓ ${s.name}: no secrets needed`);
418
+ }
419
+ });
374
420
  }
375
421
 
376
422
  // 8. Summary
377
423
  console.log("\n✅ Secrets synced successfully!\n");
378
424
 
379
- console.log("ℹ️ Next steps:");
380
- console.log(" 1. Restart terminal or run: source ~/.zshrc");
381
-
382
425
  if (fetchResults.missing.length > 0) {
383
- console.log(` 2. Missing secrets: ${fetchResults.missing.join(", ")}`);
384
- console.log(" Add them to Bitwarden or set manually");
426
+ console.log(`⚠️ Missing secrets: ${fetchResults.missing.join(", ")}`);
427
+ console.log(` Add them to your Bitwarden vault\n`);
385
428
  }
386
-
387
- console.log("");
388
429
  } finally {
389
- // Cleanup: Lock vault to invalidate session key
390
- if (sessionKey) {
430
+ // Cleanup: Only lock if we created a new session
431
+ // Don't lock if we reused the session - let MCP keep using it
432
+ if (sessionKey && sessionSource === "new") {
391
433
  try {
392
434
  execSync("bw lock", { stdio: "pipe" });
393
435
  } catch (e) {
394
436
  // Silent fail - vault may already be locked
395
437
  }
396
- sessionKey = null;
397
438
  }
439
+ sessionKey = null;
398
440
  }
399
441
  }
400
442
 
@@ -168,9 +168,13 @@ class SyncManager {
168
168
  // Add all .agent/ files except bundled package skills
169
169
  execSync("git add .agent/workflows/", { cwd: this.repoPath, stdio: "pipe" });
170
170
 
171
+ // Add MCP servers
172
+ const mcpServersDir = path.join(this.repoPath, ".agent/mcp-servers");
173
+ if (fs.existsSync(mcpServersDir)) {
174
+ execSync("git add .agent/mcp-servers/", { cwd: this.repoPath, stdio: "pipe" });
175
+ }
176
+
171
177
  // Add skills individually, excluding bundled ones
172
- const fs = require("fs");
173
- const path = require("path");
174
178
  const skillsDir = path.join(this.repoPath, ".agent/skills");
175
179
  const bundledSkills = ["ai-agent-config", "config-manager"];
176
180