Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ function loadEnvFile(): Record<string, string> {
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx).trim();
let val = trimmed.slice(eqIdx + 1).trim();
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1);
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;
}
Expand Down Expand Up @@ -147,6 +149,20 @@ export function getEnvVar(key: string): string | undefined {
return getMergedEnv()[key];
}

export function detectLlmProviderKind(): "llm" | "noop" {
const env = getMergedEnv();
if (
hasRealValue(env["ANTHROPIC_API_KEY"]) ||
hasRealValue(env["GEMINI_API_KEY"]) ||
hasRealValue(env["GOOGLE_API_KEY"]) ||
hasRealValue(env["OPENROUTER_API_KEY"]) ||
hasRealValue(env["MINIMAX_API_KEY"])
) {
return "llm";
}
return "noop";
}

export function loadEmbeddingConfig(): EmbeddingConfig {
const env = getMergedEnv();
let bm25Weight = parseFloat(env["BM25_WEIGHT"] || "0.4");
Expand Down
4 changes: 2 additions & 2 deletions src/triggers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
isAutoCompressEnabled,
isContextInjectionEnabled,
detectEmbeddingProvider,
detectLlmProviderKind,
} from "../config.js";

type Response = {
Expand Down Expand Up @@ -153,8 +154,7 @@ export function registerApiTriggers(
async (req: ApiRequest): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
const env = process.env;
const providerKind = env["ANTHROPIC_API_KEY"] || env["GEMINI_API_KEY"] || env["OPENROUTER_API_KEY"] || env["MINIMAX_API_KEY"] ? "llm" : "noop";
const providerKind = detectLlmProviderKind();
const embeddingProvider = detectEmbeddingProvider() ? "embeddings" : "none";
const flags = [
{
Expand Down
85 changes: 85 additions & 0 deletions test/env-loader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const ORIGINAL_HOME = process.env["HOME"];
const ORIGINAL_USERPROFILE = process.env["USERPROFILE"];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let sandboxHome: string;

async function freshConfig() {
vi.resetModules();
return await import("../src/config.js");
}

function writeEnv(contents: string) {
const dir = join(sandboxHome, ".agentmemory");
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, ".env"), contents);
}

describe("loadEnvFile", () => {
beforeEach(() => {
sandboxHome = mkdtempSync(join(tmpdir(), "agentmemory-env-"));
process.env["HOME"] = sandboxHome;
process.env["USERPROFILE"] = sandboxHome;
delete process.env["AGENTMEMORY_AUTO_COMPRESS"];
delete process.env["CONSOLIDATION_ENABLED"];
delete process.env["GRAPH_EXTRACTION_ENABLED"];
delete process.env["TOKEN"];
delete process.env["HASHVAL"];
});

afterEach(() => {
if (ORIGINAL_HOME === undefined) delete process.env["HOME"];
else process.env["HOME"] = ORIGINAL_HOME;
if (ORIGINAL_USERPROFILE === undefined) delete process.env["USERPROFILE"];
else process.env["USERPROFILE"] = ORIGINAL_USERPROFILE;
rmSync(sandboxHome, { recursive: true, force: true });
});

it("strips trailing inline # comments on unquoted values", async () => {
writeEnv(
[
"AGENTMEMORY_AUTO_COMPRESS=true # opt in to LLM compression",
"CONSOLIDATION_ENABLED=true # daily summarization",
"GRAPH_EXTRACTION_ENABLED=true # entity graph",
].join("\n"),
);
const cfg = await freshConfig();
expect(cfg.isAutoCompressEnabled()).toBe(true);
expect(cfg.isConsolidationEnabled()).toBe(true);
expect(cfg.isGraphExtractionEnabled()).toBe(true);
});

it("preserves # inside double-quoted values", async () => {
writeEnv('TOKEN="abc#def"');
const cfg = await freshConfig();
expect(cfg.getEnvVar("TOKEN")).toBe("abc#def");
});

it("preserves # inside single-quoted values", async () => {
writeEnv("TOKEN='abc#def'");
const cfg = await freshConfig();
expect(cfg.getEnvVar("TOKEN")).toBe("abc#def");
});

it("treats hash without leading space as part of value", async () => {
writeEnv("HASHVAL=abc#def");
const cfg = await freshConfig();
expect(cfg.getEnvVar("HASHVAL")).toBe("abc#def");
});

it("strips inline comment after a quoted value and unwraps quotes", async () => {
writeEnv('TOKEN="abc" # trailing comment');
const cfg = await freshConfig();
expect(cfg.getEnvVar("TOKEN")).toBe("abc");
});

it("strips inline comment after a single-quoted value and unwraps quotes", async () => {
writeEnv("TOKEN='abc' # trailing comment");
const cfg = await freshConfig();
expect(cfg.getEnvVar("TOKEN")).toBe("abc");
});
});
Loading