ethagent 0.2.1 → 1.0.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/LICENSE +21 -0
- package/README.md +114 -32
- package/bin/ethagent.js +11 -2
- package/package.json +25 -7
- package/src/app/FirstRun.tsx +412 -0
- package/src/app/hooks/useCancelRequest.ts +22 -0
- package/src/app/hooks/useDoublePress.ts +46 -0
- package/src/app/hooks/useExitOnCtrlC.ts +36 -0
- package/src/app/input/AppInputProvider.tsx +116 -0
- package/src/app/input/appInputParser.ts +279 -0
- package/src/app/keybindings/KeybindingProvider.tsx +134 -0
- package/src/app/keybindings/resolver.ts +42 -0
- package/src/app/keybindings/types.ts +26 -0
- package/src/chat/ChatBottomPane.tsx +280 -0
- package/src/chat/ChatInput.tsx +722 -0
- package/src/chat/ChatScreen.tsx +1575 -0
- package/src/chat/ContextLimitView.tsx +95 -0
- package/src/chat/ContinuityEditReviewView.tsx +48 -0
- package/src/chat/ConversationStack.tsx +47 -0
- package/src/chat/CopyPicker.tsx +52 -0
- package/src/chat/MessageList.tsx +609 -0
- package/src/chat/PermissionPrompt.tsx +153 -0
- package/src/chat/PermissionsView.tsx +159 -0
- package/src/chat/PlanApprovalView.tsx +91 -0
- package/src/chat/ResumeView.tsx +267 -0
- package/src/chat/RewindView.tsx +386 -0
- package/src/chat/SessionStatus.tsx +51 -0
- package/src/chat/TranscriptView.tsx +202 -0
- package/src/chat/chatInputState.ts +247 -0
- package/src/chat/chatPaste.ts +49 -0
- package/src/chat/chatScreenUtils.ts +187 -0
- package/src/chat/chatSessionState.ts +142 -0
- package/src/chat/chatTurnOrchestrator.ts +701 -0
- package/src/chat/commands.ts +673 -0
- package/src/chat/textCursor.ts +202 -0
- package/src/chat/toolResultDisplay.ts +8 -0
- package/src/chat/transcriptViewport.ts +247 -0
- package/src/cli/ResetConfirmView.tsx +61 -0
- package/src/cli/main.tsx +177 -0
- package/src/cli/preview.tsx +19 -0
- package/src/cli/reset.ts +106 -0
- package/src/identity/continuity/editor.ts +149 -0
- package/src/identity/continuity/envelope.ts +345 -0
- package/src/identity/continuity/history.ts +153 -0
- package/src/identity/continuity/privateEdit.ts +334 -0
- package/src/identity/continuity/publicSkills.ts +173 -0
- package/src/identity/continuity/snapshots.ts +183 -0
- package/src/identity/continuity/storage.ts +507 -0
- package/src/identity/crypto/backupEnvelope.ts +486 -0
- package/src/identity/crypto/eth.ts +137 -0
- package/src/identity/hub/IdentityHub.tsx +868 -0
- package/src/identity/hub/identityHubEffects.ts +1146 -0
- package/src/identity/hub/identityHubModel.ts +291 -0
- package/src/identity/hub/identityHubReducer.ts +212 -0
- package/src/identity/hub/screens/BusyScreen.tsx +26 -0
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +144 -0
- package/src/identity/hub/screens/CreateFlow.tsx +206 -0
- package/src/identity/hub/screens/DetailsScreen.tsx +64 -0
- package/src/identity/hub/screens/EditProfileFlow.tsx +145 -0
- package/src/identity/hub/screens/ErrorScreen.tsx +35 -0
- package/src/identity/hub/screens/IdentitySummary.tsx +70 -0
- package/src/identity/hub/screens/MenuScreen.tsx +117 -0
- package/src/identity/hub/screens/NetworkScreen.tsx +41 -0
- package/src/identity/hub/screens/RebackupStorageScreen.tsx +50 -0
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +85 -0
- package/src/identity/hub/screens/RestoreFlow.tsx +206 -0
- package/src/identity/hub/screens/StorageCredentialScreen.tsx +128 -0
- package/src/identity/hub/screens/WalletApprovalScreen.tsx +43 -0
- package/src/identity/profile/imagePicker.ts +180 -0
- package/src/identity/registry/erc8004.ts +1106 -0
- package/src/identity/registry/registryConfig.ts +69 -0
- package/src/identity/storage/ipfs.ts +212 -0
- package/src/identity/storage/pinataJwt.ts +53 -0
- package/src/identity/wallet/browserWallet.ts +393 -0
- package/src/identity/wallet/wallet-page/wallet.html +1082 -0
- package/src/mcp/approvals.ts +113 -0
- package/src/mcp/config.ts +235 -0
- package/src/mcp/manager.ts +541 -0
- package/src/mcp/names.ts +19 -0
- package/src/mcp/output.ts +96 -0
- package/src/models/ModelPicker.tsx +1446 -0
- package/src/models/catalog.ts +296 -0
- package/src/models/huggingface.ts +651 -0
- package/src/models/llamacpp.ts +810 -0
- package/src/models/llamacppPreflight.ts +150 -0
- package/src/models/modelDisplay.ts +105 -0
- package/src/models/modelPickerOptions.ts +421 -0
- package/src/models/modelRecommendation.ts +140 -0
- package/src/models/runtimeDetection.ts +81 -0
- package/src/models/uncensoredCatalog.ts +86 -0
- package/src/providers/anthropic.ts +259 -0
- package/src/providers/contracts.ts +62 -0
- package/src/providers/errors.ts +62 -0
- package/src/providers/gemini.ts +152 -0
- package/src/providers/openai-chat.ts +472 -0
- package/src/providers/registry.ts +42 -0
- package/src/providers/retry.ts +58 -0
- package/src/providers/sse.ts +93 -0
- package/src/runtime/compaction.ts +389 -0
- package/src/runtime/cwd.ts +43 -0
- package/src/runtime/sessionMode.ts +55 -0
- package/src/runtime/systemPrompt.ts +209 -0
- package/src/runtime/toolClaimGuards.ts +143 -0
- package/src/runtime/toolExecution.ts +304 -0
- package/src/runtime/toolIntent.ts +163 -0
- package/src/runtime/turn.ts +858 -0
- package/src/storage/atomicWrite.ts +68 -0
- package/src/storage/config.ts +189 -0
- package/src/storage/factoryReset.ts +130 -0
- package/src/storage/history.ts +58 -0
- package/src/storage/identity.ts +99 -0
- package/src/storage/permissions.ts +76 -0
- package/src/storage/rewind.ts +246 -0
- package/src/storage/secrets.ts +181 -0
- package/src/storage/sessionExport.ts +49 -0
- package/src/storage/sessions.ts +482 -0
- package/src/tools/bashSafety.ts +174 -0
- package/src/tools/bashTool.ts +140 -0
- package/src/tools/changeDirectoryTool.ts +213 -0
- package/src/tools/contracts.ts +179 -0
- package/src/tools/deleteFileTool.ts +111 -0
- package/src/tools/editTool.ts +160 -0
- package/src/tools/editUtils.ts +170 -0
- package/src/tools/listDirectoryTool.ts +55 -0
- package/src/tools/mcpResourceTools.ts +95 -0
- package/src/tools/permissionRules.ts +85 -0
- package/src/tools/privateContinuityEditTool.ts +178 -0
- package/src/tools/privateContinuityReadTool.ts +107 -0
- package/src/tools/readTool.ts +85 -0
- package/src/tools/registry.ts +67 -0
- package/src/tools/writeFileTool.ts +142 -0
- package/src/ui/BrandSplash.tsx +193 -0
- package/src/ui/ProgressBar.tsx +34 -0
- package/src/ui/Select.tsx +143 -0
- package/src/ui/Spinner.tsx +269 -0
- package/src/ui/Surface.tsx +47 -0
- package/src/ui/TextInput.tsx +97 -0
- package/src/ui/theme.ts +59 -0
- package/src/utils/clipboard.ts +216 -0
- package/src/utils/markdownSegments.ts +51 -0
- package/src/utils/messages.ts +35 -0
- package/src/utils/withRetry.ts +280 -0
- package/src/cli.tsx +0 -147
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { atomicWriteText } from '../storage/atomicWrite.js'
|
|
5
|
+
import { ensureConfigDir, getConfigDir } from '../storage/config.js'
|
|
6
|
+
|
|
7
|
+
export type ProjectMcpDecision = 'approved' | 'rejected'
|
|
8
|
+
|
|
9
|
+
const ProjectDecisionSchema = z.object({
|
|
10
|
+
workspaceRoot: z.string().min(1),
|
|
11
|
+
serverName: z.string().min(1),
|
|
12
|
+
configHash: z.string().min(1),
|
|
13
|
+
decision: z.enum(['approved', 'rejected']),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const DisabledServerSchema = z.object({
|
|
17
|
+
workspaceRoot: z.string().min(1),
|
|
18
|
+
serverName: z.string().min(1),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const McpLocalStateSchema = z.object({
|
|
22
|
+
version: z.literal(1),
|
|
23
|
+
projectServers: z.array(ProjectDecisionSchema),
|
|
24
|
+
disabledServers: z.array(DisabledServerSchema),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
type McpLocalState = z.infer<typeof McpLocalStateSchema>
|
|
28
|
+
|
|
29
|
+
function getMcpLocalStatePath(): string {
|
|
30
|
+
return path.join(getConfigDir(), 'mcp-state.json')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getProjectMcpDecision(params: {
|
|
34
|
+
workspaceRoot: string
|
|
35
|
+
serverName: string
|
|
36
|
+
configHash: string
|
|
37
|
+
}): Promise<ProjectMcpDecision | undefined> {
|
|
38
|
+
const state = await loadMcpLocalState()
|
|
39
|
+
const workspaceRoot = path.resolve(params.workspaceRoot)
|
|
40
|
+
return state.projectServers.find(entry =>
|
|
41
|
+
path.resolve(entry.workspaceRoot) === workspaceRoot &&
|
|
42
|
+
entry.serverName === params.serverName &&
|
|
43
|
+
entry.configHash === params.configHash,
|
|
44
|
+
)?.decision
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function setProjectMcpDecision(params: {
|
|
48
|
+
workspaceRoot: string
|
|
49
|
+
serverName: string
|
|
50
|
+
configHash: string
|
|
51
|
+
decision: ProjectMcpDecision
|
|
52
|
+
}): Promise<void> {
|
|
53
|
+
const state = await loadMcpLocalState()
|
|
54
|
+
const workspaceRoot = path.resolve(params.workspaceRoot)
|
|
55
|
+
const next = state.projectServers.filter(entry =>
|
|
56
|
+
!(path.resolve(entry.workspaceRoot) === workspaceRoot && entry.serverName === params.serverName),
|
|
57
|
+
)
|
|
58
|
+
next.push({
|
|
59
|
+
workspaceRoot,
|
|
60
|
+
serverName: params.serverName,
|
|
61
|
+
configHash: params.configHash,
|
|
62
|
+
decision: params.decision,
|
|
63
|
+
})
|
|
64
|
+
await writeMcpLocalState({ ...state, projectServers: next })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function isMcpServerDisabled(workspaceRoot: string, serverName: string): Promise<boolean> {
|
|
68
|
+
const state = await loadMcpLocalState()
|
|
69
|
+
const normalizedWorkspaceRoot = path.resolve(workspaceRoot)
|
|
70
|
+
return state.disabledServers.some(entry =>
|
|
71
|
+
path.resolve(entry.workspaceRoot) === normalizedWorkspaceRoot && entry.serverName === serverName,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function setMcpServerEnabled(params: {
|
|
76
|
+
workspaceRoot: string
|
|
77
|
+
serverName: string
|
|
78
|
+
enabled: boolean
|
|
79
|
+
}): Promise<void> {
|
|
80
|
+
const state = await loadMcpLocalState()
|
|
81
|
+
const workspaceRoot = path.resolve(params.workspaceRoot)
|
|
82
|
+
const withoutServer = state.disabledServers.filter(entry =>
|
|
83
|
+
!(path.resolve(entry.workspaceRoot) === workspaceRoot && entry.serverName === params.serverName),
|
|
84
|
+
)
|
|
85
|
+
const disabledServers = params.enabled
|
|
86
|
+
? withoutServer
|
|
87
|
+
: [...withoutServer, { workspaceRoot, serverName: params.serverName }]
|
|
88
|
+
await writeMcpLocalState({ ...state, disabledServers })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loadMcpLocalState(): Promise<McpLocalState> {
|
|
92
|
+
let raw: string
|
|
93
|
+
try {
|
|
94
|
+
raw = await fs.readFile(getMcpLocalStatePath(), 'utf8')
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return emptyState()
|
|
97
|
+
throw err
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return McpLocalStateSchema.parse(JSON.parse(raw))
|
|
101
|
+
} catch {
|
|
102
|
+
return emptyState()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function writeMcpLocalState(state: McpLocalState): Promise<void> {
|
|
107
|
+
await ensureConfigDir()
|
|
108
|
+
await atomicWriteText(getMcpLocalStatePath(), JSON.stringify(McpLocalStateSchema.parse(state), null, 2) + '\n')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function emptyState(): McpLocalState {
|
|
112
|
+
return { version: 1, projectServers: [], disabledServers: [] }
|
|
113
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { atomicWriteText } from '../storage/atomicWrite.js'
|
|
6
|
+
import { ensureConfigDir, getConfigDir } from '../storage/config.js'
|
|
7
|
+
|
|
8
|
+
export type McpConfigScope = 'user' | 'project'
|
|
9
|
+
|
|
10
|
+
const McpStdioServerConfigSchema = z.object({
|
|
11
|
+
type: z.literal('stdio').optional(),
|
|
12
|
+
command: z.string().min(1),
|
|
13
|
+
args: z.array(z.string()).optional(),
|
|
14
|
+
env: z.record(z.string()).optional(),
|
|
15
|
+
cwd: z.string().min(1).optional(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const McpHttpServerConfigSchema = z.object({
|
|
19
|
+
type: z.literal('http'),
|
|
20
|
+
url: z.string().url(),
|
|
21
|
+
headers: z.record(z.string()).optional(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const McpSseServerConfigSchema = z.object({
|
|
25
|
+
type: z.literal('sse'),
|
|
26
|
+
url: z.string().url(),
|
|
27
|
+
headers: z.record(z.string()).optional(),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const McpServerConfigSchema = z.union([
|
|
31
|
+
McpStdioServerConfigSchema,
|
|
32
|
+
McpHttpServerConfigSchema,
|
|
33
|
+
McpSseServerConfigSchema,
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
export const McpJsonConfigSchema = z.object({
|
|
37
|
+
mcpServers: z.record(McpServerConfigSchema),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export type McpServerConfig = z.infer<typeof McpServerConfigSchema>
|
|
41
|
+
export type McpJsonConfig = z.infer<typeof McpJsonConfigSchema>
|
|
42
|
+
|
|
43
|
+
export type ScopedMcpServerConfig = {
|
|
44
|
+
name: string
|
|
45
|
+
scope: McpConfigScope
|
|
46
|
+
config: McpServerConfig
|
|
47
|
+
configHash: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type McpConfigIssue = {
|
|
51
|
+
scope: McpConfigScope
|
|
52
|
+
filePath: string
|
|
53
|
+
serverName?: string
|
|
54
|
+
severity: 'error' | 'warning'
|
|
55
|
+
message: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type LoadedMcpConfigs = {
|
|
59
|
+
servers: ScopedMcpServerConfig[]
|
|
60
|
+
issues: McpConfigIssue[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getUserMcpConfigPath(): string {
|
|
64
|
+
return path.join(getConfigDir(), 'mcp.json')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getProjectMcpConfigPath(cwd: string): string {
|
|
68
|
+
return path.join(path.resolve(cwd), '.mcp.json')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function loadMcpConfigs(cwd: string): Promise<LoadedMcpConfigs> {
|
|
72
|
+
const user = await readMcpConfigFile(getUserMcpConfigPath(), 'user', true)
|
|
73
|
+
const project = await readMcpConfigFile(getProjectMcpConfigPath(cwd), 'project', true)
|
|
74
|
+
const byName = new Map<string, ScopedMcpServerConfig>()
|
|
75
|
+
for (const server of user.servers) byName.set(server.name, server)
|
|
76
|
+
for (const server of project.servers) byName.set(server.name, server)
|
|
77
|
+
return {
|
|
78
|
+
servers: [...byName.values()],
|
|
79
|
+
issues: [...user.issues, ...project.issues],
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function addMcpServerConfig(params: {
|
|
84
|
+
cwd: string
|
|
85
|
+
scope: McpConfigScope
|
|
86
|
+
name: string
|
|
87
|
+
config: McpServerConfig
|
|
88
|
+
}): Promise<string> {
|
|
89
|
+
const filePath = params.scope === 'user' ? getUserMcpConfigPath() : getProjectMcpConfigPath(params.cwd)
|
|
90
|
+
const current = await readRawMcpConfig(filePath)
|
|
91
|
+
current.mcpServers[params.name] = params.config
|
|
92
|
+
if (params.scope === 'user') await ensureConfigDir()
|
|
93
|
+
await atomicWriteText(filePath, JSON.stringify(current, null, 2) + '\n')
|
|
94
|
+
return filePath
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function parseMcpServerConfigJson(value: string): McpServerConfig {
|
|
98
|
+
return McpServerConfigSchema.parse(JSON.parse(value))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function mcpServerTransport(config: McpServerConfig): 'stdio' | 'http' | 'sse' {
|
|
102
|
+
return config.type === 'http' || config.type === 'sse' ? config.type : 'stdio'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function stableMcpConfigHash(config: McpServerConfig): string {
|
|
106
|
+
return crypto.createHash('sha256').update(stableJson(config)).digest('hex').slice(0, 16)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readMcpConfigFile(
|
|
110
|
+
filePath: string,
|
|
111
|
+
scope: McpConfigScope,
|
|
112
|
+
expandVars: boolean,
|
|
113
|
+
): Promise<LoadedMcpConfigs> {
|
|
114
|
+
let raw: string
|
|
115
|
+
try {
|
|
116
|
+
raw = await fs.readFile(filePath, 'utf8')
|
|
117
|
+
} catch (err: unknown) {
|
|
118
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return { servers: [], issues: [] }
|
|
119
|
+
return {
|
|
120
|
+
servers: [],
|
|
121
|
+
issues: [{ scope, filePath, severity: 'error', message: `failed to read MCP config: ${(err as Error).message}` }],
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let parsedJson: unknown
|
|
126
|
+
try {
|
|
127
|
+
parsedJson = JSON.parse(raw)
|
|
128
|
+
} catch (err: unknown) {
|
|
129
|
+
return {
|
|
130
|
+
servers: [],
|
|
131
|
+
issues: [{ scope, filePath, severity: 'error', message: `MCP config is not valid JSON: ${(err as Error).message}` }],
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parsed = McpJsonConfigSchema.safeParse(parsedJson)
|
|
136
|
+
if (!parsed.success) {
|
|
137
|
+
return {
|
|
138
|
+
servers: [],
|
|
139
|
+
issues: parsed.error.issues.map(issue => ({
|
|
140
|
+
scope,
|
|
141
|
+
filePath,
|
|
142
|
+
severity: 'error',
|
|
143
|
+
message: `MCP config schema error at ${issue.path.join('.') || 'root'}: ${issue.message}`,
|
|
144
|
+
})),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const servers: ScopedMcpServerConfig[] = []
|
|
149
|
+
const issues: McpConfigIssue[] = []
|
|
150
|
+
for (const [name, config] of Object.entries(parsed.data.mcpServers)) {
|
|
151
|
+
const expanded = expandVars ? expandEnvVars(config) : { config, missing: [] }
|
|
152
|
+
if (expanded.missing.length > 0) {
|
|
153
|
+
issues.push({
|
|
154
|
+
scope,
|
|
155
|
+
filePath,
|
|
156
|
+
serverName: name,
|
|
157
|
+
severity: 'warning',
|
|
158
|
+
message: `missing environment variables: ${expanded.missing.join(', ')}`,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
if (process.platform === 'win32' && 'command' in expanded.config && isBareNpxCommand(expanded.config.command)) {
|
|
162
|
+
issues.push({
|
|
163
|
+
scope,
|
|
164
|
+
filePath,
|
|
165
|
+
serverName: name,
|
|
166
|
+
severity: 'warning',
|
|
167
|
+
message: 'Windows MCP stdio servers should use command "cmd" with args ["/c", "npx", ...]',
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
servers.push({
|
|
171
|
+
name,
|
|
172
|
+
scope,
|
|
173
|
+
config: expanded.config,
|
|
174
|
+
configHash: stableMcpConfigHash(expanded.config),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
return { servers, issues }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readRawMcpConfig(filePath: string): Promise<McpJsonConfig> {
|
|
181
|
+
let raw: string
|
|
182
|
+
try {
|
|
183
|
+
raw = await fs.readFile(filePath, 'utf8')
|
|
184
|
+
} catch (err: unknown) {
|
|
185
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return { mcpServers: {} }
|
|
186
|
+
throw err
|
|
187
|
+
}
|
|
188
|
+
return McpJsonConfigSchema.parse(JSON.parse(raw))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function expandEnvVars(config: McpServerConfig): { config: McpServerConfig; missing: string[] } {
|
|
192
|
+
const missing = new Set<string>()
|
|
193
|
+
const expanded = expandValue(config, missing)
|
|
194
|
+
return { config: McpServerConfigSchema.parse(expanded), missing: [...missing].sort() }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function expandValue(value: unknown, missing: Set<string>): unknown {
|
|
198
|
+
if (typeof value === 'string') return expandString(value, missing)
|
|
199
|
+
if (Array.isArray(value)) return value.map(item => expandValue(item, missing))
|
|
200
|
+
if (value && typeof value === 'object') {
|
|
201
|
+
const out: Record<string, unknown> = {}
|
|
202
|
+
for (const [key, child] of Object.entries(value)) out[key] = expandValue(child, missing)
|
|
203
|
+
return out
|
|
204
|
+
}
|
|
205
|
+
return value
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function expandString(value: string, missing: Set<string>): string {
|
|
209
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-(.*?))?\}/g, (_match, name: string, fallback: string | undefined) => {
|
|
210
|
+
const envValue = process.env[name]
|
|
211
|
+
if (envValue !== undefined) return envValue
|
|
212
|
+
if (fallback !== undefined) return fallback
|
|
213
|
+
missing.add(name)
|
|
214
|
+
return ''
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isBareNpxCommand(command: string): boolean {
|
|
219
|
+
const normalized = command.replace(/\\/g, '/').toLowerCase()
|
|
220
|
+
return normalized === 'npx' || normalized.endsWith('/npx')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function stableJson(value: unknown): string {
|
|
224
|
+
return JSON.stringify(sortForStableJson(value))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sortForStableJson(value: unknown): unknown {
|
|
228
|
+
if (Array.isArray(value)) return value.map(sortForStableJson)
|
|
229
|
+
if (value && typeof value === 'object') {
|
|
230
|
+
const out: Record<string, unknown> = {}
|
|
231
|
+
for (const key of Object.keys(value).sort()) out[key] = sortForStableJson((value as Record<string, unknown>)[key])
|
|
232
|
+
return out
|
|
233
|
+
}
|
|
234
|
+
return value
|
|
235
|
+
}
|