diff --git a/apps/memos-local-plugin/README.md b/apps/memos-local-plugin/README.md index 618f596c..27922e79 100644 --- a/apps/memos-local-plugin/README.md +++ b/apps/memos-local-plugin/README.md @@ -66,6 +66,20 @@ daemon/ # bridge pid/port files Upgrading or uninstalling the plugin **never** touches `data/`, `skills/`, `logs/`, or `config.yaml`. +## Configuration Discovery + +The bridge locates its runtime home (and `config.yaml`) using this priority: + +1. **`--home=` CLI flag** — passed to `bridge.cts` directly +2. **`MEMOS_HOME` env var** — overrides the runtime home directory +3. **`MEMOS_CONFIG_FILE` env var** — overrides only the config file path +4. **Built-in default** — `~/./memos-plugin/` (e.g. `~/.hermes/memos-plugin/`) + +When running inside Docker (where the daemon is started by the container's +entrypoint rather than the Python adapter), use `--home` or `MEMOS_HOME` to +ensure the bridge finds its config. See [**Docker Deployment**](./docs/DOCKER.md) +for the full guide. + ## Quick start ```bash diff --git a/apps/memos-local-plugin/adapters/hermes/README.md b/apps/memos-local-plugin/adapters/hermes/README.md index 1bd98cd0..15ec7d5e 100644 --- a/apps/memos-local-plugin/adapters/hermes/README.md +++ b/apps/memos-local-plugin/adapters/hermes/README.md @@ -66,6 +66,22 @@ communicates over its stdin/stdout pipes. The subprocess exits when the provider's stdin closes (on `shutdown()`), yielding clean lifecycle semantics. +### Docker / Manual Daemon Startup + +When running inside Docker, the bridge may be started by the container's +entrypoint instead of being lazily spawned by the Python adapter. In this +case, pass `--home=` to tell the bridge where its runtime home is: + +```bash +node --experimental-strip-types bridge.cts \ + --agent=hermes \ + --daemon \ + --home=/opt/data/home/.hermes/memos-plugin +``` + +Alternatively, set the `MEMOS_HOME` environment variable. See +[docs/DOCKER.md](../../docs/DOCKER.md) for the full Docker deployment guide. + ## Why a subprocess instead of a long-lived daemon? Earlier prototypes used a persistent HTTP daemon on a well-known port. diff --git a/apps/memos-local-plugin/bridge.cts b/apps/memos-local-plugin/bridge.cts index 62750c3f..cf49f417 100644 --- a/apps/memos-local-plugin/bridge.cts +++ b/apps/memos-local-plugin/bridge.cts @@ -7,11 +7,22 @@ * node --experimental-strip-types bridge.cts (default: stdio) * node --experimental-strip-types bridge.cts --daemon (stdio) * node --experimental-strip-types bridge.cts --daemon --tcp=18911 + * node --experimental-strip-types bridge.cts --agent=hermes --home=/opt/data/home/.hermes/memos-plugin * * The `.cts` extension is intentional: it lets the file be required * from CommonJS environments that spawn Node with `require("...")` * semantics. Internally we re-export the ESM implementation via * `import()`. + * + * CLI flags: + * --daemon Run in daemon mode (keep process alive) + * --tcp=PORT Listen on TCP instead of stdio + * --agent=AGENT Agent type: openclaw | hermes (default: openclaw) + * --home=PATH Override runtime home (equivalent to MEMOS_HOME env) + * + * Environment variables (resolved by core/config/paths.ts): + * MEMOS_HOME Override runtime home directory + * MEMOS_CONFIG_FILE Override config.yaml path only */ // eslint-disable-next-line @typescript-eslint/no-require-imports const path = require("node:path") as typeof import("node:path"); @@ -20,6 +31,8 @@ interface BridgeArgs { daemon: boolean; tcpPort?: number; agent: "openclaw" | "hermes"; + /** Override runtime home directory (equivalent to MEMOS_HOME env var). */ + home?: string; } function parseArgs(argv: readonly string[]): BridgeArgs { @@ -29,6 +42,7 @@ function parseArgs(argv: readonly string[]): BridgeArgs { else if (raw.startsWith("--tcp=")) args.tcpPort = Number(raw.slice(6)); else if (raw === "--agent=hermes") args.agent = "hermes"; else if (raw === "--agent=openclaw") args.agent = "openclaw"; + else if (raw.startsWith("--home=")) args.home = raw.slice(7); } return args; } @@ -51,6 +65,14 @@ async function main(): Promise { pathToEsmUrl(path.resolve(__dirname, "core/logger/index.ts")) )) as typeof import("./core/logger/index.js"); + // When --home is provided, set MEMOS_HOME so resolveHome() in + // core/config/paths.ts picks it up. This is the recommended way to + // configure the bridge in Docker where the daemon is started outside + // the Python adapter (which would normally pass extra_env). + if (args.home) { + process.env["MEMOS_HOME"] = path.resolve(args.home); + } + const { core, config, home } = await bootstrapMemoryCoreFull({ agent: args.agent, pkgVersion: "2.0.0-alpha.1", diff --git a/apps/memos-local-plugin/docs/DOCKER.md b/apps/memos-local-plugin/docs/DOCKER.md new file mode 100644 index 00000000..9bde2d3a --- /dev/null +++ b/apps/memos-local-plugin/docs/DOCKER.md @@ -0,0 +1,130 @@ +# Docker Deployment + +This guide covers running MemOS Local Plugin inside a Docker container, +typically alongside an agent like Hermes Agent. + +## The Config Discovery Problem + +When MemOS runs in Docker, the bridge daemon may be started by the container's +`entrypoint.sh` rather than the Python adapter. In this scenario, the bridge +cannot find its `config.yaml` because: + +1. The `MEMOS_HOME` / `MEMOS_CONFIG_FILE` environment variables are not set. +2. The default path (`~/.hermes/memos-plugin/`) may not exist or may not + contain the config file generated by `install.sh`. + +**Symptoms:** + +- Embedding provider falls back to `local` or is disabled. +- LLM-based features (summarization, reflection) do not work. +- Database is created in an unexpected (empty) directory. +- Viewer shows default branding instead of agent-specific title/logo. + +## Solutions + +### Option 1: `--home` CLI Flag (Recommended for Docker) + +Pass `--home=` when starting the bridge. This sets `MEMOS_HOME` +internally before the config is loaded. + +```bash +# In your Docker entrypoint.sh: +node --experimental-strip-types bridge.cts \ + --agent=hermes \ + --daemon \ + --home=/opt/data/home/.hermes/memos-plugin +``` + +The path should point to the runtime home directory containing `config.yaml` +and `data/`. + +### Option 2: `MEMOS_HOME` Environment Variable + +Set the environment variable before starting the bridge: + +```bash +export MEMOS_HOME=/opt/data/home/.hermes/memos-plugin +node --experimental-strip-types bridge.cts --agent=hermes --daemon +``` + +Or in a Dockerfile / docker-compose.yml: + +```yaml +services: + hermes: + image: nousresearch/hermes-agent + environment: + - MEMOS_HOME=/opt/data/home/.hermes/memos-plugin +``` + +### Option 3: `MEMOS_CONFIG_FILE` (Config-only Override) + +If you only need to override the config file path (data/logs/skills still +derive from the same parent directory): + +```bash +export MEMOS_CONFIG_FILE=/opt/data/home/.hermes/memos-plugin/config.yaml +``` + +## Config Resolution Priority + +The bridge resolves its runtime home in this order (highest priority first): + +1. `--home=` CLI flag +2. `MEMOS_HOME` environment variable +3. `MEMOS_CONFIG_FILE` environment variable (config file only) +4. Built-in default: `~/./memos-plugin/` + +## Preparing the Config File + +Before starting the bridge, ensure `config.yaml` exists in the runtime home +with the required provider configuration. You can use the template: + +```bash +# Copy template and edit +cp templates/config.hermes.yaml /opt/data/home/.hermes/memos-plugin/config.yaml +chmod 600 /opt/data/home/.hermes/memos-plugin/config.yaml +$EDITOR /opt/data/home/.hermes/memos-plugin/config.yaml +``` + +Minimal config for a working deployment: + +```yaml +version: 1 + +viewer: + port: 18799 + +embedding: + provider: openai_compatible + apiKey: "sk-your-api-key" + # endpoint: "https://api.siliconflow.cn/v1" # or your provider + +llm: + provider: openai_compatible + apiKey: "sk-your-api-key" + model: "your-model-name" +``` + +## Verifying the Setup + +After starting the bridge, check the logs for: + +``` +bridge: viewer → http://127.0.0.1:18799/ +``` + +Then visit the viewer URL. If the config is loaded correctly, you should see: + +- Your agent's title/logo in the header +- The embedding provider connected (check the Health page) +- No error messages about missing configuration + +## Common Pitfalls + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Embedding: local (no vectors) | `config.yaml` not found | Set `--home` or `MEMOS_HOME` | +| Empty database on restart | Data stored in ephemeral path | Ensure `data/` is on a persistent volume | +| Default "MemOS" branding | Config not loaded before viewer start | Check the `--home` path is correct | +| Viewer port mismatch | Config says 18799 but bridge picks another | Port in use; check config.yaml `viewer.port` |