From 05cea633441cb3051bedd6a0a1dac2b47a10d633 Mon Sep 17 00:00:00 2001 From: davidrobertson Date: Sat, 22 Aug 2026 22:18:00 -0400 Subject: [PATCH] fix: load hook settings from env file --- plugin/scripts/antigravity-bridge.mjs | 40 +++++++++++++- plugin/scripts/notification.mjs | 40 +++++++++++++- plugin/scripts/post-commit.mjs | 39 ++++++++++++++ plugin/scripts/post-tool-failure.mjs | 40 +++++++++++++- plugin/scripts/post-tool-use.mjs | 40 +++++++++++++- plugin/scripts/pre-compact.mjs | 40 +++++++++++++- plugin/scripts/pre-tool-use.mjs | 39 ++++++++++++++ plugin/scripts/prompt-submit.mjs | 40 +++++++++++++- plugin/scripts/session-end.mjs | 41 +++++++++++++- plugin/scripts/session-start.mjs | 40 +++++++++++++- plugin/scripts/stop.mjs | 41 +++++++++++++- plugin/scripts/subagent-start.mjs | 40 +++++++++++++- plugin/scripts/subagent-stop.mjs | 40 +++++++++++++- plugin/scripts/task-completed.mjs | 40 +++++++++++++- src/config.ts | 78 +++------------------------ src/env-file.ts | 57 ++++++++++++++++++++ src/hooks/_env.ts | 3 ++ src/hooks/antigravity-bridge.ts | 1 + src/hooks/notification.ts | 1 + src/hooks/post-commit.ts | 1 + src/hooks/post-tool-failure.ts | 1 + src/hooks/post-tool-use.ts | 1 + src/hooks/pre-compact.ts | 1 + src/hooks/pre-tool-use.ts | 1 + src/hooks/prompt-submit.ts | 1 + src/hooks/session-end.ts | 1 + src/hooks/session-start.ts | 1 + src/hooks/stop.ts | 1 + src/hooks/subagent-start.ts | 1 + src/hooks/subagent-stop.ts | 1 + src/hooks/task-completed.ts | 1 + test/context-injection.test.ts | 10 +++- test/hook-delivery.test.ts | 67 +++++++++++++++++++++-- 33 files changed, 699 insertions(+), 90 deletions(-) create mode 100644 src/env-file.ts create mode 100644 src/hooks/_env.ts diff --git a/plugin/scripts/antigravity-bridge.mjs b/plugin/scripts/antigravity-bridge.mjs index 12d72ba90..c3775d310 100755 --- a/plugin/scripts/antigravity-bridge.mjs +++ b/plugin/scripts/antigravity-bridge.mjs @@ -1,7 +1,45 @@ #!/usr/bin/env node -import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/antigravity-bridge.ts const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url)); const TOOL_NAME_MAP = { diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index dcf2e3931..58210ba8c 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/post-commit.mjs b/plugin/scripts/post-commit.mjs index 56a17ccea..c71017221 100755 --- a/plugin/scripts/post-commit.mjs +++ b/plugin/scripts/post-commit.mjs @@ -1,6 +1,45 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function hookCwd(data) { if (!data || typeof data !== "object") return void 0; diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index 0af94e7cd..f3c927c0e 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 1189e35fe..50f5fbd29 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index 39ba45962..e29ba7737 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/pre-tool-use.mjs b/plugin/scripts/pre-tool-use.mjs index 97c65dc90..446c4eb99 100755 --- a/plugin/scripts/pre-tool-use.mjs +++ b/plugin/scripts/pre-tool-use.mjs @@ -1,4 +1,43 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/pre-tool-use.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index 53daba26c..381a738cb 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index f2d8f79b1..d5def7fe1 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -1,7 +1,44 @@ #!/usr/bin/env node -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index b332d5e5a..73573ae1e 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/stop.mjs b/plugin/scripts/stop.mjs index 5b8ac4cda..2b24ad4d4 100755 --- a/plugin/scripts/stop.mjs +++ b/plugin/scripts/stop.mjs @@ -1,4 +1,43 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/stop.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -36,4 +75,4 @@ main().catch(() => process.exit(0)); //#endregion export {}; -//# sourceMappingURL=stop.mjs.map +//# sourceMappingURL=stop.mjs.map \ No newline at end of file diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index 722f0c7f0..72e6ee97d 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index 8927c1af6..fda675b2f 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index 613a9cd9b..d87cb4785 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -1,6 +1,44 @@ #!/usr/bin/env node +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; import { execSync } from "node:child_process"; -import { basename } from "node:path"; +const ENV_FILE = join(process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"), ".env"); +let envFileCache; +function loadEnvFile() { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + const vars = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [key, value] of Object.entries(loadEnvFile())) if (process.env[key] === void 0) process.env[key] = value; +} +//#endregion +//#region src/hooks/_env.ts +hydrateProcessEnvFromFile(); +//#endregion //#region src/hooks/_project.ts function resolveProject(cwd) { const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; diff --git a/src/config.ts b/src/config.ts index 11451672f..28eb88d2c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,11 @@ -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; import { homedir } from "node:os"; +import { join } from "node:path"; import pc from "picocolors"; +import { AGENTMEMORY_DATA_DIR, getMergedEnv } from "./env-file.js"; +export { + __resetEnvFileCache, + hydrateProcessEnvFromFile, +} from "./env-file.js"; import type { AgentMemoryConfig, ProviderConfig, @@ -17,73 +21,12 @@ function safeParseInt(value: string | undefined, fallback: number): number { return Number.isNaN(parsed) ? fallback : parsed; } -const DATA_DIR = - process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"); -const ENV_FILE = join(DATA_DIR, ".env"); - let warnPremiumModelShown = false; -// Parsed ~/.agentmemory/.env, memoized for the process lifetime. getMergedEnv() -// runs on every config getter (~20 of them), so without this cache a single -// request would readFileSync + reparse the file dozens of times. The file is -// boot-static, so read it from disk once and reuse the result. Tests that -// mutate the file between cases reset the module (clearing this via reload) or -// call __resetEnvFileCache(). -let envFileCache: Record | undefined; - -function loadEnvFile(): Record { - if (envFileCache) return envFileCache; - if (!existsSync(ENV_FILE)) { - envFileCache = {}; - return envFileCache; - } - const content = readFileSync(ENV_FILE, "utf-8"); - const vars: Record = {}; - for (const line of content.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eqIdx = trimmed.indexOf("="); - if (eqIdx === -1) continue; - const key = trimmed.slice(0, eqIdx).trim(); - let val = trimmed.slice(eqIdx + 1).trim(); - const quoteChar = val[0] === '"' || val[0] === "'" ? val[0] : ""; - if (quoteChar) { - const closeIdx = val.indexOf(quoteChar, 1); - if (closeIdx !== -1) val = val.slice(1, closeIdx); - } else { - const hashIdx = val.indexOf(" #"); - if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); - } - vars[key] = val; - } - envFileCache = vars; - return envFileCache; -} - -// Test hook: clears the memoized .env so the next loadEnvFile() re-reads disk -// within the same module instance. vi.resetModules() reloads this module and -// resets the cache on its own; this exists for tests that mutate the file -// without a module reload. -export function __resetEnvFileCache(): void { - envFileCache = undefined; -} - function hasRealValue(v: string | undefined): v is string { return typeof v === "string" && v.trim().length > 0; } -// Hydrate ~/.agentmemory/.env into process.env at boot. loadEnvFile() is -// otherwise only consumed via getMergedEnv(), which the many modules that -// read raw process.env["X"] never call — so .env-only values were silently -// ignored by them. Copy the file's vars into process.env, but only when the -// key is currently unset so a real process.env value still wins (this -// preserves the {...fileEnv, ...process.env} precedence getMergedEnv uses). -export function hydrateProcessEnvFromFile(): void { - for (const [k, v] of Object.entries(loadEnvFile())) { - if (process.env[k] === undefined) process.env[k] = v; - } -} - function detectProvider(env: Record): ProviderConfig { const maxTokens = parseInt(env["MAX_TOKENS"] || "4096", 10); @@ -213,17 +156,10 @@ export function loadConfig(): AgentMemoryConfig { tokenBudget: safeParseInt(env["TOKEN_BUDGET"], 2000), maxObservationsPerSession: safeParseInt(env["MAX_OBS_PER_SESSION"], 500), compressionModel: provider.model, - dataDir: DATA_DIR, + dataDir: AGENTMEMORY_DATA_DIR, }; } -function getMergedEnv( - overrides?: Record, -): Record { - const fileEnv = loadEnvFile(); - return { ...fileEnv, ...process.env, ...overrides } as Record; -} - export function getEnvVar(key: string): string | undefined { return getMergedEnv()[key]; } diff --git a/src/env-file.ts b/src/env-file.ts new file mode 100644 index 000000000..f47737683 --- /dev/null +++ b/src/env-file.ts @@ -0,0 +1,57 @@ +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +export const AGENTMEMORY_DATA_DIR = + process.env["AGENTMEMORY_DATA_DIR"]?.trim() || join(homedir(), ".agentmemory"); + +const ENV_FILE = join(AGENTMEMORY_DATA_DIR, ".env"); +let envFileCache: Record | undefined; + +function loadEnvFile(): Record { + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } + + const vars: Record = {}; + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === '"' || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} + +export function __resetEnvFileCache(): void { + envFileCache = undefined; +} + +export function hydrateProcessEnvFromFile(): void { + for (const [key, value] of Object.entries(loadEnvFile())) { + if (process.env[key] === undefined) process.env[key] = value; + } +} + +export function getMergedEnv( + overrides?: Record, +): Record { + return { ...loadEnvFile(), ...process.env, ...overrides } as Record< + string, + string + >; +} diff --git a/src/hooks/_env.ts b/src/hooks/_env.ts new file mode 100644 index 000000000..d9bf9eeec --- /dev/null +++ b/src/hooks/_env.ts @@ -0,0 +1,3 @@ +import { hydrateProcessEnvFromFile } from "../env-file.js"; + +hydrateProcessEnvFromFile(); diff --git a/src/hooks/antigravity-bridge.ts b/src/hooks/antigravity-bridge.ts index 60e65bac7..083cbdd46 100644 --- a/src/hooks/antigravity-bridge.ts +++ b/src/hooks/antigravity-bridge.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { spawnSync } from "node:child_process"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/src/hooks/notification.ts b/src/hooks/notification.ts index 4c6a1063f..11841d146 100644 --- a/src/hooks/notification.ts +++ b/src/hooks/notification.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { diff --git a/src/hooks/post-commit.ts b/src/hooks/post-commit.ts index 434519077..6b287dcd5 100644 --- a/src/hooks/post-commit.ts +++ b/src/hooks/post-commit.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; diff --git a/src/hooks/post-tool-failure.ts b/src/hooks/post-tool-failure.ts index 69ec145df..46f0fca02 100644 --- a/src/hooks/post-tool-failure.ts +++ b/src/hooks/post-tool-failure.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { diff --git a/src/hooks/post-tool-use.ts b/src/hooks/post-tool-use.ts index e8fe3483c..7084cfa4e 100644 --- a/src/hooks/post-tool-use.ts +++ b/src/hooks/post-tool-use.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { diff --git a/src/hooks/pre-compact.ts b/src/hooks/pre-compact.ts index 2283e4ebb..9c0db50bb 100644 --- a/src/hooks/pre-compact.ts +++ b/src/hooks/pre-compact.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { diff --git a/src/hooks/pre-tool-use.ts b/src/hooks/pre-tool-use.ts index 0262fdea5..c494d42b3 100644 --- a/src/hooks/pre-tool-use.ts +++ b/src/hooks/pre-tool-use.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/src/hooks/prompt-submit.ts b/src/hooks/prompt-submit.ts index 91527b742..2240fe4f9 100644 --- a/src/hooks/prompt-submit.ts +++ b/src/hooks/prompt-submit.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { diff --git a/src/hooks/session-end.ts b/src/hooks/session-end.ts index f39f964a5..e35a09cf1 100644 --- a/src/hooks/session-end.ts +++ b/src/hooks/session-end.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { readFileSync } from "node:fs"; import { resolveProject, hookCwd } from "./_project.js"; diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index 2d374d855..67493462f 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; // Inlined from ./sdk-guard so each hook bundles to a single self-contained diff --git a/src/hooks/stop.ts b/src/hooks/stop.ts index 0a954f266..574645874 100644 --- a/src/hooks/stop.ts +++ b/src/hooks/stop.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; // Inlined — see src/hooks/sdk-guard.ts for canonical version. Kept local // per-hook so tsdown does not emit a shared hashed chunk that would churn diff --git a/src/hooks/subagent-start.ts b/src/hooks/subagent-start.ts index 18cfe5e30..8321b13cb 100644 --- a/src/hooks/subagent-start.ts +++ b/src/hooks/subagent-start.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; // Inlined from ./sdk-guard so each hook bundles to a single self-contained diff --git a/src/hooks/subagent-stop.ts b/src/hooks/subagent-stop.ts index d071bb36c..f6da1a64f 100644 --- a/src/hooks/subagent-stop.ts +++ b/src/hooks/subagent-stop.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { diff --git a/src/hooks/task-completed.ts b/src/hooks/task-completed.ts index 724b2594e..8e4a78670 100644 --- a/src/hooks/task-completed.ts +++ b/src/hooks/task-completed.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./_env.js"; import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { diff --git a/test/context-injection.test.ts b/test/context-injection.test.ts index f91d293e9..c2f0a5fb3 100644 --- a/test/context-injection.test.ts +++ b/test/context-injection.test.ts @@ -1,9 +1,16 @@ -import { describe, it, expect } from "vitest"; +import { afterAll, describe, it, expect } from "vitest"; import { spawn } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; import { createServer } from "node:http"; +import { tmpdir } from "node:os"; import { join } from "node:path"; const HOOKS_DIR = join(import.meta.dirname, "..", "plugin", "scripts"); +const EMPTY_DATA_DIR = mkdtempSync(join(tmpdir(), "agentmemory-context-test-")); + +afterAll(() => { + rmSync(EMPTY_DATA_DIR, { recursive: true, force: true }); +}); // Spawns a compiled plugin hook as a subprocess, feeds it JSON on stdin, // and returns { stdout, stderr, exitCode, tookMs }. The test is about @@ -31,6 +38,7 @@ function runHook( // the hook. Only pass PATH and anything explicitly set by the // test case. PATH: process.env["PATH"] ?? "", + AGENTMEMORY_DATA_DIR: EMPTY_DATA_DIR, ...env, }, stdio: ["pipe", "pipe", "pipe"], diff --git a/test/hook-delivery.test.ts b/test/hook-delivery.test.ts index 2669c7654..f1c3a7e29 100644 --- a/test/hook-delivery.test.ts +++ b/test/hook-delivery.test.ts @@ -1,7 +1,9 @@ import { spawn } from "node:child_process"; import { once } from "node:events"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import http from "node:http"; -import { resolve } from "node:path"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; const PLUGIN_ROOT = resolve(__dirname, "..", "plugin"); @@ -10,22 +12,77 @@ async function runHook(options: { readonly script: string; readonly payload: Record; readonly url: string; -}): Promise<{ readonly code: number | null; readonly elapsed: number }> { + readonly env?: NodeJS.ProcessEnv; +}): Promise<{ + readonly code: number | null; + readonly elapsed: number; + readonly stdout: string; +}> { const startedAt = Date.now(); + const env = { ...process.env }; + delete env.AGENTMEMORY_INJECT_CONTEXT; + Object.assign(env, options.env, { AGENTMEMORY_URL: options.url }); const child = spawn(process.execPath, [resolve(PLUGIN_ROOT, "scripts", options.script)], { cwd: process.cwd(), - env: { ...process.env, AGENTMEMORY_URL: options.url }, - stdio: ["pipe", "ignore", "ignore"], + env, + stdio: ["pipe", "pipe", "ignore"], + }); + let stdout = ""; + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); }); child.stdin.end(JSON.stringify(options.payload)); const code = await new Promise((resolve, reject) => { child.once("error", reject); child.once("close", (exitCode) => resolve(exitCode)); }); - return { code, elapsed: Date.now() - startedAt }; + return { code, elapsed: Date.now() - startedAt, stdout }; } describe("built capture hooks", () => { + it("loads context injection from the AgentMemory env file", async () => { + const dataDir = mkdtempSync(join(tmpdir(), "agentmemory-hook-env-")); + writeFileSync(join(dataDir, ".env"), "AGENTMEMORY_INJECT_CONTEXT=true\n"); + const requests: unknown[] = []; + const context = 'prior work'; + const server = http.createServer((request, response) => { + let body = ""; + request.on("data", (chunk: Buffer) => { + body += chunk.toString(); + }); + request.on("end", () => { + requests.push(JSON.parse(body)); + response.setHeader("Content-Type", "application/json"); + response.end(JSON.stringify({ context })); + }); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("loopback server has no TCP address"); + + try { + const result = await runHook({ + script: "session-start.mjs", + payload: { session_id: "env-file-test", cwd: process.cwd() }, + url: `http://127.0.0.1:${address.port}`, + env: { AGENTMEMORY_DATA_DIR: dataDir }, + }); + expect(result.code).toBe(0); + expect(requests).toEqual([ + expect.objectContaining({ + sessionId: "env-file-test", + includeContext: true, + }), + ]); + expect(result.stdout).toBe(context); + } finally { + server.close(); + await once(server, "close"); + rmSync(dataDir, { recursive: true, force: true }); + } + }); + it.each([ ["prompt-submit.mjs", { prompt: "remember this" }], ["post-tool-use.mjs", { tool_name: "Read", tool_output: "done" }],