link-agents 0.9.0
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/AGENTS.md +127 -0
- package/README.md +93 -0
- package/cursor-rules-notes.md +23 -0
- package/dist/cli/interactive.d.ts +9 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +1176 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/options.d.ts +3 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +107 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/cli/options.spec.d.ts +2 -0
- package/dist/cli/options.spec.d.ts.map +1 -0
- package/dist/cli/options.spec.js +74 -0
- package/dist/cli/options.spec.js.map +1 -0
- package/dist/clients/definitions.d.ts +5 -0
- package/dist/clients/definitions.d.ts.map +1 -0
- package/dist/clients/definitions.js +82 -0
- package/dist/clients/definitions.js.map +1 -0
- package/dist/clients/definitions.spec.d.ts +2 -0
- package/dist/clients/definitions.spec.d.ts.map +1 -0
- package/dist/clients/definitions.spec.js +135 -0
- package/dist/clients/definitions.spec.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +81 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/restore.d.ts +3 -0
- package/dist/commands/restore.d.ts.map +1 -0
- package/dist/commands/restore.js +36 -0
- package/dist/commands/restore.js.map +1 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +193 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +98 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/apply.d.ts +22 -0
- package/dist/utils/apply.d.ts.map +1 -0
- package/dist/utils/apply.js +215 -0
- package/dist/utils/apply.js.map +1 -0
- package/dist/utils/bootstrap.d.ts +18 -0
- package/dist/utils/bootstrap.d.ts.map +1 -0
- package/dist/utils/bootstrap.js +31 -0
- package/dist/utils/bootstrap.js.map +1 -0
- package/dist/utils/bootstrap.spec.d.ts +2 -0
- package/dist/utils/bootstrap.spec.d.ts.map +1 -0
- package/dist/utils/bootstrap.spec.js +92 -0
- package/dist/utils/bootstrap.spec.js.map +1 -0
- package/dist/utils/canonical.d.ts +17 -0
- package/dist/utils/canonical.d.ts.map +1 -0
- package/dist/utils/canonical.js +136 -0
- package/dist/utils/canonical.js.map +1 -0
- package/dist/utils/canonicalState.d.ts +19 -0
- package/dist/utils/canonicalState.d.ts.map +1 -0
- package/dist/utils/canonicalState.js +21 -0
- package/dist/utils/canonicalState.js.map +1 -0
- package/dist/utils/cursorHistory.d.ts +7 -0
- package/dist/utils/cursorHistory.d.ts.map +1 -0
- package/dist/utils/cursorHistory.js +54 -0
- package/dist/utils/cursorHistory.js.map +1 -0
- package/dist/utils/cursorPaths.d.ts +3 -0
- package/dist/utils/cursorPaths.d.ts.map +1 -0
- package/dist/utils/cursorPaths.js +17 -0
- package/dist/utils/cursorPaths.js.map +1 -0
- package/dist/utils/discovery.d.ts +8 -0
- package/dist/utils/discovery.d.ts.map +1 -0
- package/dist/utils/discovery.js +93 -0
- package/dist/utils/discovery.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +32 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +263 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/frontmatter.spec.d.ts +2 -0
- package/dist/utils/frontmatter.spec.d.ts.map +1 -0
- package/dist/utils/frontmatter.spec.js +264 -0
- package/dist/utils/frontmatter.spec.js.map +1 -0
- package/dist/utils/fs.d.ts +27 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +137 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/fs.spec.d.ts +2 -0
- package/dist/utils/fs.spec.d.ts.map +1 -0
- package/dist/utils/fs.spec.js +73 -0
- package/dist/utils/fs.spec.js.map +1 -0
- package/dist/utils/gitignore.d.ts +10 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +63 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.d.ts.map +1 -0
- package/dist/utils/manifest.js +89 -0
- package/dist/utils/manifest.js.map +1 -0
- package/dist/utils/mcp.d.ts +73 -0
- package/dist/utils/mcp.d.ts.map +1 -0
- package/dist/utils/mcp.js +529 -0
- package/dist/utils/mcp.js.map +1 -0
- package/dist/utils/mcp.spec.d.ts +2 -0
- package/dist/utils/mcp.spec.d.ts.map +1 -0
- package/dist/utils/mcp.spec.js +488 -0
- package/dist/utils/mcp.spec.js.map +1 -0
- package/dist/utils/merge.d.ts +17 -0
- package/dist/utils/merge.d.ts.map +1 -0
- package/dist/utils/merge.js +45 -0
- package/dist/utils/merge.js.map +1 -0
- package/dist/utils/merge.spec.d.ts +2 -0
- package/dist/utils/merge.spec.d.ts.map +1 -0
- package/dist/utils/merge.spec.js +134 -0
- package/dist/utils/merge.spec.js.map +1 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +164 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/paths.spec.d.ts +2 -0
- package/dist/utils/paths.spec.d.ts.map +1 -0
- package/dist/utils/paths.spec.js +282 -0
- package/dist/utils/paths.spec.js.map +1 -0
- package/dist/utils/plan.d.ts +7 -0
- package/dist/utils/plan.d.ts.map +1 -0
- package/dist/utils/plan.js +118 -0
- package/dist/utils/plan.js.map +1 -0
- package/dist/utils/plan.spec.d.ts +2 -0
- package/dist/utils/plan.spec.d.ts.map +1 -0
- package/dist/utils/plan.spec.js +420 -0
- package/dist/utils/plan.spec.js.map +1 -0
- package/dist/utils/reporting.d.ts +21 -0
- package/dist/utils/reporting.d.ts.map +1 -0
- package/dist/utils/reporting.js +82 -0
- package/dist/utils/reporting.js.map +1 -0
- package/dist/utils/reporting.spec.d.ts +2 -0
- package/dist/utils/reporting.spec.d.ts.map +1 -0
- package/dist/utils/reporting.spec.js +78 -0
- package/dist/utils/reporting.spec.js.map +1 -0
- package/dist/utils/reset.d.ts +14 -0
- package/dist/utils/reset.d.ts.map +1 -0
- package/dist/utils/reset.js +81 -0
- package/dist/utils/reset.js.map +1 -0
- package/dist/utils/revert.d.ts +30 -0
- package/dist/utils/revert.d.ts.map +1 -0
- package/dist/utils/revert.js +89 -0
- package/dist/utils/revert.js.map +1 -0
- package/dist/utils/revert.spec.d.ts +2 -0
- package/dist/utils/revert.spec.d.ts.map +1 -0
- package/dist/utils/revert.spec.js +102 -0
- package/dist/utils/revert.spec.js.map +1 -0
- package/dist/utils/similarity.d.ts +14 -0
- package/dist/utils/similarity.d.ts.map +1 -0
- package/dist/utils/similarity.js +70 -0
- package/dist/utils/similarity.js.map +1 -0
- package/dist/utils/similarity.spec.d.ts +2 -0
- package/dist/utils/similarity.spec.d.ts.map +1 -0
- package/dist/utils/similarity.spec.js +62 -0
- package/dist/utils/similarity.spec.js.map +1 -0
- package/dist/utils/snapshots.d.ts +21 -0
- package/dist/utils/snapshots.d.ts.map +1 -0
- package/dist/utils/snapshots.js +81 -0
- package/dist/utils/snapshots.js.map +1 -0
- package/dist/utils/snapshots.spec.d.ts +2 -0
- package/dist/utils/snapshots.spec.d.ts.map +1 -0
- package/dist/utils/snapshots.spec.js +56 -0
- package/dist/utils/snapshots.spec.js.map +1 -0
- package/dist/utils/syncFilters.d.ts +3 -0
- package/dist/utils/syncFilters.d.ts.map +1 -0
- package/dist/utils/syncFilters.js +8 -0
- package/dist/utils/syncFilters.js.map +1 -0
- package/dist/utils/syncRuntime.d.ts +3 -0
- package/dist/utils/syncRuntime.d.ts.map +1 -0
- package/dist/utils/syncRuntime.js +31 -0
- package/dist/utils/syncRuntime.js.map +1 -0
- package/dist/utils/validation.d.ts +3 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +19 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/validation.spec.d.ts +2 -0
- package/dist/utils/validation.spec.d.ts.map +1 -0
- package/dist/utils/validation.spec.js +36 -0
- package/dist/utils/validation.spec.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { fileExists, readFileSafe } from "./fs.js";
|
|
5
|
+
const MANIFEST_DIR = path.join(os.homedir(), ".link-agents");
|
|
6
|
+
const MANIFEST_PATH = path.join(MANIFEST_DIR, "manifest.json");
|
|
7
|
+
function createEmptyManifest() {
|
|
8
|
+
return {
|
|
9
|
+
version: 1,
|
|
10
|
+
lastSync: new Date().toISOString(),
|
|
11
|
+
generatedFiles: [],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function readManifest() {
|
|
15
|
+
const content = await readFileSafe(MANIFEST_PATH);
|
|
16
|
+
if (!content) {
|
|
17
|
+
return createEmptyManifest();
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(content);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return createEmptyManifest();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function writeManifest(manifest) {
|
|
27
|
+
await fs.mkdir(MANIFEST_DIR, { recursive: true });
|
|
28
|
+
await fs.writeFile(MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf8");
|
|
29
|
+
}
|
|
30
|
+
export async function updateManifest(generatedFiles) {
|
|
31
|
+
const manifest = {
|
|
32
|
+
version: 1,
|
|
33
|
+
lastSync: new Date().toISOString(),
|
|
34
|
+
generatedFiles: [...new Set(generatedFiles)].sort(),
|
|
35
|
+
};
|
|
36
|
+
await writeManifest(manifest);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find files that were in the previous manifest but not in the current plan.
|
|
40
|
+
* These are "stale" files that should be cleaned up.
|
|
41
|
+
*/
|
|
42
|
+
export async function findStaleFiles(currentFiles) {
|
|
43
|
+
const manifest = await readManifest();
|
|
44
|
+
const currentSet = new Set(currentFiles);
|
|
45
|
+
const staleFiles = [];
|
|
46
|
+
for (const file of manifest.generatedFiles) {
|
|
47
|
+
if (!currentSet.has(file) && (await fileExists(file))) {
|
|
48
|
+
staleFiles.push(file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return staleFiles;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Remove stale files that are no longer in the sync plan.
|
|
55
|
+
* Returns list of removed files.
|
|
56
|
+
*/
|
|
57
|
+
export async function pruneStaleFiles(currentFiles) {
|
|
58
|
+
const staleFiles = await findStaleFiles(currentFiles);
|
|
59
|
+
const removed = [];
|
|
60
|
+
for (const file of staleFiles) {
|
|
61
|
+
try {
|
|
62
|
+
await fs.unlink(file);
|
|
63
|
+
removed.push(file);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// File may have been manually deleted
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return removed;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Clear the manifest (used by reset command).
|
|
73
|
+
*/
|
|
74
|
+
export async function clearManifest() {
|
|
75
|
+
try {
|
|
76
|
+
await fs.unlink(MANIFEST_PATH);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// File doesn't exist
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get all files from manifest (for reset command).
|
|
84
|
+
*/
|
|
85
|
+
export async function getManifestFiles() {
|
|
86
|
+
const manifest = await readManifest();
|
|
87
|
+
return manifest.generatedFiles;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/utils/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AssetContent } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Obfuscate an env value if it appears to be a secret
|
|
4
|
+
* Returns the original value if not a secret, obfuscated version otherwise
|
|
5
|
+
*/
|
|
6
|
+
export declare function obfuscateEnvValue(key: string, value: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Format env vars for display, obfuscating secrets
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatEnvForDisplay(env: Record<string, string> | undefined): string;
|
|
11
|
+
/**
|
|
12
|
+
* Compare two server configs and return differences
|
|
13
|
+
*/
|
|
14
|
+
export declare function compareServerConfigs(a: McpServerConfig, b: McpServerConfig): {
|
|
15
|
+
same: boolean;
|
|
16
|
+
differences: string[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Normalized MCP config structure
|
|
20
|
+
*/
|
|
21
|
+
export interface McpConfig {
|
|
22
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface McpServerConfig {
|
|
26
|
+
command?: string;
|
|
27
|
+
args?: string[];
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export type McpFormat = "json" | "jsonc" | "toml" | "yaml" | "unknown";
|
|
32
|
+
/**
|
|
33
|
+
* Detect config format from file extension
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectMcpFormat(filePath: string): McpFormat;
|
|
36
|
+
/**
|
|
37
|
+
* Parse MCP config from various formats
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseMcpConfig(content: string, format: McpFormat): McpConfig | null;
|
|
40
|
+
/**
|
|
41
|
+
* Serialize MCP config to target format
|
|
42
|
+
*/
|
|
43
|
+
export declare function serializeMcpConfig(config: McpConfig, format: McpFormat, indent?: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Merge multiple MCP configs at entry level
|
|
46
|
+
* Later entries override earlier ones for the same server key
|
|
47
|
+
*/
|
|
48
|
+
export declare function mergeMcpConfigs(configs: McpConfig[]): McpConfig;
|
|
49
|
+
/**
|
|
50
|
+
* Merge MCP assets and serialize to target format
|
|
51
|
+
*/
|
|
52
|
+
export declare function mergeMcpAssets(assets: AssetContent[]): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Validation result for MCP config
|
|
55
|
+
*/
|
|
56
|
+
export interface McpValidationResult {
|
|
57
|
+
valid: boolean;
|
|
58
|
+
errors: string[];
|
|
59
|
+
warnings: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate MCP config content
|
|
63
|
+
*/
|
|
64
|
+
export declare function validateMcpConfig(content: string, format: McpFormat): McpValidationResult;
|
|
65
|
+
/**
|
|
66
|
+
* Get list of commands used in MCP config
|
|
67
|
+
*/
|
|
68
|
+
export declare function getMcpCommands(config: McpConfig): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Find servers that exist in target but not in source (would be removed)
|
|
71
|
+
*/
|
|
72
|
+
export declare function findRemovedServers(sourceConfig: McpConfig, targetConfig: McpConfig): string[];
|
|
73
|
+
//# sourceMappingURL=mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/utils/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAyEtD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GACtC,MAAM,CAYR;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,eAAe,EAClB,CAAC,EAAE,eAAe,GACjB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CA6C1C;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEvE;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAO3D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,SAAS,GAChB,SAAS,GAAG,IAAI,CAqBlB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,SAAS,EACjB,MAAM,SAAI,GACT,MAAM,CAYR;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,SAAS,CAqB/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,GAAG,IAAI,CA4BpE;AAwRD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,SAAS,GAChB,mBAAmB,CAkCrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,CAc1D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,SAAS,EACvB,YAAY,EAAE,SAAS,GACtB,MAAM,EAAE,CAKV"}
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret key name patterns (case-insensitive)
|
|
3
|
+
*/
|
|
4
|
+
const SECRET_KEY_PATTERNS = [
|
|
5
|
+
/key/i,
|
|
6
|
+
/token/i,
|
|
7
|
+
/secret/i,
|
|
8
|
+
/password/i,
|
|
9
|
+
/credential/i,
|
|
10
|
+
/auth/i,
|
|
11
|
+
/private/i,
|
|
12
|
+
/access/i,
|
|
13
|
+
/api_/i,
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Secret value patterns
|
|
17
|
+
*/
|
|
18
|
+
const SECRET_VALUE_PATTERNS = [
|
|
19
|
+
/^sk-/, // OpenAI
|
|
20
|
+
/^pk-/, // OpenAI public
|
|
21
|
+
/^ghp_/, // GitHub PAT
|
|
22
|
+
/^gho_/, // GitHub OAuth
|
|
23
|
+
/^ghs_/, // GitHub App
|
|
24
|
+
/^ghu_/, // GitHub user-to-server
|
|
25
|
+
/^github_pat_/, // GitHub fine-grained PAT
|
|
26
|
+
/^xox[baprs]-/, // Slack tokens
|
|
27
|
+
/^Bearer\s/i, // Bearer tokens
|
|
28
|
+
/^Basic\s/i, // Basic auth
|
|
29
|
+
/^AKIA/, // AWS access key
|
|
30
|
+
/^eyJ/, // JWT tokens (base64 JSON)
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Check if a key name suggests it contains a secret
|
|
34
|
+
*/
|
|
35
|
+
function isSecretKey(key) {
|
|
36
|
+
return SECRET_KEY_PATTERNS.some((pattern) => pattern.test(key));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if a value looks like a secret
|
|
40
|
+
*/
|
|
41
|
+
function isSecretValue(value) {
|
|
42
|
+
// Check known patterns
|
|
43
|
+
if (SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
// Long alphanumeric strings (32+ chars) are likely secrets
|
|
47
|
+
if (value.length >= 32 && /^[A-Za-z0-9_-]+$/.test(value)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Obfuscate a value, showing only prefix and suffix
|
|
54
|
+
*/
|
|
55
|
+
function obfuscateValue(value) {
|
|
56
|
+
if (value.length <= 8) {
|
|
57
|
+
return "[hidden]";
|
|
58
|
+
}
|
|
59
|
+
const prefixLen = Math.min(4, Math.floor(value.length / 4));
|
|
60
|
+
const suffixLen = Math.min(3, Math.floor(value.length / 4));
|
|
61
|
+
return `${value.slice(0, prefixLen)}...${value.slice(-suffixLen)}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Obfuscate an env value if it appears to be a secret
|
|
65
|
+
* Returns the original value if not a secret, obfuscated version otherwise
|
|
66
|
+
*/
|
|
67
|
+
export function obfuscateEnvValue(key, value) {
|
|
68
|
+
if (isSecretKey(key) || isSecretValue(value)) {
|
|
69
|
+
return obfuscateValue(value);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Format env vars for display, obfuscating secrets
|
|
75
|
+
*/
|
|
76
|
+
export function formatEnvForDisplay(env) {
|
|
77
|
+
if (!env || Object.keys(env).length === 0) {
|
|
78
|
+
return "no env";
|
|
79
|
+
}
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (const [key, value] of Object.entries(env)) {
|
|
82
|
+
const displayValue = obfuscateEnvValue(key, value);
|
|
83
|
+
parts.push(`${key}=${displayValue}`);
|
|
84
|
+
}
|
|
85
|
+
return parts.join(", ");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Compare two server configs and return differences
|
|
89
|
+
*/
|
|
90
|
+
export function compareServerConfigs(a, b) {
|
|
91
|
+
const differences = [];
|
|
92
|
+
// Compare command
|
|
93
|
+
if (a.command !== b.command) {
|
|
94
|
+
differences.push(`command: "${a.command}" vs "${b.command}"`);
|
|
95
|
+
}
|
|
96
|
+
// Compare args
|
|
97
|
+
const argsA = JSON.stringify(a.args ?? []);
|
|
98
|
+
const argsB = JSON.stringify(b.args ?? []);
|
|
99
|
+
if (argsA !== argsB) {
|
|
100
|
+
differences.push(`args differ`);
|
|
101
|
+
}
|
|
102
|
+
// Compare env
|
|
103
|
+
const envA = a.env ?? {};
|
|
104
|
+
const envB = b.env ?? {};
|
|
105
|
+
const allEnvKeys = new Set([...Object.keys(envA), ...Object.keys(envB)]);
|
|
106
|
+
for (const key of allEnvKeys) {
|
|
107
|
+
const valA = envA[key];
|
|
108
|
+
const valB = envB[key];
|
|
109
|
+
if (valA !== valB) {
|
|
110
|
+
if (valA === undefined) {
|
|
111
|
+
differences.push(`${key}: [missing] vs ${obfuscateEnvValue(key, valB)}`);
|
|
112
|
+
}
|
|
113
|
+
else if (valB === undefined) {
|
|
114
|
+
differences.push(`${key}: ${obfuscateEnvValue(key, valA)} vs [missing]`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
differences.push(`${key}: ${obfuscateEnvValue(key, valA)} vs ${obfuscateEnvValue(key, valB)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
same: differences.length === 0,
|
|
123
|
+
differences,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Detect config format from file extension
|
|
128
|
+
*/
|
|
129
|
+
export function detectMcpFormat(filePath) {
|
|
130
|
+
const lower = filePath.toLowerCase();
|
|
131
|
+
if (lower.endsWith(".json"))
|
|
132
|
+
return "json";
|
|
133
|
+
if (lower.endsWith(".jsonc"))
|
|
134
|
+
return "jsonc";
|
|
135
|
+
if (lower.endsWith(".toml"))
|
|
136
|
+
return "toml";
|
|
137
|
+
if (lower.endsWith(".yaml") || lower.endsWith(".yml"))
|
|
138
|
+
return "yaml";
|
|
139
|
+
return "unknown";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse MCP config from various formats
|
|
143
|
+
*/
|
|
144
|
+
export function parseMcpConfig(content, format) {
|
|
145
|
+
// Handle empty content
|
|
146
|
+
if (!content.trim()) {
|
|
147
|
+
return { mcpServers: {} };
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
switch (format) {
|
|
151
|
+
case "json":
|
|
152
|
+
case "jsonc":
|
|
153
|
+
return parseJsonWithComments(content);
|
|
154
|
+
case "toml":
|
|
155
|
+
return parseToml(content);
|
|
156
|
+
case "yaml":
|
|
157
|
+
return parseYaml(content);
|
|
158
|
+
default:
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Serialize MCP config to target format
|
|
168
|
+
*/
|
|
169
|
+
export function serializeMcpConfig(config, format, indent = 2) {
|
|
170
|
+
switch (format) {
|
|
171
|
+
case "json":
|
|
172
|
+
case "jsonc":
|
|
173
|
+
return JSON.stringify(config, null, indent);
|
|
174
|
+
case "toml":
|
|
175
|
+
return serializeToml(config, indent);
|
|
176
|
+
case "yaml":
|
|
177
|
+
return serializeYaml(config, indent);
|
|
178
|
+
default:
|
|
179
|
+
return JSON.stringify(config, null, indent);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Merge multiple MCP configs at entry level
|
|
184
|
+
* Later entries override earlier ones for the same server key
|
|
185
|
+
*/
|
|
186
|
+
export function mergeMcpConfigs(configs) {
|
|
187
|
+
const merged = { mcpServers: {} };
|
|
188
|
+
for (const config of configs) {
|
|
189
|
+
// Merge mcpServers
|
|
190
|
+
if (config.mcpServers) {
|
|
191
|
+
merged.mcpServers = {
|
|
192
|
+
...merged.mcpServers,
|
|
193
|
+
...config.mcpServers,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Merge other top-level keys
|
|
197
|
+
for (const [key, value] of Object.entries(config)) {
|
|
198
|
+
if (key !== "mcpServers") {
|
|
199
|
+
merged[key] = value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return merged;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Merge MCP assets and serialize to target format
|
|
207
|
+
*/
|
|
208
|
+
export function mergeMcpAssets(assets) {
|
|
209
|
+
if (assets.length === 0)
|
|
210
|
+
return null;
|
|
211
|
+
if (assets.length === 1)
|
|
212
|
+
return assets[0].content;
|
|
213
|
+
const configs = [];
|
|
214
|
+
let targetFormat = "json";
|
|
215
|
+
// Parse all configs
|
|
216
|
+
for (const asset of assets) {
|
|
217
|
+
const format = detectMcpFormat(asset.path);
|
|
218
|
+
const parsed = parseMcpConfig(asset.content, format);
|
|
219
|
+
if (parsed) {
|
|
220
|
+
configs.push(parsed);
|
|
221
|
+
// Use format of first asset as target
|
|
222
|
+
if (configs.length === 1) {
|
|
223
|
+
targetFormat = format;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (configs.length === 0) {
|
|
228
|
+
// Fallback: concatenate if parsing fails
|
|
229
|
+
return assets.map((a) => a.content).join("\n\n---\n\n");
|
|
230
|
+
}
|
|
231
|
+
// Merge and serialize
|
|
232
|
+
const merged = mergeMcpConfigs(configs);
|
|
233
|
+
return serializeMcpConfig(merged, targetFormat);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parse JSON with comments (JSONC format)
|
|
237
|
+
* Handles // and /* comments while preserving URLs inside strings
|
|
238
|
+
*/
|
|
239
|
+
function parseJsonWithComments(content) {
|
|
240
|
+
// Remove comments while preserving string contents
|
|
241
|
+
// Strategy: tokenize strings first, then remove comments from non-string parts
|
|
242
|
+
const result = [];
|
|
243
|
+
let i = 0;
|
|
244
|
+
while (i < content.length) {
|
|
245
|
+
const char = content[i];
|
|
246
|
+
// Handle string literals - copy them verbatim
|
|
247
|
+
if (char === '"') {
|
|
248
|
+
const start = i;
|
|
249
|
+
i++; // skip opening quote
|
|
250
|
+
while (i < content.length) {
|
|
251
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
252
|
+
i += 2; // skip escaped char
|
|
253
|
+
}
|
|
254
|
+
else if (content[i] === '"') {
|
|
255
|
+
i++; // skip closing quote
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
i++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
result.push(content.slice(start, i));
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Handle single-line comments
|
|
266
|
+
if (char === "/" && content[i + 1] === "/") {
|
|
267
|
+
// Skip until end of line
|
|
268
|
+
while (i < content.length && content[i] !== "\n") {
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Handle multi-line comments
|
|
274
|
+
if (char === "/" && content[i + 1] === "*") {
|
|
275
|
+
i += 2; // skip /*
|
|
276
|
+
while (i < content.length &&
|
|
277
|
+
!(content[i] === "*" && content[i + 1] === "/")) {
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
i += 2; // skip */
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// Regular character
|
|
284
|
+
result.push(char);
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
return JSON.parse(result.join(""));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Parse TOML (basic implementation)
|
|
291
|
+
* For production, consider using a library like @iarna/toml
|
|
292
|
+
*/
|
|
293
|
+
function parseToml(content) {
|
|
294
|
+
const config = { mcpServers: {} };
|
|
295
|
+
const lines = content.split("\n");
|
|
296
|
+
let currentSection = null;
|
|
297
|
+
let currentServer = null;
|
|
298
|
+
for (let line of lines) {
|
|
299
|
+
line = line.trim();
|
|
300
|
+
if (!line || line.startsWith("#"))
|
|
301
|
+
continue;
|
|
302
|
+
// Section header [mcpServers.servername]
|
|
303
|
+
const sectionMatch = line.match(/^\[mcpServers\.(.+)\]$/);
|
|
304
|
+
if (sectionMatch) {
|
|
305
|
+
currentSection = sectionMatch[1];
|
|
306
|
+
currentServer = {};
|
|
307
|
+
config.mcpServers[currentSection] = currentServer;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
// Key-value pair
|
|
311
|
+
const kvMatch = line.match(/^(\w+)\s*=\s*(.+)$/);
|
|
312
|
+
if (kvMatch && currentServer) {
|
|
313
|
+
const [, key, value] = kvMatch;
|
|
314
|
+
currentServer[key] = parseTomlValue(value);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return config;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Parse TOML value
|
|
321
|
+
*/
|
|
322
|
+
function parseTomlValue(value) {
|
|
323
|
+
value = value.trim();
|
|
324
|
+
// String
|
|
325
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
326
|
+
return value.slice(1, -1);
|
|
327
|
+
}
|
|
328
|
+
// Array
|
|
329
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
330
|
+
const items = value
|
|
331
|
+
.slice(1, -1)
|
|
332
|
+
.split(",")
|
|
333
|
+
.map((v) => parseTomlValue(v));
|
|
334
|
+
return items;
|
|
335
|
+
}
|
|
336
|
+
// Boolean
|
|
337
|
+
if (value === "true")
|
|
338
|
+
return true;
|
|
339
|
+
if (value === "false")
|
|
340
|
+
return false;
|
|
341
|
+
// Number
|
|
342
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
343
|
+
return parseFloat(value);
|
|
344
|
+
}
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Serialize to TOML
|
|
349
|
+
*/
|
|
350
|
+
function serializeToml(config, indent) {
|
|
351
|
+
let output = "";
|
|
352
|
+
if (config.mcpServers) {
|
|
353
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
354
|
+
output += `[mcpServers.${serverName}]\n`;
|
|
355
|
+
for (const [key, value] of Object.entries(serverConfig)) {
|
|
356
|
+
output += `${key} = ${serializeTomlValue(value)}\n`;
|
|
357
|
+
}
|
|
358
|
+
output += "\n";
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return output.trim();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Serialize TOML value
|
|
365
|
+
*/
|
|
366
|
+
function serializeTomlValue(value) {
|
|
367
|
+
if (typeof value === "string") {
|
|
368
|
+
return `"${value}"`;
|
|
369
|
+
}
|
|
370
|
+
if (Array.isArray(value)) {
|
|
371
|
+
return `[${value.map(serializeTomlValue).join(", ")}]`;
|
|
372
|
+
}
|
|
373
|
+
if (typeof value === "object" && value !== null) {
|
|
374
|
+
return JSON.stringify(value);
|
|
375
|
+
}
|
|
376
|
+
return String(value);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Parse YAML (basic implementation)
|
|
380
|
+
* For production, consider using a library like js-yaml
|
|
381
|
+
*/
|
|
382
|
+
function parseYaml(content) {
|
|
383
|
+
const config = { mcpServers: {} };
|
|
384
|
+
const lines = content.split("\n");
|
|
385
|
+
let currentKey = null;
|
|
386
|
+
let currentServer = null;
|
|
387
|
+
let indent = 0;
|
|
388
|
+
for (let line of lines) {
|
|
389
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
390
|
+
continue;
|
|
391
|
+
const leadingSpaces = line.length - line.trimStart().length;
|
|
392
|
+
line = line.trim();
|
|
393
|
+
// mcpServers section
|
|
394
|
+
if (line === "mcpServers:") {
|
|
395
|
+
indent = leadingSpaces;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
// Server name
|
|
399
|
+
if (leadingSpaces === indent + 2 && line.endsWith(":")) {
|
|
400
|
+
currentKey = line.slice(0, -1);
|
|
401
|
+
currentServer = {};
|
|
402
|
+
config.mcpServers[currentKey] = currentServer;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
// Server properties
|
|
406
|
+
const kvMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
407
|
+
if (kvMatch && currentServer) {
|
|
408
|
+
const [, key, value] = kvMatch;
|
|
409
|
+
currentServer[key] = parseYamlValue(value);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return config;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Parse YAML value
|
|
416
|
+
*/
|
|
417
|
+
function parseYamlValue(value) {
|
|
418
|
+
value = value.trim();
|
|
419
|
+
if (!value)
|
|
420
|
+
return "";
|
|
421
|
+
// Array
|
|
422
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
423
|
+
return value
|
|
424
|
+
.slice(1, -1)
|
|
425
|
+
.split(",")
|
|
426
|
+
.map((v) => v.trim().replace(/^["']|["']$/g, ""));
|
|
427
|
+
}
|
|
428
|
+
// String with quotes
|
|
429
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
430
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
431
|
+
return value.slice(1, -1);
|
|
432
|
+
}
|
|
433
|
+
// Boolean
|
|
434
|
+
if (value === "true")
|
|
435
|
+
return true;
|
|
436
|
+
if (value === "false")
|
|
437
|
+
return false;
|
|
438
|
+
// Number
|
|
439
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
440
|
+
return parseFloat(value);
|
|
441
|
+
}
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Serialize to YAML
|
|
446
|
+
*/
|
|
447
|
+
function serializeYaml(config, indent) {
|
|
448
|
+
let output = "mcpServers:\n";
|
|
449
|
+
if (config.mcpServers) {
|
|
450
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
451
|
+
output += ` ${serverName}:\n`;
|
|
452
|
+
for (const [key, value] of Object.entries(serverConfig)) {
|
|
453
|
+
output += ` ${key}: ${serializeYamlValue(value)}\n`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return output;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Serialize YAML value
|
|
461
|
+
*/
|
|
462
|
+
function serializeYamlValue(value) {
|
|
463
|
+
if (typeof value === "string") {
|
|
464
|
+
return `"${value}"`;
|
|
465
|
+
}
|
|
466
|
+
if (Array.isArray(value)) {
|
|
467
|
+
return `[${value.map((v) => `"${v}"`).join(", ")}]`;
|
|
468
|
+
}
|
|
469
|
+
if (typeof value === "object" && value !== null) {
|
|
470
|
+
return JSON.stringify(value);
|
|
471
|
+
}
|
|
472
|
+
return String(value);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Validate MCP config content
|
|
476
|
+
*/
|
|
477
|
+
export function validateMcpConfig(content, format) {
|
|
478
|
+
const errors = [];
|
|
479
|
+
const warnings = [];
|
|
480
|
+
// Try to parse
|
|
481
|
+
const config = parseMcpConfig(content, format);
|
|
482
|
+
if (!config) {
|
|
483
|
+
errors.push(`Failed to parse ${format} config`);
|
|
484
|
+
return { valid: false, errors, warnings };
|
|
485
|
+
}
|
|
486
|
+
// Check for mcpServers
|
|
487
|
+
if (!config.mcpServers) {
|
|
488
|
+
warnings.push("Config has no mcpServers key");
|
|
489
|
+
return { valid: true, errors, warnings };
|
|
490
|
+
}
|
|
491
|
+
// Validate each server
|
|
492
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
493
|
+
if (!serverConfig.command) {
|
|
494
|
+
errors.push(`Server "${serverName}" has no command`);
|
|
495
|
+
}
|
|
496
|
+
// Check for empty command
|
|
497
|
+
if (serverConfig.command &&
|
|
498
|
+
typeof serverConfig.command === "string" &&
|
|
499
|
+
serverConfig.command.trim() === "") {
|
|
500
|
+
errors.push(`Server "${serverName}" has empty command`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get list of commands used in MCP config
|
|
507
|
+
*/
|
|
508
|
+
export function getMcpCommands(config) {
|
|
509
|
+
const commands = new Set();
|
|
510
|
+
if (config.mcpServers) {
|
|
511
|
+
for (const serverConfig of Object.values(config.mcpServers)) {
|
|
512
|
+
if (serverConfig.command) {
|
|
513
|
+
// Extract base command (first word)
|
|
514
|
+
const baseCommand = serverConfig.command.split(/\s+/)[0];
|
|
515
|
+
commands.add(baseCommand);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return Array.from(commands);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Find servers that exist in target but not in source (would be removed)
|
|
523
|
+
*/
|
|
524
|
+
export function findRemovedServers(sourceConfig, targetConfig) {
|
|
525
|
+
const sourceServers = new Set(Object.keys(sourceConfig.mcpServers ?? {}));
|
|
526
|
+
const targetServers = Object.keys(targetConfig.mcpServers ?? {});
|
|
527
|
+
return targetServers.filter((name) => !sourceServers.has(name));
|
|
528
|
+
}
|
|
529
|
+
//# sourceMappingURL=mcp.js.map
|