pi-mcp-adapter 2.5.1 → 2.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.5.3] - 2026-05-01
11
+
12
+ ### Added
13
+ - Added environment variable and `~` expansion for stdio server `cwd` values.
14
+
15
+ ## [2.5.2] - 2026-04-29
16
+
17
+ ### Fixed
18
+ - Respected `PI_CODING_AGENT_DIR` for Pi-owned MCP config and state files, including metadata cache, npx cache, onboarding state, OAuth credentials, and `pi-mcp-adapter init` writes.
19
+
10
20
  ## [2.5.1] - 2026-04-24
11
21
 
12
22
  ### Fixed
package/README.md CHANGED
@@ -34,7 +34,7 @@ The adapter reads standard MCP files automatically. No extra setup needed if you
34
34
  | Host-specific configs (Cursor, Claude Code, Codex, etc.) but no standard MCP files | Run `/mcp setup` to adopt those host configs into Pi. The setup flow shows exactly what it found, lets you pick which ones to import, and previews the exact file changes before writing. |
35
35
  | Nothing configured yet | Run `/mcp setup` to scaffold a minimal `.mcp.json`, quick-add RepoPrompt, or inspect what the adapter discovered on your machine. |
36
36
 
37
- If you prefer the terminal, you can also run `pi-mcp-adapter init` after install to scan for host-specific configs and add missing compatibility imports to `~/.pi/agent/mcp.json`.
37
+ If you prefer the terminal, you can also run `pi-mcp-adapter init` after install to scan for host-specific configs and add missing compatibility imports to the Pi agent dir (`~/.pi/agent/mcp.json` by default, or `$PI_CODING_AGENT_DIR/mcp.json` when set).
38
38
 
39
39
  ## Quick Start
40
40
 
@@ -55,13 +55,13 @@ Preferred user-global shared config: `~/.config/mcp/mcp.json`
55
55
 
56
56
  Pi also reads Pi-owned override files for settings and host-specific compatibility:
57
57
 
58
- - `~/.pi/agent/mcp.json` — Pi global override
58
+ - `<Pi agent dir>/mcp.json` — Pi global override (`~/.pi/agent/mcp.json` by default)
59
59
  - `.pi/mcp.json` — Pi project override
60
60
 
61
61
  Precedence is:
62
62
 
63
63
  1. `~/.config/mcp/mcp.json`
64
- 2. `~/.pi/agent/mcp.json`
64
+ 2. `<Pi agent dir>/mcp.json`
65
65
  3. `.mcp.json`
66
66
  4. `.pi/mcp.json`
67
67
 
@@ -96,7 +96,7 @@ Use the shared MCP files when you want one setup to work across hosts, and Pi-ow
96
96
  |------|---------|
97
97
  | `~/.config/mcp/mcp.json` | User-global shared MCP config |
98
98
  | `.mcp.json` | Project-local shared MCP config |
99
- | `~/.pi/agent/mcp.json` | Pi global override and compatibility imports |
99
+ | `<Pi agent dir>/mcp.json` | Pi global override and compatibility imports (`~/.pi/agent/mcp.json` by default) |
100
100
  | `.pi/mcp.json` | Pi project override |
101
101
 
102
102
  Pi-specific files are the write targets for imported or shared global servers when Pi needs to persist adapter-only settings such as `directTools`.
@@ -120,9 +120,10 @@ Pi-specific files are the write targets for imported or shared global servers wh
120
120
  |-------|-------------|
121
121
  | `command` | Executable for stdio transport |
122
122
  | `args` | Command arguments |
123
- | `env` | Environment variables (`${VAR}` interpolation) |
124
- | `cwd` | Working directory |
123
+ | `env` | Environment variables; supports `${VAR}` and `$env:VAR` interpolation |
124
+ | `cwd` | Working directory; supports `${VAR}`, `$env:VAR`, and `~` expansion |
125
125
  | `url` | HTTP endpoint (StreamableHTTP with SSE fallback) |
126
+ | `headers` | HTTP headers; supports `${VAR}` and `$env:VAR` interpolation |
126
127
  | `auth` | `"bearer"` or `"oauth"` |
127
128
  | `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
128
129
  | `bearerToken` / `bearerTokenEnv` | Token or env var name |
@@ -231,7 +232,7 @@ To exclude specific tools while still using `directTools: true`, add `excludeToo
231
232
 
232
233
  Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`.
233
234
 
234
- Direct tools register from the metadata cache (`~/.pi/agent/mcp-cache.json`), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only and the cache populates in the background. To force it: `/mcp reconnect <server>`.
235
+ Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/mcp-cache.json` when set), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only and the cache populates in the background. To force it: `/mcp reconnect <server>`.
235
236
 
236
237
  When you change direct-tool toggles in `/mcp` or write new config through `/mcp setup`, the extension triggers Pi's normal reload flow automatically. That refreshes extensions, prompts, skills, and MCP tool registration in one shot, so newly configured direct tools can appear without a manual restart.
237
238
 
@@ -313,7 +314,7 @@ Shared MCP files are loaded automatically. Use `imports` only for host-specific
313
314
 
314
315
  Supported compatibility imports: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`
315
316
 
316
- `pi-mcp-adapter init` detects these host-specific configs and adds missing imports to `~/.pi/agent/mcp.json` for you.
317
+ `pi-mcp-adapter init` detects these host-specific configs and adds missing imports to the Pi agent dir config for you.
317
318
 
318
319
  ### Project Config
319
320
 
package/agent-dir.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+
4
+ export function getAgentDir(): string {
5
+ const configured = process.env.PI_CODING_AGENT_DIR?.trim();
6
+ if (!configured) {
7
+ return join(homedir(), ".pi", "agent");
8
+ }
9
+ if (configured === "~") {
10
+ return homedir();
11
+ }
12
+ if (configured.startsWith("~/")) {
13
+ return resolve(homedir(), configured.slice(2));
14
+ }
15
+ return resolve(configured);
16
+ }
17
+
18
+ export function getAgentPath(...segments: string[]): string {
19
+ return join(getAgentDir(), ...segments);
20
+ }
package/cli.js CHANGED
@@ -6,7 +6,17 @@ import os from "node:os";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
8
  const HOME = os.homedir();
9
- const PI_CONFIG_PATH = path.join(HOME, ".pi", "agent", "mcp.json");
9
+
10
+ function expandHome(input) {
11
+ if (input === "~") return HOME;
12
+ if (input.startsWith("~/")) return path.resolve(HOME, input.slice(2));
13
+ return path.resolve(input);
14
+ }
15
+
16
+ const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim()
17
+ ? expandHome(process.env.PI_CODING_AGENT_DIR.trim())
18
+ : path.join(HOME, ".pi", "agent");
19
+ const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
10
20
  const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
11
21
  const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
12
22
  const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), ".pi", "mcp.json");
package/config.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
+ import { getAgentPath } from "./agent-dir.js";
5
6
  import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.js";
6
7
 
7
- const PI_GLOBAL_CONFIG_PATH = join(homedir(), ".pi", "agent", "mcp.json");
8
8
  const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
9
9
  const PROJECT_CONFIG_NAME = ".mcp.json";
10
10
  const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
@@ -90,7 +90,7 @@ export interface ConfigWritePreview {
90
90
  }
91
91
 
92
92
  export function getPiGlobalConfigPath(overridePath?: string): string {
93
- return overridePath ? resolve(overridePath) : PI_GLOBAL_CONFIG_PATH;
93
+ return overridePath ? resolve(overridePath) : getAgentPath("mcp.json");
94
94
  }
95
95
 
96
96
  export function getGenericGlobalConfigPath(): string {
package/mcp-auth.ts CHANGED
@@ -5,12 +5,13 @@
5
5
  * and PKCE state for MCP servers. Maintains backward compatibility with
6
6
  * per-server directory structure.
7
7
  *
8
- * Token storage location: ~/.pi/agent/mcp-oauth/<server>/tokens.json
8
+ * Token storage location: $MCP_OAUTH_DIR/<server>/tokens.json when set,
9
+ * otherwise <Pi agent dir>/mcp-oauth/<server>/tokens.json
9
10
  */
10
11
 
11
12
  import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
12
13
  import { join } from 'path';
13
- import { homedir } from 'os';
14
+ import { getAgentPath } from './agent-dir.js';
14
15
 
15
16
  /** OAuth token storage format */
16
17
  export interface StoredTokens {
@@ -39,9 +40,8 @@ export interface AuthEntry {
39
40
 
40
41
  // Base directory for auth storage - can be overridden via env var for testing
41
42
  function getAuthBaseDir(): string {
42
- return process.env.MCP_OAUTH_DIR
43
- ? process.env.MCP_OAUTH_DIR
44
- : join(homedir(), '.pi', 'agent', 'mcp-oauth');
43
+ const override = process.env.MCP_OAUTH_DIR?.trim();
44
+ return override ? override : getAgentPath('mcp-oauth');
45
45
  }
46
46
 
47
47
  /**
@@ -459,7 +459,7 @@ export class McpSetupPanel {
459
459
  this.callbacks.previewImports(this.discovery.imports.filter((entry) => this.selectedImports.has(entry.kind)).map((entry) => entry.kind)),
460
460
  [
461
461
  `Detected imports: ${this.discovery.imports.map((entry) => `${entry.kind} (${entry.serverCount} servers)`).join(", ")}`,
462
- "Selected imports are written into `~/.pi/agent/mcp.json` as Pi-owned compatibility state.",
462
+ "Selected imports are written into the Pi agent dir config as Pi-owned compatibility state.",
463
463
  ],
464
464
  );
465
465
  case "view-example":
@@ -480,7 +480,7 @@ export class McpSetupPanel {
480
480
  return this.formatPreview([
481
481
  "Read order:",
482
482
  "1. ~/.config/mcp/mcp.json",
483
- "2. ~/.pi/agent/mcp.json",
483
+ "2. <Pi agent dir>/mcp.json",
484
484
  "3. .mcp.json",
485
485
  "4. .pi/mcp.json",
486
486
  "Pi writes compatibility imports and adapter-only overrides to Pi-owned files.",
package/metadata-cache.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  // metadata-cache.ts - Persistent MCP metadata cache
2
2
  import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
3
+ import { dirname } from "node:path";
4
+ import { getAgentPath } from "./agent-dir.js";
5
5
  import { createHash } from "node:crypto";
6
6
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
7
7
  import type { McpTool, McpResource, ServerEntry, ToolMetadata } from "./types.js";
8
8
  import { formatToolName, isToolExcluded } from "./types.js";
9
9
  import { resourceNameToToolName } from "./resource-tools.js";
10
- import { extractToolUiStreamMode } from "./utils.js";
10
+ import { extractToolUiStreamMode, interpolateEnvRecord, resolveConfigPath } from "./utils.js";
11
11
 
12
12
  const CACHE_VERSION = 1;
13
13
  const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
14
- const CACHE_PATH = join(homedir(), ".pi", "agent", "mcp-cache.json");
15
14
 
16
15
  export interface CachedTool {
17
16
  name: string;
@@ -40,13 +39,14 @@ export interface MetadataCache {
40
39
  }
41
40
 
42
41
  export function getMetadataCachePath(): string {
43
- return CACHE_PATH;
42
+ return getAgentPath("mcp-cache.json");
44
43
  }
45
44
 
46
45
  export function loadMetadataCache(): MetadataCache | null {
47
- if (!existsSync(CACHE_PATH)) return null;
46
+ const cachePath = getMetadataCachePath();
47
+ if (!existsSync(cachePath)) return null;
48
48
  try {
49
- const raw = JSON.parse(readFileSync(CACHE_PATH, "utf-8"));
49
+ const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
50
50
  if (!raw || typeof raw !== "object") return null;
51
51
  if (raw.version !== CACHE_VERSION) return null;
52
52
  if (!raw.servers || typeof raw.servers !== "object") return null;
@@ -57,13 +57,14 @@ export function loadMetadataCache(): MetadataCache | null {
57
57
  }
58
58
 
59
59
  export function saveMetadataCache(cache: MetadataCache): void {
60
- const dir = dirname(CACHE_PATH);
60
+ const cachePath = getMetadataCachePath();
61
+ const dir = dirname(cachePath);
61
62
  mkdirSync(dir, { recursive: true });
62
63
 
63
64
  let merged: MetadataCache = { version: CACHE_VERSION, servers: {} };
64
65
  try {
65
- if (existsSync(CACHE_PATH)) {
66
- const existing = JSON.parse(readFileSync(CACHE_PATH, "utf-8")) as MetadataCache;
66
+ if (existsSync(cachePath)) {
67
+ const existing = JSON.parse(readFileSync(cachePath, "utf-8")) as MetadataCache;
67
68
  if (existing && existing.version === CACHE_VERSION && existing.servers) {
68
69
  merged.servers = { ...existing.servers };
69
70
  }
@@ -75,9 +76,9 @@ export function saveMetadataCache(cache: MetadataCache): void {
75
76
  merged.version = CACHE_VERSION;
76
77
  merged.servers = { ...merged.servers, ...cache.servers };
77
78
 
78
- const tmpPath = `${CACHE_PATH}.${process.pid}.tmp`;
79
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
79
80
  writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
80
- renameSync(tmpPath, CACHE_PATH);
81
+ renameSync(tmpPath, cachePath);
81
82
  }
82
83
 
83
84
  export function computeServerHash(definition: ServerEntry): string {
@@ -87,12 +88,12 @@ export function computeServerHash(definition: ServerEntry): string {
87
88
  const identity: Record<string, unknown> = {
88
89
  command: definition.command,
89
90
  args: definition.args,
90
- env: definition.env,
91
- cwd: definition.cwd,
91
+ env: interpolateEnvRecord(definition.env),
92
+ cwd: resolveConfigPath(definition.cwd),
92
93
  url: definition.url,
93
- headers: definition.headers,
94
+ headers: interpolateEnvRecord(definition.headers),
94
95
  auth: definition.auth,
95
- bearerToken: definition.bearerToken,
96
+ bearerToken: definition.bearerToken ?? (definition.bearerTokenEnv ? process.env[definition.bearerTokenEnv] : undefined),
96
97
  bearerTokenEnv: definition.bearerTokenEnv,
97
98
  exposeResources: definition.exposeResources,
98
99
  excludeTools: definition.excludeTools,
package/npx-resolver.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  // npx-resolver.ts - Resolve npx/npm exec binaries to avoid npm parent processes
2
2
  import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync } from "node:fs";
3
- import { homedir } from "node:os";
4
3
  import { join, dirname, extname, resolve, sep } from "node:path";
4
+ import { getAgentPath } from "./agent-dir.js";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
 
7
7
  const CACHE_VERSION = 1;
8
8
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
9
- const CACHE_PATH = join(homedir(), ".pi", "agent", "mcp-npx-cache.json");
10
9
 
11
10
  interface NpxCacheEntry {
12
11
  resolvedBin: string;
@@ -367,10 +366,15 @@ function getNpmCacheDir(): string | null {
367
366
  return null;
368
367
  }
369
368
 
369
+ function getNpxCachePath(): string {
370
+ return getAgentPath("mcp-npx-cache.json");
371
+ }
372
+
370
373
  function loadCache(): NpxCache | null {
371
- if (!existsSync(CACHE_PATH)) return null;
374
+ const cachePath = getNpxCachePath();
375
+ if (!existsSync(cachePath)) return null;
372
376
  try {
373
- const raw = JSON.parse(readFileSync(CACHE_PATH, "utf-8"));
377
+ const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
374
378
  if (!raw || typeof raw !== "object") return null;
375
379
  if (raw.version !== CACHE_VERSION) return null;
376
380
  if (!raw.entries || typeof raw.entries !== "object") return null;
@@ -381,13 +385,14 @@ function loadCache(): NpxCache | null {
381
385
  }
382
386
 
383
387
  function saveCacheEntry(key: string, entry: NpxCacheEntry): void {
384
- const dir = dirname(CACHE_PATH);
388
+ const cachePath = getNpxCachePath();
389
+ const dir = dirname(cachePath);
385
390
  mkdirSync(dir, { recursive: true });
386
391
 
387
392
  let merged: NpxCache = { version: CACHE_VERSION, entries: {} };
388
393
  try {
389
- if (existsSync(CACHE_PATH)) {
390
- const existing = JSON.parse(readFileSync(CACHE_PATH, "utf-8")) as NpxCache;
394
+ if (existsSync(cachePath)) {
395
+ const existing = JSON.parse(readFileSync(cachePath, "utf-8")) as NpxCache;
391
396
  if (existing && existing.version === CACHE_VERSION && existing.entries) {
392
397
  merged.entries = { ...existing.entries };
393
398
  }
@@ -397,9 +402,9 @@ function saveCacheEntry(key: string, entry: NpxCacheEntry): void {
397
402
  }
398
403
 
399
404
  merged.entries[key] = entry;
400
- const tmpPath = `${CACHE_PATH}.${process.pid}.tmp`;
405
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
401
406
  writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
402
- renameSync(tmpPath, CACHE_PATH);
407
+ renameSync(tmpPath, cachePath);
403
408
  }
404
409
 
405
410
  function safeRealpath(path: string): string {
package/oauth-handler.ts CHANGED
@@ -1,19 +1,22 @@
1
1
  // oauth-handler.ts - OAuth token management for MCP servers
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { homedir } from "node:os";
4
+ import { getAgentPath } from "./agent-dir.js";
5
5
  import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
6
6
 
7
7
  // Token storage path for a server
8
8
  function getTokensPath(serverName: string): string {
9
- return join(homedir(), ".pi", "agent", "mcp-oauth", serverName, "tokens.json");
9
+ const override = process.env.MCP_OAUTH_DIR?.trim();
10
+ const authDir = override ? override : getAgentPath("mcp-oauth");
11
+ return join(authDir, serverName, "tokens.json");
10
12
  }
11
13
 
12
14
  /**
13
15
  * Get stored OAuth tokens for a server (if any).
14
16
  * Returns undefined if no tokens or tokens are expired.
15
17
  *
16
- * Token file location: ~/.pi/agent/mcp-oauth/<server>/tokens.json
18
+ * Token file location: $MCP_OAUTH_DIR/<server>/tokens.json when set,
19
+ * otherwise <Pi agent dir>/mcp-oauth/<server>/tokens.json
17
20
  *
18
21
  * Expected format:
19
22
  * {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
2
+ import { dirname } from "node:path";
3
+ import { getAgentPath } from "./agent-dir.js";
4
4
 
5
5
  export interface McpOnboardingState {
6
6
  version: 1;
@@ -16,7 +16,7 @@ const DEFAULT_STATE: McpOnboardingState = {
16
16
  };
17
17
 
18
18
  export function getOnboardingStatePath(): string {
19
- return join(homedir(), ".pi", "agent", "mcp-onboarding.json");
19
+ return getAgentPath("mcp-onboarding.json");
20
20
  }
21
21
 
22
22
  export function loadOnboardingState(): McpOnboardingState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "files": [
39
39
  "cli.js",
40
+ "agent-dir.ts",
40
41
  "index.ts",
41
42
  "state.ts",
42
43
  "utils.ts",
package/server-manager.ts CHANGED
@@ -17,6 +17,7 @@ import { logger } from "./logger.js";
17
17
  import { McpOAuthProvider } from "./mcp-oauth-provider.js";
18
18
  import { supportsOAuth } from "./mcp-auth-flow.js";
19
19
  import { registerSamplingHandler, type ServerSamplingConfig } from "./sampling-handler.js";
20
+ import { interpolateEnvRecord, resolveConfigPath } from "./utils.js";
20
21
 
21
22
  interface ServerConnection {
22
23
  client: Client;
@@ -91,7 +92,7 @@ export class McpServerManager {
91
92
  command,
92
93
  args,
93
94
  env: resolveEnv(definition.env),
94
- cwd: definition.cwd,
95
+ cwd: resolveConfigPath(definition.cwd),
95
96
  stderr: definition.debug ? "inherit" : "ignore",
96
97
  });
97
98
  } else if (definition.url) {
@@ -362,28 +363,14 @@ function resolveEnv(env?: Record<string, string>): Record<string, string> {
362
363
  }
363
364
 
364
365
  if (!env) return resolved;
365
-
366
- for (const [key, value] of Object.entries(env)) {
367
- // Support ${VAR} and $env:VAR interpolation
368
- resolved[key] = value
369
- .replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
370
- .replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
371
- }
372
-
373
- return resolved;
366
+
367
+ const overrides = interpolateEnvRecord(env);
368
+ return overrides ? { ...resolved, ...overrides } : resolved;
374
369
  }
375
370
 
376
371
  /**
377
372
  * Resolve headers with environment variable interpolation.
378
373
  */
379
374
  function resolveHeaders(headers?: Record<string, string>): Record<string, string> | undefined {
380
- if (!headers) return undefined;
381
-
382
- const resolved: Record<string, string> = {};
383
- for (const [key, value] of Object.entries(headers)) {
384
- resolved[key] = value
385
- .replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
386
- .replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
387
- }
388
- return resolved;
375
+ return interpolateEnvRecord(headers);
389
376
  }
package/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { platform } from "node:os";
2
+ import { homedir, platform } from "node:os";
3
+ import { join } from "node:path";
3
4
  import type { McpConfig } from "./types.js";
4
5
 
5
6
  async function execOpen(pi: ExtensionAPI, target: string, browser?: string) {
@@ -58,6 +59,33 @@ export function getConfigPathFromArgv(): string | undefined {
58
59
  return undefined;
59
60
  }
60
61
 
62
+ export function interpolateEnvVars(value: string): string {
63
+ return value
64
+ .replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
65
+ .replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
66
+ }
67
+
68
+ export function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
69
+ if (!values) return undefined;
70
+
71
+ const resolved: Record<string, string> = {};
72
+ for (const [key, value] of Object.entries(values)) {
73
+ resolved[key] = interpolateEnvVars(value);
74
+ }
75
+ return resolved;
76
+ }
77
+
78
+ export function resolveConfigPath(value: string | undefined): string | undefined {
79
+ if (value === undefined) return undefined;
80
+
81
+ const resolved = interpolateEnvVars(value);
82
+ if (resolved === "~") return homedir();
83
+ if (resolved.startsWith("~/") || resolved.startsWith("~\\")) {
84
+ return join(homedir(), resolved.slice(2));
85
+ }
86
+ return resolved;
87
+ }
88
+
61
89
  export function truncateAtWord(text: string, target: number): string {
62
90
  if (!text || text.length <= target) return text;
63
91