pi-mcp-adapter 2.4.0 → 2.4.2
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/CHANGELOG.md +23 -0
- package/README.md +53 -7
- package/cli.js +152 -86
- package/commands.ts +116 -9
- package/config.ts +566 -126
- package/index.ts +15 -3
- package/mcp-panel.ts +11 -1
- package/mcp-setup-panel.ts +576 -0
- package/onboarding-state.ts +68 -0
- package/package.json +4 -2
- package/utils.ts +18 -9
package/config.ts
CHANGED
|
@@ -1,110 +1,350 @@
|
|
|
1
1
|
// config.ts - Config loading with import support
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, resolve
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.js";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
7
|
+
const PI_GLOBAL_CONFIG_PATH = join(homedir(), ".pi", "agent", "mcp.json");
|
|
8
|
+
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
9
|
+
const PROJECT_CONFIG_NAME = ".mcp.json";
|
|
10
|
+
const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
|
|
11
|
+
const REPOPROMPT_BINARY_CANDIDATES = [
|
|
12
|
+
join(homedir(), "RepoPrompt", "repoprompt_cli"),
|
|
13
|
+
"/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const IMPORT_PATHS: Record<ImportKind, string[]> = {
|
|
17
|
+
cursor: [join(homedir(), ".cursor", "mcp.json")],
|
|
18
|
+
"claude-code": [
|
|
19
|
+
join(homedir(), ".claude", "mcp.json"),
|
|
20
|
+
join(homedir(), ".claude.json"),
|
|
21
|
+
join(homedir(), ".claude", "claude_desktop_config.json"),
|
|
22
|
+
],
|
|
23
|
+
"claude-desktop": [join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")],
|
|
24
|
+
codex: [join(homedir(), ".codex", "config.json")],
|
|
25
|
+
windsurf: [join(homedir(), ".windsurf", "mcp.json")],
|
|
26
|
+
vscode: [".vscode/mcp.json"],
|
|
18
27
|
};
|
|
19
28
|
|
|
29
|
+
interface ConfigSourceSpec {
|
|
30
|
+
id: "shared-global" | "pi-global" | "shared-project" | "pi-project";
|
|
31
|
+
label: string;
|
|
32
|
+
readPath: string;
|
|
33
|
+
writePath: string;
|
|
34
|
+
kind: "user" | "project" | "import";
|
|
35
|
+
importKind?: string;
|
|
36
|
+
shared: boolean;
|
|
37
|
+
scope: "global" | "project";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ConfigDiscoveryPath {
|
|
41
|
+
label: string;
|
|
42
|
+
path: string;
|
|
43
|
+
exists: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DiscoveredImportConfig {
|
|
47
|
+
kind: ImportKind;
|
|
48
|
+
path: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ConfigDiscoverySource extends ConfigDiscoveryPath {
|
|
52
|
+
id: ConfigSourceSpec["id"];
|
|
53
|
+
scope: ConfigSourceSpec["scope"];
|
|
54
|
+
kind: "shared" | "pi";
|
|
55
|
+
serverCount: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ImportConfigSummary extends DiscoveredImportConfig {
|
|
59
|
+
serverCount: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RepoPromptDiscovery {
|
|
63
|
+
configured: boolean;
|
|
64
|
+
configuredPath?: string;
|
|
65
|
+
executablePath?: string;
|
|
66
|
+
targetPath?: string;
|
|
67
|
+
serverName?: string;
|
|
68
|
+
entry?: ServerEntry;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface McpDiscoverySummary {
|
|
72
|
+
sources: ConfigDiscoverySource[];
|
|
73
|
+
imports: ImportConfigSummary[];
|
|
74
|
+
hasAnyConfig: boolean;
|
|
75
|
+
hasAnyDetectedPaths: boolean;
|
|
76
|
+
hasSharedServers: boolean;
|
|
77
|
+
hasPiOwnedServers: boolean;
|
|
78
|
+
totalServerCount: number;
|
|
79
|
+
fingerprint: string;
|
|
80
|
+
repoPrompt: RepoPromptDiscovery;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ConfigWritePreview {
|
|
84
|
+
path: string;
|
|
85
|
+
existed: boolean;
|
|
86
|
+
changed: boolean;
|
|
87
|
+
beforeText: string;
|
|
88
|
+
afterText: string;
|
|
89
|
+
diffText: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getPiGlobalConfigPath(overridePath?: string): string {
|
|
93
|
+
return overridePath ? resolve(overridePath) : PI_GLOBAL_CONFIG_PATH;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getGenericGlobalConfigPath(): string {
|
|
97
|
+
return GENERIC_GLOBAL_CONFIG_PATH;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getProjectConfigPath(cwd = process.cwd()): string {
|
|
101
|
+
return resolve(cwd, PROJECT_CONFIG_NAME);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getProjectPiConfigPath(cwd = process.cwd()): string {
|
|
105
|
+
return resolve(cwd, PROJECT_PI_CONFIG_NAME);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getConfigDiscoveryPaths(overridePath?: string): ConfigDiscoveryPath[] {
|
|
109
|
+
return getConfigSources(overridePath).map((source) => ({
|
|
110
|
+
label: source.label,
|
|
111
|
+
path: source.readPath,
|
|
112
|
+
exists: existsSync(source.readPath),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function findAvailableImportConfigs(cwd = process.cwd()): DiscoveredImportConfig[] {
|
|
117
|
+
const discovered: DiscoveredImportConfig[] = [];
|
|
118
|
+
|
|
119
|
+
for (const importKind of Object.keys(IMPORT_PATHS) as ImportKind[]) {
|
|
120
|
+
const importPath = resolveImportPath(importKind, cwd);
|
|
121
|
+
if (importPath) {
|
|
122
|
+
discovered.push({ kind: importKind, path: importPath });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return discovered;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getMcpDiscoverySummary(overridePath?: string, cwd = process.cwd()): McpDiscoverySummary {
|
|
130
|
+
const sources = getConfigSources(overridePath, cwd).map((source) => {
|
|
131
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
132
|
+
return {
|
|
133
|
+
id: source.id,
|
|
134
|
+
label: source.label,
|
|
135
|
+
path: source.readPath,
|
|
136
|
+
exists: existsSync(source.readPath),
|
|
137
|
+
scope: source.scope,
|
|
138
|
+
kind: source.shared ? "shared" : "pi",
|
|
139
|
+
serverCount: loaded ? Object.keys(loaded.mcpServers).length : 0,
|
|
140
|
+
} satisfies ConfigDiscoverySource;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const imports = (Object.keys(IMPORT_PATHS) as ImportKind[])
|
|
144
|
+
.map((kind) => {
|
|
145
|
+
const path = resolveImportPath(kind, cwd);
|
|
146
|
+
if (!path) return null;
|
|
147
|
+
return {
|
|
148
|
+
kind,
|
|
149
|
+
path,
|
|
150
|
+
serverCount: getImportServerCount(kind, path),
|
|
151
|
+
} satisfies ImportConfigSummary;
|
|
152
|
+
})
|
|
153
|
+
.filter((value): value is ImportConfigSummary => value !== null);
|
|
154
|
+
|
|
155
|
+
const totalServerCount = sources.reduce((sum, source) => sum + source.serverCount, 0);
|
|
156
|
+
const hasSharedServers = sources.some((source) => source.kind === "shared" && source.serverCount > 0);
|
|
157
|
+
const hasPiOwnedServers = sources.some((source) => source.kind === "pi" && source.serverCount > 0);
|
|
158
|
+
const hasAnyDetectedPaths = sources.some((source) => source.exists) || imports.length > 0;
|
|
159
|
+
const hasAnyConfig = totalServerCount > 0 || imports.some((entry) => entry.serverCount > 0) || hasAnyDetectedPaths;
|
|
160
|
+
|
|
161
|
+
const summaryWithoutRepoPrompt = {
|
|
162
|
+
sources,
|
|
163
|
+
imports,
|
|
164
|
+
hasAnyConfig,
|
|
165
|
+
hasAnyDetectedPaths,
|
|
166
|
+
hasSharedServers,
|
|
167
|
+
hasPiOwnedServers,
|
|
168
|
+
totalServerCount,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const fingerprint = JSON.stringify({
|
|
172
|
+
sources: sources.map((source) => [source.id, source.exists, source.serverCount]),
|
|
173
|
+
imports: imports.map((entry) => [entry.kind, entry.path, entry.serverCount]),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...summaryWithoutRepoPrompt,
|
|
178
|
+
fingerprint,
|
|
179
|
+
repoPrompt: detectRepoPrompt(summaryWithoutRepoPrompt, cwd),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
20
183
|
export function loadMcpConfig(overridePath?: string): McpConfig {
|
|
21
|
-
const configPath = overridePath ? resolve(overridePath) : DEFAULT_CONFIG_PATH;
|
|
22
|
-
|
|
23
|
-
// Load base config
|
|
24
184
|
let config: McpConfig = { mcpServers: {} };
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.warn(`Failed to load MCP config from ${configPath}:`, error);
|
|
32
|
-
}
|
|
185
|
+
|
|
186
|
+
for (const source of getConfigSources(overridePath)) {
|
|
187
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
188
|
+
if (!loaded) continue;
|
|
189
|
+
config = mergeConfigs(config, expandImports(loaded));
|
|
33
190
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.warn(`Failed to import MCP config from ${importKind}:`, error);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
191
|
+
|
|
192
|
+
return config;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSourceSpec[] {
|
|
196
|
+
const userPath = getPiGlobalConfigPath(overridePath);
|
|
197
|
+
const projectPath = getProjectConfigPath(cwd);
|
|
198
|
+
const projectPiPath = getProjectPiConfigPath(cwd);
|
|
199
|
+
const sources: ConfigSourceSpec[] = [];
|
|
200
|
+
|
|
201
|
+
if (GENERIC_GLOBAL_CONFIG_PATH !== userPath) {
|
|
202
|
+
sources.push({
|
|
203
|
+
id: "shared-global",
|
|
204
|
+
label: "user-global standard MCP",
|
|
205
|
+
readPath: GENERIC_GLOBAL_CONFIG_PATH,
|
|
206
|
+
writePath: userPath,
|
|
207
|
+
kind: "import",
|
|
208
|
+
importKind: "global MCP config",
|
|
209
|
+
shared: true,
|
|
210
|
+
scope: "global",
|
|
211
|
+
});
|
|
61
212
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
213
|
+
|
|
214
|
+
sources.push({
|
|
215
|
+
id: "pi-global",
|
|
216
|
+
label: "Pi global override",
|
|
217
|
+
readPath: userPath,
|
|
218
|
+
writePath: userPath,
|
|
219
|
+
kind: "user",
|
|
220
|
+
shared: false,
|
|
221
|
+
scope: "global",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (projectPath !== userPath) {
|
|
225
|
+
sources.push({
|
|
226
|
+
id: "shared-project",
|
|
227
|
+
label: "project standard MCP",
|
|
228
|
+
readPath: projectPath,
|
|
229
|
+
writePath: projectPath,
|
|
230
|
+
kind: "project",
|
|
231
|
+
shared: true,
|
|
232
|
+
scope: "project",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (projectPiPath !== userPath && projectPiPath !== projectPath) {
|
|
237
|
+
sources.push({
|
|
238
|
+
id: "pi-project",
|
|
239
|
+
label: "project Pi override",
|
|
240
|
+
readPath: projectPiPath,
|
|
241
|
+
writePath: projectPiPath,
|
|
242
|
+
kind: "project",
|
|
243
|
+
shared: false,
|
|
244
|
+
scope: "project",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return sources;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
252
|
+
return {
|
|
253
|
+
mcpServers: { ...base.mcpServers, ...next.mcpServers },
|
|
254
|
+
imports: mergeImports(base.imports, next.imports),
|
|
255
|
+
settings: next.settings ? { ...base.settings, ...next.settings } : base.settings,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function mergeImports(left: ImportKind[] | undefined, right: ImportKind[] | undefined): ImportKind[] | undefined {
|
|
260
|
+
const merged = [...(left ?? []), ...(right ?? [])];
|
|
261
|
+
if (merged.length === 0) return undefined;
|
|
262
|
+
return [...new Set(merged)];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function expandImports(config: McpConfig): McpConfig {
|
|
266
|
+
if (!config.imports?.length) return config;
|
|
267
|
+
|
|
268
|
+
const importedServers: Record<string, ServerEntry> = {};
|
|
269
|
+
for (const importKind of config.imports) {
|
|
270
|
+
const importPath = resolveImportPath(importKind);
|
|
271
|
+
if (!importPath) continue;
|
|
272
|
+
|
|
66
273
|
try {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
config.settings = { ...config.settings, ...validated.settings };
|
|
274
|
+
const imported = JSON.parse(readFileSync(importPath, "utf-8"));
|
|
275
|
+
const servers = extractServers(imported, importKind);
|
|
276
|
+
for (const [name, definition] of Object.entries(servers)) {
|
|
277
|
+
if (!importedServers[name]) {
|
|
278
|
+
importedServers[name] = definition;
|
|
279
|
+
}
|
|
74
280
|
}
|
|
75
281
|
} catch (error) {
|
|
76
|
-
console.warn(`Failed to
|
|
282
|
+
console.warn(`Failed to import MCP config from ${importKind}:`, error);
|
|
77
283
|
}
|
|
78
284
|
}
|
|
79
|
-
|
|
80
|
-
return
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
imports: config.imports,
|
|
288
|
+
settings: config.settings,
|
|
289
|
+
mcpServers: { ...importedServers, ...config.mcpServers },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveImportPath(importKind: ImportKind, cwd = process.cwd()): string | null {
|
|
294
|
+
const candidates = IMPORT_PATHS[importKind] ?? [];
|
|
295
|
+
for (const candidate of candidates) {
|
|
296
|
+
const fullPath = candidate.startsWith(".") ? resolve(cwd, candidate) : candidate;
|
|
297
|
+
if (existsSync(fullPath)) {
|
|
298
|
+
return fullPath;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function getImportServerCount(importKind: ImportKind, path: string): number {
|
|
305
|
+
try {
|
|
306
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
307
|
+
return Object.keys(extractServers(raw, importKind)).length;
|
|
308
|
+
} catch {
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function readValidatedConfig(path: string, label: string): McpConfig | null {
|
|
314
|
+
if (!existsSync(path)) return null;
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.warn(`Failed to load ${label}:`, error);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
81
322
|
}
|
|
82
323
|
|
|
83
324
|
function validateConfig(raw: unknown): McpConfig {
|
|
84
325
|
if (!raw || typeof raw !== "object") {
|
|
85
326
|
return { mcpServers: {} };
|
|
86
327
|
}
|
|
87
|
-
|
|
328
|
+
|
|
88
329
|
const obj = raw as Record<string, unknown>;
|
|
89
330
|
const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
|
|
90
|
-
|
|
91
|
-
// Must be a plain object, not an array or null
|
|
331
|
+
|
|
92
332
|
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
|
|
93
333
|
return { mcpServers: {} };
|
|
94
334
|
}
|
|
95
|
-
|
|
335
|
+
|
|
96
336
|
return {
|
|
97
337
|
mcpServers: servers as Record<string, ServerEntry>,
|
|
98
|
-
imports: Array.isArray(obj.imports) ? obj.imports as ImportKind[] : undefined,
|
|
338
|
+
imports: Array.isArray(obj.imports) ? (obj.imports as ImportKind[]) : undefined,
|
|
99
339
|
settings: obj.settings as McpSettings | undefined,
|
|
100
340
|
};
|
|
101
341
|
}
|
|
102
342
|
|
|
103
343
|
function extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {
|
|
104
344
|
if (!config || typeof config !== "object") return {};
|
|
105
|
-
|
|
345
|
+
|
|
106
346
|
const obj = config as Record<string, unknown>;
|
|
107
|
-
|
|
347
|
+
|
|
108
348
|
let servers: unknown;
|
|
109
349
|
switch (kind) {
|
|
110
350
|
case "claude-desktop":
|
|
@@ -120,56 +360,269 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
|
|
|
120
360
|
default:
|
|
121
361
|
return {};
|
|
122
362
|
}
|
|
123
|
-
|
|
363
|
+
|
|
124
364
|
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
125
365
|
return {};
|
|
126
366
|
}
|
|
127
|
-
|
|
367
|
+
|
|
128
368
|
return servers as Record<string, ServerEntry>;
|
|
129
369
|
}
|
|
130
370
|
|
|
371
|
+
function serializeRawConfig(raw: Record<string, unknown>): string {
|
|
372
|
+
return `${JSON.stringify(raw, null, 2)}\n`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildUnifiedDiff(beforeText: string, afterText: string): string {
|
|
376
|
+
if (beforeText === afterText) return "(no changes)";
|
|
377
|
+
|
|
378
|
+
const before = beforeText.split("\n");
|
|
379
|
+
const after = afterText.split("\n");
|
|
380
|
+
const rows = before.length;
|
|
381
|
+
const cols = after.length;
|
|
382
|
+
const lcs = Array.from({ length: rows + 1 }, () => Array<number>(cols + 1).fill(0));
|
|
383
|
+
|
|
384
|
+
for (let i = rows - 1; i >= 0; i--) {
|
|
385
|
+
for (let j = cols - 1; j >= 0; j--) {
|
|
386
|
+
lcs[i][j] = before[i] === after[j]
|
|
387
|
+
? lcs[i + 1][j + 1] + 1
|
|
388
|
+
: Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const lines: string[] = ["--- before", "+++ after"];
|
|
393
|
+
let i = 0;
|
|
394
|
+
let j = 0;
|
|
395
|
+
while (i < rows || j < cols) {
|
|
396
|
+
if (i < rows && j < cols && before[i] === after[j]) {
|
|
397
|
+
lines.push(` ${before[i]}`);
|
|
398
|
+
i++;
|
|
399
|
+
j++;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (j < cols && (i === rows || lcs[i][j + 1] >= lcs[i + 1][j])) {
|
|
403
|
+
lines.push(`+ ${after[j]}`);
|
|
404
|
+
j++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (i < rows) {
|
|
408
|
+
lines.push(`- ${before[i]}`);
|
|
409
|
+
i++;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return lines.join("\n");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function buildConfigWritePreview(filePath: string, nextRaw: Record<string, unknown>): ConfigWritePreview {
|
|
417
|
+
const existed = existsSync(filePath);
|
|
418
|
+
const beforeRaw = readRawConfigObject(filePath);
|
|
419
|
+
const beforeText = existed ? serializeRawConfig(beforeRaw) : "";
|
|
420
|
+
const afterText = serializeRawConfig(nextRaw);
|
|
421
|
+
return {
|
|
422
|
+
path: filePath,
|
|
423
|
+
existed,
|
|
424
|
+
changed: beforeText !== afterText,
|
|
425
|
+
beforeText,
|
|
426
|
+
afterText,
|
|
427
|
+
diffText: buildUnifiedDiff(beforeText, afterText),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function readRawConfigObject(filePath: string): Record<string, unknown> {
|
|
432
|
+
if (!existsSync(filePath)) return {};
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
436
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
437
|
+
} catch {
|
|
438
|
+
return {};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function writeRawConfigObject(filePath: string, raw: Record<string, unknown>): void {
|
|
443
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
444
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
445
|
+
writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
446
|
+
renameSync(tmpPath, filePath);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getServersObject(raw: Record<string, unknown>): Record<string, ServerEntry> {
|
|
450
|
+
const existing = raw.mcpServers ?? raw["mcp-servers"] ?? {};
|
|
451
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
452
|
+
return {};
|
|
453
|
+
}
|
|
454
|
+
return existing as Record<string, ServerEntry>;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function setServersObject(raw: Record<string, unknown>, servers: Record<string, ServerEntry>): void {
|
|
458
|
+
delete raw["mcp-servers"];
|
|
459
|
+
raw.mcpServers = servers;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function isRepoPromptServer(name: string, entry: ServerEntry): boolean {
|
|
463
|
+
const normalizedName = name.toLowerCase();
|
|
464
|
+
if (normalizedName.includes("repoprompt") || normalizedName === "rp") {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const command = entry.command?.toLowerCase() ?? "";
|
|
469
|
+
if (command.includes("repoprompt") || command.includes("rp-mcp") || command.endsWith("repoprompt_cli")) {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return (entry.args ?? []).some((arg) => typeof arg === "string" && arg.toLowerCase().includes("repoprompt"));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function findProjectRoot(cwd = process.cwd()): string | null {
|
|
477
|
+
let current = resolve(cwd);
|
|
478
|
+
while (true) {
|
|
479
|
+
if (
|
|
480
|
+
existsSync(join(current, ".git"))
|
|
481
|
+
|| existsSync(join(current, "package.json"))
|
|
482
|
+
|| existsSync(join(current, PROJECT_CONFIG_NAME))
|
|
483
|
+
|| existsSync(join(current, ".pi"))
|
|
484
|
+
) {
|
|
485
|
+
return current;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const parent = dirname(current);
|
|
489
|
+
if (parent === current) return null;
|
|
490
|
+
current = parent;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function buildRepoPromptEntry(executablePath: string): ServerEntry {
|
|
495
|
+
return {
|
|
496
|
+
command: executablePath,
|
|
497
|
+
args: [],
|
|
498
|
+
lifecycle: "lazy",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function detectRepoPrompt(summary: Omit<McpDiscoverySummary, "fingerprint" | "repoPrompt">, cwd = process.cwd()): RepoPromptDiscovery {
|
|
503
|
+
for (const source of summary.sources) {
|
|
504
|
+
if (source.kind !== "shared" || source.serverCount === 0) continue;
|
|
505
|
+
const config = readValidatedConfig(source.path, `MCP config from ${source.path}`);
|
|
506
|
+
if (!config) continue;
|
|
507
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
508
|
+
if (isRepoPromptServer(name, entry)) {
|
|
509
|
+
return { configured: true, configuredPath: source.path };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const executablePath = REPOPROMPT_BINARY_CANDIDATES.find((candidate) => existsSync(candidate));
|
|
515
|
+
if (!executablePath) {
|
|
516
|
+
return { configured: false };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const projectRoot = findProjectRoot(cwd);
|
|
520
|
+
const targetPath = projectRoot ? join(projectRoot, PROJECT_CONFIG_NAME) : GENERIC_GLOBAL_CONFIG_PATH;
|
|
521
|
+
return {
|
|
522
|
+
configured: false,
|
|
523
|
+
executablePath,
|
|
524
|
+
targetPath,
|
|
525
|
+
serverName: "repoprompt",
|
|
526
|
+
entry: buildRepoPromptEntry(executablePath),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function previewCompatibilityImports(importKinds: ImportKind[], overridePath?: string): ConfigWritePreview {
|
|
531
|
+
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
532
|
+
const raw = readRawConfigObject(targetPath);
|
|
533
|
+
const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value): value is ImportKind => typeof value === "string") : [];
|
|
534
|
+
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
535
|
+
const nextRaw = { ...raw, imports: merged };
|
|
536
|
+
setServersObject(nextRaw, getServersObject(nextRaw));
|
|
537
|
+
return buildConfigWritePreview(targetPath, nextRaw);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function ensureCompatibilityImports(importKinds: ImportKind[], overridePath?: string): { path: string; added: ImportKind[] } {
|
|
541
|
+
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
542
|
+
const raw = readRawConfigObject(targetPath);
|
|
543
|
+
const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value): value is ImportKind => typeof value === "string") : [];
|
|
544
|
+
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
545
|
+
const added = merged.filter((kind) => !currentImports.includes(kind));
|
|
546
|
+
if (added.length === 0) {
|
|
547
|
+
return { path: targetPath, added: [] };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
raw.imports = merged;
|
|
551
|
+
const servers = getServersObject(raw);
|
|
552
|
+
setServersObject(raw, servers);
|
|
553
|
+
writeRawConfigObject(targetPath, raw);
|
|
554
|
+
return { path: targetPath, added };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export function buildStarterProjectConfig(): McpConfig {
|
|
558
|
+
return {
|
|
559
|
+
mcpServers: {},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function previewStarterProjectConfig(cwd = process.cwd()): ConfigWritePreview {
|
|
564
|
+
const targetPath = getProjectConfigPath(cwd);
|
|
565
|
+
const nextRaw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
566
|
+
return buildConfigWritePreview(targetPath, nextRaw);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function writeStarterProjectConfig(cwd = process.cwd()): string {
|
|
570
|
+
const targetPath = getProjectConfigPath(cwd);
|
|
571
|
+
const raw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
572
|
+
writeRawConfigObject(targetPath, raw);
|
|
573
|
+
return targetPath;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function previewSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): ConfigWritePreview {
|
|
577
|
+
const raw = readRawConfigObject(filePath);
|
|
578
|
+
const nextRaw = { ...raw };
|
|
579
|
+
const servers = getServersObject(nextRaw);
|
|
580
|
+
servers[serverName] = entry;
|
|
581
|
+
setServersObject(nextRaw, servers);
|
|
582
|
+
return buildConfigWritePreview(filePath, nextRaw);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function writeSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): string {
|
|
586
|
+
const raw = readRawConfigObject(filePath);
|
|
587
|
+
const servers = getServersObject(raw);
|
|
588
|
+
servers[serverName] = entry;
|
|
589
|
+
setServersObject(raw, servers);
|
|
590
|
+
writeRawConfigObject(filePath, raw);
|
|
591
|
+
return filePath;
|
|
592
|
+
}
|
|
593
|
+
|
|
131
594
|
export function getServerProvenance(overridePath?: string): Map<string, ServerProvenance> {
|
|
132
595
|
const provenance = new Map<string, ServerProvenance>();
|
|
133
|
-
const userPath =
|
|
596
|
+
const userPath = getPiGlobalConfigPath(overridePath);
|
|
134
597
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
: importPath;
|
|
152
|
-
if (!existsSync(fullPath)) continue;
|
|
153
|
-
try {
|
|
154
|
-
const imported = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
155
|
-
const servers = extractServers(imported, importKind);
|
|
156
|
-
for (const name of Object.keys(servers)) {
|
|
157
|
-
if (!provenance.has(name)) {
|
|
158
|
-
provenance.set(name, { path: userPath, kind: "import", importKind });
|
|
598
|
+
for (const source of getConfigSources(overridePath)) {
|
|
599
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
600
|
+
if (!loaded) continue;
|
|
601
|
+
|
|
602
|
+
if (loaded.imports?.length) {
|
|
603
|
+
for (const importKind of loaded.imports) {
|
|
604
|
+
const importPath = resolveImportPath(importKind);
|
|
605
|
+
if (!importPath) continue;
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const imported = JSON.parse(readFileSync(importPath, "utf-8"));
|
|
609
|
+
const servers = extractServers(imported, importKind);
|
|
610
|
+
for (const name of Object.keys(servers)) {
|
|
611
|
+
if (!provenance.has(name)) {
|
|
612
|
+
provenance.set(name, { path: userPath, kind: "import", importKind });
|
|
613
|
+
}
|
|
159
614
|
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
615
|
+
} catch {}
|
|
616
|
+
}
|
|
162
617
|
}
|
|
163
|
-
}
|
|
164
618
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
} catch {}
|
|
619
|
+
for (const name of Object.keys(loaded.mcpServers)) {
|
|
620
|
+
provenance.set(name, {
|
|
621
|
+
path: source.writePath,
|
|
622
|
+
kind: source.kind,
|
|
623
|
+
importKind: source.importKind,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
173
626
|
}
|
|
174
627
|
|
|
175
628
|
return provenance;
|
|
@@ -193,16 +646,8 @@ export function writeDirectToolsConfig(
|
|
|
193
646
|
}
|
|
194
647
|
|
|
195
648
|
for (const [filePath, entries] of byPath) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
200
|
-
} catch {}
|
|
201
|
-
}
|
|
202
|
-
if (!raw || typeof raw !== "object") raw = {};
|
|
203
|
-
|
|
204
|
-
const servers = (raw.mcpServers ?? raw["mcp-servers"] ?? {}) as Record<string, ServerEntry>;
|
|
205
|
-
if (typeof servers !== "object" || Array.isArray(servers)) continue;
|
|
649
|
+
const raw = readRawConfigObject(filePath);
|
|
650
|
+
const servers = getServersObject(raw);
|
|
206
651
|
|
|
207
652
|
for (const { name, value, prov } of entries) {
|
|
208
653
|
if (prov.kind === "import") {
|
|
@@ -215,12 +660,7 @@ export function writeDirectToolsConfig(
|
|
|
215
660
|
}
|
|
216
661
|
}
|
|
217
662
|
|
|
218
|
-
|
|
219
|
-
raw
|
|
220
|
-
|
|
221
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
222
|
-
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
223
|
-
writeFileSync(tmpPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
224
|
-
renameSync(tmpPath, filePath);
|
|
663
|
+
setServersObject(raw, servers);
|
|
664
|
+
writeRawConfigObject(filePath, raw);
|
|
225
665
|
}
|
|
226
666
|
}
|