@yagni-app/code 1.0.5 → 1.0.7

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.
Files changed (111) hide show
  1. package/README.md +89 -27
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -3
  5. package/dist/claudePlugins.js +129 -19
  6. package/dist/cli.js +28 -4
  7. package/dist/doctor.d.ts +42 -4
  8. package/dist/doctor.js +150 -15
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +18 -5
  12. package/dist/extension/index.js +319 -72
  13. package/dist/extension/mcp/approval.d.ts +45 -0
  14. package/dist/extension/mcp/approval.js +164 -0
  15. package/dist/extension/mcp/auth.d.ts +124 -0
  16. package/dist/extension/mcp/auth.js +560 -0
  17. package/dist/extension/mcp/authStore.d.ts +61 -0
  18. package/dist/extension/mcp/authStore.js +105 -0
  19. package/dist/extension/mcp/callbackPage.d.ts +31 -0
  20. package/dist/extension/mcp/callbackPage.js +222 -0
  21. package/dist/extension/mcp/cliConfig.d.ts +12 -0
  22. package/dist/extension/mcp/cliConfig.js +12 -0
  23. package/dist/extension/mcp/config.d.ts +153 -0
  24. package/dist/extension/mcp/config.js +381 -0
  25. package/dist/extension/mcp/log.d.ts +28 -0
  26. package/dist/extension/mcp/log.js +82 -0
  27. package/dist/extension/mcp/manager.d.ts +100 -0
  28. package/dist/extension/mcp/manager.js +273 -0
  29. package/dist/extension/mcp/names.d.ts +25 -0
  30. package/dist/extension/mcp/names.js +40 -0
  31. package/dist/extension/mcp/panel.d.ts +33 -0
  32. package/dist/extension/mcp/panel.js +268 -0
  33. package/dist/extension/mcp/prompts.d.ts +23 -0
  34. package/dist/extension/mcp/prompts.js +93 -0
  35. package/dist/extension/mcp/startup.d.ts +55 -0
  36. package/dist/extension/mcp/startup.js +152 -0
  37. package/dist/extension/mcp/tools.d.ts +31 -0
  38. package/dist/extension/mcp/tools.js +117 -0
  39. package/dist/extension/mcp/transports.d.ts +17 -0
  40. package/dist/extension/mcp/transports.js +44 -0
  41. package/dist/extension/permission/gate.d.ts +104 -2
  42. package/dist/extension/permission/gate.js +387 -31
  43. package/dist/extension/permission/guardian.d.ts +24 -5
  44. package/dist/extension/permission/guardian.js +162 -24
  45. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  46. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  47. package/dist/extension/permissionRules/engine.d.ts +50 -0
  48. package/dist/extension/permissionRules/engine.js +238 -0
  49. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  50. package/dist/extension/permissionRules/loadConfig.js +90 -0
  51. package/dist/extension/permissionRules/parser.d.ts +38 -0
  52. package/dist/extension/permissionRules/parser.js +136 -0
  53. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  54. package/dist/extension/permissionRules/pathRules.js +120 -0
  55. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  56. package/dist/extension/permissionRules/shellRules.js +221 -0
  57. package/dist/extension/pipeline/invocation.d.ts +3 -6
  58. package/dist/extension/pipeline/invocation.js +3 -6
  59. package/dist/extension/pipeline/personas.js +5 -0
  60. package/dist/extension/pipeline/runner.d.ts +0 -1
  61. package/dist/extension/pipeline/runner.js +6 -14
  62. package/dist/extension/plugins/inventory.d.ts +88 -0
  63. package/dist/extension/plugins/inventory.js +144 -0
  64. package/dist/extension/plugins/panel.d.ts +45 -0
  65. package/dist/extension/plugins/panel.js +293 -0
  66. package/dist/extension/sandbox/bash.d.ts +99 -0
  67. package/dist/extension/sandbox/bash.js +190 -0
  68. package/dist/extension/sandbox/config.d.ts +114 -0
  69. package/dist/extension/sandbox/config.js +366 -0
  70. package/dist/extension/sandbox/manager.d.ts +98 -0
  71. package/dist/extension/sandbox/manager.js +216 -0
  72. package/dist/extension/sandbox/panel.d.ts +111 -0
  73. package/dist/extension/sandbox/panel.js +342 -0
  74. package/dist/extension/sandbox/session.d.ts +85 -0
  75. package/dist/extension/sandbox/session.js +775 -0
  76. package/dist/extension/telemetry/attrs.d.ts +96 -0
  77. package/dist/extension/telemetry/attrs.js +149 -0
  78. package/dist/extension/telemetry/config.d.ts +99 -0
  79. package/dist/extension/telemetry/config.js +193 -0
  80. package/dist/extension/telemetry/index.d.ts +7 -0
  81. package/dist/extension/telemetry/index.js +7 -0
  82. package/dist/extension/telemetry/probe.d.ts +29 -0
  83. package/dist/extension/telemetry/probe.js +122 -0
  84. package/dist/extension/telemetry/register.d.ts +40 -0
  85. package/dist/extension/telemetry/register.js +192 -0
  86. package/dist/extension/telemetry/sdk.d.ts +63 -0
  87. package/dist/extension/telemetry/sdk.js +207 -0
  88. package/dist/extension/telemetry/tracker.d.ts +131 -0
  89. package/dist/extension/telemetry/tracker.js +551 -0
  90. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  91. package/dist/extension/vendor/ignore.d.ts +86 -0
  92. package/dist/extension/vendor/ignore.js +788 -0
  93. package/dist/goHeadless.d.ts +1 -1
  94. package/dist/goHeadless.js +2 -2
  95. package/dist/launch.d.ts +4 -3
  96. package/dist/launch.js +7 -4
  97. package/dist/mcpCommand.d.ts +122 -0
  98. package/dist/mcpCommand.js +787 -0
  99. package/dist/otel.d.ts +69 -63
  100. package/dist/otel.js +154 -119
  101. package/dist/paths.d.ts +13 -0
  102. package/dist/paths.js +18 -0
  103. package/dist/pluginCommand.d.ts +43 -0
  104. package/dist/pluginCommand.js +499 -0
  105. package/dist/pluginStore.d.ts +170 -0
  106. package/dist/pluginStore.js +554 -0
  107. package/dist/upgrade.d.ts +11 -2
  108. package/dist/upgrade.js +48 -8
  109. package/package.json +20 -3
  110. package/dist/extension/mcpTools.d.ts +0 -57
  111. package/dist/extension/mcpTools.js +0 -132
@@ -0,0 +1,381 @@
1
+ /**
2
+ * MCP server configuration for YAGNI Code: three scopes, Claude Code-compatible.
3
+ *
4
+ * - project: `.mcp.json` at the repo root — the exact Claude Code schema
5
+ * (`{"mcpServers": {...}}`), VCS-shared, approval-gated (see approval.ts).
6
+ * A repo configured for Claude Code works here with zero changes.
7
+ * - user: top-level `mcpServers` in `~/.yagni-code/mcp.json`.
8
+ * - local: `projects[<absPath>].mcpServers` in the same file, keyed by cwd.
9
+ * - plugin: servers bundled in Claude Code plugins (`.mcp.json` at the plugin
10
+ * root), handed over by the yagni launcher as JSON in
11
+ * `YAGNI_CLAUDE_PLUGIN_MCP` (see yagni-code-cli claudeCompat.ts). Entries
12
+ * marked `gated` came from repo config and ride the project approval gate.
13
+ *
14
+ * Merge precedence (Claude Code parity): plugin < user < project < local —
15
+ * later wins, so an explicit user entry beats a plugin's.
16
+ * Writes are atomic (temp file + rename) so two concurrent sessions can never
17
+ * interleave into a corrupt config. Validation errors are collected and
18
+ * surfaced, never fatal: one malformed server entry must not hide the rest.
19
+ */
20
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+ import { codeStateHome } from "../stateHome.js";
23
+ /** Launcher → extension: plugin-bundled MCP servers (JSON array). */
24
+ export const PLUGIN_MCP_ENV = "YAGNI_CLAUDE_PLUGIN_MCP";
25
+ const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
26
+ /** Parse the launcher's plugin MCP payload; malformed input degrades to errors, never a throw. */
27
+ export function readPluginMcpServers(env = process.env) {
28
+ const disabled = env[CLAUDE_COMPAT_DISABLE_ENV];
29
+ if (disabled !== undefined && disabled !== "" && disabled !== "0")
30
+ return { servers: [], errors: [] };
31
+ const raw = env[PLUGIN_MCP_ENV];
32
+ if (!raw)
33
+ return { servers: [], errors: [] };
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(raw);
37
+ }
38
+ catch (err) {
39
+ return {
40
+ servers: [],
41
+ errors: [{ sourcePath: PLUGIN_MCP_ENV, message: `could not parse plugin MCP payload (${err instanceof Error ? err.message : String(err)})` }],
42
+ };
43
+ }
44
+ if (!Array.isArray(parsed))
45
+ return { servers: [], errors: [{ sourcePath: PLUGIN_MCP_ENV, message: "plugin MCP payload is not an array" }] };
46
+ const servers = [];
47
+ const errors = [];
48
+ const seen = new Map();
49
+ for (const item of parsed) {
50
+ if (!item || typeof item !== "object")
51
+ continue;
52
+ const entry = item;
53
+ if (typeof entry.plugin !== "string" || !entry.servers || typeof entry.servers !== "object")
54
+ continue;
55
+ const sourcePath = typeof entry.sourcePath === "string" ? entry.sourcePath : entry.plugin;
56
+ for (const [name, value] of Object.entries(entry.servers)) {
57
+ const validation = validateServerConfig(value);
58
+ if (!validation.ok) {
59
+ errors.push({ sourcePath, serverName: name, message: `${validation.message} (plugin ${entry.plugin})` });
60
+ continue;
61
+ }
62
+ const owner = seen.get(name);
63
+ if (owner) {
64
+ errors.push({
65
+ sourcePath,
66
+ serverName: name,
67
+ message: `also defined by plugin ${owner} — keeping that one, skipping ${entry.plugin}'s`,
68
+ });
69
+ continue;
70
+ }
71
+ const config = value;
72
+ const expanded = expandServerEnv(config, env);
73
+ if (expanded.missingVars.length > 0) {
74
+ errors.push({
75
+ sourcePath,
76
+ serverName: name,
77
+ message: `missing environment variable(s): ${expanded.missingVars.join(", ")} — server skipped`,
78
+ });
79
+ continue;
80
+ }
81
+ seen.set(name, entry.plugin);
82
+ servers.push({ name, config, scope: "plugin", sourcePath, plugin: entry.plugin, gated: entry.gated === true });
83
+ }
84
+ }
85
+ return { servers, errors };
86
+ }
87
+ export const PROJECT_CONFIG_FILENAME = ".mcp.json";
88
+ /**
89
+ * Expand `${VAR}` / `${VAR:-default}` in a config string (Claude Code parity —
90
+ * .mcp.json files are VCS-shared, so secrets come from the environment).
91
+ * Missing vars with no default are reported, left in place, and skipped at
92
+ * connect time so a half-expanded command never spawns.
93
+ */
94
+ export function expandEnvVarsInString(value, env = process.env) {
95
+ const missingVars = [];
96
+ const expanded = value.replace(/\$\{([^}]+)\}/g, (match, varContent) => {
97
+ const separator = varContent.indexOf(":-");
98
+ const varName = separator === -1 ? varContent : varContent.slice(0, separator);
99
+ const defaultValue = separator === -1 ? undefined : varContent.slice(separator + 2);
100
+ const envValue = env[varName];
101
+ if (envValue !== undefined)
102
+ return envValue;
103
+ if (defaultValue !== undefined)
104
+ return defaultValue;
105
+ missingVars.push(varName);
106
+ return match;
107
+ });
108
+ return { expanded, missingVars };
109
+ }
110
+ /** Where env expansion applies within one server config (stdio: command,
111
+ * args, env values; http/sse: url and header values). */
112
+ export function expandServerEnv(config, env = process.env) {
113
+ const missingVars = [];
114
+ if (config.type === "stdio" || config.type === undefined) {
115
+ const command = expandEnvVarsInString(config.command, env);
116
+ missingVars.push(...command.missingVars);
117
+ const args = (config.args ?? []).map((a) => {
118
+ const r = expandEnvVarsInString(a, env);
119
+ missingVars.push(...r.missingVars);
120
+ return r.expanded;
121
+ });
122
+ const envValues = {};
123
+ for (const [k, v] of Object.entries(config.env ?? {})) {
124
+ const r = expandEnvVarsInString(v, env);
125
+ missingVars.push(...r.missingVars);
126
+ envValues[k] = r.expanded;
127
+ }
128
+ return {
129
+ config: { ...config, command: command.expanded, args, env: Object.keys(envValues).length > 0 ? envValues : undefined },
130
+ missingVars: [...new Set(missingVars)],
131
+ };
132
+ }
133
+ // http/sse: url + header values. Used as a load-time gate (missing vars
134
+ // → server skipped with a clear error) and at connect time to build the
135
+ // actual transport. The stored config keeps the raw ${VAR} references —
136
+ // display and the OAuth auth-store key (sha256 of type+url+headers) must
137
+ // see the raw form, so env changes never orphan stored tokens.
138
+ if (config.type === "http" || config.type === "sse") {
139
+ const url = expandEnvVarsInString(config.url, env);
140
+ missingVars.push(...url.missingVars);
141
+ const headerEntries = Object.entries(config.headers ?? {}).map(([k, v]) => {
142
+ const r = expandEnvVarsInString(v, env);
143
+ missingVars.push(...r.missingVars);
144
+ return [k, r.expanded];
145
+ });
146
+ return {
147
+ config: {
148
+ ...config,
149
+ url: url.expanded,
150
+ headers: headerEntries.length > 0 ? Object.fromEntries(headerEntries) : config.headers,
151
+ },
152
+ missingVars: [...new Set(missingVars)],
153
+ };
154
+ }
155
+ return { config, missingVars };
156
+ }
157
+ /** Test seam: point the state home at a tmpdir, mirroring errorSink's pattern. */
158
+ let homeOverride = null;
159
+ export function _setMcpHomeForTest(dir) {
160
+ homeOverride = dir;
161
+ }
162
+ export function mcpConfigPath() {
163
+ return join(codeStateHome(homeOverride), "mcp.json");
164
+ }
165
+ function projectEntry(file, projectPath) {
166
+ return (file.projects ?? {})[projectPath] ?? {};
167
+ }
168
+ /** Read + parse `~/.yagni-code/mcp.json`; unreadable/missing → empty with error collected. */
169
+ export function readUserMcpConfig() {
170
+ const path = mcpConfigPath();
171
+ if (!existsSync(path))
172
+ return { file: {}, errors: [] };
173
+ try {
174
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
175
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
176
+ return { file: {}, errors: [{ sourcePath: path, message: "not a JSON object at the top level" }] };
177
+ }
178
+ return { file: parsed, errors: [] };
179
+ }
180
+ catch (err) {
181
+ return {
182
+ file: {},
183
+ errors: [
184
+ {
185
+ sourcePath: path,
186
+ message: `could not parse (${err instanceof Error ? err.message : String(err)})`,
187
+ },
188
+ ],
189
+ };
190
+ }
191
+ }
192
+ /** Atomic write: temp file in the same directory, then rename over the target. */
193
+ export function writeUserMcpConfig(file) {
194
+ const path = mcpConfigPath();
195
+ mkdirSync(dirname(path), { recursive: true });
196
+ const tmp = join(dirname(path), `.${basename(path)}.tmp-${process.pid}`);
197
+ writeFileSync(tmp, JSON.stringify(file, null, 2) + "\n", "utf-8");
198
+ renameSync(tmp, path);
199
+ }
200
+ function basename(p) {
201
+ return p.split(/[\\/]/).filter(Boolean).pop() ?? p;
202
+ }
203
+ /** Read + parse a project `.mcp.json`; missing → empty (not an error). */
204
+ export function readProjectMcpConfig(repoRoot, read = defaultReadFile) {
205
+ const path = join(repoRoot, PROJECT_CONFIG_FILENAME);
206
+ let raw;
207
+ try {
208
+ raw = read(path);
209
+ }
210
+ catch (err) {
211
+ return {
212
+ servers: {},
213
+ errors: [{ sourcePath: path, message: `could not read (${err instanceof Error ? err.message : String(err)})` }],
214
+ };
215
+ }
216
+ if (raw === undefined)
217
+ return { servers: {}, errors: [] };
218
+ let parsed;
219
+ try {
220
+ parsed = JSON.parse(raw);
221
+ }
222
+ catch (err) {
223
+ return {
224
+ servers: {},
225
+ errors: [{ sourcePath: path, message: `could not parse (${err instanceof Error ? err.message : String(err)})` }],
226
+ };
227
+ }
228
+ if (typeof parsed !== "object" || parsed === null) {
229
+ return { servers: {}, errors: [{ sourcePath: path, message: "not a JSON object at the top level" }] };
230
+ }
231
+ const mcpServers = parsed.mcpServers;
232
+ if (mcpServers !== undefined && (typeof mcpServers !== "object" || mcpServers === null)) {
233
+ return { servers: {}, errors: [{ sourcePath: path, message: `"mcpServers" must be an object` }] };
234
+ }
235
+ const servers = {};
236
+ const errors = [];
237
+ for (const [name, value] of Object.entries(mcpServers ?? {})) {
238
+ const validation = validateServerConfig(value);
239
+ if (validation.ok)
240
+ servers[name] = value;
241
+ else
242
+ errors.push({ sourcePath: path, serverName: name, message: validation.message });
243
+ }
244
+ return { servers, errors };
245
+ }
246
+ function defaultReadFile(path) {
247
+ return existsSync(path) ? readFileSync(path, "utf-8") : undefined;
248
+ }
249
+ /**
250
+ * Structural validation of one server entry. Claude Code uses zod; we don't
251
+ * depend on zod, so this hand-checks the same union: stdio (no type / "stdio",
252
+ * non-empty command) vs http/sse (type + url).
253
+ */
254
+ export function validateServerConfig(value) {
255
+ if (typeof value !== "object" || value === null) {
256
+ return { ok: false, message: "server entry must be an object" };
257
+ }
258
+ const v = value;
259
+ const type = v["type"];
260
+ if (type === undefined || type === "stdio") {
261
+ if (typeof v["command"] !== "string" || v["command"].length === 0) {
262
+ return { ok: false, message: 'stdio server requires a non-empty "command"' };
263
+ }
264
+ if (v["args"] !== undefined && !Array.isArray(v["args"])) {
265
+ return { ok: false, message: '"args" must be an array of strings' };
266
+ }
267
+ return { ok: true };
268
+ }
269
+ if (type === "http" || type === "sse") {
270
+ if (typeof v["url"] !== "string" || v["url"].length === 0) {
271
+ return { ok: false, message: `${type} server requires a non-empty "url"` };
272
+ }
273
+ return { ok: true };
274
+ }
275
+ return {
276
+ ok: false,
277
+ message: 'unknown "type" — expected "stdio", "http", or "sse" (Claude Code also accepts ws/sdk; YAGNI Code v1 does not)',
278
+ };
279
+ }
280
+ /**
281
+ * The repo root for project-scope lookup: the nearest ancestor of `cwd` that
282
+ * looks like a repo (has `.git`, `.mcp.json`, or is a known project root).
283
+ * Falls back to cwd itself. CC walks ALL ancestors and merges nearest-wins;
284
+ * we take the nearest repo boundary — same result for the normal case of one
285
+ * repo, and it keeps the approval prompt anchored to one file.
286
+ */
287
+ export function resolveProjectRoot(cwd, exists = defaultExists) {
288
+ let current = cwd;
289
+ for (;;) {
290
+ if (exists(join(current, ".git")) || exists(join(current, PROJECT_CONFIG_FILENAME)))
291
+ return current;
292
+ const parent = dirname(current);
293
+ if (parent === current)
294
+ return cwd;
295
+ current = parent;
296
+ }
297
+ }
298
+ function defaultExists(p) {
299
+ return existsSync(p);
300
+ }
301
+ /**
302
+ * Load all three scopes and merge with precedence user < project < local.
303
+ * Invalid entries are dropped (with errors) rather than failing the load; a
304
+ * same-name entry in a higher-precedence scope replaces the lower one.
305
+ * Project servers are returned regardless of approval state — the caller
306
+ * (session startup, panel, CLI) applies the approval gate via approval.ts.
307
+ */
308
+ export function loadMcpServers(cwd, env = process.env) {
309
+ const repoRoot = resolveProjectRoot(cwd);
310
+ const errors = [];
311
+ const byScope = { plugin: [], user: [], project: [], local: [] };
312
+ const pluginLoad = readPluginMcpServers(env);
313
+ errors.push(...pluginLoad.errors);
314
+ byScope.plugin.push(...pluginLoad.servers);
315
+ const { file: userFile, errors: userErrors } = readUserMcpConfig();
316
+ errors.push(...userErrors);
317
+ const userServers = userFile.mcpServers ?? {};
318
+ for (const [name, value] of Object.entries(userServers)) {
319
+ const validation = validateServerConfig(value);
320
+ if (validation.ok) {
321
+ // Gate: a ${VAR} reference with no matching env var (and no default)
322
+ // skips the server with a clear error, rather than sending a literal
323
+ // "${VAR}" header on the wire. The pushed config stays raw.
324
+ const expanded = expandServerEnv(value, env);
325
+ if (expanded.missingVars.length > 0) {
326
+ errors.push({
327
+ sourcePath: mcpConfigPath(),
328
+ serverName: name,
329
+ message: `missing environment variable(s): ${expanded.missingVars.join(", ")} — server skipped`,
330
+ });
331
+ continue;
332
+ }
333
+ byScope.user.push({ name, config: value, scope: "user", sourcePath: mcpConfigPath() });
334
+ }
335
+ else {
336
+ errors.push({ sourcePath: mcpConfigPath(), serverName: name, message: validation.message });
337
+ }
338
+ }
339
+ const projectPath = join(repoRoot, PROJECT_CONFIG_FILENAME);
340
+ const { servers: projectServers, errors: projectErrors } = readProjectMcpConfig(repoRoot);
341
+ errors.push(...projectErrors);
342
+ for (const [name, raw] of Object.entries(projectServers)) {
343
+ const expanded = expandServerEnv(raw, env);
344
+ if (expanded.missingVars.length > 0) {
345
+ errors.push({
346
+ sourcePath: projectPath,
347
+ serverName: name,
348
+ message: `missing environment variable(s): ${expanded.missingVars.join(", ")} — server skipped`,
349
+ });
350
+ continue;
351
+ }
352
+ byScope.project.push({ name, config: raw, scope: "project", sourcePath: projectPath });
353
+ }
354
+ const localEntry = projectEntry(userFile, repoRoot);
355
+ const localServers = localEntry.mcpServers ?? {};
356
+ for (const [name, value] of Object.entries(localServers)) {
357
+ const validation = validateServerConfig(value);
358
+ if (validation.ok) {
359
+ const expanded = expandServerEnv(value, env);
360
+ if (expanded.missingVars.length > 0) {
361
+ errors.push({
362
+ sourcePath: mcpConfigPath(),
363
+ serverName: name,
364
+ message: `missing environment variable(s): ${expanded.missingVars.join(", ")} — server skipped`,
365
+ });
366
+ continue;
367
+ }
368
+ byScope.local.push({ name, config: value, scope: "local", sourcePath: mcpConfigPath() });
369
+ }
370
+ else {
371
+ errors.push({ sourcePath: mcpConfigPath(), serverName: name, message: validation.message });
372
+ }
373
+ }
374
+ const merged = new Map();
375
+ for (const scope of ["plugin", "user", "project", "local"]) {
376
+ for (const server of byScope[scope])
377
+ merged.set(server.name, server);
378
+ }
379
+ return { servers: [...merged.values()], errors };
380
+ }
381
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MCP logging primitives shared by session wiring (startup.ts) and the OAuth
3
+ * flow (auth.ts). Kept as a standalone module so neither import site drags in
4
+ * the other (startup → manager → auth is already a chain; an auth → startup
5
+ * edge for the log helper would make it cyclic).
6
+ *
7
+ * `appendMcpLogLine` writes the capped/rotated `~/.yagni-code/logs/mcp.log`
8
+ * (fail-soft — logging never blocks the session or the CLI). The OAuth helpers
9
+ * here are redaction-first: authorization URLs drop `state` / `nonce` /
10
+ * `code_challenge` / `code_verifier` / `code`, and error text scrubs any
11
+ * credential-looking token, so nothing secret reaches the on-disk log.
12
+ */
13
+ export declare function _setMcpLogHomeForTest(dir: string | null): void;
14
+ /** Fail-soft append with size cap + rotation, mirroring errorSink's pattern. */
15
+ export declare function appendMcpLogLine(line: string): void;
16
+ /**
17
+ * Redact sensitive OAuth query params from a URL for safe logging (Claude Code
18
+ * parity — see its `redactSensitiveUrlParams`). Returns the URL unchanged when
19
+ * it does not parse.
20
+ */
21
+ export declare function redactSensitiveUrlParams(url: string): string;
22
+ /**
23
+ * Append one structured MCP event to the log. `fields` are caller-controlled
24
+ * and expected to already be redacted; only the `server` / `event` / timestamp
25
+ * are synthesized here.
26
+ */
27
+ export declare function logMcpEvent(serverName: string, event: string, fields?: Record<string, unknown>): void;
28
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * MCP logging primitives shared by session wiring (startup.ts) and the OAuth
3
+ * flow (auth.ts). Kept as a standalone module so neither import site drags in
4
+ * the other (startup → manager → auth is already a chain; an auth → startup
5
+ * edge for the log helper would make it cyclic).
6
+ *
7
+ * `appendMcpLogLine` writes the capped/rotated `~/.yagni-code/logs/mcp.log`
8
+ * (fail-soft — logging never blocks the session or the CLI). The OAuth helpers
9
+ * here are redaction-first: authorization URLs drop `state` / `nonce` /
10
+ * `code_challenge` / `code_verifier` / `code`, and error text scrubs any
11
+ * credential-looking token, so nothing secret reaches the on-disk log.
12
+ */
13
+ import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { codeStateHome } from "../stateHome.js";
16
+ const MCP_LOG_MAX_BYTES = 256 * 1024;
17
+ const MCP_LOG_ROTATIONS = 2;
18
+ let mcpLogHomeOverride = null;
19
+ export function _setMcpLogHomeForTest(dir) {
20
+ mcpLogHomeOverride = dir;
21
+ }
22
+ function mcpLogDir() {
23
+ return join(codeStateHome(mcpLogHomeOverride), "logs");
24
+ }
25
+ /** Fail-soft append with size cap + rotation, mirroring errorSink's pattern. */
26
+ export function appendMcpLogLine(line) {
27
+ try {
28
+ const dir = mcpLogDir();
29
+ const path = join(dir, "mcp.log");
30
+ mkdirSync(dir, { recursive: true });
31
+ try {
32
+ if (statSync(path).isFile() && statSync(path).size >= MCP_LOG_MAX_BYTES) {
33
+ for (let i = MCP_LOG_ROTATIONS; i >= 1; i--) {
34
+ const from = i === 1 ? path : `${path}.${i - 1}`;
35
+ try {
36
+ renameSync(from, `${path}.${i}`);
37
+ }
38
+ catch {
39
+ /* absent source — fine */
40
+ }
41
+ }
42
+ }
43
+ }
44
+ catch {
45
+ /* stat on missing file — fine */
46
+ }
47
+ appendFileSync(path, line.endsWith("\n") ? line : line + "\n", "utf8");
48
+ }
49
+ catch {
50
+ // logging must never take the session down
51
+ }
52
+ }
53
+ /** OAuth query params that must never reach a log line (Claude Code parity). */
54
+ const SENSITIVE_OAUTH_PARAMS = ["state", "nonce", "code_challenge", "code_verifier", "code"];
55
+ /**
56
+ * Redact sensitive OAuth query params from a URL for safe logging (Claude Code
57
+ * parity — see its `redactSensitiveUrlParams`). Returns the URL unchanged when
58
+ * it does not parse.
59
+ */
60
+ export function redactSensitiveUrlParams(url) {
61
+ try {
62
+ const parsed = new URL(url);
63
+ for (const param of SENSITIVE_OAUTH_PARAMS) {
64
+ if (parsed.searchParams.has(param)) {
65
+ parsed.searchParams.set(param, "[REDACTED]");
66
+ }
67
+ }
68
+ return parsed.toString();
69
+ }
70
+ catch {
71
+ return url;
72
+ }
73
+ }
74
+ /**
75
+ * Append one structured MCP event to the log. `fields` are caller-controlled
76
+ * and expected to already be redacted; only the `server` / `event` / timestamp
77
+ * are synthesized here.
78
+ */
79
+ export function logMcpEvent(serverName, event, fields = {}) {
80
+ appendMcpLogLine(JSON.stringify({ ts: new Date().toISOString(), server: serverName, event, ...fields }));
81
+ }
82
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The MCP manager: owns one live client per configured server, tracks
3
+ * connection state, and exposes connect/reconnect/disconnect operations used
4
+ * by both the /mcp panel and session startup. Connection state is what the
5
+ * panel renders; tool/prompt registration is layered on top by tools.ts /
6
+ * prompts.ts once a server connects.
7
+ *
8
+ * Everything here is fail-soft: a server that fails to connect lands in
9
+ * `failed` with its error message, never blocking the session or the other
10
+ * servers. `MCP_TIMEOUT` (connect, ms) and `MCP_TOOL_TIMEOUT` (call, ms) are
11
+ * honored per Claude Code's env parity.
12
+ */
13
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
14
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
15
+ import type { McpStdioServerConfig, McpHttpServerConfig, McpScope } from "./config.js";
16
+ import { type AuthDeps } from "./auth.js";
17
+ export type McpServerStatus = "connecting" | "connected" | "failed" | "needs_auth" | "disabled" | "reconnecting";
18
+ export interface ManagedServer {
19
+ name: string;
20
+ scope: McpScope;
21
+ config: McpStdioServerConfig | McpHttpServerConfig;
22
+ status: McpServerStatus;
23
+ /** Plugin server sourced from repo config: rides the project approval gate. */
24
+ gated?: boolean;
25
+ /** Failure detail for the failed/needs_auth states (panel + logs). */
26
+ error?: string;
27
+ client?: Client;
28
+ transport?: Transport;
29
+ /** Reconnect attempt count while in `reconnecting`. */
30
+ reconnectAttempt?: number;
31
+ connectedAt?: number;
32
+ }
33
+ export interface ManagerEvents {
34
+ onStateChange?: (server: ManagedServer) => void;
35
+ /** Structured line for ~/.yagni-code/logs/mcp.log (already sanitized). */
36
+ onLog?: (line: string) => void;
37
+ }
38
+ export interface ManagerOpts {
39
+ connectTimeoutMs?: number;
40
+ events?: ManagerEvents;
41
+ /** Environment for ${VAR} expansion at connect time (default: process.env). */
42
+ env?: NodeJS.ProcessEnv;
43
+ }
44
+ export declare function connectTimeoutFromEnv(env: NodeJS.ProcessEnv, fallback?: number): number;
45
+ export declare function toolTimeoutFromEnv(env: NodeJS.ProcessEnv): number | undefined;
46
+ export declare class McpManager {
47
+ private servers;
48
+ private events;
49
+ private connectTimeoutMs;
50
+ private env;
51
+ private closed;
52
+ constructor(opts?: ManagerOpts);
53
+ list(): ManagedServer[];
54
+ get(name: string): ManagedServer | undefined;
55
+ /** Registers a server without connecting (panel listing, kill-switch states). */
56
+ register(name: string, scope: McpScope, config: McpStdioServerConfig | McpHttpServerConfig, status?: McpServerStatus, gated?: boolean): ManagedServer;
57
+ connect(name: string): Promise<ManagedServer | undefined>;
58
+ disconnect(name: string): Promise<void>;
59
+ /** Reconnect with bounded attempts, panel-visible progress. */
60
+ reconnect(name: string, maxAttempts?: number): Promise<ManagedServer | undefined>;
61
+ /**
62
+ * Drive the interactive OAuth flow for a `needs_auth` server, then reconnect.
63
+ * The flow opens a browser and returns once tokens are persisted; a cancelled
64
+ * or failed flow leaves the server in `needs_auth` and returns its error.
65
+ * `signal` cancels the loopback wait (Esc / session abort) instead of holding
66
+ * the command handler for the full 5-minute timeout. `authDeps` is the same
67
+ * seam `authenticate` takes (openUrl/fetch) so tests drive the real path
68
+ * without a browser or network.
69
+ */
70
+ authenticateServer(name: string, signal?: AbortSignal, authDeps?: AuthDeps): Promise<ManagedServer | undefined>;
71
+ /** Closes every connected client. Called on session_shutdown (any reason). */
72
+ closeAll(): Promise<void>;
73
+ private setStatus;
74
+ }
75
+ export type McpHealthStatus = "connected" | "needs_auth" | "failed";
76
+ export interface McpHealthResult {
77
+ status: McpHealthStatus;
78
+ /** Sanitized failure detail for needs_auth/failed. */
79
+ error?: string;
80
+ }
81
+ /**
82
+ * One-off health check for a configured server, used by `yagni mcp list` /
83
+ * `get` and `yagni doctor`. Does NOT mutate manager state: it builds a fresh
84
+ * transport + client, connect()s against it, classifies the result, and closes
85
+ * the client. OAuth-capable (http/sse) servers get an auth provider so a fresh
86
+ * token (or the redirect-required `needs_auth`) is reported faithfully.
87
+ *
88
+ * Callers apply the approval gate themselves: a project-scope server that is
89
+ * undecided/disabled must be reported as such WITHOUT connecting (fail-closed,
90
+ * never spawn a process the user has not approved), so `probeServer` never
91
+ * asks about approval — it only ever connects what it is given.
92
+ */
93
+ export declare function probeServer(name: string, config: McpStdioServerConfig | McpHttpServerConfig, opts?: {
94
+ connectTimeoutMs?: number;
95
+ env?: NodeJS.ProcessEnv;
96
+ }): Promise<McpHealthResult>;
97
+ /** 401/403 → needs_auth; everything else → failed. */
98
+ export declare function classifyFailure(message: string): "needs_auth" | "failed";
99
+ export declare function sanitizeError(message: string): string;
100
+ //# sourceMappingURL=manager.d.ts.map