brainctl 0.1.16 → 0.1.18
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/dist/cli.d.ts +4 -6
- package/dist/cli.js +11 -16
- package/dist/commands/profile.d.ts +4 -0
- package/dist/commands/profile.js +106 -16
- package/dist/commands/status.js +7 -7
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +85 -154
- package/dist/services/agent-asset-installer.d.ts +3 -0
- package/dist/services/agent-asset-installer.js +109 -0
- package/dist/services/agent-availability-service.d.ts +11 -0
- package/dist/services/agent-availability-service.js +32 -0
- package/dist/services/credential-redaction-service.d.ts +1 -0
- package/dist/services/credential-redaction-service.js +9 -3
- package/dist/services/doctor-service.d.ts +2 -2
- package/dist/services/doctor-service.js +7 -63
- package/dist/services/portable-profile-pack-service.d.ts +6 -0
- package/dist/services/portable-profile-pack-service.js +78 -4
- package/dist/services/profile-apply-service.d.ts +34 -0
- package/dist/services/profile-apply-service.js +102 -0
- package/dist/services/profile-export-service.d.ts +5 -1
- package/dist/services/profile-export-service.js +3 -1
- package/dist/services/profile-import-service.js +82 -127
- package/dist/services/profile-service.d.ts +3 -11
- package/dist/services/profile-service.js +57 -102
- package/dist/services/profile-snapshot-service.d.ts +12 -0
- package/dist/services/profile-snapshot-service.js +47 -0
- package/dist/services/status-service.d.ts +9 -7
- package/dist/services/status-service.js +14 -13
- package/dist/types.d.ts +2 -57
- package/dist/ui/routes.d.ts +0 -2
- package/dist/ui/routes.js +71 -120
- package/dist/web/assets/index-CGmTbSgk.js +63 -0
- package/dist/web/assets/index-EIVU5Woh.css +2 -0
- package/dist/web/brainctl-mark.svg +13 -0
- package/dist/web/index.html +2 -5
- package/package.json +2 -1
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +0 -27
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -25
- package/dist/commands/sync.d.ts +0 -3
- package/dist/commands/sync.js +0 -31
- package/dist/config.d.ts +0 -14
- package/dist/config.js +0 -96
- package/dist/context/builder.d.ts +0 -6
- package/dist/context/builder.js +0 -13
- package/dist/context/memory.d.ts +0 -5
- package/dist/context/memory.js +0 -43
- package/dist/context/skills.d.ts +0 -2
- package/dist/context/skills.js +0 -8
- package/dist/executor/claude.d.ts +0 -12
- package/dist/executor/claude.js +0 -16
- package/dist/executor/codex.d.ts +0 -12
- package/dist/executor/codex.js +0 -16
- package/dist/executor/process.d.ts +0 -11
- package/dist/executor/process.js +0 -40
- package/dist/executor/resolver.d.ts +0 -13
- package/dist/executor/resolver.js +0 -60
- package/dist/executor/types.d.ts +0 -14
- package/dist/executor/types.js +0 -1
- package/dist/services/config-write-service.d.ts +0 -12
- package/dist/services/config-write-service.js +0 -70
- package/dist/services/init-service.d.ts +0 -14
- package/dist/services/init-service.js +0 -88
- package/dist/services/memory-write-service.d.ts +0 -12
- package/dist/services/memory-write-service.js +0 -56
- package/dist/services/run-service.d.ts +0 -15
- package/dist/services/run-service.js +0 -94
- package/dist/services/sync-service.d.ts +0 -15
- package/dist/services/sync-service.js +0 -69
- package/dist/ui/streaming.d.ts +0 -3
- package/dist/ui/streaming.js +0 -16
- package/dist/web/assets/index-CuNIAQ7N.js +0 -65
- package/dist/web/assets/index-Ow6x3bQk.css +0 -2
package/dist/mcp/server.js
CHANGED
|
@@ -1,74 +1,25 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
3
|
import { FastMCP } from 'fastmcp';
|
|
5
4
|
import { z } from 'zod';
|
|
6
|
-
import { loadConfig } from '../config.js';
|
|
7
|
-
import { loadMemory } from '../context/memory.js';
|
|
8
5
|
import { createAgentConfigService } from '../services/agent-config-service.js';
|
|
9
6
|
import { createDoctorService } from '../services/doctor-service.js';
|
|
10
7
|
import { startUiServer } from '../ui/server.js';
|
|
11
|
-
import { createMemoryWriteService } from '../services/memory-write-service.js';
|
|
12
8
|
import { createProfileExportService } from '../services/profile-export-service.js';
|
|
13
9
|
import { createProfileImportService } from '../services/profile-import-service.js';
|
|
10
|
+
import { createProfileApplyService } from '../services/profile-apply-service.js';
|
|
14
11
|
import { createProfileService } from '../services/profile-service.js';
|
|
15
|
-
import {
|
|
12
|
+
import { createProfileSnapshotService, defaultBackupProfileName, } from '../services/profile-snapshot-service.js';
|
|
16
13
|
import { createStatusService } from '../services/status-service.js';
|
|
17
|
-
|
|
14
|
+
const ALL_AGENTS = ['claude', 'codex', 'gemini'];
|
|
18
15
|
const packageVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
19
16
|
export function createMcpServer(options = {}) {
|
|
20
17
|
const cwd = options.cwd ?? process.cwd();
|
|
18
|
+
const uiState = options.uiServerState ?? { current: null };
|
|
21
19
|
const server = new FastMCP({
|
|
22
20
|
name: 'brainctl',
|
|
23
21
|
version: packageVersion.version,
|
|
24
22
|
});
|
|
25
|
-
server.addTool({
|
|
26
|
-
name: 'brainctl_list_skills',
|
|
27
|
-
description: 'List available skills from the ai-stack.yaml config',
|
|
28
|
-
parameters: z.object({}),
|
|
29
|
-
execute: async () => {
|
|
30
|
-
const config = await loadConfig({ cwd });
|
|
31
|
-
const skills = Object.entries(config.skills).map(([name, skill]) => ({
|
|
32
|
-
name,
|
|
33
|
-
description: skill.description ?? null,
|
|
34
|
-
}));
|
|
35
|
-
return JSON.stringify(skills, null, 2);
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
server.addTool({
|
|
39
|
-
name: 'brainctl_run',
|
|
40
|
-
description: 'Execute a skill with input text. Runs the skill through the configured agent and returns the output.',
|
|
41
|
-
parameters: z.object({
|
|
42
|
-
skill: z.string().describe('Skill name as defined in ai-stack.yaml'),
|
|
43
|
-
input: z.string().describe('Input text to pass to the skill'),
|
|
44
|
-
agent: z.enum(['claude', 'codex']).default('claude').describe('Agent to use for execution'),
|
|
45
|
-
fallback_agent: z.enum(['claude', 'codex']).optional().describe('Fallback agent if primary is unavailable'),
|
|
46
|
-
}),
|
|
47
|
-
execute: async (args) => {
|
|
48
|
-
const inputPath = path.join(cwd, `.brainctl-mcp-input-${Date.now()}.tmp`);
|
|
49
|
-
const { writeFile: writeFileAsync, unlink } = await import('node:fs/promises');
|
|
50
|
-
try {
|
|
51
|
-
await writeFileAsync(inputPath, args.input, 'utf8');
|
|
52
|
-
const runService = createRunService();
|
|
53
|
-
const trace = await runService.execute({
|
|
54
|
-
cwd,
|
|
55
|
-
skill: args.skill,
|
|
56
|
-
inputFile: path.basename(inputPath),
|
|
57
|
-
primaryAgent: args.agent,
|
|
58
|
-
fallbackAgent: args.fallback_agent,
|
|
59
|
-
});
|
|
60
|
-
return trace.finalOutput;
|
|
61
|
-
}
|
|
62
|
-
finally {
|
|
63
|
-
try {
|
|
64
|
-
await unlink(inputPath);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// temp file cleanup is best-effort
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
23
|
server.addTool({
|
|
73
24
|
name: 'brainctl_status',
|
|
74
25
|
description: 'Show project status: config path, memory files, available skills, and agent availability',
|
|
@@ -90,94 +41,67 @@ export function createMcpServer(options = {}) {
|
|
|
90
41
|
},
|
|
91
42
|
});
|
|
92
43
|
server.addTool({
|
|
93
|
-
name: '
|
|
94
|
-
description: '
|
|
44
|
+
name: 'brainctl_list_profiles',
|
|
45
|
+
description: 'List available profiles and show which one is active.',
|
|
95
46
|
parameters: z.object({}),
|
|
96
47
|
execute: async () => {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const result = {
|
|
100
|
-
count: memory.count,
|
|
101
|
-
files: memory.entries.map((entry) => ({
|
|
102
|
-
path: entry.path,
|
|
103
|
-
content: entry.content,
|
|
104
|
-
})),
|
|
105
|
-
};
|
|
48
|
+
const profileService = createProfileService();
|
|
49
|
+
const result = await profileService.list({ cwd });
|
|
106
50
|
return JSON.stringify(result, null, 2);
|
|
107
51
|
},
|
|
108
52
|
});
|
|
109
53
|
server.addTool({
|
|
110
|
-
name: '
|
|
111
|
-
description: '
|
|
54
|
+
name: 'brainctl_apply_profile',
|
|
55
|
+
description: 'Apply a profile (MCPs, plugins, user skills) to the specified agents. Selective by --agents and --items. Auto-backs up live agent state before a full apply unless backup=false.',
|
|
112
56
|
parameters: z.object({
|
|
113
|
-
|
|
114
|
-
|
|
57
|
+
name: z.string().describe('Profile name to apply'),
|
|
58
|
+
agents: z
|
|
59
|
+
.array(z.enum(['claude', 'codex', 'gemini']))
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Agents to target (default: all three)'),
|
|
62
|
+
items: z
|
|
63
|
+
.array(z.object({
|
|
64
|
+
type: z.enum(['mcp', 'plugin', 'skill']),
|
|
65
|
+
name: z.string(),
|
|
66
|
+
}))
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Specific items to apply (default: everything matching)'),
|
|
69
|
+
backup: z
|
|
70
|
+
.boolean()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('Force backup on/off (default: on for full apply, off for partial)'),
|
|
115
73
|
}),
|
|
116
74
|
execute: async (args) => {
|
|
117
|
-
const
|
|
118
|
-
const result = await
|
|
75
|
+
const applyService = createProfileApplyService();
|
|
76
|
+
const result = await applyService.execute({
|
|
119
77
|
cwd,
|
|
120
|
-
|
|
121
|
-
|
|
78
|
+
profileName: args.name,
|
|
79
|
+
agents: args.agents ?? ALL_AGENTS,
|
|
80
|
+
items: args.items,
|
|
81
|
+
backup: args.backup,
|
|
122
82
|
});
|
|
123
|
-
return JSON.stringify({ written: result.filePath });
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
server.addTool({
|
|
127
|
-
name: 'brainctl_get_skill',
|
|
128
|
-
description: 'Get the full details of a specific skill including its prompt text and description. Use this to understand what a skill does before running it.',
|
|
129
|
-
parameters: z.object({
|
|
130
|
-
skill: z.string().describe('Skill name as defined in ai-stack.yaml'),
|
|
131
|
-
}),
|
|
132
|
-
execute: async (args) => {
|
|
133
|
-
const config = await loadConfig({ cwd });
|
|
134
|
-
const skillConfig = config.skills[args.skill];
|
|
135
|
-
if (!skillConfig) {
|
|
136
|
-
throw new Error(`Skill "${args.skill}" is not defined in ai-stack.yaml.`);
|
|
137
|
-
}
|
|
138
|
-
return JSON.stringify({
|
|
139
|
-
name: args.skill,
|
|
140
|
-
description: skillConfig.description ?? null,
|
|
141
|
-
prompt: skillConfig.prompt,
|
|
142
|
-
}, null, 2);
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
server.addTool({
|
|
146
|
-
name: 'brainctl_list_profiles',
|
|
147
|
-
description: 'List available profiles and show which one is active.',
|
|
148
|
-
parameters: z.object({}),
|
|
149
|
-
execute: async () => {
|
|
150
|
-
const profileService = createProfileService();
|
|
151
|
-
const result = await profileService.list({ cwd });
|
|
152
83
|
return JSON.stringify(result, null, 2);
|
|
153
84
|
},
|
|
154
85
|
});
|
|
155
86
|
server.addTool({
|
|
156
|
-
name: '
|
|
157
|
-
description: '
|
|
87
|
+
name: 'brainctl_snapshot_agent',
|
|
88
|
+
description: "Snapshot a live agent's MCPs+plugins+skills into a new profile folder. Useful for backups or capturing your current setup as a shareable profile.",
|
|
158
89
|
parameters: z.object({
|
|
159
|
-
|
|
90
|
+
agent: z.enum(['claude', 'codex', 'gemini']),
|
|
91
|
+
as: z
|
|
92
|
+
.string()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('Profile name to write into (default: backup-<agent>-<timestamp>)'),
|
|
160
95
|
}),
|
|
161
96
|
execute: async (args) => {
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}, null, 2);
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
server.addTool({
|
|
174
|
-
name: 'brainctl_sync',
|
|
175
|
-
description: 'Sync the active profile to all configured agent configs (Claude, Codex). Creates backups before overwriting.',
|
|
176
|
-
parameters: z.object({}),
|
|
177
|
-
execute: async () => {
|
|
178
|
-
const syncService = createSyncService();
|
|
179
|
-
const result = await syncService.execute({ cwd });
|
|
180
|
-
return JSON.stringify(result, null, 2);
|
|
97
|
+
const snapshotService = createProfileSnapshotService();
|
|
98
|
+
const profileName = args.as ?? defaultBackupProfileName(args.agent);
|
|
99
|
+
const result = await snapshotService.execute({
|
|
100
|
+
cwd,
|
|
101
|
+
agent: args.agent,
|
|
102
|
+
profileName,
|
|
103
|
+
});
|
|
104
|
+
return JSON.stringify({ profileName, ...result }, null, 2);
|
|
181
105
|
},
|
|
182
106
|
});
|
|
183
107
|
server.addTool({
|
|
@@ -211,20 +135,13 @@ export function createMcpServer(options = {}) {
|
|
|
211
135
|
});
|
|
212
136
|
server.addTool({
|
|
213
137
|
name: 'brainctl_update_profile',
|
|
214
|
-
description: 'Update a profile config. Pass the full profile object with
|
|
138
|
+
description: 'Update a profile config. Pass the full profile object with name, optional description, and mcps map.',
|
|
215
139
|
parameters: z.object({
|
|
216
140
|
name: z.string().describe('Profile name to update'),
|
|
217
141
|
config: z.object({
|
|
218
142
|
name: z.string(),
|
|
219
143
|
description: z.string().optional(),
|
|
220
|
-
skills: z.record(z.string(), z.object({
|
|
221
|
-
description: z.string().optional(),
|
|
222
|
-
prompt: z.string(),
|
|
223
|
-
})),
|
|
224
144
|
mcps: z.record(z.string(), z.unknown()),
|
|
225
|
-
memory: z.object({
|
|
226
|
-
paths: z.array(z.string()),
|
|
227
|
-
}),
|
|
228
145
|
}).describe('Full profile config object'),
|
|
229
146
|
}),
|
|
230
147
|
execute: async (args) => {
|
|
@@ -251,25 +168,17 @@ export function createMcpServer(options = {}) {
|
|
|
251
168
|
});
|
|
252
169
|
server.addTool({
|
|
253
170
|
name: 'brainctl_copy_profile_items',
|
|
254
|
-
description: 'Copy
|
|
171
|
+
description: 'Copy MCPs from one profile to another. Existing MCPs with the same key in the target are overwritten.',
|
|
255
172
|
parameters: z.object({
|
|
256
173
|
source: z.string().describe('Source profile name'),
|
|
257
174
|
target: z.string().describe('Target profile name'),
|
|
258
|
-
skills: z.array(z.string()).default([]).describe('Skill keys to copy'),
|
|
259
175
|
mcps: z.array(z.string()).default([]).describe('MCP keys to copy'),
|
|
260
176
|
}),
|
|
261
177
|
execute: async (args) => {
|
|
262
178
|
const profileService = createProfileService();
|
|
263
179
|
const sourceProfile = await profileService.get({ cwd, name: args.source });
|
|
264
180
|
const targetProfile = await profileService.get({ cwd, name: args.target });
|
|
265
|
-
const copiedSkills = [];
|
|
266
181
|
const copiedMcps = [];
|
|
267
|
-
for (const key of args.skills) {
|
|
268
|
-
if (sourceProfile.skills[key]) {
|
|
269
|
-
targetProfile.skills[key] = sourceProfile.skills[key];
|
|
270
|
-
copiedSkills.push(key);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
182
|
for (const key of args.mcps) {
|
|
274
183
|
if (sourceProfile.mcps[key]) {
|
|
275
184
|
targetProfile.mcps[key] = sourceProfile.mcps[key];
|
|
@@ -280,7 +189,6 @@ export function createMcpServer(options = {}) {
|
|
|
280
189
|
return JSON.stringify({
|
|
281
190
|
source: args.source,
|
|
282
191
|
target: args.target,
|
|
283
|
-
copiedSkills,
|
|
284
192
|
copiedMcps,
|
|
285
193
|
}, null, 2);
|
|
286
194
|
},
|
|
@@ -327,33 +235,32 @@ export function createMcpServer(options = {}) {
|
|
|
327
235
|
return JSON.stringify(result, null, 2);
|
|
328
236
|
},
|
|
329
237
|
});
|
|
330
|
-
let uiServerInstance = null;
|
|
331
238
|
server.addTool({
|
|
332
239
|
name: 'brainctl_open_ui',
|
|
333
|
-
description: '
|
|
240
|
+
description: 'Open the brainctl web dashboard in the default browser. The dashboard auto-starts with the MCP server; this tool just opens the URL. Starts the server on demand if auto-start was skipped or failed.',
|
|
334
241
|
parameters: z.object({
|
|
335
|
-
port: z.number().default(3333).describe('Port
|
|
242
|
+
port: z.number().default(3333).describe('Port to use if the dashboard needs to be started'),
|
|
336
243
|
openBrowser: z
|
|
337
244
|
.boolean()
|
|
338
245
|
.default(true)
|
|
339
246
|
.describe('Whether to launch the default browser at the UI URL'),
|
|
340
247
|
}),
|
|
341
248
|
execute: async (args) => {
|
|
342
|
-
if (
|
|
249
|
+
if (uiState.current) {
|
|
343
250
|
if (args.openBrowser)
|
|
344
|
-
openInBrowser(
|
|
251
|
+
openInBrowser(uiState.current.url);
|
|
345
252
|
return JSON.stringify({
|
|
346
|
-
url:
|
|
253
|
+
url: uiState.current.url,
|
|
347
254
|
status: 'already_running',
|
|
348
255
|
browserOpened: args.openBrowser,
|
|
349
256
|
});
|
|
350
257
|
}
|
|
351
258
|
try {
|
|
352
|
-
|
|
259
|
+
uiState.current = await startUiServer({ cwd, port: args.port });
|
|
353
260
|
if (args.openBrowser)
|
|
354
|
-
openInBrowser(
|
|
261
|
+
openInBrowser(uiState.current.url);
|
|
355
262
|
return JSON.stringify({
|
|
356
|
-
url:
|
|
263
|
+
url: uiState.current.url,
|
|
357
264
|
status: 'started',
|
|
358
265
|
browserOpened: args.openBrowser,
|
|
359
266
|
});
|
|
@@ -368,11 +275,11 @@ export function createMcpServer(options = {}) {
|
|
|
368
275
|
description: 'Stop the brainctl web dashboard if it is running.',
|
|
369
276
|
parameters: z.object({}),
|
|
370
277
|
execute: async () => {
|
|
371
|
-
if (!
|
|
278
|
+
if (!uiState.current) {
|
|
372
279
|
return JSON.stringify({ status: 'not_running' });
|
|
373
280
|
}
|
|
374
|
-
await
|
|
375
|
-
|
|
281
|
+
await uiState.current.close();
|
|
282
|
+
uiState.current = null;
|
|
376
283
|
return JSON.stringify({ status: 'stopped' });
|
|
377
284
|
},
|
|
378
285
|
});
|
|
@@ -422,9 +329,33 @@ export function createMcpServer(options = {}) {
|
|
|
422
329
|
return server;
|
|
423
330
|
}
|
|
424
331
|
export async function startMcpServer(options = {}) {
|
|
425
|
-
const
|
|
332
|
+
const cwd = options.cwd ?? process.cwd();
|
|
333
|
+
const uiServerState = { current: null };
|
|
334
|
+
if (process.env.BRAINCTL_AUTO_UI !== '0') {
|
|
335
|
+
uiServerState.current = await tryAutoStartUi(cwd, 3333);
|
|
336
|
+
}
|
|
337
|
+
const server = createMcpServer({ cwd, uiServerState });
|
|
426
338
|
await server.start({ transportType: 'stdio' });
|
|
427
339
|
}
|
|
340
|
+
async function tryAutoStartUi(cwd, port) {
|
|
341
|
+
try {
|
|
342
|
+
return await startUiServer({ cwd, port });
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
const code = err.code;
|
|
346
|
+
if (code === 'EADDRINUSE') {
|
|
347
|
+
const url = `http://127.0.0.1:${port}`;
|
|
348
|
+
process.stderr.write(`brainctl: UI port ${port} already in use; assuming another brainctl instance owns ${url}\n`);
|
|
349
|
+
return {
|
|
350
|
+
server: null,
|
|
351
|
+
url,
|
|
352
|
+
close: async () => { },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
process.stderr.write(`brainctl: UI auto-start failed: ${err.message}\n`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
428
359
|
function openInBrowser(url) {
|
|
429
360
|
const platform = process.platform;
|
|
430
361
|
const { command, args } = platform === 'darwin'
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PortablePluginSnapshot, PortableUserSkillSnapshot } from '../types.js';
|
|
2
|
+
export declare function installPlugin(sourceDir: string, plugin: PortablePluginSnapshot): Promise<void>;
|
|
3
|
+
export declare function installUserSkill(sourceDir: string, skill: PortableUserSkillSnapshot): Promise<void>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { copyFile, cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ProfileError } from '../errors.js';
|
|
5
|
+
import { formatTimestamp } from './sync/agent-writer.js';
|
|
6
|
+
export async function installPlugin(sourceDir, plugin) {
|
|
7
|
+
try {
|
|
8
|
+
await stat(sourceDir);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new ProfileError(`Bundled plugin "${plugin.name}" source missing at ${sourceDir}.`);
|
|
12
|
+
}
|
|
13
|
+
if (plugin.agent === 'gemini') {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const marketplace = plugin.marketplace ?? plugin.source;
|
|
17
|
+
const version = plugin.version ?? 'unknown';
|
|
18
|
+
const cacheRoot = path.join(homedir(), `.${plugin.agent}`, 'plugins', 'cache');
|
|
19
|
+
const targetDir = path.join(cacheRoot, marketplace, plugin.name, version);
|
|
20
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
21
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
22
|
+
await cp(sourceDir, targetDir, { recursive: true });
|
|
23
|
+
if (plugin.agent === 'claude') {
|
|
24
|
+
await registerClaudePlugin({
|
|
25
|
+
pluginKey: `${plugin.name}@${marketplace}`,
|
|
26
|
+
installPath: targetDir,
|
|
27
|
+
version,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (plugin.agent === 'codex') {
|
|
32
|
+
await registerCodexPlugin({
|
|
33
|
+
pluginKey: `${plugin.name}@${marketplace}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function installUserSkill(sourceDir, skill) {
|
|
38
|
+
try {
|
|
39
|
+
await stat(sourceDir);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ProfileError(`Bundled user skill "${skill.name}" source missing at ${sourceDir}.`);
|
|
43
|
+
}
|
|
44
|
+
const targetDir = path.join(homedir(), `.${skill.agent}`, 'skills', skill.name);
|
|
45
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
46
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
47
|
+
await cp(sourceDir, targetDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
async function registerClaudePlugin(options) {
|
|
50
|
+
const filePath = path.join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
51
|
+
let existing = { version: 2, plugins: {} };
|
|
52
|
+
try {
|
|
53
|
+
const source = await readFile(filePath, 'utf8');
|
|
54
|
+
existing = JSON.parse(source);
|
|
55
|
+
await backupFile(filePath);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// fresh file
|
|
59
|
+
}
|
|
60
|
+
const plugins = (existing.plugins ?? {});
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
const entry = {
|
|
63
|
+
scope: 'user',
|
|
64
|
+
installPath: options.installPath,
|
|
65
|
+
version: options.version,
|
|
66
|
+
installedAt: now,
|
|
67
|
+
lastUpdated: now,
|
|
68
|
+
};
|
|
69
|
+
plugins[options.pluginKey] = [entry];
|
|
70
|
+
existing.plugins = plugins;
|
|
71
|
+
if (typeof existing.version !== 'number')
|
|
72
|
+
existing.version = 2;
|
|
73
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
74
|
+
await atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
75
|
+
}
|
|
76
|
+
async function registerCodexPlugin(options) {
|
|
77
|
+
const filePath = path.join(homedir(), '.codex', 'config.toml');
|
|
78
|
+
let existing = '';
|
|
79
|
+
try {
|
|
80
|
+
existing = await readFile(filePath, 'utf8');
|
|
81
|
+
await backupFile(filePath);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
existing = '';
|
|
85
|
+
}
|
|
86
|
+
const header = `[plugins."${options.pluginKey}"]`;
|
|
87
|
+
if (existing.includes(header))
|
|
88
|
+
return;
|
|
89
|
+
const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
90
|
+
const separator = existing.length > 0 ? '\n' : '';
|
|
91
|
+
const block = `${header}\nenabled = true\n`;
|
|
92
|
+
const next = existing + prefix + separator + block;
|
|
93
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
94
|
+
await atomicWrite(filePath, next);
|
|
95
|
+
}
|
|
96
|
+
async function backupFile(filePath) {
|
|
97
|
+
const backupPath = `${filePath}.bak.${formatTimestamp()}`;
|
|
98
|
+
try {
|
|
99
|
+
await copyFile(filePath, backupPath);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// file may not exist
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function atomicWrite(filePath, content) {
|
|
106
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
107
|
+
await writeFile(tmpPath, content, 'utf8');
|
|
108
|
+
await rename(tmpPath, filePath);
|
|
109
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentName } from '../types.js';
|
|
2
|
+
export interface AgentAvailability {
|
|
3
|
+
agent: AgentName;
|
|
4
|
+
available: boolean;
|
|
5
|
+
command: string;
|
|
6
|
+
resolvedPath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentAvailabilityService {
|
|
9
|
+
getAll(): Promise<Record<AgentName, AgentAvailability>>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createAgentAvailabilityService(): AgentAvailabilityService;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { findExecutable } from '../system/executables.js';
|
|
2
|
+
const SUPPORTED_AGENTS = ['claude', 'codex', 'gemini'];
|
|
3
|
+
const AGENT_COMMANDS = {
|
|
4
|
+
claude: 'claude',
|
|
5
|
+
codex: 'codex',
|
|
6
|
+
gemini: 'gemini',
|
|
7
|
+
};
|
|
8
|
+
export function createAgentAvailabilityService() {
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
const check = (agent) => {
|
|
11
|
+
if (!cache.has(agent)) {
|
|
12
|
+
cache.set(agent, checkAvailability(agent));
|
|
13
|
+
}
|
|
14
|
+
return cache.get(agent);
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
async getAll() {
|
|
18
|
+
const entries = await Promise.all(SUPPORTED_AGENTS.map(async (agent) => [agent, await check(agent)]));
|
|
19
|
+
return Object.fromEntries(entries);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function checkAvailability(agent) {
|
|
24
|
+
const command = AGENT_COMMANDS[agent];
|
|
25
|
+
const resolvedPath = await findExecutable(command);
|
|
26
|
+
return {
|
|
27
|
+
agent,
|
|
28
|
+
command,
|
|
29
|
+
available: resolvedPath !== null,
|
|
30
|
+
resolvedPath: resolvedPath ?? undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { McpServerConfig, PortableCredentialSpec } from '../types.js';
|
|
|
2
2
|
export interface CredentialRedactionResult<T extends McpServerConfig> {
|
|
3
3
|
redacted: T;
|
|
4
4
|
credentials: PortableCredentialSpec[];
|
|
5
|
+
rawValues: Record<string, string>;
|
|
5
6
|
}
|
|
6
7
|
export declare function redactPortableMcpCredentials<T extends McpServerConfig>(config: T): CredentialRedactionResult<T>;
|
|
7
8
|
interface CredentialAccumulator {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export function redactPortableMcpCredentials(config) {
|
|
2
2
|
const credentialsByKey = new Map();
|
|
3
|
-
const
|
|
3
|
+
const rawValues = {};
|
|
4
|
+
const redactedEnv = redactStringMap(config.env, 'env', credentialsByKey, rawValues);
|
|
4
5
|
if (config.kind === 'remote') {
|
|
5
|
-
const redactedHeaders = redactStringMap(config.headers, 'header', credentialsByKey);
|
|
6
|
+
const redactedHeaders = redactStringMap(config.headers, 'header', credentialsByKey, rawValues);
|
|
6
7
|
return {
|
|
7
8
|
redacted: {
|
|
8
9
|
...config,
|
|
@@ -10,6 +11,7 @@ export function redactPortableMcpCredentials(config) {
|
|
|
10
11
|
...(redactedHeaders ? { headers: redactedHeaders } : {}),
|
|
11
12
|
},
|
|
12
13
|
credentials: finalizePortableCredentialSpecs(credentialsByKey),
|
|
14
|
+
rawValues,
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
return {
|
|
@@ -18,9 +20,10 @@ export function redactPortableMcpCredentials(config) {
|
|
|
18
20
|
...(redactedEnv ? { env: redactedEnv } : {}),
|
|
19
21
|
},
|
|
20
22
|
credentials: finalizePortableCredentialSpecs(credentialsByKey),
|
|
23
|
+
rawValues,
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
|
-
function redactStringMap(values, source, credentialsByKey) {
|
|
26
|
+
function redactStringMap(values, source, credentialsByKey, rawValues) {
|
|
24
27
|
if (!values) {
|
|
25
28
|
return undefined;
|
|
26
29
|
}
|
|
@@ -32,6 +35,9 @@ function redactStringMap(values, source, credentialsByKey) {
|
|
|
32
35
|
}
|
|
33
36
|
const credentialKey = normalizeCredentialKey(key);
|
|
34
37
|
addCredentialSpec(credentialsByKey, credentialKey, source, key);
|
|
38
|
+
if (!isCredentialPlaceholder(value)) {
|
|
39
|
+
rawValues[credentialKey] = value;
|
|
40
|
+
}
|
|
35
41
|
redacted[key] = isCredentialPlaceholder(value)
|
|
36
42
|
? value
|
|
37
43
|
: `\${credentials.${credentialKey}}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentAvailabilityService } from './agent-availability-service.js';
|
|
2
2
|
import type { DiagnosticCheck } from '../types.js';
|
|
3
3
|
export interface DoctorResult {
|
|
4
4
|
checks: DiagnosticCheck[];
|
|
@@ -10,5 +10,5 @@ export interface DoctorService {
|
|
|
10
10
|
}): Promise<DoctorResult>;
|
|
11
11
|
}
|
|
12
12
|
export declare function createDoctorService(dependencies?: {
|
|
13
|
-
|
|
13
|
+
availabilityService?: AgentAvailabilityService;
|
|
14
14
|
}): DoctorService;
|