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 +49 -10
- package/bin/cli.js +34 -1
- package/package.json +1 -1
- package/scripts/installer.js +12 -0
- package/scripts/mcp-installer.js +280 -0
- package/scripts/platforms.js +4 -0
- package/scripts/secret-manager.js +113 -71
- package/scripts/sync-manager.js +6 -2
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
|
|
37
|
-
| `
|
|
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.
|
|
97
|
+
"version": "2.5",
|
|
63
98
|
"repository": {
|
|
64
99
|
"url": "https://github.com/youruser/my-ai-skills.git",
|
|
65
100
|
"branch": "main",
|
|
66
|
-
"local": "
|
|
101
|
+
"local": "/Users/you/.ai-agent/sync-repo"
|
|
67
102
|
},
|
|
68
|
-
"sources":
|
|
69
|
-
|
|
70
|
-
|
|
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
package/scripts/installer.js
CHANGED
|
@@ -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
|
+
};
|
package/scripts/platforms.js
CHANGED
|
@@ -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
|
-
*
|
|
157
|
-
*
|
|
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
|
|
159
|
+
function tryReuseAntigravitySession() {
|
|
160
160
|
const platforms = require("./platforms");
|
|
161
161
|
const antigravity = platforms.getByName("antigravity");
|
|
162
162
|
|
|
163
|
-
if (!antigravity) {
|
|
164
|
-
return {
|
|
163
|
+
if (!antigravity || !antigravity.mcpConfigPath) {
|
|
164
|
+
return { success: false, reason: "Antigravity not configured" };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
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
|
|
172
|
+
const mcpConfig = JSON.parse(fs.readFileSync(antigravity.mcpConfigPath, "utf-8"));
|
|
173
|
+
const bitwardenServer = mcpConfig.mcpServers?.bitwarden;
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
//
|
|
199
|
-
const
|
|
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
|
-
//
|
|
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.
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
|
|
363
|
+
console.log("✓ Vault unlocked\n");
|
|
364
|
+
sessionKey = unlockResult.sessionKey;
|
|
365
|
+
sessionSource = "new";
|
|
366
|
+
}
|
|
336
367
|
|
|
337
|
-
//
|
|
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💡
|
|
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
|
|
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
|
-
//
|
|
358
|
-
console.log(`\n🔐 Fetching from Bitwarden
|
|
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(
|
|
393
|
+
console.log(` ✓ ${secret.name}`);
|
|
363
394
|
});
|
|
364
395
|
|
|
365
396
|
fetchResults.missing.forEach((name) => {
|
|
366
|
-
console.log(
|
|
397
|
+
console.log(` ⚠ ${name} (not found in vault)`);
|
|
367
398
|
});
|
|
368
399
|
|
|
369
|
-
// 7.
|
|
400
|
+
// 7. Install MCP servers with resolved secrets to Antigravity
|
|
370
401
|
if (fetchResults.found.length > 0) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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(
|
|
384
|
-
console.log(
|
|
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:
|
|
390
|
-
if
|
|
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
|
|
package/scripts/sync-manager.js
CHANGED
|
@@ -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
|
|