coder-config 0.40.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 +553 -0
- package/cli.js +431 -0
- package/config-loader.js +294 -0
- package/hooks/activity-track.sh +56 -0
- package/hooks/codex-workstream.sh +44 -0
- package/hooks/gemini-workstream.sh +44 -0
- package/hooks/workstream-inject.sh +20 -0
- package/lib/activity.js +283 -0
- package/lib/apply.js +344 -0
- package/lib/cli.js +267 -0
- package/lib/config.js +171 -0
- package/lib/constants.js +55 -0
- package/lib/env.js +114 -0
- package/lib/index.js +47 -0
- package/lib/init.js +122 -0
- package/lib/mcps.js +139 -0
- package/lib/memory.js +201 -0
- package/lib/projects.js +138 -0
- package/lib/registry.js +83 -0
- package/lib/utils.js +129 -0
- package/lib/workstreams.js +652 -0
- package/package.json +80 -0
- package/scripts/capture-screenshots.js +142 -0
- package/scripts/postinstall.js +122 -0
- package/scripts/release.sh +71 -0
- package/scripts/sync-version.js +77 -0
- package/scripts/tauri-prepare.js +328 -0
- package/shared/mcp-registry.json +76 -0
- package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
- package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
- package/ui/dist/icons/icon-192.svg +16 -0
- package/ui/dist/icons/icon-512.svg +16 -0
- package/ui/dist/index.html +39 -0
- package/ui/dist/manifest.json +25 -0
- package/ui/dist/sw.js +24 -0
- package/ui/dist/tutorial/claude-settings.png +0 -0
- package/ui/dist/tutorial/header.png +0 -0
- package/ui/dist/tutorial/mcp-registry.png +0 -0
- package/ui/dist/tutorial/memory-view.png +0 -0
- package/ui/dist/tutorial/permissions.png +0 -0
- package/ui/dist/tutorial/plugins-view.png +0 -0
- package/ui/dist/tutorial/project-explorer.png +0 -0
- package/ui/dist/tutorial/projects-view.png +0 -0
- package/ui/dist/tutorial/sidebar.png +0 -0
- package/ui/dist/tutorial/tutorial-view.png +0 -0
- package/ui/dist/tutorial/workstreams-view.png +0 -0
- package/ui/routes/activity.js +58 -0
- package/ui/routes/commands.js +74 -0
- package/ui/routes/configs.js +329 -0
- package/ui/routes/env.js +40 -0
- package/ui/routes/file-explorer.js +668 -0
- package/ui/routes/index.js +41 -0
- package/ui/routes/mcp-discovery.js +235 -0
- package/ui/routes/memory.js +385 -0
- package/ui/routes/package.json +3 -0
- package/ui/routes/plugins.js +466 -0
- package/ui/routes/projects.js +198 -0
- package/ui/routes/registry.js +30 -0
- package/ui/routes/rules.js +74 -0
- package/ui/routes/search.js +125 -0
- package/ui/routes/settings.js +381 -0
- package/ui/routes/subprojects.js +208 -0
- package/ui/routes/tool-sync.js +127 -0
- package/ui/routes/updates.js +339 -0
- package/ui/routes/workstreams.js +224 -0
- package/ui/server.cjs +773 -0
- package/ui/terminal-server.cjs +160 -0
package/lib/activity.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity tracking for workstream suggestions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { loadWorkstreams } = require('./workstreams');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get activity file path
|
|
11
|
+
*/
|
|
12
|
+
function getActivityPath(installDir) {
|
|
13
|
+
return path.join(installDir, 'activity.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get default activity structure
|
|
18
|
+
*/
|
|
19
|
+
function getDefaultActivity() {
|
|
20
|
+
return {
|
|
21
|
+
sessions: [],
|
|
22
|
+
projectStats: {},
|
|
23
|
+
coActivity: {},
|
|
24
|
+
lastUpdated: null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load activity data
|
|
30
|
+
*/
|
|
31
|
+
function loadActivity(installDir) {
|
|
32
|
+
const activityPath = getActivityPath(installDir);
|
|
33
|
+
if (fs.existsSync(activityPath)) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(activityPath, 'utf8'));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return getDefaultActivity();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return getDefaultActivity();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Save activity data
|
|
45
|
+
*/
|
|
46
|
+
function saveActivity(installDir, data) {
|
|
47
|
+
const activityPath = getActivityPath(installDir);
|
|
48
|
+
const dir = path.dirname(activityPath);
|
|
49
|
+
if (!fs.existsSync(dir)) {
|
|
50
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
data.lastUpdated = new Date().toISOString();
|
|
53
|
+
fs.writeFileSync(activityPath, JSON.stringify(data, null, 2) + '\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect project root by finding .git or .claude folder
|
|
58
|
+
*/
|
|
59
|
+
function detectProjectRoot(filePath) {
|
|
60
|
+
let dir = path.dirname(filePath);
|
|
61
|
+
const home = process.env.HOME || '';
|
|
62
|
+
|
|
63
|
+
while (dir && dir !== '/' && dir !== home) {
|
|
64
|
+
if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, '.claude'))) {
|
|
65
|
+
return dir;
|
|
66
|
+
}
|
|
67
|
+
dir = path.dirname(dir);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Log activity from a Claude session
|
|
74
|
+
*/
|
|
75
|
+
function activityLog(installDir, files, sessionId = null) {
|
|
76
|
+
const data = loadActivity(installDir);
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
|
|
79
|
+
if (!sessionId) {
|
|
80
|
+
sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let session = data.sessions.find(s => s.id === sessionId);
|
|
84
|
+
if (!session) {
|
|
85
|
+
session = { id: sessionId, startedAt: now, files: [], projects: [] };
|
|
86
|
+
data.sessions.push(session);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const projectsInSession = new Set(session.projects);
|
|
90
|
+
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const rawPath = typeof file === 'string' ? file : file.path;
|
|
93
|
+
if (!rawPath) continue;
|
|
94
|
+
const filePath = path.resolve(rawPath.replace(/^~/, process.env.HOME || ''));
|
|
95
|
+
const action = typeof file === 'object' ? (file.action || 'access') : 'access';
|
|
96
|
+
|
|
97
|
+
session.files.push({ path: filePath, action, timestamp: now });
|
|
98
|
+
|
|
99
|
+
const projectPath = detectProjectRoot(filePath);
|
|
100
|
+
if (projectPath) {
|
|
101
|
+
projectsInSession.add(projectPath);
|
|
102
|
+
|
|
103
|
+
if (!data.projectStats[projectPath]) {
|
|
104
|
+
data.projectStats[projectPath] = { fileCount: 0, lastActive: now, sessionCount: 0 };
|
|
105
|
+
}
|
|
106
|
+
data.projectStats[projectPath].fileCount++;
|
|
107
|
+
data.projectStats[projectPath].lastActive = now;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
session.projects = Array.from(projectsInSession);
|
|
112
|
+
|
|
113
|
+
const projects = session.projects;
|
|
114
|
+
for (let i = 0; i < projects.length; i++) {
|
|
115
|
+
for (let j = i + 1; j < projects.length; j++) {
|
|
116
|
+
const p1 = projects[i], p2 = projects[j];
|
|
117
|
+
if (!data.coActivity[p1]) data.coActivity[p1] = {};
|
|
118
|
+
if (!data.coActivity[p2]) data.coActivity[p2] = {};
|
|
119
|
+
data.coActivity[p1][p2] = (data.coActivity[p1][p2] || 0) + 1;
|
|
120
|
+
data.coActivity[p2][p1] = (data.coActivity[p2][p1] || 0) + 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (data.sessions.length > 100) {
|
|
125
|
+
data.sessions = data.sessions.slice(-100);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
saveActivity(installDir, data);
|
|
129
|
+
return { sessionId, filesLogged: files.length, projects: session.projects };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get activity summary for UI
|
|
134
|
+
*/
|
|
135
|
+
function activitySummary(installDir) {
|
|
136
|
+
const data = loadActivity(installDir);
|
|
137
|
+
const now = new Date();
|
|
138
|
+
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
|
139
|
+
|
|
140
|
+
const recentSessions = data.sessions.filter(s => new Date(s.startedAt) > oneDayAgo);
|
|
141
|
+
|
|
142
|
+
const projectActivity = Object.entries(data.projectStats)
|
|
143
|
+
.map(([projectPath, stats]) => ({
|
|
144
|
+
path: projectPath,
|
|
145
|
+
name: path.basename(projectPath),
|
|
146
|
+
...stats,
|
|
147
|
+
isRecent: new Date(stats.lastActive) > oneDayAgo
|
|
148
|
+
}))
|
|
149
|
+
.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
|
|
150
|
+
|
|
151
|
+
const coActiveProjects = [];
|
|
152
|
+
for (const [project, coProjects] of Object.entries(data.coActivity)) {
|
|
153
|
+
for (const [otherProject, count] of Object.entries(coProjects)) {
|
|
154
|
+
if (count >= 2 && project < otherProject) {
|
|
155
|
+
coActiveProjects.push({
|
|
156
|
+
projects: [project, otherProject],
|
|
157
|
+
names: [path.basename(project), path.basename(otherProject)],
|
|
158
|
+
count
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
coActiveProjects.sort((a, b) => b.count - a.count);
|
|
164
|
+
|
|
165
|
+
const totalFiles = data.sessions.reduce((sum, s) => sum + (s.files?.length || 0), 0);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
totalSessions: data.sessions.length,
|
|
169
|
+
recentSessions: recentSessions.length,
|
|
170
|
+
totalFiles,
|
|
171
|
+
projectCount: Object.keys(data.projectStats).length,
|
|
172
|
+
topProjects: projectActivity.slice(0, 10),
|
|
173
|
+
projectActivity: projectActivity.slice(0, 20),
|
|
174
|
+
coActiveProjects: coActiveProjects.slice(0, 10),
|
|
175
|
+
lastUpdated: data.lastUpdated
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Generate a workstream name from project names
|
|
181
|
+
*/
|
|
182
|
+
function generateWorkstreamName(projects) {
|
|
183
|
+
const names = projects.map(p => path.basename(p));
|
|
184
|
+
if (names.length <= 2) return names.join(' + ');
|
|
185
|
+
return `${names[0]} + ${names.length - 1} more`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Suggest workstreams based on activity patterns
|
|
190
|
+
*/
|
|
191
|
+
function activitySuggestWorkstreams(installDir) {
|
|
192
|
+
const data = loadActivity(installDir);
|
|
193
|
+
const workstreams = loadWorkstreams(installDir);
|
|
194
|
+
const suggestions = [];
|
|
195
|
+
|
|
196
|
+
const coGroups = new Map();
|
|
197
|
+
|
|
198
|
+
for (const session of data.sessions) {
|
|
199
|
+
if (session.projects.length >= 2) {
|
|
200
|
+
const key = session.projects.sort().join('|');
|
|
201
|
+
coGroups.set(key, (coGroups.get(key) || 0) + 1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const [key, count] of coGroups) {
|
|
206
|
+
if (count >= 3) {
|
|
207
|
+
const projects = key.split('|');
|
|
208
|
+
const existingWs = workstreams.workstreams.find(ws =>
|
|
209
|
+
projects.every(p => ws.projects.includes(p))
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (!existingWs) {
|
|
213
|
+
const totalSessions = data.sessions.length;
|
|
214
|
+
const coActivityScore = totalSessions > 0 ? Math.round((count / totalSessions) * 100) : 0;
|
|
215
|
+
|
|
216
|
+
suggestions.push({
|
|
217
|
+
projects,
|
|
218
|
+
name: generateWorkstreamName(projects),
|
|
219
|
+
names: projects.map(p => path.basename(p)),
|
|
220
|
+
sessionCount: count,
|
|
221
|
+
coActivityScore: Math.min(coActivityScore, 100),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
suggestions.sort((a, b) => b.sessionCount - a.sessionCount);
|
|
228
|
+
return suggestions.slice(0, 5);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Clear old activity data
|
|
233
|
+
*/
|
|
234
|
+
function activityClear(installDir, olderThanDays = 30) {
|
|
235
|
+
const data = loadActivity(installDir);
|
|
236
|
+
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
|
|
237
|
+
|
|
238
|
+
data.sessions = data.sessions.filter(s => new Date(s.startedAt) > cutoff);
|
|
239
|
+
data.projectStats = {};
|
|
240
|
+
data.coActivity = {};
|
|
241
|
+
|
|
242
|
+
for (const session of data.sessions) {
|
|
243
|
+
for (const file of session.files) {
|
|
244
|
+
const projectPath = detectProjectRoot(file.path);
|
|
245
|
+
if (projectPath) {
|
|
246
|
+
if (!data.projectStats[projectPath]) {
|
|
247
|
+
data.projectStats[projectPath] = { fileCount: 0, lastActive: session.startedAt, sessionCount: 0 };
|
|
248
|
+
}
|
|
249
|
+
data.projectStats[projectPath].fileCount++;
|
|
250
|
+
if (session.startedAt > data.projectStats[projectPath].lastActive) {
|
|
251
|
+
data.projectStats[projectPath].lastActive = session.startedAt;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const projects = session.projects;
|
|
257
|
+
for (let i = 0; i < projects.length; i++) {
|
|
258
|
+
for (let j = i + 1; j < projects.length; j++) {
|
|
259
|
+
const p1 = projects[i], p2 = projects[j];
|
|
260
|
+
if (!data.coActivity[p1]) data.coActivity[p1] = {};
|
|
261
|
+
if (!data.coActivity[p2]) data.coActivity[p2] = {};
|
|
262
|
+
data.coActivity[p1][p2] = (data.coActivity[p1][p2] || 0) + 1;
|
|
263
|
+
data.coActivity[p2][p1] = (data.coActivity[p2][p1] || 0) + 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
saveActivity(installDir, data);
|
|
269
|
+
return { sessionsRemaining: data.sessions.length };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
getActivityPath,
|
|
274
|
+
getDefaultActivity,
|
|
275
|
+
loadActivity,
|
|
276
|
+
saveActivity,
|
|
277
|
+
detectProjectRoot,
|
|
278
|
+
activityLog,
|
|
279
|
+
activitySummary,
|
|
280
|
+
generateWorkstreamName,
|
|
281
|
+
activitySuggestWorkstreams,
|
|
282
|
+
activityClear,
|
|
283
|
+
};
|
package/lib/apply.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply commands for generating tool configs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const { TOOL_PATHS } = require('./constants');
|
|
9
|
+
const { loadJson, saveJson, loadEnvFile, interpolate, resolveEnvVars } = require('./utils');
|
|
10
|
+
const { findProjectRoot, findAllConfigs, mergeConfigs, findAllConfigsForTool } = require('./config');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate .mcp.json for a project (with hierarchical config merging)
|
|
14
|
+
*/
|
|
15
|
+
function apply(registryPath, projectDir = null) {
|
|
16
|
+
const dir = projectDir || findProjectRoot() || process.cwd();
|
|
17
|
+
|
|
18
|
+
const registry = loadJson(registryPath);
|
|
19
|
+
if (!registry) {
|
|
20
|
+
console.error('Error: Could not load MCP registry from', registryPath);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const configLocations = findAllConfigs(dir);
|
|
25
|
+
|
|
26
|
+
if (configLocations.length === 0) {
|
|
27
|
+
console.error(`No .claude/mcps.json found in ${dir} or parent directories`);
|
|
28
|
+
console.error('Run: claude-config init');
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const loadedConfigs = configLocations.map(loc => ({
|
|
33
|
+
...loc,
|
|
34
|
+
config: loadJson(loc.configPath)
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
if (loadedConfigs.length > 1) {
|
|
38
|
+
console.log('📚 Config hierarchy (merged):');
|
|
39
|
+
for (const { dir: d, configPath } of loadedConfigs) {
|
|
40
|
+
const relPath = d === process.env.HOME ? '~' : path.relative(process.cwd(), d) || '.';
|
|
41
|
+
console.log(` • ${relPath}/.claude/mcps.json`);
|
|
42
|
+
}
|
|
43
|
+
console.log('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mergedConfig = mergeConfigs(loadedConfigs);
|
|
47
|
+
|
|
48
|
+
const globalEnvPath = path.join(path.dirname(registryPath), '.env');
|
|
49
|
+
let env = loadEnvFile(globalEnvPath);
|
|
50
|
+
|
|
51
|
+
for (const { dir: d } of loadedConfigs) {
|
|
52
|
+
const envPath = path.join(d, '.claude', '.env');
|
|
53
|
+
env = { ...env, ...loadEnvFile(envPath) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const output = { mcpServers: {} };
|
|
57
|
+
|
|
58
|
+
if (mergedConfig.include && Array.isArray(mergedConfig.include)) {
|
|
59
|
+
for (const name of mergedConfig.include) {
|
|
60
|
+
if (registry.mcpServers && registry.mcpServers[name]) {
|
|
61
|
+
output.mcpServers[name] = interpolate(registry.mcpServers[name], env);
|
|
62
|
+
} else {
|
|
63
|
+
console.warn(`Warning: MCP "${name}" not found in registry`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (mergedConfig.mcpServers) {
|
|
69
|
+
for (const [name, config] of Object.entries(mergedConfig.mcpServers)) {
|
|
70
|
+
if (name.startsWith('_')) continue;
|
|
71
|
+
output.mcpServers[name] = interpolate(config, env);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const outputPath = path.join(dir, '.mcp.json');
|
|
76
|
+
saveJson(outputPath, output);
|
|
77
|
+
|
|
78
|
+
const count = Object.keys(output.mcpServers).length;
|
|
79
|
+
console.log(`✓ Generated ${outputPath}`);
|
|
80
|
+
console.log(` └─ ${count} MCP(s): ${Object.keys(output.mcpServers).join(', ')}`);
|
|
81
|
+
|
|
82
|
+
// Generate settings.json with enabledPlugins if any are configured
|
|
83
|
+
if (mergedConfig.enabledPlugins && Object.keys(mergedConfig.enabledPlugins).length > 0) {
|
|
84
|
+
const settingsPath = path.join(dir, '.claude', 'settings.json');
|
|
85
|
+
let existingSettings = {};
|
|
86
|
+
|
|
87
|
+
// Load existing settings if present
|
|
88
|
+
if (fs.existsSync(settingsPath)) {
|
|
89
|
+
try {
|
|
90
|
+
existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
existingSettings = {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Merge enabledPlugins into settings
|
|
97
|
+
const settingsOutput = {
|
|
98
|
+
...existingSettings,
|
|
99
|
+
enabledPlugins: mergedConfig.enabledPlugins
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
saveJson(settingsPath, settingsOutput);
|
|
103
|
+
|
|
104
|
+
const pluginCount = Object.entries(mergedConfig.enabledPlugins)
|
|
105
|
+
.filter(([_, enabled]) => enabled).length;
|
|
106
|
+
console.log(`✓ Generated ${settingsPath}`);
|
|
107
|
+
console.log(` └─ ${pluginCount} plugin(s) enabled`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generate MCP config for Antigravity
|
|
115
|
+
*/
|
|
116
|
+
function applyForAntigravity(registryPath, projectDir = null) {
|
|
117
|
+
const dir = projectDir || findProjectRoot() || process.cwd();
|
|
118
|
+
const paths = TOOL_PATHS.antigravity;
|
|
119
|
+
const homeDir = process.env.HOME || '';
|
|
120
|
+
|
|
121
|
+
const registry = loadJson(registryPath);
|
|
122
|
+
if (!registry) {
|
|
123
|
+
console.error('Error: Could not load MCP registry');
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const configLocations = findAllConfigsForTool('antigravity', dir);
|
|
128
|
+
|
|
129
|
+
if (configLocations.length === 0) {
|
|
130
|
+
console.log(` ℹ No .agent/mcps.json found - skipping Antigravity`);
|
|
131
|
+
console.log(` Create one with: mkdir -p .agent && echo '{"include":["filesystem"]}' > .agent/mcps.json`);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const loadedConfigs = configLocations.map(loc => ({
|
|
136
|
+
...loc,
|
|
137
|
+
config: loadJson(loc.configPath)
|
|
138
|
+
}));
|
|
139
|
+
const mergedConfig = mergeConfigs(loadedConfigs);
|
|
140
|
+
|
|
141
|
+
let env = {};
|
|
142
|
+
const globalEnvPath = path.join(homeDir, '.gemini', 'antigravity', '.env');
|
|
143
|
+
env = { ...env, ...loadEnvFile(globalEnvPath) };
|
|
144
|
+
|
|
145
|
+
for (const { dir: d } of configLocations) {
|
|
146
|
+
if (d !== homeDir) {
|
|
147
|
+
const envPath = path.join(d, '.agent', '.env');
|
|
148
|
+
env = { ...env, ...loadEnvFile(envPath) };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const output = { mcpServers: {} };
|
|
153
|
+
|
|
154
|
+
if (mergedConfig.include && Array.isArray(mergedConfig.include)) {
|
|
155
|
+
for (const name of mergedConfig.include) {
|
|
156
|
+
if (registry.mcpServers && registry.mcpServers[name]) {
|
|
157
|
+
output.mcpServers[name] = resolveEnvVars(registry.mcpServers[name], env);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (mergedConfig.mcpServers) {
|
|
163
|
+
for (const [name, config] of Object.entries(mergedConfig.mcpServers)) {
|
|
164
|
+
if (name.startsWith('_')) continue;
|
|
165
|
+
output.mcpServers[name] = resolveEnvVars(config, env);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const outputPath = paths.outputFile.replace(/^~/, homeDir);
|
|
170
|
+
const outputDir = path.dirname(outputPath);
|
|
171
|
+
if (!fs.existsSync(outputDir)) {
|
|
172
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
saveJson(outputPath, output);
|
|
176
|
+
|
|
177
|
+
const count = Object.keys(output.mcpServers).length;
|
|
178
|
+
console.log(`✓ Generated ${outputPath} (Antigravity)`);
|
|
179
|
+
console.log(` └─ ${count} MCP(s): ${Object.keys(output.mcpServers).join(', ')}`);
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate MCP config for Gemini CLI
|
|
186
|
+
*/
|
|
187
|
+
function applyForGemini(registryPath, projectDir = null) {
|
|
188
|
+
const dir = projectDir || findProjectRoot() || process.cwd();
|
|
189
|
+
const paths = TOOL_PATHS.gemini;
|
|
190
|
+
const homeDir = process.env.HOME || '';
|
|
191
|
+
|
|
192
|
+
const registry = loadJson(registryPath);
|
|
193
|
+
if (!registry) {
|
|
194
|
+
console.error('Error: Could not load MCP registry');
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const configLocations = findAllConfigsForTool('gemini', dir);
|
|
199
|
+
|
|
200
|
+
if (configLocations.length === 0) {
|
|
201
|
+
console.log(` ℹ No .gemini/mcps.json found - skipping Gemini CLI`);
|
|
202
|
+
console.log(` Create one with: mkdir -p .gemini && echo '{"include":["filesystem"]}' > .gemini/mcps.json`);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const loadedConfigs = configLocations.map(loc => ({
|
|
207
|
+
...loc,
|
|
208
|
+
config: loadJson(loc.configPath)
|
|
209
|
+
}));
|
|
210
|
+
const mergedConfig = mergeConfigs(loadedConfigs);
|
|
211
|
+
|
|
212
|
+
let env = {};
|
|
213
|
+
const globalEnvPath = path.join(homeDir, '.gemini', '.env');
|
|
214
|
+
env = { ...env, ...loadEnvFile(globalEnvPath) };
|
|
215
|
+
|
|
216
|
+
for (const { dir: d } of configLocations) {
|
|
217
|
+
if (d !== homeDir) {
|
|
218
|
+
const envPath = path.join(d, '.gemini', '.env');
|
|
219
|
+
env = { ...env, ...loadEnvFile(envPath) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const mcpServers = {};
|
|
224
|
+
|
|
225
|
+
if (mergedConfig.include && Array.isArray(mergedConfig.include)) {
|
|
226
|
+
for (const name of mergedConfig.include) {
|
|
227
|
+
if (registry.mcpServers && registry.mcpServers[name]) {
|
|
228
|
+
mcpServers[name] = interpolate(registry.mcpServers[name], env);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (mergedConfig.mcpServers) {
|
|
234
|
+
for (const [name, config] of Object.entries(mergedConfig.mcpServers)) {
|
|
235
|
+
if (name.startsWith('_')) continue;
|
|
236
|
+
mcpServers[name] = interpolate(config, env);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Generate global settings (~/.gemini/settings.json)
|
|
241
|
+
const globalOutputPath = paths.outputFile.replace(/^~/, homeDir);
|
|
242
|
+
const globalOutputDir = path.dirname(globalOutputPath);
|
|
243
|
+
if (!fs.existsSync(globalOutputDir)) {
|
|
244
|
+
fs.mkdirSync(globalOutputDir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let existingSettings = {};
|
|
248
|
+
if (fs.existsSync(globalOutputPath)) {
|
|
249
|
+
try {
|
|
250
|
+
existingSettings = JSON.parse(fs.readFileSync(globalOutputPath, 'utf8'));
|
|
251
|
+
} catch (e) {
|
|
252
|
+
existingSettings = {};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const globalOutput = {
|
|
257
|
+
...existingSettings,
|
|
258
|
+
mcpServers
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
saveJson(globalOutputPath, globalOutput);
|
|
262
|
+
|
|
263
|
+
const count = Object.keys(mcpServers).length;
|
|
264
|
+
console.log(`✓ Generated ${globalOutputPath} (Gemini CLI - global)`);
|
|
265
|
+
console.log(` └─ ${count} MCP(s): ${Object.keys(mcpServers).join(', ')}`);
|
|
266
|
+
|
|
267
|
+
// Generate per-project settings (.gemini/settings.json)
|
|
268
|
+
const projectOutputDir = path.join(dir, '.gemini');
|
|
269
|
+
const projectOutputPath = path.join(projectOutputDir, 'settings.json');
|
|
270
|
+
|
|
271
|
+
if (!fs.existsSync(projectOutputDir)) {
|
|
272
|
+
fs.mkdirSync(projectOutputDir, { recursive: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For per-project, only include mcpServers (not merge with existing)
|
|
276
|
+
const projectOutput = { mcpServers };
|
|
277
|
+
saveJson(projectOutputPath, projectOutput);
|
|
278
|
+
|
|
279
|
+
console.log(`✓ Generated ${projectOutputPath} (Gemini CLI - project)`);
|
|
280
|
+
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Detect which AI coding tools are installed
|
|
286
|
+
*/
|
|
287
|
+
function detectInstalledTools() {
|
|
288
|
+
const homeDir = process.env.HOME || '';
|
|
289
|
+
const results = {};
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
execSync('which claude', { stdio: 'ignore' });
|
|
293
|
+
results.claude = { installed: true, method: 'command' };
|
|
294
|
+
} catch {
|
|
295
|
+
results.claude = {
|
|
296
|
+
installed: fs.existsSync(path.join(homeDir, '.claude')),
|
|
297
|
+
method: 'directory'
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
execSync('which gemini', { stdio: 'ignore' });
|
|
303
|
+
results.gemini = { installed: true, method: 'command' };
|
|
304
|
+
} catch {
|
|
305
|
+
results.gemini = {
|
|
306
|
+
installed: fs.existsSync(path.join(homeDir, '.gemini')),
|
|
307
|
+
method: 'directory'
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
results.antigravity = {
|
|
312
|
+
installed: fs.existsSync(path.join(homeDir, '.gemini', 'antigravity')),
|
|
313
|
+
method: 'directory'
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return results;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Apply config for multiple tools based on preferences
|
|
321
|
+
*/
|
|
322
|
+
function applyForTools(registryPath, projectDir = null, tools = ['claude']) {
|
|
323
|
+
const results = {};
|
|
324
|
+
|
|
325
|
+
for (const tool of tools) {
|
|
326
|
+
if (tool === 'claude') {
|
|
327
|
+
results.claude = apply(registryPath, projectDir);
|
|
328
|
+
} else if (tool === 'gemini') {
|
|
329
|
+
results.gemini = applyForGemini(registryPath, projectDir);
|
|
330
|
+
} else if (tool === 'antigravity') {
|
|
331
|
+
results.antigravity = applyForAntigravity(registryPath, projectDir);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return results;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = {
|
|
339
|
+
apply,
|
|
340
|
+
applyForAntigravity,
|
|
341
|
+
applyForGemini,
|
|
342
|
+
detectInstalledTools,
|
|
343
|
+
applyForTools,
|
|
344
|
+
};
|