memtrace 0.8.9 → 0.8.11

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.
@@ -66,6 +66,17 @@ function hermesMcpHasMemtrace(file) {
66
66
  const raw = fs.readFileSync(file, 'utf-8');
67
67
  return /^mcp_servers:\s*$(?:[\s\S]*?)^\s{2}memtrace:\s*$/m.test(raw);
68
68
  }
69
+ function piPackageRegistered(file) {
70
+ const { value } = safeReadJson(file);
71
+ if (!value?.packages?.length)
72
+ return false;
73
+ return value.packages.some(entry => {
74
+ const source = typeof entry === 'string' ? entry : entry.source;
75
+ if (!source)
76
+ return false;
77
+ return source.endsWith(`${path.sep}pi-package`) || source.endsWith('/pi-package');
78
+ });
79
+ }
69
80
  function opencodeConfigDir() {
70
81
  const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config');
71
82
  return path.join(xdg, 'opencode');
@@ -170,6 +181,17 @@ function checkAgent(agent) {
170
181
  mcpConfigPath,
171
182
  };
172
183
  }
184
+ if (agent === 'pi') {
185
+ const skillsDir = path.join(os.homedir(), '.pi', 'agent', 'skills');
186
+ const mcpConfigPath = path.join(os.homedir(), '.pi', 'agent', 'settings.json');
187
+ return {
188
+ agent,
189
+ skillsFound: countMemtraceSkills(skillsDir),
190
+ skillsDir,
191
+ mcpRegistered: piPackageRegistered(mcpConfigPath),
192
+ mcpConfigPath,
193
+ };
194
+ }
173
195
  const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
174
196
  const mcpConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
175
197
  return {
@@ -197,7 +219,8 @@ export function formatReport(r) {
197
219
  lines.push(` version: ${r.binaryVersion}`);
198
220
  for (const a of r.agents) {
199
221
  lines.push(`${check(a.skillsFound > 0)} ${a.agent} skills installed ${a.skillsFound} in ${a.skillsDir}`);
200
- lines.push(`${check(a.mcpRegistered)} ${a.agent} MCP registered ${a.mcpConfigPath}`);
222
+ const integrationLabel = a.agent === 'pi' ? 'Pi package registered' : 'MCP registered';
223
+ lines.push(`${check(a.mcpRegistered)} ${a.agent} ${integrationLabel} ${a.mcpConfigPath}`);
201
224
  }
202
225
  return lines.join('\n');
203
226
  }
@@ -61,12 +61,18 @@ export async function runInstall(options) {
61
61
  try {
62
62
  const r = await t.install(skills, ctx);
63
63
  console.log(` [${t.name}] ✓ ${r.skillsWritten} skills → ${r.skillsDir}`);
64
- if (r.mcpRegistered)
65
- console.log(` [${t.name}] ✓ MCP registered`);
66
- else if (ctx.skipMcp)
64
+ if (r.mcpRegistered) {
65
+ console.log(` [${t.name}] ✓ ${t.name === 'pi' ? 'Pi package registered' : 'MCP registered'}`);
66
+ }
67
+ else if (t.name === 'pi') {
68
+ console.log(` [${t.name}] ⚠ Pi package registration failed`);
69
+ }
70
+ else if (ctx.skipMcp) {
67
71
  console.log(` [${t.name}] (MCP skipped)`);
68
- else
72
+ }
73
+ else {
69
74
  console.log(` [${t.name}] ⚠ MCP registration failed`);
75
+ }
70
76
  for (const w of r.warnings)
71
77
  console.warn(` [${t.name}] warn: ${w}`);
72
78
  }
@@ -15,7 +15,7 @@ function parseOnly(val) {
15
15
  program
16
16
  .command('install')
17
17
  .description('Install memtrace skills and register MCP for selected agents')
18
- .option('--only <agents>', 'comma-separated agent names (claude,cursor,codex,gemini,windsurf,vscode,hermes,opencode,kiro,warp)', parseOnly)
18
+ .option('--only <agents>', 'comma-separated agent names (claude,cursor,codex,gemini,windsurf,vscode,hermes,opencode,kiro,warp,pi)', parseOnly)
19
19
  .option('--local', 'install into the current project where the selected agent supports project scope', false)
20
20
  .option('--global', 'install globally (~/.claude/, ~/.cursor/, ~/.agents/) [default]', false)
21
21
  .option('--skip-mcp', 'write skills only, skip MCP server registration', false)
@@ -9,7 +9,8 @@ import { hermesTransformer } from './hermes.js';
9
9
  import { opencodeTransformer } from './opencode.js';
10
10
  import { kiroTransformer } from './kiro.js';
11
11
  import { warpTransformer } from './warp.js';
12
+ import { piTransformer } from './pi.js';
12
13
  export declare const ALL_TRANSFORMERS: Transformer[];
13
14
  export declare function findTransformer(name: string): Transformer | undefined;
14
- export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, warpTransformer, };
15
+ export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, warpTransformer, piTransformer, };
15
16
  export type { Transformer, InstallContext, InstallResult, TransformResult } from './types.js';
@@ -8,6 +8,7 @@ import { hermesTransformer } from './hermes.js';
8
8
  import { opencodeTransformer } from './opencode.js';
9
9
  import { kiroTransformer } from './kiro.js';
10
10
  import { warpTransformer } from './warp.js';
11
+ import { piTransformer } from './pi.js';
11
12
  export const ALL_TRANSFORMERS = [
12
13
  claudeTransformer,
13
14
  cursorTransformer,
@@ -19,8 +20,9 @@ export const ALL_TRANSFORMERS = [
19
20
  opencodeTransformer,
20
21
  kiroTransformer,
21
22
  warpTransformer,
23
+ piTransformer,
22
24
  ];
23
25
  export function findTransformer(name) {
24
26
  return ALL_TRANSFORMERS.find(t => t.name === name);
25
27
  }
26
- export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, warpTransformer, };
28
+ export { claudeTransformer, cursorTransformer, codexTransformer, geminiTransformer, windsurfTransformer, vscodeTransformer, hermesTransformer, opencodeTransformer, kiroTransformer, warpTransformer, piTransformer, };
@@ -0,0 +1,5 @@
1
+ import { Transformer, InstallContext } from './types.js';
2
+ export declare function piSettingsPath(ctx: InstallContext): string;
3
+ /** Resolve the pi-package directory shipped inside the memtrace npm bundle. */
4
+ export declare function resolveBundledPiPackage(fromDir?: string): string;
5
+ export declare const piTransformer: Transformer;
@@ -0,0 +1,152 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { removeMemtraceSkills, skillName } from './shared.js';
6
+ import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
7
+ import { commandExists } from '../utils.js';
8
+ // Pi (https://pi.dev/docs/latest/packages) uses native pi packages — not MCP JSON
9
+ // config like Cursor. The bundled `pi-package/` extension spawns `memtrace mcp`
10
+ // itself and registers every Memtrace tool as a native Pi tool (`memtrace_*`).
11
+ //
12
+ // `memtrace install --only pi` writes skills + registers this bundled package.
13
+ function piAgentRoot(ctx) {
14
+ return ctx.scope === 'global'
15
+ ? path.join(os.homedir(), '.pi', 'agent')
16
+ : path.join(ctx.cwd, '.pi');
17
+ }
18
+ function skillsRoot(ctx) {
19
+ return path.join(piAgentRoot(ctx), 'skills');
20
+ }
21
+ export function piSettingsPath(ctx) {
22
+ return path.join(piAgentRoot(ctx), 'settings.json');
23
+ }
24
+ /** Resolve the pi-package directory shipped inside the memtrace npm bundle. */
25
+ export function resolveBundledPiPackage(fromDir = path.dirname(fileURLToPath(import.meta.url))) {
26
+ for (const rel of ['../../../pi-package', '../../pi-package']) {
27
+ const candidate = path.resolve(fromDir, rel);
28
+ if (fs.existsSync(path.join(candidate, 'package.json')))
29
+ return candidate;
30
+ }
31
+ throw new Error('bundled pi-package not found (reinstall memtrace from npm)');
32
+ }
33
+ function transformForPi(text) {
34
+ return text.replace(/mcp__memtrace__/g, 'memtrace_');
35
+ }
36
+ function transformAllowedTool(tool) {
37
+ return tool
38
+ .replace(/^mcp__memtrace__/, 'memtrace_')
39
+ .replace(/^Read$/, 'read')
40
+ .replace(/^Grep$/, 'grep')
41
+ .replace(/^Glob$/, 'find');
42
+ }
43
+ function skillMarkdownForPi(skill) {
44
+ const name = skillName(skill);
45
+ const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
46
+ const tools = skill.frontmatter['allowed-tools'];
47
+ const toolsLine = tools?.length
48
+ ? `\nallowed-tools: ${tools.map(transformAllowedTool).join(' ')}`
49
+ : '';
50
+ const body = transformForPi(skill.body);
51
+ return `---\nname: ${name}\ndescription: "${safeDesc}"${toolsLine}\n---\n\n${body}`;
52
+ }
53
+ function writeSkillsForPi(skills, rootDir) {
54
+ removeMemtraceSkills(rootDir);
55
+ for (const skill of skills) {
56
+ const name = skillName(skill);
57
+ const outDir = path.join(rootDir, name);
58
+ fs.mkdirSync(outDir, { recursive: true });
59
+ fs.writeFileSync(path.join(outDir, 'SKILL.md'), skillMarkdownForPi(skill));
60
+ }
61
+ return skills.length;
62
+ }
63
+ function packageEntrySource(entry) {
64
+ if (typeof entry === 'string')
65
+ return entry;
66
+ return typeof entry.source === 'string' ? entry.source : undefined;
67
+ }
68
+ function isMemtracePiPackageEntry(entry, packagePath) {
69
+ const source = packageEntrySource(entry);
70
+ if (!source)
71
+ return false;
72
+ if (source === packagePath)
73
+ return true;
74
+ const base = `${path.sep}pi-package`;
75
+ return source.endsWith(base) || source.endsWith('/pi-package');
76
+ }
77
+ function registerPiPackageAt(settingsFile, packagePath) {
78
+ const { value, corrupted, backupPath } = safeReadJson(settingsFile);
79
+ if (corrupted)
80
+ return { registered: false, backupPath, packagePath };
81
+ const cfg = (value ?? {});
82
+ cfg.packages = (cfg.packages ?? []).filter(entry => !isMemtracePiPackageEntry(entry, packagePath));
83
+ cfg.packages.push(packagePath);
84
+ writeJsonAtomic(settingsFile, cfg);
85
+ return { registered: true, packagePath };
86
+ }
87
+ function removePiPackageAt(settingsFile, packagePath) {
88
+ const { value, corrupted } = safeReadJson(settingsFile);
89
+ if (corrupted || !value?.packages?.length)
90
+ return;
91
+ const kept = value.packages.filter(entry => !isMemtracePiPackageEntry(entry, packagePath));
92
+ if (kept.length === value.packages.length)
93
+ return;
94
+ if (kept.length === 0) {
95
+ delete value.packages;
96
+ if (Object.keys(value).length === 0)
97
+ fs.unlinkSync(settingsFile);
98
+ else
99
+ writeJsonAtomic(settingsFile, value);
100
+ return;
101
+ }
102
+ value.packages = kept;
103
+ writeJsonAtomic(settingsFile, value);
104
+ }
105
+ export const piTransformer = {
106
+ name: 'pi',
107
+ async install(skills, ctx) {
108
+ const rootDir = skillsRoot(ctx);
109
+ const count = writeSkillsForPi(skills, rootDir);
110
+ const warnings = [];
111
+ const settingsFile = piSettingsPath(ctx);
112
+ let packagePath;
113
+ try {
114
+ packagePath = resolveBundledPiPackage();
115
+ }
116
+ catch (e) {
117
+ return {
118
+ agent: 'pi',
119
+ skillsWritten: count,
120
+ skillsDir: rootDir,
121
+ mcpConfigPath: settingsFile,
122
+ mcpRegistered: false,
123
+ warnings: [`${e.message}`],
124
+ };
125
+ }
126
+ const result = registerPiPackageAt(settingsFile, packagePath);
127
+ if (!result.registered && result.backupPath) {
128
+ warnings.push(`Pi settings.json was malformed; backed up to ${result.backupPath}.`);
129
+ }
130
+ if (!(await commandExists('pi'))) {
131
+ warnings.push('Pi CLI not found on PATH — skills and package path are configured; install Pi from https://pi.dev to use them.');
132
+ }
133
+ if (ctx.skipMcp) {
134
+ warnings.push('memtrace binary not found — the Pi extension will connect once `memtrace` is on PATH.');
135
+ }
136
+ return {
137
+ agent: 'pi',
138
+ skillsWritten: count,
139
+ skillsDir: rootDir,
140
+ mcpConfigPath: settingsFile,
141
+ mcpRegistered: result.registered,
142
+ warnings,
143
+ };
144
+ },
145
+ async uninstall(ctx) {
146
+ removeMemtraceSkills(skillsRoot(ctx));
147
+ try {
148
+ removePiPackageAt(piSettingsPath(ctx), resolveBundledPiPackage());
149
+ }
150
+ catch { /* bundled pi-package path unavailable */ }
151
+ },
152
+ };
@@ -29,7 +29,7 @@ export interface InstallResult {
29
29
  mcpRegistered: boolean;
30
30
  warnings: string[];
31
31
  }
32
- export type AgentName = 'claude' | 'cursor' | 'codex' | 'gemini' | 'windsurf' | 'vscode' | 'hermes' | 'opencode' | 'kiro' | 'warp';
32
+ export type AgentName = 'claude' | 'cursor' | 'codex' | 'gemini' | 'windsurf' | 'vscode' | 'hermes' | 'opencode' | 'kiro' | 'warp' | 'pi';
33
33
  /**
34
34
  * One transformer per supported AI coding agent.
35
35
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace-skills",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "description": "Memtrace skills for AI coding agents — codebase exploration, temporal evolution, impact analysis, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -85,7 +85,7 @@ function buildMutationList({ home, cwd, projectOnly = false, noHooks = false })
85
85
  { path: path.join(claude, "plugins", "installed_plugins.json"), change: "plugin registry entry (written by the `claude` CLI when available)" },
86
86
  { path: path.join(home, ".claude.json"), change: "possible memtrace MCP entry (written by `claude mcp add-json --scope user` when the `claude` CLI is available)" },
87
87
  { path: path.join(home, ".memtrace") + path.sep, change: "create memtrace data dir: persisted uninstall.js + install-backup/ (backups + manifest)" },
88
- { path: path.join(home, "{.cursor,.codex,.gemini,.codeium,.hermes,.kiro,.copilot,.warp,.config/opencode,VS Code user config}"), change: "skills + memtrace MCP registration for other detected agents (same merge-add pattern)" },
88
+ { path: path.join(home, "{.cursor,.codex,.gemini,.codeium,.hermes,.kiro,.copilot,.warp,.pi,.config/opencode,VS Code user config}"), change: "skills + memtrace MCP registration (or Pi native package) for other detected agents (same merge-add pattern)" },
89
89
  );
90
90
  return list;
91
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
5
5
  "keywords": [
6
6
  "mcp",
@@ -27,7 +27,8 @@
27
27
  "skills/",
28
28
  "installer/",
29
29
  "install.js",
30
- "uninstall.js"
30
+ "uninstall.js",
31
+ "pi-package/"
31
32
  ],
32
33
  "scripts": {
33
34
  "prepack": "node ../../installer/scripts/prepare-npm-package.js",
@@ -39,12 +40,12 @@
39
40
  "fs-extra": "^11.0.0"
40
41
  },
41
42
  "optionalDependencies": {
42
- "@memtrace/darwin-arm64": "0.8.9",
43
- "@memtrace/linux-x64": "0.8.9",
44
- "@memtrace/linux-arm64": "0.8.9",
45
- "@memtrace/win32-x64": "0.8.9",
46
- "@memtrace/linux-x64-noavx2": "0.8.9",
47
- "@memtrace/win32-x64-noavx2": "0.8.9"
43
+ "@memtrace/darwin-arm64": "0.8.11",
44
+ "@memtrace/linux-x64": "0.8.11",
45
+ "@memtrace/linux-arm64": "0.8.11",
46
+ "@memtrace/win32-x64": "0.8.11",
47
+ "@memtrace/linux-x64-noavx2": "0.8.11",
48
+ "@memtrace/win32-x64-noavx2": "0.8.11"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=18"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Memtrace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,51 @@
1
+ # Memtrace for Pi
2
+
3
+ Bundled Pi extension for [Memtrace](https://memtrace.io). Installed automatically by `memtrace install --only pi` — not published or installed separately.
4
+
5
+ This package speaks the Memtrace MCP protocol over stdio and registers every Memtrace tool as a **native Pi tool** (`memtrace_*`), so Pi's model sees real Typebox schemas and tool names instead of a generic MCP passthrough.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g memtrace
11
+ memtrace install --only pi -y
12
+ # or pick pi from the interactive harness list:
13
+ memtrace install
14
+ ```
15
+
16
+ This writes all Memtrace skills to `~/.pi/agent/skills/` and registers this package in `~/.pi/agent/settings.json`.
17
+
18
+ Use `--local` for project scope (`.pi/skills/` + `.pi/settings.json`).
19
+
20
+ ## Requirements
21
+
22
+ - [Pi](https://pi.dev) on PATH
23
+ - `memtrace` CLI on PATH (`npm install -g memtrace`)
24
+
25
+ The extension spawns `memtrace mcp` itself. Run `memtrace start` separately only if you want the daemon/HTTP API to stay up outside Pi sessions.
26
+
27
+ ## Usage
28
+
29
+ | Command | What it does |
30
+ |---|---|
31
+ | `/memtrace` | Connection state, indexed repos, cwd coverage |
32
+ | `/memtrace:connect` | Reconnect and refresh tools |
33
+ | `/memtrace:index [path]` | Index a directory (defaults to cwd) |
34
+ | `/memtrace:reindex` | Incremental re-index for cwd repo |
35
+ | `/memtrace:tools` | List registered `memtrace_*` tools |
36
+ | `/memtrace:setup` | Check binary install, print next steps |
37
+
38
+ ## Configuration
39
+
40
+ Precedence (highest first):
41
+
42
+ 1. `MEMTRACE_PI_COMMAND`, `MEMTRACE_PI_ARGS` env vars
43
+ 2. `.pi/memtrace.json` (project)
44
+ 3. `~/.pi/agent/memtrace.json` (user)
45
+ 4. Defaults: `memtrace` + `["mcp"]`
46
+
47
+ Set `MEMTRACE_PI_DEBUG=1` to log connection lifecycle to stderr.
48
+
49
+ ## License
50
+
51
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,96 @@
1
+ // config.ts — resolve the Memtrace server connection config.
2
+ //
3
+ // Precedence (highest first):
4
+ // 1. MEMTRACE_*_PI env overrides (MEMTRACE_PI_COMMAND, MEMTRACE_PI_ARGS)
5
+ // 2. project file .pi/memtrace.json
6
+ // 3. user file ~/.pi/agent/memtrace.json
7
+ // 4. base env process.env (MEMTRACE_EMBED_KEY, MEMTRACE_MEMDB_ENDPOINT, …)
8
+ // 5. defaults command "memtrace", args ["mcp"]
9
+ //
10
+ // `memtrace mcp` is the real stdio MCP subcommand (verified against memtrace
11
+ // 0.8.7's CLI); it attaches to an already-running `memtrace start` workspace
12
+ // daemon or boots its own embedded MemDB sidecar if none is running. Older
13
+ // docs/configs may still reference a `serve` subcommand that never shipped —
14
+ // index.ts retries with ["mcp"] if a configured `serve` is rejected.
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { resolve } from "node:path";
19
+
20
+ export interface MemtraceConfig {
21
+ /** Executable used to launch the Memtrace MCP server over stdio. */
22
+ command: string;
23
+ /** Args passed to the command. Default ["mcp"]; a configured legacy ["serve"] falls back to ["mcp"] on failure. */
24
+ args: string[];
25
+ /** Extra env merged on top of process.env for the child process. */
26
+ env: Record<string, string>;
27
+ /** Working directory for the child process. */
28
+ cwd?: string;
29
+ /** Idle disconnect timeout in minutes (0 = never). */
30
+ idleTimeoutMin: number;
31
+ /** Per-request timeout in ms. */
32
+ requestTimeoutMs: number;
33
+ /** Startup connect timeout in ms (eager connect at session start). */
34
+ startupTimeoutMs: number;
35
+ }
36
+
37
+ const DEFAULTS: MemtraceConfig = {
38
+ command: "memtrace",
39
+ args: ["mcp"],
40
+ env: {},
41
+ idleTimeoutMin: 10,
42
+ requestTimeoutMs: 60_000,
43
+ startupTimeoutMs: 4000,
44
+ };
45
+
46
+ function tryRead(path: string): Record<string, unknown> | null {
47
+ try {
48
+ const raw = readFileSync(path, "utf8");
49
+ return JSON.parse(raw) as Record<string, unknown>;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function shallowMerge(base: MemtraceConfig, extra: Record<string, unknown>): MemtraceConfig {
56
+ const out = { ...base };
57
+ if (typeof extra.command === "string" && extra.command.trim()) out.command = extra.command;
58
+ if (Array.isArray(extra.args) && extra.args.every((a) => typeof a === "string")) {
59
+ out.args = extra.args as string[];
60
+ }
61
+ if (extra.env && typeof extra.env === "object") {
62
+ out.env = { ...out.env, ...(extra.env as Record<string, string>) };
63
+ }
64
+ if (typeof extra.cwd === "string") out.cwd = extra.cwd;
65
+ if (typeof extra.idleTimeoutMin === "number") out.idleTimeoutMin = extra.idleTimeoutMin;
66
+ if (typeof extra.requestTimeoutMs === "number") out.requestTimeoutMs = extra.requestTimeoutMs;
67
+ if (typeof extra.startupTimeoutMs === "number") out.startupTimeoutMs = extra.startupTimeoutMs;
68
+ return out;
69
+ }
70
+
71
+ export function loadConfig(projectDir: string): MemtraceConfig {
72
+ let cfg: MemtraceConfig = { ...DEFAULTS, env: {} };
73
+
74
+ const userFile = resolve(homedir(), ".pi/agent/memtrace.json");
75
+ const userJson = tryRead(userFile);
76
+ if (userJson) cfg = shallowMerge(cfg, userJson);
77
+
78
+ const projectFile = resolve(projectDir, ".pi/memtrace.json");
79
+ const projectJson = tryRead(projectFile);
80
+ if (projectJson) cfg = shallowMerge(cfg, projectJson);
81
+
82
+ // MEMTRACE_EMBED_KEY / MEMTRACE_MEMDB_ENDPOINT etc. propagate automatically
83
+ // because the child inherits process.env; surface a couple of common ones into
84
+ // the explicit env map only if the user already set them in config.
85
+ if (process.env.MEMTRACE_EMBED_KEY && !cfg.env.MEMTRACE_EMBED_KEY) {
86
+ cfg.env.MEMTRACE_EMBED_KEY = process.env.MEMTRACE_EMBED_KEY;
87
+ }
88
+
89
+ // Highest-precedence env overrides for the launcher itself.
90
+ if (process.env.MEMTRACE_PI_COMMAND) cfg.command = process.env.MEMTRACE_PI_COMMAND;
91
+ if (process.env.MEMTRACE_PI_ARGS) {
92
+ cfg.args = process.env.MEMTRACE_PI_ARGS.split(/\s+/).filter(Boolean);
93
+ }
94
+
95
+ return cfg;
96
+ }