@patchen0518/agentbrew 1.1.0 → 1.2.5
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 +88 -45
- package/dist/cli.js +177 -15
- package/dist/cli.js.map +1 -1
- package/dist/dispatcher.d.ts +18 -0
- package/dist/dispatcher.js +43 -5
- package/dist/dispatcher.js.map +1 -1
- package/dist/installer.js +15 -11
- package/dist/installer.js.map +1 -1
- package/dist/migration.js +35 -1
- package/dist/migration.js.map +1 -1
- package/dist/registry.d.ts +6 -1
- package/dist/registry.js +86 -41
- package/dist/registry.js.map +1 -1
- package/dist/router.d.ts +1 -1
- package/dist/router.js +24 -4
- package/dist/router.js.map +1 -1
- package/dist/state.d.ts +4 -2
- package/dist/state.js +9 -4
- package/dist/state.js.map +1 -1
- package/dist/sync.d.ts +121 -0
- package/dist/sync.js +788 -1
- package/dist/sync.js.map +1 -1
- package/dist/updater.js +2 -1
- package/dist/updater.js.map +1 -1
- package/package.json +1 -1
package/dist/sync.js
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.INSTRUCTIONS_FILE = exports.MARKER_END = exports.MARKER_START = void 0;
|
|
40
|
+
exports.extractSkillEntries = extractSkillEntries;
|
|
41
|
+
exports.syncSkillsToClaudeCode = syncSkillsToClaudeCode;
|
|
42
|
+
exports.unsyncSkillsFromClaudeCode = unsyncSkillsFromClaudeCode;
|
|
43
|
+
exports.syncSkillsToGeminiCLI = syncSkillsToGeminiCLI;
|
|
44
|
+
exports.unsyncSkillsFromGeminiCLI = unsyncSkillsFromGeminiCLI;
|
|
45
|
+
exports.syncSkillsToWindsurf = syncSkillsToWindsurf;
|
|
46
|
+
exports.unsyncSkillsFromWindsurf = unsyncSkillsFromWindsurf;
|
|
47
|
+
exports.syncSkillsToAntigravityCLI = syncSkillsToAntigravityCLI;
|
|
48
|
+
exports.unsyncSkillsFromAntigravityCLI = unsyncSkillsFromAntigravityCLI;
|
|
49
|
+
exports.cleanOrphanSkills = cleanOrphanSkills;
|
|
50
|
+
exports.syncMcpServerToCursor = syncMcpServerToCursor;
|
|
51
|
+
exports.unsyncMcpServerFromCursor = unsyncMcpServerFromCursor;
|
|
52
|
+
exports.syncMcpServerToCodex = syncMcpServerToCodex;
|
|
53
|
+
exports.unsyncMcpServerFromCodex = unsyncMcpServerFromCodex;
|
|
54
|
+
exports.syncSkillsToCursor = syncSkillsToCursor;
|
|
55
|
+
exports.unsyncSkillsFromCursor = unsyncSkillsFromCursor;
|
|
56
|
+
exports.syncSkillsToKiro = syncSkillsToKiro;
|
|
57
|
+
exports.unsyncSkillsFromKiro = unsyncSkillsFromKiro;
|
|
58
|
+
exports.syncMcpServerToKiro = syncMcpServerToKiro;
|
|
59
|
+
exports.unsyncMcpServerFromKiro = unsyncMcpServerFromKiro;
|
|
7
60
|
exports.getDefaultTargets = getDefaultTargets;
|
|
8
61
|
exports.getInstructionsPath = getInstructionsPath;
|
|
9
62
|
exports.buildInjectedSection = buildInjectedSection;
|
|
@@ -14,7 +67,724 @@ exports.unsyncInstructions = unsyncInstructions;
|
|
|
14
67
|
const fs_1 = __importDefault(require("fs"));
|
|
15
68
|
const os_1 = __importDefault(require("os"));
|
|
16
69
|
const path_1 = __importDefault(require("path"));
|
|
70
|
+
const toml = __importStar(require("smol-toml"));
|
|
17
71
|
const config_1 = require("./config");
|
|
72
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
73
|
+
/**
|
|
74
|
+
* Extracts SkillEntry objects from discovered packages by scanning for SKILL.md prompts.
|
|
75
|
+
*/
|
|
76
|
+
function extractSkillEntries(packages) {
|
|
77
|
+
const skills = [];
|
|
78
|
+
for (const pkg of packages) {
|
|
79
|
+
if (!pkg.manifest.prompts)
|
|
80
|
+
continue;
|
|
81
|
+
for (const prompt of pkg.manifest.prompts) {
|
|
82
|
+
if (path_1.default.basename(prompt.file).toUpperCase() !== 'SKILL.MD')
|
|
83
|
+
continue;
|
|
84
|
+
skills.push({
|
|
85
|
+
packageName: pkg.packageName,
|
|
86
|
+
skillName: prompt.name,
|
|
87
|
+
skillDir: path_1.default.dirname(path_1.default.resolve(pkg.path, prompt.file)),
|
|
88
|
+
description: prompt.description,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return skills;
|
|
93
|
+
}
|
|
94
|
+
const SYNCED_SKILLS_FILE = 'synced-skills.json';
|
|
95
|
+
const AGENTBREW_EXTENSION_NAME = 'agentbrew';
|
|
96
|
+
const CURSOR_SKILLS_INDEX_FILE = 'agentbrew-skills-index.md';
|
|
97
|
+
function getSyncedSkillsPath(brewRoot) {
|
|
98
|
+
return path_1.default.join(brewRoot ?? (0, config_1.getBrewRoot)(), SYNCED_SKILLS_FILE);
|
|
99
|
+
}
|
|
100
|
+
function loadSyncedState(brewRoot) {
|
|
101
|
+
try {
|
|
102
|
+
const raw = JSON.parse(fs_1.default.readFileSync(getSyncedSkillsPath(brewRoot), 'utf-8'));
|
|
103
|
+
// Migrate old flat format: { skills: [...] } → { claude: [...] }
|
|
104
|
+
if (raw.skills && !raw.claude) {
|
|
105
|
+
return { claude: raw.skills, gemini: [], windsurf: [], cursor: false, cursorMcp: false, antigravity: [], codexMcp: false, kiro: [], kiroMcp: false };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
claude: raw.claude ?? [],
|
|
109
|
+
gemini: raw.gemini ?? [],
|
|
110
|
+
windsurf: raw.windsurf ?? [],
|
|
111
|
+
cursor: raw.cursor ?? false,
|
|
112
|
+
cursorMcp: raw.cursorMcp ?? false,
|
|
113
|
+
antigravity: raw.antigravity ?? [],
|
|
114
|
+
codexMcp: raw.codexMcp ?? false,
|
|
115
|
+
kiro: raw.kiro ?? [],
|
|
116
|
+
kiroMcp: raw.kiroMcp ?? false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { claude: [], gemini: [], windsurf: [], cursor: false, cursorMcp: false, antigravity: [], codexMcp: false, kiro: [], kiroMcp: false };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function saveSyncedState(state, brewRoot) {
|
|
124
|
+
const p = getSyncedSkillsPath(brewRoot);
|
|
125
|
+
fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
|
|
126
|
+
fs_1.default.writeFileSync(p, JSON.stringify(state, null, 2), 'utf-8');
|
|
127
|
+
}
|
|
128
|
+
function symlinkSkills(skills, targetDir, state, agentKey, brewRoot) {
|
|
129
|
+
const results = [];
|
|
130
|
+
const newEntries = [];
|
|
131
|
+
for (const skill of skills) {
|
|
132
|
+
const entryName = `${skill.packageName}-${skill.skillName}`;
|
|
133
|
+
const entryPath = path_1.default.join(targetDir, entryName);
|
|
134
|
+
if (!fs_1.default.existsSync(skill.skillDir)) {
|
|
135
|
+
results.push({ entryName, status: 'skipped', note: 'Source directory not found' });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
let exists = false;
|
|
139
|
+
try {
|
|
140
|
+
fs_1.default.lstatSync(entryPath);
|
|
141
|
+
exists = true;
|
|
142
|
+
}
|
|
143
|
+
catch { }
|
|
144
|
+
if (exists) {
|
|
145
|
+
let currentTarget = null;
|
|
146
|
+
try {
|
|
147
|
+
currentTarget = fs_1.default.readlinkSync(entryPath);
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
if (currentTarget === null) {
|
|
151
|
+
// Not a symlink — not created by AgentBrew, leave it alone
|
|
152
|
+
results.push({ entryName, status: 'skipped', note: 'Path exists and is not a symlink' });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (currentTarget === skill.skillDir) {
|
|
156
|
+
newEntries.push(entryName);
|
|
157
|
+
results.push({ entryName, status: 'already_exists', path: entryPath });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Stale symlink pointing at a different target — remove and re-create
|
|
161
|
+
try {
|
|
162
|
+
fs_1.default.rmSync(entryPath, { force: true });
|
|
163
|
+
}
|
|
164
|
+
catch { }
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
fs_1.default.symlinkSync(skill.skillDir, entryPath);
|
|
168
|
+
newEntries.push(entryName);
|
|
169
|
+
results.push({ entryName, status: 'linked', path: entryPath });
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
results.push({ entryName, status: 'error', note: e.message });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
state[agentKey] = [...new Set([...state[agentKey], ...newEntries])];
|
|
176
|
+
saveSyncedState(state, brewRoot);
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
function removeTrackedSymlinks(tracked, skillsDir) {
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const entryName of tracked) {
|
|
182
|
+
const entryPath = path_1.default.join(skillsDir, entryName);
|
|
183
|
+
let exists = false;
|
|
184
|
+
try {
|
|
185
|
+
fs_1.default.lstatSync(entryPath);
|
|
186
|
+
exists = true;
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
if (!exists) {
|
|
190
|
+
results.push({ entryName, status: 'skipped', note: 'Not found' });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
fs_1.default.rmSync(entryPath, { recursive: true, force: true });
|
|
195
|
+
results.push({ entryName, status: 'removed', path: entryPath });
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
results.push({ entryName, status: 'error', note: e.message });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
// ─── Claude Code ────────────────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* Symlinks each skill directory into ~/.claude/skills/<pkgName>-<skillName>
|
|
206
|
+
* so Claude Code can discover them as invocable skills.
|
|
207
|
+
*/
|
|
208
|
+
function syncSkillsToClaudeCode(skills, brewRoot) {
|
|
209
|
+
const claudeDir = path_1.default.join(os_1.default.homedir(), '.claude');
|
|
210
|
+
if (!fs_1.default.existsSync(claudeDir))
|
|
211
|
+
return [];
|
|
212
|
+
const skillsDir = path_1.default.join(claudeDir, 'skills');
|
|
213
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
214
|
+
const state = loadSyncedState(brewRoot);
|
|
215
|
+
return symlinkSkills(skills, skillsDir, state, 'claude', brewRoot);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Removes all skill symlinks previously created by syncSkillsToClaudeCode.
|
|
219
|
+
*/
|
|
220
|
+
function unsyncSkillsFromClaudeCode(brewRoot) {
|
|
221
|
+
const skillsDir = path_1.default.join(os_1.default.homedir(), '.claude', 'skills');
|
|
222
|
+
const state = loadSyncedState(brewRoot);
|
|
223
|
+
const results = removeTrackedSymlinks(state.claude, skillsDir);
|
|
224
|
+
state.claude = [];
|
|
225
|
+
saveSyncedState(state, brewRoot);
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
// ─── Gemini CLI ──────────────────────────────────────────────────────────────
|
|
229
|
+
/**
|
|
230
|
+
* Registers skills with Gemini CLI by creating an agentbrew extension at
|
|
231
|
+
* ~/.gemini/extensions/agentbrew/ and symlinking each skill's directory into
|
|
232
|
+
* ~/.gemini/extensions/agentbrew/skills/<pkgName>-<skillName>.
|
|
233
|
+
*/
|
|
234
|
+
function syncSkillsToGeminiCLI(skills, brewRoot) {
|
|
235
|
+
const geminiDir = path_1.default.join(os_1.default.homedir(), '.gemini');
|
|
236
|
+
if (!fs_1.default.existsSync(geminiDir))
|
|
237
|
+
return [];
|
|
238
|
+
const extensionDir = path_1.default.join(geminiDir, 'extensions', AGENTBREW_EXTENSION_NAME);
|
|
239
|
+
const skillsDir = path_1.default.join(extensionDir, 'skills');
|
|
240
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
241
|
+
// Write extension manifest (we own this file)
|
|
242
|
+
const manifestPath = path_1.default.join(extensionDir, 'gemini-extension.json');
|
|
243
|
+
fs_1.default.writeFileSync(manifestPath, JSON.stringify({ name: AGENTBREW_EXTENSION_NAME, version: package_json_1.default.version }, null, 2), 'utf-8');
|
|
244
|
+
// Enable extension in extension-enablement.json
|
|
245
|
+
_enableGeminiExtension(geminiDir);
|
|
246
|
+
const state = loadSyncedState(brewRoot);
|
|
247
|
+
return symlinkSkills(skills, skillsDir, state, 'gemini', brewRoot);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Removes Gemini CLI skill symlinks, the extension manifest, and the
|
|
251
|
+
* agentbrew extension entry from extension-enablement.json.
|
|
252
|
+
*/
|
|
253
|
+
function unsyncSkillsFromGeminiCLI(brewRoot) {
|
|
254
|
+
const geminiDir = path_1.default.join(os_1.default.homedir(), '.gemini');
|
|
255
|
+
const extensionDir = path_1.default.join(geminiDir, 'extensions', AGENTBREW_EXTENSION_NAME);
|
|
256
|
+
const skillsDir = path_1.default.join(extensionDir, 'skills');
|
|
257
|
+
const state = loadSyncedState(brewRoot);
|
|
258
|
+
const results = removeTrackedSymlinks(state.gemini, skillsDir);
|
|
259
|
+
// Clean up extension dir (best-effort; ignores non-empty)
|
|
260
|
+
try {
|
|
261
|
+
fs_1.default.rmSync(path_1.default.join(extensionDir, 'gemini-extension.json'), { force: true });
|
|
262
|
+
}
|
|
263
|
+
catch { }
|
|
264
|
+
try {
|
|
265
|
+
fs_1.default.rmdirSync(skillsDir);
|
|
266
|
+
}
|
|
267
|
+
catch { }
|
|
268
|
+
try {
|
|
269
|
+
fs_1.default.rmdirSync(extensionDir);
|
|
270
|
+
}
|
|
271
|
+
catch { }
|
|
272
|
+
_disableGeminiExtension(geminiDir);
|
|
273
|
+
state.gemini = [];
|
|
274
|
+
saveSyncedState(state, brewRoot);
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
function _enableGeminiExtension(geminiDir) {
|
|
278
|
+
const enablementPath = path_1.default.join(geminiDir, 'extensions', 'extension-enablement.json');
|
|
279
|
+
let data = {};
|
|
280
|
+
try {
|
|
281
|
+
data = JSON.parse(fs_1.default.readFileSync(enablementPath, 'utf-8'));
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
284
|
+
if (!data[AGENTBREW_EXTENSION_NAME]) {
|
|
285
|
+
data[AGENTBREW_EXTENSION_NAME] = { overrides: [`${os_1.default.homedir()}/*`] };
|
|
286
|
+
fs_1.default.writeFileSync(enablementPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function _disableGeminiExtension(geminiDir) {
|
|
290
|
+
const enablementPath = path_1.default.join(geminiDir, 'extensions', 'extension-enablement.json');
|
|
291
|
+
try {
|
|
292
|
+
const data = JSON.parse(fs_1.default.readFileSync(enablementPath, 'utf-8'));
|
|
293
|
+
if (AGENTBREW_EXTENSION_NAME in data) {
|
|
294
|
+
delete data[AGENTBREW_EXTENSION_NAME];
|
|
295
|
+
if (Object.keys(data).length === 0) {
|
|
296
|
+
fs_1.default.rmSync(enablementPath, { force: true });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
fs_1.default.writeFileSync(enablementPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch { }
|
|
304
|
+
}
|
|
305
|
+
// ─── Windsurf ────────────────────────────────────────────────────────────────
|
|
306
|
+
/**
|
|
307
|
+
* Symlinks each skill directory into ~/.codeium/windsurf/skills/<pkgName>-<skillName>
|
|
308
|
+
* so Windsurf can discover them.
|
|
309
|
+
*/
|
|
310
|
+
function syncSkillsToWindsurf(skills, brewRoot) {
|
|
311
|
+
const windsurfDir = path_1.default.join(os_1.default.homedir(), '.codeium', 'windsurf');
|
|
312
|
+
if (!fs_1.default.existsSync(windsurfDir))
|
|
313
|
+
return [];
|
|
314
|
+
const skillsDir = path_1.default.join(windsurfDir, 'skills');
|
|
315
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
316
|
+
const state = loadSyncedState(brewRoot);
|
|
317
|
+
return symlinkSkills(skills, skillsDir, state, 'windsurf', brewRoot);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Removes all Windsurf skill symlinks previously created by syncSkillsToWindsurf.
|
|
321
|
+
*/
|
|
322
|
+
function unsyncSkillsFromWindsurf(brewRoot) {
|
|
323
|
+
const skillsDir = path_1.default.join(os_1.default.homedir(), '.codeium', 'windsurf', 'skills');
|
|
324
|
+
const state = loadSyncedState(brewRoot);
|
|
325
|
+
const results = removeTrackedSymlinks(state.windsurf, skillsDir);
|
|
326
|
+
state.windsurf = [];
|
|
327
|
+
saveSyncedState(state, brewRoot);
|
|
328
|
+
return results;
|
|
329
|
+
}
|
|
330
|
+
// ─── Antigravity CLI ─────────────────────────────────────────────────────────
|
|
331
|
+
/**
|
|
332
|
+
* Symlinks each skill directory into ~/.gemini/antigravity-cli/skills/<pkgName>-<skillName>
|
|
333
|
+
* so Antigravity CLI can auto-discover them.
|
|
334
|
+
*/
|
|
335
|
+
function syncSkillsToAntigravityCLI(skills, brewRoot) {
|
|
336
|
+
const antigravityDir = path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity-cli');
|
|
337
|
+
if (!fs_1.default.existsSync(antigravityDir))
|
|
338
|
+
return [];
|
|
339
|
+
const skillsDir = path_1.default.join(antigravityDir, 'skills');
|
|
340
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
341
|
+
const state = loadSyncedState(brewRoot);
|
|
342
|
+
return symlinkSkills(skills, skillsDir, state, 'antigravity', brewRoot);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Removes all Antigravity CLI skill symlinks previously created by syncSkillsToAntigravityCLI.
|
|
346
|
+
*/
|
|
347
|
+
function unsyncSkillsFromAntigravityCLI(brewRoot) {
|
|
348
|
+
const skillsDir = path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity-cli', 'skills');
|
|
349
|
+
const state = loadSyncedState(brewRoot);
|
|
350
|
+
const results = removeTrackedSymlinks(state.antigravity, skillsDir);
|
|
351
|
+
state.antigravity = [];
|
|
352
|
+
saveSyncedState(state, brewRoot);
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
355
|
+
// ─── Orphan cleanup ──────────────────────────────────────────────────────────
|
|
356
|
+
/**
|
|
357
|
+
* Removes symlinks whose targets no longer exist (e.g. after a package is uninstalled).
|
|
358
|
+
* Call after `agentbrew uninstall` to prevent stale entries.
|
|
359
|
+
*/
|
|
360
|
+
function cleanOrphanSkills(brewRoot) {
|
|
361
|
+
const state = loadSyncedState(brewRoot);
|
|
362
|
+
const results = [];
|
|
363
|
+
const agentDirs = [
|
|
364
|
+
{ key: 'claude', dir: path_1.default.join(os_1.default.homedir(), '.claude', 'skills') },
|
|
365
|
+
{ key: 'gemini', dir: path_1.default.join(os_1.default.homedir(), '.gemini', 'extensions', AGENTBREW_EXTENSION_NAME, 'skills') },
|
|
366
|
+
{ key: 'windsurf', dir: path_1.default.join(os_1.default.homedir(), '.codeium', 'windsurf', 'skills') },
|
|
367
|
+
{ key: 'antigravity', dir: path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity-cli', 'skills') },
|
|
368
|
+
{ key: 'kiro', dir: path_1.default.join(os_1.default.homedir(), '.kiro', 'skills') },
|
|
369
|
+
];
|
|
370
|
+
for (const { key, dir } of agentDirs) {
|
|
371
|
+
const remaining = [];
|
|
372
|
+
for (const entryName of state[key]) {
|
|
373
|
+
const entryPath = path_1.default.join(dir, entryName);
|
|
374
|
+
let symlinkTarget = null;
|
|
375
|
+
try {
|
|
376
|
+
symlinkTarget = fs_1.default.readlinkSync(entryPath);
|
|
377
|
+
}
|
|
378
|
+
catch { }
|
|
379
|
+
if (symlinkTarget !== null && !fs_1.default.existsSync(symlinkTarget)) {
|
|
380
|
+
try {
|
|
381
|
+
fs_1.default.rmSync(entryPath, { force: true });
|
|
382
|
+
results.push({ entryName, status: 'removed', path: entryPath });
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
results.push({ entryName, status: 'error', note: e.message });
|
|
386
|
+
remaining.push(entryName);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
remaining.push(entryName);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
state[key] = remaining;
|
|
394
|
+
}
|
|
395
|
+
// Handle Cursor index file: parse referenced SKILL.md paths and remove the file if any are stale
|
|
396
|
+
if (state.cursor) {
|
|
397
|
+
const indexPath = path_1.default.join(os_1.default.homedir(), '.cursor', 'rules', CURSOR_SKILLS_INDEX_FILE);
|
|
398
|
+
let indexContent = null;
|
|
399
|
+
try {
|
|
400
|
+
indexContent = fs_1.default.readFileSync(indexPath, 'utf-8');
|
|
401
|
+
}
|
|
402
|
+
catch { }
|
|
403
|
+
if (indexContent === null) {
|
|
404
|
+
state.cursor = false;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
const pathMatches = [...indexContent.matchAll(/`([^`]+SKILL\.md)`/gi)];
|
|
408
|
+
const hasStalePath = pathMatches.some(m => !fs_1.default.existsSync(m[1]));
|
|
409
|
+
if (hasStalePath) {
|
|
410
|
+
try {
|
|
411
|
+
fs_1.default.rmSync(indexPath, { force: true });
|
|
412
|
+
state.cursor = false;
|
|
413
|
+
results.push({ entryName: CURSOR_SKILLS_INDEX_FILE, status: 'removed', path: indexPath });
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
results.push({ entryName: CURSOR_SKILLS_INDEX_FILE, status: 'error', note: e.message });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (state.codexMcp && !fs_1.default.existsSync(path_1.default.join(os_1.default.homedir(), '.codex'))) {
|
|
422
|
+
state.codexMcp = false;
|
|
423
|
+
}
|
|
424
|
+
if (state.kiroMcp && !fs_1.default.existsSync(path_1.default.join(os_1.default.homedir(), '.kiro'))) {
|
|
425
|
+
state.kiroMcp = false;
|
|
426
|
+
}
|
|
427
|
+
saveSyncedState(state, brewRoot);
|
|
428
|
+
return results;
|
|
429
|
+
}
|
|
430
|
+
// ─── Cursor MCP server registration ─────────────────────────────────────────
|
|
431
|
+
const CURSOR_MCP_ENTRY = 'agentbrew';
|
|
432
|
+
/**
|
|
433
|
+
* Adds agentbrew to ~/.cursor/mcp.json so Cursor can discover MCP tools directly.
|
|
434
|
+
* Merges into any existing config without disturbing other servers.
|
|
435
|
+
*/
|
|
436
|
+
function syncMcpServerToCursor(brewRoot) {
|
|
437
|
+
const cursorDir = path_1.default.join(os_1.default.homedir(), '.cursor');
|
|
438
|
+
if (!fs_1.default.existsSync(cursorDir))
|
|
439
|
+
return [];
|
|
440
|
+
const mcpJsonPath = path_1.default.join(cursorDir, 'mcp.json');
|
|
441
|
+
const entryName = 'agentbrew (Cursor MCP)';
|
|
442
|
+
let config = {};
|
|
443
|
+
try {
|
|
444
|
+
config = JSON.parse(fs_1.default.readFileSync(mcpJsonPath, 'utf-8'));
|
|
445
|
+
}
|
|
446
|
+
catch { }
|
|
447
|
+
const mcpServers = config.mcpServers ?? {};
|
|
448
|
+
const existing = mcpServers[CURSOR_MCP_ENTRY];
|
|
449
|
+
if (existing?.command === 'agentbrew') {
|
|
450
|
+
const state = loadSyncedState(brewRoot);
|
|
451
|
+
state.cursorMcp = true;
|
|
452
|
+
saveSyncedState(state, brewRoot);
|
|
453
|
+
return [{ entryName, status: 'already_exists', path: mcpJsonPath }];
|
|
454
|
+
}
|
|
455
|
+
config.mcpServers = { ...mcpServers, [CURSOR_MCP_ENTRY]: { command: 'agentbrew' } };
|
|
456
|
+
try {
|
|
457
|
+
fs_1.default.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
458
|
+
const state = loadSyncedState(brewRoot);
|
|
459
|
+
state.cursorMcp = true;
|
|
460
|
+
saveSyncedState(state, brewRoot);
|
|
461
|
+
return [{ entryName, status: 'linked', path: mcpJsonPath }];
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
return [{ entryName, status: 'error', note: e.message }];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Removes the agentbrew entry from ~/.cursor/mcp.json.
|
|
469
|
+
* Leaves other servers intact; removes the file only if it becomes empty.
|
|
470
|
+
*/
|
|
471
|
+
function unsyncMcpServerFromCursor(brewRoot) {
|
|
472
|
+
const state = loadSyncedState(brewRoot);
|
|
473
|
+
if (!state.cursorMcp)
|
|
474
|
+
return [];
|
|
475
|
+
const mcpJsonPath = path_1.default.join(os_1.default.homedir(), '.cursor', 'mcp.json');
|
|
476
|
+
const entryName = 'agentbrew (Cursor MCP)';
|
|
477
|
+
let config = {};
|
|
478
|
+
try {
|
|
479
|
+
config = JSON.parse(fs_1.default.readFileSync(mcpJsonPath, 'utf-8'));
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
state.cursorMcp = false;
|
|
483
|
+
saveSyncedState(state, brewRoot);
|
|
484
|
+
return [{ entryName, status: 'skipped', note: 'Not found' }];
|
|
485
|
+
}
|
|
486
|
+
if (!config.mcpServers?.[CURSOR_MCP_ENTRY]) {
|
|
487
|
+
state.cursorMcp = false;
|
|
488
|
+
saveSyncedState(state, brewRoot);
|
|
489
|
+
return [{ entryName, status: 'skipped', note: 'Not found' }];
|
|
490
|
+
}
|
|
491
|
+
delete config.mcpServers[CURSOR_MCP_ENTRY];
|
|
492
|
+
if (Object.keys(config.mcpServers).length === 0)
|
|
493
|
+
delete config.mcpServers;
|
|
494
|
+
try {
|
|
495
|
+
if (Object.keys(config).length === 0) {
|
|
496
|
+
fs_1.default.rmSync(mcpJsonPath, { force: true });
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
fs_1.default.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
500
|
+
}
|
|
501
|
+
state.cursorMcp = false;
|
|
502
|
+
saveSyncedState(state, brewRoot);
|
|
503
|
+
return [{ entryName, status: 'removed', path: mcpJsonPath }];
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
return [{ entryName, status: 'error', note: e.message }];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// ─── Codex MCP server registration ──────────────────────────────────────────
|
|
510
|
+
function _removeTomlSection(content, sectionHeader) {
|
|
511
|
+
const lines = content.split('\n');
|
|
512
|
+
const headerLine = `[${sectionHeader}]`;
|
|
513
|
+
const startIdx = lines.findIndex(l => l.trim() === headerLine);
|
|
514
|
+
if (startIdx === -1)
|
|
515
|
+
return content;
|
|
516
|
+
let endIdx = lines.length;
|
|
517
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
518
|
+
if (lines[i].trim().startsWith('[')) {
|
|
519
|
+
endIdx = i;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Also absorb a preceding blank line
|
|
524
|
+
let removeFrom = startIdx;
|
|
525
|
+
if (removeFrom > 0 && lines[removeFrom - 1].trim() === '')
|
|
526
|
+
removeFrom--;
|
|
527
|
+
return [...lines.slice(0, removeFrom), ...lines.slice(endIdx)].join('\n');
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Adds agentbrew to ~/.codex/config.toml so Codex CLI can discover MCP tools directly.
|
|
531
|
+
* Appends the [mcp_servers.agentbrew] section without disturbing existing content.
|
|
532
|
+
*/
|
|
533
|
+
function syncMcpServerToCodex(brewRoot) {
|
|
534
|
+
const codexDir = path_1.default.join(os_1.default.homedir(), '.codex');
|
|
535
|
+
if (!fs_1.default.existsSync(codexDir))
|
|
536
|
+
return [];
|
|
537
|
+
const configPath = path_1.default.join(codexDir, 'config.toml');
|
|
538
|
+
const entryName = 'agentbrew (Codex MCP)';
|
|
539
|
+
let raw = '';
|
|
540
|
+
try {
|
|
541
|
+
raw = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
542
|
+
}
|
|
543
|
+
catch { }
|
|
544
|
+
// Check if already registered with the right command
|
|
545
|
+
try {
|
|
546
|
+
const parsed = toml.parse(raw);
|
|
547
|
+
if (parsed?.mcp_servers?.agentbrew?.command === 'agentbrew') {
|
|
548
|
+
const state = loadSyncedState(brewRoot);
|
|
549
|
+
state.codexMcp = true;
|
|
550
|
+
saveSyncedState(state, brewRoot);
|
|
551
|
+
return [{ entryName, status: 'already_exists', path: configPath }];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch { }
|
|
555
|
+
// Remove any stale entry (e.g. wrong command) before re-adding to avoid duplicate TOML table headers
|
|
556
|
+
const cleaned = _removeTomlSection(raw, 'mcp_servers.agentbrew');
|
|
557
|
+
const sep = cleaned.length > 0 && !cleaned.endsWith('\n') ? '\n' : '';
|
|
558
|
+
const newContent = cleaned + sep + '\n[mcp_servers.agentbrew]\ncommand = "agentbrew"\n';
|
|
559
|
+
try {
|
|
560
|
+
fs_1.default.writeFileSync(configPath, newContent, 'utf-8');
|
|
561
|
+
const state = loadSyncedState(brewRoot);
|
|
562
|
+
state.codexMcp = true;
|
|
563
|
+
saveSyncedState(state, brewRoot);
|
|
564
|
+
return [{ entryName, status: 'linked', path: configPath }];
|
|
565
|
+
}
|
|
566
|
+
catch (e) {
|
|
567
|
+
return [{ entryName, status: 'error', note: e.message }];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Removes the agentbrew entry from ~/.codex/config.toml.
|
|
572
|
+
* Leaves the file in place; other sections are untouched.
|
|
573
|
+
*/
|
|
574
|
+
function unsyncMcpServerFromCodex(brewRoot) {
|
|
575
|
+
const state = loadSyncedState(brewRoot);
|
|
576
|
+
if (!state.codexMcp)
|
|
577
|
+
return [];
|
|
578
|
+
const configPath = path_1.default.join(os_1.default.homedir(), '.codex', 'config.toml');
|
|
579
|
+
const entryName = 'agentbrew (Codex MCP)';
|
|
580
|
+
let raw;
|
|
581
|
+
try {
|
|
582
|
+
raw = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
state.codexMcp = false;
|
|
586
|
+
saveSyncedState(state, brewRoot);
|
|
587
|
+
return [{ entryName, status: 'skipped', note: 'Not found' }];
|
|
588
|
+
}
|
|
589
|
+
const cleaned = _removeTomlSection(raw, 'mcp_servers.agentbrew');
|
|
590
|
+
if (cleaned === raw) {
|
|
591
|
+
state.codexMcp = false;
|
|
592
|
+
saveSyncedState(state, brewRoot);
|
|
593
|
+
return [{ entryName, status: 'skipped', note: 'Not found' }];
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const trimmed = cleaned.trimEnd();
|
|
597
|
+
fs_1.default.writeFileSync(configPath, trimmed ? trimmed + '\n' : '', 'utf-8');
|
|
598
|
+
state.codexMcp = false;
|
|
599
|
+
saveSyncedState(state, brewRoot);
|
|
600
|
+
return [{ entryName, status: 'removed', path: configPath }];
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
return [{ entryName, status: 'error', note: e.message }];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// ─── Cursor ──────────────────────────────────────────────────────────────────
|
|
607
|
+
function buildCursorSkillsIndex(skills) {
|
|
608
|
+
const lines = [
|
|
609
|
+
'---',
|
|
610
|
+
'description: AgentBrew skills index — reference when asked about available skills, tools, or capabilities',
|
|
611
|
+
'alwaysApply: false',
|
|
612
|
+
'---',
|
|
613
|
+
'<!-- Managed by AgentBrew. Run `agentbrew sync` to update. Do not edit manually. -->',
|
|
614
|
+
'',
|
|
615
|
+
'# AgentBrew Skills',
|
|
616
|
+
'',
|
|
617
|
+
'Skills installed via AgentBrew. Read a SKILL.md file to learn how to invoke it.',
|
|
618
|
+
'',
|
|
619
|
+
];
|
|
620
|
+
for (const skill of skills) {
|
|
621
|
+
const desc = skill.description ? ` — ${skill.description}` : '';
|
|
622
|
+
lines.push(`- **${skill.packageName}/${skill.skillName}**${desc}: \`${path_1.default.join(skill.skillDir, 'SKILL.md')}\``);
|
|
623
|
+
}
|
|
624
|
+
return lines.join('\n') + '\n';
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Writes a single skills index file to ~/.cursor/rules/agentbrew-skills-index.md
|
|
628
|
+
* listing all AgentBrew skills with paths for on-demand discovery.
|
|
629
|
+
* Does NOT copy individual SKILL.md files — Cursor rules are always-on context.
|
|
630
|
+
*/
|
|
631
|
+
function syncSkillsToCursor(skills, brewRoot) {
|
|
632
|
+
const cursorDir = path_1.default.join(os_1.default.homedir(), '.cursor');
|
|
633
|
+
if (!fs_1.default.existsSync(cursorDir))
|
|
634
|
+
return [];
|
|
635
|
+
if (skills.length === 0)
|
|
636
|
+
return unsyncSkillsFromCursor(brewRoot);
|
|
637
|
+
const rulesDir = path_1.default.join(cursorDir, 'rules');
|
|
638
|
+
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
639
|
+
const indexPath = path_1.default.join(rulesDir, CURSOR_SKILLS_INDEX_FILE);
|
|
640
|
+
const content = buildCursorSkillsIndex(skills);
|
|
641
|
+
const state = loadSyncedState(brewRoot);
|
|
642
|
+
try {
|
|
643
|
+
fs_1.default.writeFileSync(indexPath, content, 'utf-8');
|
|
644
|
+
state.cursor = true;
|
|
645
|
+
saveSyncedState(state, brewRoot);
|
|
646
|
+
return [{ entryName: CURSOR_SKILLS_INDEX_FILE, status: 'linked', path: indexPath }];
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
return [{ entryName: CURSOR_SKILLS_INDEX_FILE, status: 'error', note: e.message }];
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Removes the AgentBrew skills index file from ~/.cursor/rules/.
|
|
654
|
+
*/
|
|
655
|
+
function unsyncSkillsFromCursor(brewRoot) {
|
|
656
|
+
const state = loadSyncedState(brewRoot);
|
|
657
|
+
if (!state.cursor)
|
|
658
|
+
return [];
|
|
659
|
+
const indexPath = path_1.default.join(os_1.default.homedir(), '.cursor', 'rules', CURSOR_SKILLS_INDEX_FILE);
|
|
660
|
+
let exists = false;
|
|
661
|
+
try {
|
|
662
|
+
fs_1.default.lstatSync(indexPath);
|
|
663
|
+
exists = true;
|
|
664
|
+
}
|
|
665
|
+
catch { }
|
|
666
|
+
if (!exists) {
|
|
667
|
+
state.cursor = false;
|
|
668
|
+
saveSyncedState(state, brewRoot);
|
|
669
|
+
return [{ entryName: CURSOR_SKILLS_INDEX_FILE, status: 'skipped', note: 'Not found' }];
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
fs_1.default.rmSync(indexPath, { force: true });
|
|
673
|
+
state.cursor = false;
|
|
674
|
+
saveSyncedState(state, brewRoot);
|
|
675
|
+
return [{ entryName: CURSOR_SKILLS_INDEX_FILE, status: 'removed', path: indexPath }];
|
|
676
|
+
}
|
|
677
|
+
catch (e) {
|
|
678
|
+
return [{ entryName: CURSOR_SKILLS_INDEX_FILE, status: 'error', note: e.message }];
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// ─── Kiro ────────────────────────────────────────────────────────────────────
|
|
682
|
+
/**
|
|
683
|
+
* Symlinks each skill directory into ~/.kiro/skills/<pkgName>-<skillName>
|
|
684
|
+
* so Kiro can discover them as invocable skills.
|
|
685
|
+
*/
|
|
686
|
+
function syncSkillsToKiro(skills, brewRoot) {
|
|
687
|
+
const kiroDir = path_1.default.join(os_1.default.homedir(), '.kiro');
|
|
688
|
+
if (!fs_1.default.existsSync(kiroDir))
|
|
689
|
+
return [];
|
|
690
|
+
const skillsDir = path_1.default.join(kiroDir, 'skills');
|
|
691
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
692
|
+
const state = loadSyncedState(brewRoot);
|
|
693
|
+
return symlinkSkills(skills, skillsDir, state, 'kiro', brewRoot);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Removes all Kiro skill symlinks previously created by syncSkillsToKiro.
|
|
697
|
+
*/
|
|
698
|
+
function unsyncSkillsFromKiro(brewRoot) {
|
|
699
|
+
const skillsDir = path_1.default.join(os_1.default.homedir(), '.kiro', 'skills');
|
|
700
|
+
const state = loadSyncedState(brewRoot);
|
|
701
|
+
const results = removeTrackedSymlinks(state.kiro, skillsDir);
|
|
702
|
+
state.kiro = [];
|
|
703
|
+
saveSyncedState(state, brewRoot);
|
|
704
|
+
return results;
|
|
705
|
+
}
|
|
706
|
+
// ─── Kiro MCP server registration ────────────────────────────────────────────
|
|
707
|
+
const KIRO_MCP_ENTRY = 'agentbrew';
|
|
708
|
+
/**
|
|
709
|
+
* Adds agentbrew to ~/.kiro/settings/mcp.json so Kiro can discover MCP tools directly.
|
|
710
|
+
* Merges into any existing config without disturbing other servers.
|
|
711
|
+
*/
|
|
712
|
+
function syncMcpServerToKiro(brewRoot) {
|
|
713
|
+
const kiroDir = path_1.default.join(os_1.default.homedir(), '.kiro');
|
|
714
|
+
if (!fs_1.default.existsSync(kiroDir))
|
|
715
|
+
return [];
|
|
716
|
+
const settingsDir = path_1.default.join(kiroDir, 'settings');
|
|
717
|
+
fs_1.default.mkdirSync(settingsDir, { recursive: true });
|
|
718
|
+
const mcpJsonPath = path_1.default.join(settingsDir, 'mcp.json');
|
|
719
|
+
const entryName = 'agentbrew (Kiro MCP)';
|
|
720
|
+
let config = {};
|
|
721
|
+
try {
|
|
722
|
+
config = JSON.parse(fs_1.default.readFileSync(mcpJsonPath, 'utf-8'));
|
|
723
|
+
}
|
|
724
|
+
catch { }
|
|
725
|
+
const mcpServers = config.mcpServers ?? {};
|
|
726
|
+
const existing = mcpServers[KIRO_MCP_ENTRY];
|
|
727
|
+
if (existing?.command === 'agentbrew') {
|
|
728
|
+
const state = loadSyncedState(brewRoot);
|
|
729
|
+
state.kiroMcp = true;
|
|
730
|
+
saveSyncedState(state, brewRoot);
|
|
731
|
+
return [{ entryName, status: 'already_exists', path: mcpJsonPath }];
|
|
732
|
+
}
|
|
733
|
+
config.mcpServers = { ...mcpServers, [KIRO_MCP_ENTRY]: { command: 'agentbrew' } };
|
|
734
|
+
try {
|
|
735
|
+
fs_1.default.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
736
|
+
const state = loadSyncedState(brewRoot);
|
|
737
|
+
state.kiroMcp = true;
|
|
738
|
+
saveSyncedState(state, brewRoot);
|
|
739
|
+
return [{ entryName, status: 'linked', path: mcpJsonPath }];
|
|
740
|
+
}
|
|
741
|
+
catch (e) {
|
|
742
|
+
return [{ entryName, status: 'error', note: e.message }];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Removes the agentbrew entry from ~/.kiro/settings/mcp.json.
|
|
747
|
+
* Leaves other servers intact; removes the file only if it becomes empty.
|
|
748
|
+
*/
|
|
749
|
+
function unsyncMcpServerFromKiro(brewRoot) {
|
|
750
|
+
const state = loadSyncedState(brewRoot);
|
|
751
|
+
if (!state.kiroMcp)
|
|
752
|
+
return [];
|
|
753
|
+
const mcpJsonPath = path_1.default.join(os_1.default.homedir(), '.kiro', 'settings', 'mcp.json');
|
|
754
|
+
const entryName = 'agentbrew (Kiro MCP)';
|
|
755
|
+
let config = {};
|
|
756
|
+
try {
|
|
757
|
+
config = JSON.parse(fs_1.default.readFileSync(mcpJsonPath, 'utf-8'));
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
state.kiroMcp = false;
|
|
761
|
+
saveSyncedState(state, brewRoot);
|
|
762
|
+
return [{ entryName, status: 'skipped', note: 'Not found' }];
|
|
763
|
+
}
|
|
764
|
+
if (!config.mcpServers?.[KIRO_MCP_ENTRY]) {
|
|
765
|
+
state.kiroMcp = false;
|
|
766
|
+
saveSyncedState(state, brewRoot);
|
|
767
|
+
return [{ entryName, status: 'skipped', note: 'Not found' }];
|
|
768
|
+
}
|
|
769
|
+
delete config.mcpServers[KIRO_MCP_ENTRY];
|
|
770
|
+
if (Object.keys(config.mcpServers).length === 0)
|
|
771
|
+
delete config.mcpServers;
|
|
772
|
+
try {
|
|
773
|
+
if (Object.keys(config).length === 0) {
|
|
774
|
+
fs_1.default.rmSync(mcpJsonPath, { force: true });
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
fs_1.default.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
778
|
+
}
|
|
779
|
+
state.kiroMcp = false;
|
|
780
|
+
saveSyncedState(state, brewRoot);
|
|
781
|
+
return [{ entryName, status: 'removed', path: mcpJsonPath }];
|
|
782
|
+
}
|
|
783
|
+
catch (e) {
|
|
784
|
+
return [{ entryName, status: 'error', note: e.message }];
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// ─── Instruction sync (unchanged) ───────────────────────────────────────────
|
|
18
788
|
exports.MARKER_START = '<!-- agentbrew:shared:start -->';
|
|
19
789
|
exports.MARKER_END = '<!-- agentbrew:shared:end -->';
|
|
20
790
|
exports.INSTRUCTIONS_FILE = 'INSTRUCTIONS.md';
|
|
@@ -57,12 +827,22 @@ function getDefaultTargets() {
|
|
|
57
827
|
// We own this specific file entirely — no markers needed.
|
|
58
828
|
configPath: path_1.default.join(home, '.cursor', 'rules', 'agentbrew-shared.md'),
|
|
59
829
|
isFileOwned: true,
|
|
830
|
+
agentRootDir: path_1.default.join(home, '.cursor'),
|
|
60
831
|
},
|
|
61
832
|
{
|
|
62
833
|
name: 'Windsurf',
|
|
63
834
|
configPath: path_1.default.join(home, '.codeium', 'windsurf', 'memories', 'global_rules.md'),
|
|
64
835
|
isFileOwned: false,
|
|
65
836
|
},
|
|
837
|
+
{
|
|
838
|
+
name: 'Kiro',
|
|
839
|
+
// Steering files in ~/.kiro/steering/ are auto-loaded in every Kiro interaction.
|
|
840
|
+
// We own this file entirely and must place the frontmatter at the very top.
|
|
841
|
+
configPath: path_1.default.join(home, '.kiro', 'steering', 'agentbrew-shared.md'),
|
|
842
|
+
isFileOwned: true,
|
|
843
|
+
frontmatter: '---\ninclusion: always\n---',
|
|
844
|
+
agentRootDir: path_1.default.join(home, '.kiro'),
|
|
845
|
+
},
|
|
66
846
|
];
|
|
67
847
|
}
|
|
68
848
|
function getInstructionsPath(brewRoot) {
|
|
@@ -140,10 +920,17 @@ function syncInstructions(targets, brewRoot) {
|
|
|
140
920
|
continue;
|
|
141
921
|
}
|
|
142
922
|
if (target.isFileOwned) {
|
|
923
|
+
// Skip if the agent's root directory doesn't exist (agent not installed).
|
|
924
|
+
// Without this guard, mkdirSync below would create the directory unconditionally.
|
|
925
|
+
if (target.agentRootDir && !fs_1.default.existsSync(target.agentRootDir)) {
|
|
926
|
+
results.push({ agent: target.name, status: 'skipped', note: 'Agent not installed (config directory not found)' });
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
143
929
|
// We own this file entirely — write raw content, no markers needed.
|
|
144
930
|
// unsync deletes the file; there is no surrounding user content to delimit around.
|
|
145
931
|
const header = `> ⚠️ Managed by AgentBrew. Edit \`~/.agentbrew/INSTRUCTIONS.md\` and run \`agentbrew sync\` to update.\n`;
|
|
146
|
-
const
|
|
932
|
+
const prefix = target.frontmatter ? target.frontmatter + '\n' : '';
|
|
933
|
+
const fileContent = prefix + header + '\n' + content.trim() + '\n';
|
|
147
934
|
fs_1.default.mkdirSync(path_1.default.dirname(target.configPath), { recursive: true });
|
|
148
935
|
const existing = fs_1.default.existsSync(target.configPath)
|
|
149
936
|
? fs_1.default.readFileSync(target.configPath, 'utf-8')
|