brainctl 0.1.22 → 0.1.25

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.
@@ -1,2 +1,14 @@
1
1
  import type { Command } from 'commander';
2
+ import { createUpdateCheckService } from '../services/platform/update-check-service.js';
2
3
  export declare function registerMcpCommand(program: Command): void;
4
+ export interface AutoUpdateDeps {
5
+ service?: ReturnType<typeof createUpdateCheckService>;
6
+ env?: NodeJS.ProcessEnv;
7
+ log?: (msg: string) => void;
8
+ relaunch?: (env: NodeJS.ProcessEnv) => number;
9
+ exit?: (code: number) => void;
10
+ }
11
+ export declare function autoUpdateAndRelaunch(deps?: AutoUpdateDeps): Promise<{
12
+ updated: boolean;
13
+ reason?: string;
14
+ }>;
@@ -7,16 +7,48 @@ export function registerMcpCommand(program) {
7
7
  .description('Start the brainctl MCP server (stdio transport)')
8
8
  .action(async () => {
9
9
  killPriorMcpServers();
10
+ await autoUpdateAndRelaunch();
10
11
  process.stdin.on('end', () => process.exit(0));
11
12
  process.stdin.on('close', () => process.exit(0));
12
13
  await startMcpServer({ cwd: process.cwd() });
13
14
  if (!process.env.BRAINCTL_NO_UPDATE_CHECK) {
14
- // Fire-and-forget after the server is up so cold-start isn't blocked
15
- // on a network round-trip (or `npm install brainctl@latest`).
16
15
  void notifyIfOutdated();
17
16
  }
18
17
  });
19
18
  }
19
+ export async function autoUpdateAndRelaunch(deps = {}) {
20
+ const env = deps.env ?? process.env;
21
+ if (env.BRAINCTL_NO_UPDATE_CHECK)
22
+ return { updated: false, reason: 'disabled' };
23
+ if (env.BRAINCTL_NO_AUTO_UPDATE)
24
+ return { updated: false, reason: 'disabled' };
25
+ // Guard against a re-launch loop if the new version still reports outdated.
26
+ if (env.BRAINCTL_SELF_UPDATED === '1')
27
+ return { updated: false, reason: 'already-updated' };
28
+ const log = deps.log ?? ((m) => process.stderr.write(m));
29
+ const relaunch = deps.relaunch ??
30
+ ((nextEnv) => {
31
+ const child = spawnSync(process.execPath, process.argv.slice(1), {
32
+ stdio: 'inherit',
33
+ env: nextEnv,
34
+ });
35
+ return child.status ?? 0;
36
+ });
37
+ const exit = deps.exit ?? ((code) => process.exit(code));
38
+ const service = deps.service ?? createUpdateCheckService();
39
+ const check = await service.check().catch(() => null);
40
+ if (!check || !check.isOutdated)
41
+ return { updated: false, reason: 'up-to-date' };
42
+ log(`brainctl: auto-updating ${check.current} -> ${check.latest}...\n`);
43
+ const result = await service.selfUpdate();
44
+ if (!result.success) {
45
+ log(`brainctl: self-update failed (${result.error ?? 'unknown'}); continuing on ${check.current}\n`);
46
+ return { updated: false, reason: 'install-failed' };
47
+ }
48
+ const status = relaunch({ ...env, BRAINCTL_SELF_UPDATED: '1' });
49
+ exit(status);
50
+ return { updated: true };
51
+ }
20
52
  function killPriorMcpServers() {
21
53
  const self = process.pid;
22
54
  const ppid = process.ppid;
@@ -14,11 +14,13 @@ export interface AgentConfigService {
14
14
  key: string;
15
15
  entry?: AgentMcpEntry;
16
16
  remoteEntry?: PortableRemoteMcpMetadata;
17
+ scope?: 'global' | 'project';
17
18
  }): Promise<void>;
18
19
  removeMcp(options: {
19
20
  cwd: string;
20
21
  agent: AgentName;
21
22
  key: string;
23
+ scope?: 'global' | 'project';
22
24
  }): Promise<void>;
23
25
  copySkill(options: {
24
26
  sourceAgent: AgentName;
@@ -25,52 +25,83 @@ export function createAgentConfigService(dependencies = {}) {
25
25
  return results;
26
26
  },
27
27
  async addMcp(options) {
28
- const { cwd, agent, key, entry, remoteEntry } = options;
28
+ const { cwd, agent, key, entry, remoteEntry, scope = 'global' } = options;
29
29
  const preflight = await mcpPreflightService.execute({ cwd, agent, key, entry, remoteEntry });
30
30
  const firstError = preflight.checks.find((check) => check.status === 'error');
31
31
  if (firstError) {
32
32
  throw new ValidationError(`MCP "${key}" cannot be added to ${agent}: ${firstError.message}`);
33
33
  }
34
34
  if (agent === 'claude') {
35
- await mutateClaudeConfig(cwd, (servers) => {
35
+ await mutateClaudeConfig(cwd, scope, (servers) => {
36
36
  servers[key] = remoteEntry ? toClaudeRemoteEntry(remoteEntry) : toClaudeEntry(entry);
37
37
  });
38
38
  }
39
39
  else if (agent === 'codex') {
40
- await mutateCodexConfig(cwd, (state) => {
41
- delete state.mcpServers[key];
42
- delete state.remoteMcpServers[key];
43
- if (remoteEntry) {
44
- state.remoteMcpServers[key] = remoteEntry;
45
- }
46
- else {
47
- state.mcpServers[key] = entry;
48
- }
49
- });
40
+ if (scope === 'project') {
41
+ await mutateBrainctlProjectMcps(cwd, 'codex', (state) => {
42
+ delete state.mcpServers[key];
43
+ delete state.remoteMcpServers[key];
44
+ if (remoteEntry) {
45
+ state.remoteMcpServers[key] = remoteEntry;
46
+ }
47
+ else {
48
+ state.mcpServers[key] = entry;
49
+ }
50
+ });
51
+ }
52
+ else {
53
+ await mutateCodexConfig(cwd, (state) => {
54
+ delete state.mcpServers[key];
55
+ delete state.remoteMcpServers[key];
56
+ if (remoteEntry) {
57
+ state.remoteMcpServers[key] = remoteEntry;
58
+ }
59
+ else {
60
+ state.mcpServers[key] = entry;
61
+ }
62
+ });
63
+ }
50
64
  }
51
65
  else if (agent === 'gemini') {
52
- await mutateGeminiConfig(cwd, (servers) => {
53
- servers[key] = remoteEntry ? toGeminiRemoteEntry(remoteEntry) : toGeminiEntry(entry);
54
- });
66
+ if (scope === 'project') {
67
+ await mutateBrainctlProjectMcps(cwd, 'gemini', (state) => {
68
+ delete state.mcpServers[key];
69
+ delete state.remoteMcpServers[key];
70
+ if (remoteEntry) {
71
+ state.remoteMcpServers[key] = remoteEntry;
72
+ }
73
+ else {
74
+ state.mcpServers[key] = entry;
75
+ }
76
+ });
77
+ }
78
+ else {
79
+ await mutateGeminiConfig(cwd, (servers) => {
80
+ servers[key] = remoteEntry ? toGeminiRemoteEntry(remoteEntry) : toGeminiEntry(entry);
81
+ });
82
+ }
55
83
  }
56
84
  },
57
85
  async removeMcp(options) {
58
- const { cwd, agent, key } = options;
86
+ const { cwd, agent, key, scope = 'global' } = options;
59
87
  if (agent === 'claude') {
60
- await mutateClaudeConfig(cwd, (servers) => {
61
- delete servers[key];
62
- });
88
+ await mutateClaudeConfig(cwd, scope, (servers) => { delete servers[key]; });
63
89
  }
64
90
  else if (agent === 'codex') {
65
- await mutateCodexConfig(cwd, (state) => {
66
- delete state.mcpServers[key];
67
- delete state.remoteMcpServers[key];
68
- });
91
+ if (scope === 'project') {
92
+ await mutateBrainctlProjectMcps(cwd, 'codex', (state) => { delete state.mcpServers[key]; delete state.remoteMcpServers[key]; });
93
+ }
94
+ else {
95
+ await mutateCodexConfig(cwd, (state) => { delete state.mcpServers[key]; delete state.remoteMcpServers[key]; });
96
+ }
69
97
  }
70
98
  else if (agent === 'gemini') {
71
- await mutateGeminiConfig(cwd, (servers) => {
72
- delete servers[key];
73
- });
99
+ if (scope === 'project') {
100
+ await mutateBrainctlProjectMcps(cwd, 'gemini', (state) => { delete state.mcpServers[key]; delete state.remoteMcpServers[key]; });
101
+ }
102
+ else {
103
+ await mutateGeminiConfig(cwd, (servers) => { delete servers[key]; });
104
+ }
74
105
  }
75
106
  },
76
107
  async copySkill(options) {
@@ -98,7 +129,7 @@ export function createAgentConfigService(dependencies = {}) {
98
129
  };
99
130
  }
100
131
  /* ---- Claude: JSON with projects[cwd].mcpServers ---- */
101
- async function mutateClaudeConfig(cwd, mutate) {
132
+ async function mutateClaudeConfig(cwd, scope, mutate) {
102
133
  const configPath = path.join(homedir(), '.claude.json');
103
134
  let existing = {};
104
135
  try {
@@ -108,18 +139,20 @@ async function mutateClaudeConfig(cwd, mutate) {
108
139
  catch {
109
140
  // fresh config
110
141
  }
111
- // Apply mutation to user-scoped (top-level) MCPs
112
- const userServers = (existing.mcpServers ?? {});
113
- mutate(userServers);
114
- existing.mcpServers = userServers;
115
- // Apply mutation to project-scoped MCPs
116
- const projects = (existing.projects ?? {});
117
- const projectConfig = projects[cwd] ?? {};
118
- const servers = (projectConfig.mcpServers ?? {});
119
- mutate(servers);
120
- projectConfig.mcpServers = servers;
121
- projects[cwd] = projectConfig;
122
- existing.projects = projects;
142
+ if (scope === 'global') {
143
+ const servers = (existing.mcpServers ?? {});
144
+ mutate(servers);
145
+ existing.mcpServers = servers;
146
+ }
147
+ else {
148
+ const projects = (existing.projects ?? {});
149
+ const projectConfig = (projects[cwd] ?? {});
150
+ const servers = (projectConfig.mcpServers ?? {});
151
+ mutate(servers);
152
+ projectConfig.mcpServers = servers;
153
+ projects[cwd] = projectConfig;
154
+ existing.projects = projects;
155
+ }
123
156
  await atomicWriteJson(configPath, existing);
124
157
  }
125
158
  function toClaudeEntry(entry) {
@@ -237,6 +270,40 @@ function toGeminiRemoteEntry(entry) {
237
270
  ...(entry.env ? { env: entry.env } : {}),
238
271
  };
239
272
  }
273
+ /* ---- Brainctl project MCPs: .brainctl/project-mcps.json ---- */
274
+ async function mutateBrainctlProjectMcps(cwd, agent, mutate) {
275
+ const filePath = path.join(cwd, '.brainctl', 'project-mcps.json');
276
+ let data = {};
277
+ try {
278
+ data = JSON.parse(await readFile(filePath, 'utf8'));
279
+ }
280
+ catch {
281
+ // fresh file
282
+ }
283
+ const agentData = (data[agent] ?? {});
284
+ const rawServers = (agentData.mcpServers ?? {});
285
+ const mcpServers = {};
286
+ const remoteMcpServers = {};
287
+ for (const [name, entry] of Object.entries(rawServers)) {
288
+ if (typeof entry.url === 'string' && entry.url) {
289
+ remoteMcpServers[name] = { transport: entry.transport ?? 'http', url: entry.url };
290
+ }
291
+ else {
292
+ mcpServers[name] = { command: String(entry.command ?? ''), args: Array.isArray(entry.args) ? entry.args.map(String) : undefined };
293
+ }
294
+ }
295
+ mutate({ mcpServers, remoteMcpServers });
296
+ const newServers = {};
297
+ for (const [name, entry] of Object.entries(mcpServers)) {
298
+ newServers[name] = { command: entry.command, ...(entry.args ? { args: entry.args } : {}), ...(entry.env ? { env: entry.env } : {}) };
299
+ }
300
+ for (const [name, entry] of Object.entries(remoteMcpServers)) {
301
+ newServers[name] = { transport: entry.transport, url: entry.url };
302
+ }
303
+ data[agent] = { mcpServers: newServers };
304
+ await mkdir(path.join(cwd, '.brainctl'), { recursive: true });
305
+ await atomicWriteJson(filePath, data);
306
+ }
240
307
  /* ---- Shared helpers ---- */
241
308
  async function backupFile(filePath) {
242
309
  const backupPath = `${filePath}.bak.${formatTimestamp()}`;
@@ -0,0 +1,16 @@
1
+ interface FsLike {
2
+ readFile(p: string): Promise<string>;
3
+ writeFile(p: string, content: string): Promise<void>;
4
+ mkdir(p: string, opts?: {
5
+ recursive: boolean;
6
+ }): Promise<unknown>;
7
+ }
8
+ export interface RecentProjectsService {
9
+ read(): Promise<string[]>;
10
+ addRecent(cwd: string): Promise<string[]>;
11
+ }
12
+ export declare function createRecentProjectsService(deps: {
13
+ filePath: string;
14
+ fs?: FsLike;
15
+ }): RecentProjectsService;
16
+ export {};
@@ -0,0 +1,51 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const MAX_RECENTS = 20;
4
+ const FILE_VERSION = 1;
5
+ const defaultFs = {
6
+ readFile: (p) => readFile(p, 'utf8'),
7
+ writeFile: (p, content) => writeFile(p, content, 'utf8'),
8
+ mkdir: (p, opts) => mkdir(p, opts),
9
+ };
10
+ export function createRecentProjectsService(deps) {
11
+ const { filePath, fs: fsImpl = defaultFs } = deps;
12
+ async function readFile_() {
13
+ try {
14
+ const content = await fsImpl.readFile(filePath);
15
+ const parsed = JSON.parse(content);
16
+ if (typeof parsed === 'object' && parsed !== null && 'recents' in parsed) {
17
+ return parsed;
18
+ }
19
+ return null;
20
+ }
21
+ catch (err) {
22
+ if (err.code === 'ENOENT') {
23
+ return null;
24
+ }
25
+ throw err;
26
+ }
27
+ }
28
+ async function persist(recents) {
29
+ const dir = path.dirname(filePath);
30
+ await fsImpl.mkdir(dir, { recursive: true });
31
+ const data = { version: FILE_VERSION, recents };
32
+ await fsImpl.writeFile(filePath, JSON.stringify(data, null, 2));
33
+ }
34
+ async function read() {
35
+ const data = await readFile_();
36
+ if (!data)
37
+ return [];
38
+ return (data.recents ?? []).filter((e) => typeof e === 'string');
39
+ }
40
+ async function addRecent(cwd) {
41
+ if (!path.isAbsolute(cwd)) {
42
+ throw new Error(`cwd must be an absolute path, got: ${cwd}`);
43
+ }
44
+ const current = await read();
45
+ const deduplicated = current.filter((p) => p !== cwd);
46
+ const updated = [cwd, ...deduplicated].slice(0, MAX_RECENTS);
47
+ await persist(updated);
48
+ return updated;
49
+ }
50
+ return { read, addRecent };
51
+ }
@@ -27,6 +27,8 @@ export interface AgentLiveConfig {
27
27
  exists: boolean;
28
28
  mcpServers: Record<string, AgentMcpEntry>;
29
29
  remoteMcpServers: Record<string, PortableRemoteMcpMetadata>;
30
+ projectMcpServers: Record<string, AgentMcpEntry>;
31
+ projectRemoteMcpServers: Record<string, PortableRemoteMcpMetadata>;
30
32
  skills: AgentSkillEntry[];
31
33
  }
32
34
  export interface AgentConfigReader {
@@ -10,39 +10,40 @@ export function createClaudeReader() {
10
10
  try {
11
11
  const source = await readFile(configPath, 'utf8');
12
12
  const data = JSON.parse(source);
13
- // Merge user-scoped MCPs (top-level) with project-scoped MCPs (project overrides user)
14
13
  const userServers = (data.mcpServers ?? {});
15
14
  const projects = (data.projects ?? {});
16
- const projectConfig = projects[options.cwd] ?? {};
15
+ const projectConfig = (projects[options.cwd] ?? {});
17
16
  const projectServers = (projectConfig.mcpServers ?? {});
18
- const rawServers = { ...userServers, ...projectServers };
19
17
  const mcpServers = {};
20
18
  const remoteMcpServers = {};
21
- for (const [name, entry] of Object.entries(rawServers)) {
19
+ for (const [name, entry] of Object.entries(userServers)) {
22
20
  if (isClaudeRemoteEntry(entry)) {
23
- const url = typeof entry.url === 'string' ? entry.url : '';
24
- remoteMcpServers[name] = {
25
- transport: entry.type === 'sse' ? 'sse' : 'http',
26
- url,
27
- headers: parseEnvObject(entry.headers),
28
- env: parseEnvObject(entry.env),
29
- };
30
- continue;
21
+ remoteMcpServers[name] = { transport: entry.type === 'sse' ? 'sse' : 'http', url: String(entry.url ?? ''), headers: parseEnvObject(entry.headers), env: parseEnvObject(entry.env) };
22
+ }
23
+ else {
24
+ mcpServers[name] = { command: String(entry.command ?? ''), args: Array.isArray(entry.args) ? entry.args.map(String) : undefined, env: parseEnvObject(entry.env) };
25
+ }
26
+ }
27
+ const projectMcpServers = {};
28
+ const projectRemoteMcpServers = {};
29
+ for (const [name, entry] of Object.entries(projectServers)) {
30
+ if (isClaudeRemoteEntry(entry)) {
31
+ projectRemoteMcpServers[name] = { transport: entry.type === 'sse' ? 'sse' : 'http', url: String(entry.url ?? ''), headers: parseEnvObject(entry.headers), env: parseEnvObject(entry.env) };
32
+ }
33
+ else {
34
+ projectMcpServers[name] = { command: String(entry.command ?? ''), args: Array.isArray(entry.args) ? entry.args.map(String) : undefined, env: parseEnvObject(entry.env) };
31
35
  }
32
- mcpServers[name] = {
33
- command: String(entry.command ?? ''),
34
- args: Array.isArray(entry.args) ? entry.args.map(String) : undefined,
35
- env: parseEnvObject(entry.env),
36
- };
37
36
  }
38
37
  const skills = await readClaudePlugins();
39
- const filtered = filterPluginOwnedMcps({ mcpServers, remoteMcpServers, skills });
38
+ const filtered = filterPluginOwnedMcps({ mcpServers, remoteMcpServers, projectMcpServers, projectRemoteMcpServers, skills });
40
39
  return {
41
40
  agent: 'claude',
42
41
  configPath,
43
42
  exists: true,
44
43
  mcpServers: filtered.mcpServers,
45
44
  remoteMcpServers: filtered.remoteMcpServers,
45
+ projectMcpServers: filtered.projectMcpServers,
46
+ projectRemoteMcpServers: filtered.projectRemoteMcpServers,
46
47
  skills,
47
48
  };
48
49
  }
@@ -54,6 +55,8 @@ export function createClaudeReader() {
54
55
  exists: false,
55
56
  mcpServers: {},
56
57
  remoteMcpServers: {},
58
+ projectMcpServers: {},
59
+ projectRemoteMcpServers: {},
57
60
  skills,
58
61
  };
59
62
  }
@@ -62,19 +65,22 @@ export function createClaudeReader() {
62
65
  }
63
66
  export function createCodexReader() {
64
67
  return {
65
- async read() {
68
+ async read(options) {
66
69
  const configPath = path.join(homedir(), '.codex', 'config.toml');
70
+ const { projectMcpServers, projectRemoteMcpServers } = await readBrainctlProjectMcps(options.cwd, 'codex');
67
71
  try {
68
72
  const source = await readFile(configPath, 'utf8');
69
73
  const { mcpServers, remoteMcpServers } = parseCodexToml(source);
70
74
  const skills = await readCodexSkills();
71
- const filtered = filterPluginOwnedMcps({ mcpServers, remoteMcpServers, skills });
75
+ const filtered = filterPluginOwnedMcps({ mcpServers, remoteMcpServers, projectMcpServers, projectRemoteMcpServers, skills });
72
76
  return {
73
77
  agent: 'codex',
74
78
  configPath,
75
79
  exists: true,
76
80
  mcpServers: filtered.mcpServers,
77
81
  remoteMcpServers: filtered.remoteMcpServers,
82
+ projectMcpServers: filtered.projectMcpServers,
83
+ projectRemoteMcpServers: filtered.projectRemoteMcpServers,
78
84
  skills,
79
85
  };
80
86
  }
@@ -86,6 +92,8 @@ export function createCodexReader() {
86
92
  exists: false,
87
93
  mcpServers: {},
88
94
  remoteMcpServers: {},
95
+ projectMcpServers,
96
+ projectRemoteMcpServers,
89
97
  skills,
90
98
  };
91
99
  }
@@ -94,8 +102,9 @@ export function createCodexReader() {
94
102
  }
95
103
  export function createGeminiReader() {
96
104
  return {
97
- async read() {
105
+ async read(options) {
98
106
  const configPath = path.join(homedir(), '.gemini', 'settings.json');
107
+ const { projectMcpServers, projectRemoteMcpServers } = await readBrainctlProjectMcps(options.cwd, 'gemini');
99
108
  let rawServers = {};
100
109
  let exists = false;
101
110
  try {
@@ -115,33 +124,57 @@ export function createGeminiReader() {
115
124
  remoteMcpServers[name] = remoteEntry;
116
125
  continue;
117
126
  }
118
- mcpServers[name] = {
119
- command: String(entry.command ?? ''),
120
- args: Array.isArray(entry.args) ? entry.args.map(String) : undefined,
121
- env: parseEnvObject(entry.env),
122
- };
127
+ mcpServers[name] = { command: String(entry.command ?? ''), args: Array.isArray(entry.args) ? entry.args.map(String) : undefined, env: parseEnvObject(entry.env) };
123
128
  }
124
129
  const skills = await readGeminiSkills();
125
- const filtered = filterPluginOwnedMcps({ mcpServers, remoteMcpServers, skills });
130
+ const filtered = filterPluginOwnedMcps({ mcpServers, remoteMcpServers, projectMcpServers, projectRemoteMcpServers, skills });
126
131
  return {
127
132
  agent: 'gemini',
128
133
  configPath,
129
134
  exists,
130
135
  mcpServers: filtered.mcpServers,
131
136
  remoteMcpServers: filtered.remoteMcpServers,
137
+ projectMcpServers: filtered.projectMcpServers,
138
+ projectRemoteMcpServers: filtered.projectRemoteMcpServers,
132
139
  skills,
133
140
  };
134
141
  },
135
142
  };
136
143
  }
144
+ async function readBrainctlProjectMcps(cwd, agent) {
145
+ try {
146
+ const filePath = path.join(cwd, '.brainctl', 'project-mcps.json');
147
+ const data = JSON.parse(await readFile(filePath, 'utf8'));
148
+ const agentData = (data[agent] ?? {});
149
+ const rawServers = (agentData.mcpServers ?? {});
150
+ const projectMcpServers = {};
151
+ const projectRemoteMcpServers = {};
152
+ for (const [name, entry] of Object.entries(rawServers)) {
153
+ if (typeof entry.url === 'string' && entry.url) {
154
+ projectRemoteMcpServers[name] = { transport: entry.transport ?? 'http', url: entry.url, headers: parseEnvObject(entry.headers), env: parseEnvObject(entry.env) };
155
+ }
156
+ else {
157
+ projectMcpServers[name] = { command: String(entry.command ?? ''), args: Array.isArray(entry.args) ? entry.args.map(String) : undefined, env: parseEnvObject(entry.env) };
158
+ }
159
+ }
160
+ return { projectMcpServers, projectRemoteMcpServers };
161
+ }
162
+ catch {
163
+ return { projectMcpServers: {}, projectRemoteMcpServers: {} };
164
+ }
165
+ }
137
166
  function filterPluginOwnedMcps(options) {
138
167
  const pluginOwned = new Set(options.skills.flatMap((skill) => skill.pluginMcps ?? []));
139
168
  if (pluginOwned.size === 0) {
140
- return { mcpServers: options.mcpServers, remoteMcpServers: options.remoteMcpServers };
169
+ return { mcpServers: options.mcpServers, remoteMcpServers: options.remoteMcpServers, projectMcpServers: options.projectMcpServers, projectRemoteMcpServers: options.projectRemoteMcpServers };
141
170
  }
142
- const mcpServers = Object.fromEntries(Object.entries(options.mcpServers).filter(([key]) => !pluginOwned.has(key)));
143
- const remoteMcpServers = Object.fromEntries(Object.entries(options.remoteMcpServers).filter(([key]) => !pluginOwned.has(key)));
144
- return { mcpServers, remoteMcpServers };
171
+ const notOwned = ([key]) => !pluginOwned.has(key);
172
+ return {
173
+ mcpServers: Object.fromEntries(Object.entries(options.mcpServers).filter(notOwned)),
174
+ remoteMcpServers: Object.fromEntries(Object.entries(options.remoteMcpServers).filter(notOwned)),
175
+ projectMcpServers: Object.fromEntries(Object.entries(options.projectMcpServers).filter(notOwned)),
176
+ projectRemoteMcpServers: Object.fromEntries(Object.entries(options.projectRemoteMcpServers).filter(notOwned)),
177
+ };
145
178
  }
146
179
  function parseEnvObject(value) {
147
180
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -3,6 +3,8 @@ import type { StatusService } from '../services/platform/status-service.js';
3
3
  export interface UiRouteDependencies {
4
4
  cwd: string;
5
5
  statusService?: StatusService;
6
+ recentsFilePath?: string;
7
+ claudeJsonPath?: string;
6
8
  }
7
9
  export type UiRouteHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
8
10
  export declare function createUiRouteHandler(dependencies: UiRouteDependencies): UiRouteHandler;