Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ integrations/hermes/__pycache__/
eval/reports/
# LongMemEval download is 278MB; fetched on demand
eval/data/longmemeval/

.idea
.opencode
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,17 @@ GRAPH_EXTRACTION_ENABLED=true
CONSOLIDATION_ENABLED=true
```

### Engine Config Lookup

agentmemory starts `iii-engine` with the first `iii-config.yaml` it can find in this order:

1. `AGENTMEMORY_III_CONFIG` — explicit full-path override.
2. `./iii-config.yaml` — the current command directory, useful for source development where relative paths like `./data/state_store.db` should stay inside the repo.
3. `~/.agentmemory/iii-config.yaml` — user-level runtime override for installed packages.
4. The bundled npm `iii-config.yaml` — fallback default.

If you want data to live in a stable user directory, put a custom config at `~/.agentmemory/iii-config.yaml` and use absolute paths for `iii-state.config.adapter.config.file_path` and `iii-stream.config.adapter.config.file_path`. On Windows, prefer forward-slash absolute paths such as `C:/Users/you/.agentmemory/data/state_store.db`; do not rely on `~` expansion inside YAML paths.

### Environment Variables

Create `~/.agentmemory/.env`:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"agentmemory": "dist/cli.mjs"
},
"scripts": {
"build": "tsdown && (cp iii-config.yaml dist/ 2>/dev/null || true) && (cp iii-config.docker.yaml dist/ 2>/dev/null || true) && (cp docker-compose.yml dist/ 2>/dev/null || true) && (cp .env.example dist/ 2>/dev/null || true) && mkdir -p dist/viewer && cp src/viewer/index.html dist/viewer/ && cp src/viewer/favicon.svg dist/viewer/",
"build": "tsdown && node scripts/copy-build-assets.mjs",
"dev": "tsx src/index.ts",
"start": "node dist/cli.mjs",
"migrate": "node dist/functions/migrate.js",
Expand Down
21 changes: 21 additions & 0 deletions scripts/copy-build-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { copyFile, mkdir } from "node:fs/promises";
import { join } from "node:path";

const root = process.cwd();
const dist = join(root, "dist");

async function copyToDist(source, target = source) {
await copyFile(join(root, source), join(dist, target));
}

await mkdir(dist, { recursive: true });
await mkdir(join(dist, "viewer"), { recursive: true });

await Promise.all([
copyToDist("iii-config.yaml"),
copyToDist("iii-config.docker.yaml"),
copyToDist("docker-compose.yml"),
copyToDist(".env.example"),
copyToDist(join("src", "viewer", "index.html"), join("viewer", "index.html")),
copyToDist(join("src", "viewer", "favicon.svg"), join("viewer", "favicon.svg")),
]);
33 changes: 14 additions & 19 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { renderSplash } from "./cli/splash.js";
import { isFirstRun, readPrefs, resetPrefs, writePrefs } from "./cli/preferences.js";
import { runOnboarding } from "./cli/onboarding.js";
import { resolveIiiConfigPath } from "./cli/iii-config.js";
import { setBootVerbose } from "./logger.js";
import { VERSION } from "./version.js";

Expand Down Expand Up @@ -151,7 +152,9 @@ Environment:
AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
Honored by status, doctor, and MCP shim commands.
AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
native iii-engine binary on first run.
native iii-engine binary on first run.
AGENTMEMORY_III_CONFIG Full path to iii-config.yaml. Overrides cwd,
~/.agentmemory, and bundled config lookup.
AGENTMEMORY_III_VERSION Override pinned iii-engine version (default ${IIPINNED_VERSION}).

Quick start:
Expand Down Expand Up @@ -302,23 +305,13 @@ async function isAgentmemoryReady(): Promise<boolean> {
}

function findIiiConfig(): string {
// Precedence (user-overridable wins): explicit env > project cwd >
// ~/.agentmemory/ > bundled. The bundled config used to win
// unconditionally, so users hitting the observability log-feedback
// loop (#519) had no way to drop a tamer config in place without
// editing node_modules.
const envPath = process.env["AGENTMEMORY_III_CONFIG"];
const candidates = [
...(envPath ? [envPath] : []),
join(process.cwd(), "iii-config.yaml"),
join(homedir(), ".agentmemory", "iii-config.yaml"),
join(__dirname, "iii-config.yaml"),
join(__dirname, "..", "iii-config.yaml"),
];
for (const c of candidates) {
if (existsSync(c)) return c;
}
return "";
return resolveIiiConfigPath({
envPath: process.env["AGENTMEMORY_III_CONFIG"],
cwd: process.cwd(),
home: homedir(),
packageDir: __dirname,
exists: existsSync,
});
}

function whichBinary(name: string): string | null {
Expand Down Expand Up @@ -1216,7 +1209,9 @@ async function runStatus() {
lines.push(`Provider: ${provider}`);
lines.push(`Embeddings: ${embed}`);
lines.push(`Flags:`);
flagRows.forEach((r: string) => lines.push(r));
flagRows.forEach((r: string) => {
lines.push(r);
});
}

p.note(lines.join("\n"), "agentmemory");
Expand Down
28 changes: 28 additions & 0 deletions src/cli/iii-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

export type IiiConfigLookupOptions = {
envPath?: string;
cwd: string;
home: string;
packageDir: string;
exists: (path: string) => boolean;
};

export function iiiConfigCandidates(
options: Omit<IiiConfigLookupOptions, "exists">,
): string[] {
return [
...(options.envPath ? [options.envPath] : []),
join(options.cwd, "iii-config.yaml"),
join(options.home, ".agentmemory", "iii-config.yaml"),
join(options.packageDir, "iii-config.yaml"),
join(options.packageDir, "..", "iii-config.yaml"),
];
}

export function resolveIiiConfigPath(options: IiiConfigLookupOptions): string {
for (const candidate of iiiConfigCandidates(options)) {
if (options.exists(candidate)) return candidate;
}
return "";
}
92 changes: 92 additions & 0 deletions test/cli-iii-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, expect, it } from "vitest";
import { join } from "node:path";
import {
iiiConfigCandidates,
resolveIiiConfigPath,
} from "../src/cli/iii-config.js";

function paths() {
const cwd = join("tmp", "project");
const home = join("tmp", "home");
const packageDir = join("tmp", "node_modules", "@agentmemory", "agentmemory", "dist");
return {
envConfig: join("tmp", "custom", "iii-config.yaml"),
cwd,
cwdConfig: join(cwd, "iii-config.yaml"),
home,
homeConfig: join(home, ".agentmemory", "iii-config.yaml"),
packageDir,
bundledConfig: join(packageDir, "iii-config.yaml"),
bundledRootConfig: join(packageDir, "..", "iii-config.yaml"),
};
}

function resolve(existing: string[], envPath?: string): string {
const p = paths();
const existingSet = new Set(existing);
return resolveIiiConfigPath({
envPath,
cwd: p.cwd,
home: p.home,
packageDir: p.packageDir,
exists: (path) => existingSet.has(path),
});
}

describe("iii config lookup", () => {
it("documents the lookup order", () => {
const p = paths();
expect(
iiiConfigCandidates({
envPath: p.envConfig,
cwd: p.cwd,
home: p.home,
packageDir: p.packageDir,
}),
).toEqual([
p.envConfig,
p.cwdConfig,
p.homeConfig,
p.bundledConfig,
p.bundledRootConfig,
]);
});

it("uses AGENTMEMORY_III_CONFIG first", () => {
const p = paths();
expect(
resolve(
[p.envConfig, p.cwdConfig, p.homeConfig, p.bundledConfig],
p.envConfig,
),
).toBe(p.envConfig);
});

it("uses cwd config before ~/.agentmemory and bundled configs", () => {
const p = paths();
expect(resolve([p.cwdConfig, p.homeConfig, p.bundledConfig])).toBe(
p.cwdConfig,
);
});

it("uses ~/.agentmemory config before bundled configs", () => {
const p = paths();
expect(resolve([p.homeConfig, p.bundledConfig])).toBe(p.homeConfig);
});

it("falls back to the bundled dist config", () => {
const p = paths();
expect(resolve([p.bundledConfig, p.bundledRootConfig])).toBe(
p.bundledConfig,
);
});

it("falls back to the bundled root config when dist config is absent", () => {
const p = paths();
expect(resolve([p.bundledRootConfig])).toBe(p.bundledRootConfig);
});

it("returns an empty string when no config exists", () => {
expect(resolve([])).toBe("");
});
});