@yagni-app/code 1.0.5 → 1.0.6

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 (50) hide show
  1. package/README.md +30 -6
  2. package/dist/claudePlugins.d.ts +3 -1
  3. package/dist/claudePlugins.js +3 -1
  4. package/dist/cli.js +12 -0
  5. package/dist/doctor.d.ts +28 -3
  6. package/dist/doctor.js +117 -7
  7. package/dist/extension/index.d.ts +5 -5
  8. package/dist/extension/index.js +89 -29
  9. package/dist/extension/mcp/approval.d.ts +45 -0
  10. package/dist/extension/mcp/approval.js +164 -0
  11. package/dist/extension/mcp/auth.d.ts +124 -0
  12. package/dist/extension/mcp/auth.js +560 -0
  13. package/dist/extension/mcp/authStore.d.ts +61 -0
  14. package/dist/extension/mcp/authStore.js +105 -0
  15. package/dist/extension/mcp/callbackPage.d.ts +31 -0
  16. package/dist/extension/mcp/callbackPage.js +222 -0
  17. package/dist/extension/mcp/cliConfig.d.ts +12 -0
  18. package/dist/extension/mcp/cliConfig.js +12 -0
  19. package/dist/extension/mcp/config.d.ts +131 -0
  20. package/dist/extension/mcp/config.js +309 -0
  21. package/dist/extension/mcp/log.d.ts +28 -0
  22. package/dist/extension/mcp/log.js +82 -0
  23. package/dist/extension/mcp/manager.d.ts +98 -0
  24. package/dist/extension/mcp/manager.js +273 -0
  25. package/dist/extension/mcp/names.d.ts +25 -0
  26. package/dist/extension/mcp/names.js +40 -0
  27. package/dist/extension/mcp/panel.d.ts +34 -0
  28. package/dist/extension/mcp/panel.js +258 -0
  29. package/dist/extension/mcp/prompts.d.ts +23 -0
  30. package/dist/extension/mcp/prompts.js +93 -0
  31. package/dist/extension/mcp/startup.d.ts +55 -0
  32. package/dist/extension/mcp/startup.js +150 -0
  33. package/dist/extension/mcp/tools.d.ts +31 -0
  34. package/dist/extension/mcp/tools.js +117 -0
  35. package/dist/extension/mcp/transports.d.ts +17 -0
  36. package/dist/extension/mcp/transports.js +44 -0
  37. package/dist/extension/permission/gate.d.ts +7 -0
  38. package/dist/extension/permission/gate.js +12 -5
  39. package/dist/extension/permission/guardian.d.ts +24 -5
  40. package/dist/extension/permission/guardian.js +162 -24
  41. package/dist/extension/pipeline/personas.js +5 -0
  42. package/dist/mcpCommand.d.ts +113 -0
  43. package/dist/mcpCommand.js +755 -0
  44. package/dist/otel.d.ts +36 -7
  45. package/dist/otel.js +90 -12
  46. package/dist/upgrade.d.ts +11 -2
  47. package/dist/upgrade.js +48 -8
  48. package/package.json +3 -2
  49. package/dist/extension/mcpTools.d.ts +0 -57
  50. package/dist/extension/mcpTools.js +0 -132
@@ -0,0 +1,105 @@
1
+ /**
2
+ * OAuth token + client-credential store for MCP servers, pure file I/O (no
3
+ * network, no pi imports) so it is shared verbatim between the extension
4
+ * session and the `yagni mcp` CLI (revoke-on-remove reads it too).
5
+ *
6
+ * ~/.yagni-code/mcp-auth.json (0600 — access/refresh tokens and any
7
+ * pre-registered client secret are secrets)
8
+ *
9
+ * Entries are keyed `serverName|<sha256(type+url+headers)[:16]>` — the same
10
+ * key Claude Code derives via `getServerKey`, so a server whose URL/headers
11
+ * change invalidates its stored credentials automatically. Writes are atomic
12
+ * (temp + rename) and chmod 0600, exactly like the mcp.json config writes.
13
+ */
14
+ import { createHash } from "node:crypto";
15
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { codeStateHome } from "../stateHome.js";
18
+ /** Test seam (same shape as _setMcpHomeForTest in config.ts). */
19
+ let authHomeOverride = null;
20
+ export function _setMcpAuthHomeForTest(dir) {
21
+ authHomeOverride = dir;
22
+ }
23
+ export function mcpAuthPath() {
24
+ return join(codeStateHome(authHomeOverride), "mcp-auth.json");
25
+ }
26
+ /**
27
+ * The stable key for one server's credentials. Hash covers type + url +
28
+ * headers (NOT the client id, matching Claude Code) so a URL/header change
29
+ * orphans old tokens rather than silently reusing them against a different
30
+ * endpoint.
31
+ */
32
+ export function getServerKey(serverName, config) {
33
+ const hash = createHash("sha256")
34
+ .update(JSON.stringify({ type: config.type, url: config.url, headers: config.headers ?? {} }))
35
+ .digest("hex")
36
+ .substring(0, 16);
37
+ return `${serverName}|${hash}`;
38
+ }
39
+ /** Read + parse the auth store; missing/unparseable → empty with error noted. */
40
+ export function readMcpAuth() {
41
+ const path = mcpAuthPath();
42
+ if (!existsSync(path))
43
+ return { file: {}, errors: [] };
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
46
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
47
+ return { file: {}, errors: ["mcp-auth.json is not a JSON object"] };
48
+ }
49
+ return { file: parsed, errors: [] };
50
+ }
51
+ catch (err) {
52
+ return { file: {}, errors: [`mcp-auth.json unreadable (${err instanceof Error ? err.message : String(err)})`] };
53
+ }
54
+ }
55
+ /** Atomic write + chmod 0600 (tokens/secrets must never be world-readable). */
56
+ export function writeMcpAuth(file) {
57
+ const path = mcpAuthPath();
58
+ mkdirSync(dirname(path), { recursive: true });
59
+ const tmp = join(dirname(path), `.mcp-auth.json.tmp-${process.pid}`);
60
+ writeFileSync(tmp, JSON.stringify(file, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
61
+ renameSync(tmp, path);
62
+ chmodSync(path, 0o600);
63
+ }
64
+ /** Read one entry by server key; undefined when absent. */
65
+ export function getStoredOAuthEntry(serverName, config) {
66
+ const { file } = readMcpAuth();
67
+ return file.servers?.[getServerKey(serverName, config)];
68
+ }
69
+ /**
70
+ * Read-modify-write one entry. `mutate` receives the current entry (or a fresh
71
+ * one) and may edit it in place; returning `false` deletes it. Used by both
72
+ * the provider (save tokens/creds) and revocation (clear on remove).
73
+ */
74
+ export function updateStoredOAuthEntry(serverName, config, mutate) {
75
+ const { file, errors } = readMcpAuth();
76
+ if (errors.length > 0)
77
+ return { errors };
78
+ const key = getServerKey(serverName, config);
79
+ const servers = file.servers ?? {};
80
+ const entry = servers[key] ?? { serverName, serverUrl: config.url };
81
+ const result = mutate(entry);
82
+ if (result === false) {
83
+ delete servers[key];
84
+ }
85
+ else {
86
+ servers[key] = entry;
87
+ }
88
+ // Drop the file entirely when every entry is gone, so a fresh dir is clean.
89
+ if (Object.keys(servers).length > 0)
90
+ file.servers = servers;
91
+ else
92
+ delete file.servers;
93
+ try {
94
+ writeMcpAuth(file);
95
+ return { errors: [] };
96
+ }
97
+ catch (err) {
98
+ return { errors: [`could not write mcp-auth.json (${err instanceof Error ? err.message : String(err)})`] };
99
+ }
100
+ }
101
+ /** Remove every credential entry (used by revoke-on-remove). */
102
+ export function deleteStoredOAuthEntry(serverName, config) {
103
+ return updateStoredOAuthEntry(serverName, config, () => false);
104
+ }
105
+ //# sourceMappingURL=authStore.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The loopback OAuth callback page — the only surface a user ever sees served
3
+ * from the local `127.0.0.1:<port>/callback` listener. Rendered with the same
4
+ * Glass & Print language as the app: tinted blue field, floating glass card,
5
+ * one blue accent, three type voices (Instrument Sans interface / Newsreader
6
+ * authored / IBM Plex Mono fact).
7
+ *
8
+ * Self-contained: no external assets, fonts, or network calls — the page must
9
+ * render offline and never phone home. Tokens are inlined from
10
+ * packages/frontend/src/styles/tokens.css so this module can't drift a runtime
11
+ * dependency on the frontend build; token names are kept in comments so a
12
+ * re-derivation stays mechanical.
13
+ *
14
+ * `escapeHtml` is applied by the caller to every provider-controlled value
15
+ * before interpolation (a local process can hit the loopback with a crafted
16
+ * ?error=<script> payload — the page never reflects raw input).
17
+ */
18
+ /** The success page shown after the provider redirects back with a code. */
19
+ export declare function renderAuthSuccessPage(): string;
20
+ /**
21
+ * The error page shown when the provider bounces the flow. `error` and
22
+ * `errorDescription` are provider-controlled and already HTML-escaped by the
23
+ * caller.
24
+ */
25
+ export declare function renderAuthErrorPage(opts: {
26
+ error?: string;
27
+ errorDescription?: string;
28
+ }): string;
29
+ /** The state-mismatch page — a hard failure, shown before the flow aborts. */
30
+ export declare function renderAuthStateMismatchPage(): string;
31
+ //# sourceMappingURL=callbackPage.d.ts.map
@@ -0,0 +1,222 @@
1
+ /**
2
+ * The loopback OAuth callback page — the only surface a user ever sees served
3
+ * from the local `127.0.0.1:<port>/callback` listener. Rendered with the same
4
+ * Glass & Print language as the app: tinted blue field, floating glass card,
5
+ * one blue accent, three type voices (Instrument Sans interface / Newsreader
6
+ * authored / IBM Plex Mono fact).
7
+ *
8
+ * Self-contained: no external assets, fonts, or network calls — the page must
9
+ * render offline and never phone home. Tokens are inlined from
10
+ * packages/frontend/src/styles/tokens.css so this module can't drift a runtime
11
+ * dependency on the frontend build; token names are kept in comments so a
12
+ * re-derivation stays mechanical.
13
+ *
14
+ * `escapeHtml` is applied by the caller to every provider-controlled value
15
+ * before interpolation (a local process can hit the loopback with a crafted
16
+ * ?error=<script> payload — the page never reflects raw input).
17
+ */
18
+ const CSS = `
19
+ :root {
20
+ /* tokens.css — field, ink, accent, materials */
21
+ --field-base: #f4f6fc;
22
+ --field-tint-blue: #dfe8ff;
23
+ --field-tint-violet: #e8e4ff;
24
+ --field-tint-cyan: #e3f0f6;
25
+ --ink: #23252e;
26
+ --ink-strong: #181a22;
27
+ --text-secondary: #565b72;
28
+ --text-quiet: #8b90a5;
29
+ --text-label: #7b8098;
30
+ --accent: #2f56d3;
31
+ --accent-muted: rgba(47, 86, 211, 0.12);
32
+ --accent-border: rgba(47, 86, 211, 0.3);
33
+ --error: #b4452f;
34
+ --error-muted: rgba(180, 69, 47, 0.07);
35
+ --glass-bg: rgba(255, 255, 255, 0.55);
36
+ --glass-border: rgba(255, 255, 255, 0.85);
37
+ --glass-blur: 18px;
38
+ --radius-pane: 16px;
39
+ --shadow-staged: 0 1px 2px rgba(24, 34, 64, 0.07), 0 18px 40px -30px rgba(38, 52, 110, 0.35);
40
+ --shadow-staged-high: 0 1px 2px rgba(24, 34, 64, 0.08), 0 28px 60px -28px rgba(38, 52, 110, 0.42);
41
+ --body: "Instrument Sans Variable", "Instrument Sans", system-ui, sans-serif;
42
+ --prose: "Newsreader", ui-serif, Georgia, serif;
43
+ --mono: "IBM Plex Mono", ui-monospace, monospace;
44
+ }
45
+
46
+ * { box-sizing: border-box; }
47
+
48
+ html, body { height: 100%; }
49
+
50
+ body {
51
+ margin: 0;
52
+ min-height: 100%;
53
+ display: grid;
54
+ place-items: center;
55
+ padding: 24px;
56
+ font-family: var(--body);
57
+ color: var(--ink);
58
+ background:
59
+ radial-gradient(1200px 700px at 8% -5%, var(--field-tint-blue) 0, rgba(223, 232, 255, 0) 60%),
60
+ radial-gradient(900px 600px at 95% 0%, var(--field-tint-violet) 0, rgba(232, 228, 255, 0) 55%),
61
+ radial-gradient(900px 700px at 60% 100%, var(--field-tint-cyan) 0, rgba(227, 240, 246, 0) 60%),
62
+ var(--field-base);
63
+ }
64
+
65
+ .card {
66
+ width: 100%;
67
+ max-width: 400px;
68
+ text-align: center;
69
+ padding: 40px 32px 32px;
70
+ background: var(--glass-bg);
71
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.15);
72
+ backdrop-filter: blur(var(--glass-blur)) saturate(1.15);
73
+ border: 1px solid var(--glass-border);
74
+ border-radius: var(--radius-pane);
75
+ box-shadow: var(--shadow-staged-high);
76
+ animation: rise 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
77
+ }
78
+
79
+ @keyframes rise {
80
+ from { opacity: 0; transform: translateY(8px); }
81
+ to { opacity: 1; transform: none; }
82
+ }
83
+
84
+ .wordmark {
85
+ font-family: var(--mono);
86
+ font-size: 10px;
87
+ font-weight: 600;
88
+ letter-spacing: 0.22em;
89
+ text-transform: uppercase;
90
+ color: var(--text-label);
91
+ margin: 0 0 28px;
92
+ }
93
+
94
+ .badge {
95
+ width: 52px;
96
+ height: 52px;
97
+ margin: 0 auto 20px;
98
+ display: grid;
99
+ place-items: center;
100
+ border-radius: 14px;
101
+ background: var(--accent-muted);
102
+ border: 1px solid var(--accent-border);
103
+ color: var(--accent);
104
+ }
105
+
106
+ .card--error .badge {
107
+ background: var(--error-muted);
108
+ border-color: rgba(180, 69, 47, 0.28);
109
+ color: var(--error);
110
+ }
111
+
112
+ h1 {
113
+ font-family: var(--prose);
114
+ font-size: 30px;
115
+ font-weight: 600;
116
+ line-height: 1.15;
117
+ letter-spacing: -0.01em;
118
+ color: var(--ink-strong);
119
+ margin: 0 0 10px;
120
+ }
121
+
122
+ .lede {
123
+ font-size: 14px;
124
+ line-height: 1.55;
125
+ color: var(--text-secondary);
126
+ margin: 0 0 24px;
127
+ }
128
+
129
+ .detail {
130
+ font-family: var(--mono);
131
+ font-size: 12px;
132
+ line-height: 1.5;
133
+ color: var(--error);
134
+ background: var(--error-muted);
135
+ border: 1px solid rgba(180, 69, 47, 0.2);
136
+ border-radius: 8px;
137
+ padding: 8px 10px;
138
+ margin: 0 0 24px;
139
+ word-break: break-word;
140
+ }
141
+
142
+ .divider {
143
+ height: 1px;
144
+ margin: 0 0 20px;
145
+ background: linear-gradient(90deg, transparent, var(--accent-border), transparent);
146
+ }
147
+
148
+ .hint {
149
+ font-size: 12.5px;
150
+ color: var(--text-quiet);
151
+ margin: 0;
152
+ }
153
+
154
+ @media (prefers-reduced-motion: reduce) {
155
+ .card { animation: none; }
156
+ }
157
+ `;
158
+ const CHECK_ICON = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
159
+ const ERROR_ICON = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="16.5" x2="12.01" y2="16.5"/></svg>`;
160
+ function shell(opts) {
161
+ return `<!DOCTYPE html>
162
+ <html lang="en">
163
+ <head>
164
+ <meta charset="utf-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1">
166
+ <meta name="color-scheme" content="light">
167
+ <title>${opts.title} — YAGNI Code</title>
168
+ <style>${CSS}</style>
169
+ </head>
170
+ <body>
171
+ <main class="card${opts.error ? " card--error" : ""}">
172
+ <p class="wordmark">YAGNI Code</p>
173
+ <div class="badge">${opts.error ? ERROR_ICON : CHECK_ICON}</div>
174
+ <h1>${opts.heading}</h1>
175
+ <p class="lede">${opts.lede}</p>
176
+ ${opts.detail ? `<p class="detail">${opts.detail}</p>` : ""}
177
+ <div class="divider"></div>
178
+ <p class="hint">${opts.hint}</p>
179
+ </main>
180
+ </body>
181
+ </html>`;
182
+ }
183
+ /** The success page shown after the provider redirects back with a code. */
184
+ export function renderAuthSuccessPage() {
185
+ return shell({
186
+ title: "Connected",
187
+ heading: "Connected to YAGNI Code",
188
+ lede: "Your MCP server is authorized and ready to use.",
189
+ hint: "You can close this window and return to your session.",
190
+ });
191
+ }
192
+ /**
193
+ * The error page shown when the provider bounces the flow. `error` and
194
+ * `errorDescription` are provider-controlled and already HTML-escaped by the
195
+ * caller.
196
+ */
197
+ export function renderAuthErrorPage(opts) {
198
+ const detail = opts.error
199
+ ? opts.errorDescription
200
+ ? `${opts.error}: ${opts.errorDescription}`
201
+ : opts.error
202
+ : undefined;
203
+ return shell({
204
+ title: "Connection failed",
205
+ error: true,
206
+ heading: "Authentication didn't complete",
207
+ lede: "The provider didn't finish the sign-in flow.",
208
+ detail,
209
+ hint: "You can close this window and try again from YAGNI Code.",
210
+ });
211
+ }
212
+ /** The state-mismatch page — a hard failure, shown before the flow aborts. */
213
+ export function renderAuthStateMismatchPage() {
214
+ return shell({
215
+ title: "Connection failed",
216
+ error: true,
217
+ heading: "Authentication didn't complete",
218
+ lede: "The sign-in response didn't match the request, so it was rejected.",
219
+ hint: "You can close this window and try again from YAGNI Code.",
220
+ });
221
+ }
222
+ //# sourceMappingURL=callbackPage.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The extension's MCP config surface, re-exported for the CLI launcher
3
+ * (`yagni mcp …`, `yagni-code-cli/src/mcpCommand.ts` imports this file by path
4
+ * from its bundled copy). Pure config I/O — no pi imports, no TUI, no network —
5
+ * so importing it from the launcher is side-effect-free.
6
+ */
7
+ export { McpServerConfig, McpScope, PROJECT_CONFIG_FILENAME, ScopedMcpServerConfig, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
8
+ export { ProjectApprovalState, decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
9
+ export { McpAuthFile, StoredOAuthEntry, deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
10
+ export { revokeTokensOnRemove } from "./auth.js";
11
+ export { probeServer, McpHealthResult, McpHealthStatus } from "./manager.js";
12
+ //# sourceMappingURL=cliConfig.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The extension's MCP config surface, re-exported for the CLI launcher
3
+ * (`yagni mcp …`, `yagni-code-cli/src/mcpCommand.ts` imports this file by path
4
+ * from its bundled copy). Pure config I/O — no pi imports, no TUI, no network —
5
+ * so importing it from the launcher is side-effect-free.
6
+ */
7
+ export { PROJECT_CONFIG_FILENAME, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
8
+ export { decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
9
+ export { deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
10
+ export { revokeTokensOnRemove } from "./auth.js";
11
+ export { probeServer } from "./manager.js";
12
+ //# sourceMappingURL=cliConfig.js.map
@@ -0,0 +1,131 @@
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
+ *
10
+ * Merge precedence (Claude Code parity): user < project < local — later wins.
11
+ * Writes are atomic (temp file + rename) so two concurrent sessions can never
12
+ * interleave into a corrupt config. Validation errors are collected and
13
+ * surfaced, never fatal: one malformed server entry must not hide the rest.
14
+ */
15
+ export type McpScope = "user" | "project" | "local";
16
+ export interface McpStdioServerConfig {
17
+ type?: "stdio";
18
+ command: string;
19
+ args?: string[];
20
+ env?: Record<string, string>;
21
+ }
22
+ /** OAuth 2.1 settings for an http/sse server (Claude Code's `oauth` shape). */
23
+ export interface McpOAuthConfig {
24
+ /** Pre-registered client id (overrides/avoids DCR when present). */
25
+ clientId?: string;
26
+ /** Fixed loopback callback port for servers that require a registered redirect URI. */
27
+ callbackPort?: number;
28
+ }
29
+ export interface McpHttpServerConfig {
30
+ type: "http" | "sse";
31
+ url: string;
32
+ headers?: Record<string, string>;
33
+ oauth?: McpOAuthConfig;
34
+ }
35
+ export type McpServerConfig = McpStdioServerConfig | McpHttpServerConfig;
36
+ export interface ScopedMcpServerConfig {
37
+ name: string;
38
+ config: McpServerConfig;
39
+ scope: McpScope;
40
+ /** Where this entry came from (file path) — surfaced in /mcp and `yagni mcp get`. */
41
+ sourcePath: string;
42
+ }
43
+ export interface McpConfigError {
44
+ /** File the problem was found in. */
45
+ sourcePath: string;
46
+ /** Server name when the error is about one entry; the file itself otherwise. */
47
+ serverName?: string;
48
+ message: string;
49
+ }
50
+ export declare const PROJECT_CONFIG_FILENAME = ".mcp.json";
51
+ /**
52
+ * Expand `${VAR}` / `${VAR:-default}` in a config string (Claude Code parity —
53
+ * .mcp.json files are VCS-shared, so secrets come from the environment).
54
+ * Missing vars with no default are reported, left in place, and skipped at
55
+ * connect time so a half-expanded command never spawns.
56
+ */
57
+ export declare function expandEnvVarsInString(value: string, env?: NodeJS.ProcessEnv): {
58
+ expanded: string;
59
+ missingVars: string[];
60
+ };
61
+ /** Where env expansion applies within one server config (stdio: command,
62
+ * args, env values; http/sse: url and header values). */
63
+ export declare function expandServerEnv(config: McpServerConfig, env?: NodeJS.ProcessEnv): {
64
+ config: McpServerConfig;
65
+ missingVars: string[];
66
+ };
67
+ /**
68
+ * The `~/.yagni-code/mcp.json` shape. Top-level `mcpServers` is the user scope;
69
+ * `projects[<absPath>]` holds per-directory local servers plus this project's
70
+ * `.mcp.json` approval choices (Claude Code's exact key names, so the approval
71
+ * state reads the same way in both tools).
72
+ */
73
+ /** Per-project entry inside `~/.yagni-code/mcp.json`. */
74
+ export interface UserMcpProjectEntry {
75
+ mcpServers?: Record<string, McpServerConfig>;
76
+ enabledMcpjsonServers?: string[];
77
+ disabledMcpjsonServers?: string[];
78
+ enableAllProjectMcpServers?: boolean;
79
+ /** User/local-scope servers disabled from the /mcp panel for this project. */
80
+ disabledMcpServers?: string[];
81
+ }
82
+ export interface UserMcpConfigFile {
83
+ mcpServers?: Record<string, McpServerConfig>;
84
+ projects?: Record<string, UserMcpProjectEntry>;
85
+ }
86
+ export declare function _setMcpHomeForTest(dir: string | null): void;
87
+ export declare function mcpConfigPath(): string;
88
+ /** Read + parse `~/.yagni-code/mcp.json`; unreadable/missing → empty with error collected. */
89
+ export declare function readUserMcpConfig(): {
90
+ file: UserMcpConfigFile;
91
+ errors: McpConfigError[];
92
+ };
93
+ /** Atomic write: temp file in the same directory, then rename over the target. */
94
+ export declare function writeUserMcpConfig(file: UserMcpConfigFile): void;
95
+ /** Read + parse a project `.mcp.json`; missing → empty (not an error). */
96
+ export declare function readProjectMcpConfig(repoRoot: string, read?: (path: string) => string | undefined): {
97
+ servers: Record<string, McpServerConfig>;
98
+ errors: McpConfigError[];
99
+ };
100
+ /**
101
+ * Structural validation of one server entry. Claude Code uses zod; we don't
102
+ * depend on zod, so this hand-checks the same union: stdio (no type / "stdio",
103
+ * non-empty command) vs http/sse (type + url).
104
+ */
105
+ export declare function validateServerConfig(value: unknown): {
106
+ ok: true;
107
+ } | {
108
+ ok: false;
109
+ message: string;
110
+ };
111
+ export interface LoadMcpServersResult {
112
+ servers: ScopedMcpServerConfig[];
113
+ errors: McpConfigError[];
114
+ }
115
+ /**
116
+ * The repo root for project-scope lookup: the nearest ancestor of `cwd` that
117
+ * looks like a repo (has `.git`, `.mcp.json`, or is a known project root).
118
+ * Falls back to cwd itself. CC walks ALL ancestors and merges nearest-wins;
119
+ * we take the nearest repo boundary — same result for the normal case of one
120
+ * repo, and it keeps the approval prompt anchored to one file.
121
+ */
122
+ export declare function resolveProjectRoot(cwd: string, exists?: (p: string) => boolean): string;
123
+ /**
124
+ * Load all three scopes and merge with precedence user < project < local.
125
+ * Invalid entries are dropped (with errors) rather than failing the load; a
126
+ * same-name entry in a higher-precedence scope replaces the lower one.
127
+ * Project servers are returned regardless of approval state — the caller
128
+ * (session startup, panel, CLI) applies the approval gate via approval.ts.
129
+ */
130
+ export declare function loadMcpServers(cwd: string, env?: NodeJS.ProcessEnv): LoadMcpServersResult;
131
+ //# sourceMappingURL=config.d.ts.map