diff --git a/README.md b/README.md index 22e61944..b8054b93 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,36 @@ curl http://127.0.0.1:8420/health --- +### 3. Hermes (Windows native) + +For a Windows-native Hermes install, run the bundled batch script from the +repository root in Command Prompt or PowerShell: + +```powershell +$env:TDAI_LLM_API_KEY="your-api-key" +$env:TDAI_LLM_BASE_URL="https://api.openai.com/v1" +$env:TDAI_LLM_MODEL="gpt-4o" +.\scripts\setup-hermes-memory-tencentdb.bat +``` + +The script checks `node`, `npm`, Python, and Hermes, requires Node.js +`>=22.16.0`, runs `npm install --omit=dev` when Gateway dependencies are +missing, creates `%USERPROFILE%\.memory-tencentdb\memory-tdai`, copies the +provider to `%USERPROFILE%\.hermes\plugins\memory_tencentdb`, writes Gateway +environment variables to `%USERPROFILE%\.hermes\.env`, and starts the Gateway +before polling: + +```powershell +curl.exe http://127.0.0.1:8420/health +``` + +If `%USERPROFILE%\.hermes\config.yaml` already exists, make sure it contains: + +```yaml +memory: + provider: memory_tencentdb +``` + ## 🔒 Gateway Security (optional) diff --git a/README_CN.md b/README_CN.md index e4298bf2..3e5208ad 100644 --- a/README_CN.md +++ b/README_CN.md @@ -353,9 +353,38 @@ curl http://127.0.0.1:8420/health > Provider 的完整参考(环境变量、故障排查、LLM 工具 schema、supervisor 行为)见 [`hermes-plugin/memory/memory_tencentdb/README.md`](./hermes-plugin/memory/memory_tencentdb/README.md),调整 supervisor / circuit-breaker 默认值之前请先读它。 - --- +### 3. Hermes(Windows 原生安装) + +Windows 原生 Hermes 环境下,在仓库根目录用 Command Prompt 或 PowerShell +运行内置批处理脚本: + +```powershell +$env:TDAI_LLM_API_KEY="your-api-key" +$env:TDAI_LLM_BASE_URL="https://api.openai.com/v1" +$env:TDAI_LLM_MODEL="gpt-4o" +.\scripts\setup-hermes-memory-tencentdb.bat +``` + +脚本会检查 `node`、`npm`、Python 和 Hermes,要求 Node.js `>=22.16.0`; +当 Gateway 依赖缺失时执行 `npm install --omit=dev`,创建 +`%USERPROFILE%\.memory-tencentdb\memory-tdai`,复制插件到 +`%USERPROFILE%\.hermes\plugins\memory_tencentdb`,把 Gateway 环境变量写入 +`%USERPROFILE%\.hermes\.env`,随后启动 Gateway 并轮询: + +```powershell +curl.exe http://127.0.0.1:8420/health +``` + +如果 `%USERPROFILE%\.hermes\config.yaml` 已存在,请确认包含: + +```yaml +memory: + provider: memory_tencentdb +``` + + ## 🔒 Gateway 安全配置(可选) Hermes Gateway 监听 `:8420`,对外提供 capture / search / recall 的 HTTP 接口。新增两个开关,可以把它从“开放的本地 sidecar”切换为“需要鉴权的网络服务”。**两个开关默认都关闭,已有部署的行为不变。** diff --git a/hermes-plugin/memory/memory_tencentdb/README.md b/hermes-plugin/memory/memory_tencentdb/README.md index 831fac62..146b4499 100644 --- a/hermes-plugin/memory/memory_tencentdb/README.md +++ b/hermes-plugin/memory/memory_tencentdb/README.md @@ -70,8 +70,8 @@ Hermes scans two locations for memory providers, in precedence order (see collision. 2. **User-installed** — `$HERMES_HOME/plugins//`, where `$HERMES_HOME` defaults to `~/.hermes` (see - `hermes_constants.get_hermes_home()`). This path is for third-party - providers; we don't use it for memory_tencentdb. + `hermes_constants.get_hermes_home()`). This path is used by the Windows + native `.bat` installer and by third-party provider installs. **The trailing directory name must be exactly `memory_tencentdb`** — Hermes uses that directory name as the provider key; it must match @@ -79,6 +79,47 @@ uses that directory name as the provider key; it must match (The hyphenated form `memory-tencentdb` is a *config-side alias*, not a valid directory name.) +### Windows native install + +On Windows, use the bundled batch script from the memory-tencentdb repository +root: + +```cmd +set TDAI_LLM_API_KEY=sk-... +set TDAI_LLM_BASE_URL=https://api.openai.com/v1 +set TDAI_LLM_MODEL=gpt-4o +scripts\setup-hermes-memory-tencentdb.bat +``` + +PowerShell equivalent: + +```powershell +$env:TDAI_LLM_API_KEY="sk-..." +$env:TDAI_LLM_BASE_URL="https://api.openai.com/v1" +$env:TDAI_LLM_MODEL="gpt-4o" +.\scripts\setup-hermes-memory-tencentdb.bat +``` + +The script performs the minimal native setup: + +- checks `node`, `npm`, Python, and the optional `hermes` command; +- requires Node.js `>=22.16.0`; +- runs `npm install --omit=dev` when `node_modules` is missing; +- creates `%USERPROFILE%\.memory-tencentdb\memory-tdai`; +- sets `TDAI_LLM_*` and `MEMORY_TENCENTDB_GATEWAY_*` in the current process + and writes them to `%USERPROFILE%\.hermes\.env`; +- copies this provider to `%USERPROFILE%\.hermes\plugins\memory_tencentdb`; +- creates `%USERPROFILE%\.hermes\config.yaml` with + `memory.provider: memory_tencentdb` when the file does not exist, or prints + the exact YAML to add when it already exists; +- starts the Gateway and polls `GET /health`. + +After it finishes, verify the sidecar directly: + +```cmd +curl.exe http://127.0.0.1:8420/health +``` + Pick one of the two installation styles: **Install A — symlink (recommended for developers working on both repos @@ -140,14 +181,20 @@ memory: ### 2. Provide Gateway runtime + LLM credentials At minimum the Gateway needs an OpenAI-compatible endpoint for L1/L2/L3 -extraction. Set these in the Hermes process environment: +extraction. Set the Gateway-native variables in the Hermes process +environment: ```bash -export MEMORY_TENCENTDB_LLM_API_KEY="sk-..." -export MEMORY_TENCENTDB_LLM_BASE_URL="https://api.openai.com/v1" # optional -export MEMORY_TENCENTDB_LLM_MODEL="gpt-4o" # optional +export TDAI_LLM_API_KEY="sk-..." +export TDAI_LLM_BASE_URL="https://api.openai.com/v1" # optional +export TDAI_LLM_MODEL="gpt-4o" # optional ``` +When the provider supervises the Gateway, it also accepts the legacy +Hermes-side aliases `MEMORY_TENCENTDB_LLM_API_KEY`, +`MEMORY_TENCENTDB_LLM_BASE_URL`, and `MEMORY_TENCENTDB_LLM_MODEL` and mirrors +them into `TDAI_LLM_*` for the child process. + ### 3. Start the Gateway You have three options; pick whichever fits your deployment. @@ -229,18 +276,27 @@ The old `MEMORY_TENCENTDB_DATA_DIR` env var is no longer read — it was never consumed by the Gateway anyway (names did not match), so removing it just eliminates a silent no-op. -### Gateway LLM (consumed by the Node sidecar, not by this provider) +### Gateway LLM (consumed by the Node sidecar) + +| Variable | Default | Description | +|----------|---------|-------------| +| `TDAI_LLM_API_KEY` | — | LLM API key (required for L1/L2/L3) | +| `TDAI_LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible API base URL | +| `TDAI_LLM_MODEL` | `gpt-4o` | Model name | + +When the provider starts the Gateway subprocess, it mirrors the legacy +Hermes aliases below into the Gateway-native names above if the `TDAI_*` +variables are not already set: -| Variable | Default | Description | -|-----------------------------------|------------------------------|-------------------------------------| -| `MEMORY_TENCENTDB_LLM_API_KEY` | — | LLM API key (required for L1/L2/L3) | -| `MEMORY_TENCENTDB_LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible API base URL | -| `MEMORY_TENCENTDB_LLM_MODEL` | `gpt-4o` | Model name | +| Alias | Mirrored to | +|-------|-------------| +| `MEMORY_TENCENTDB_LLM_API_KEY` | `TDAI_LLM_API_KEY` | +| `MEMORY_TENCENTDB_LLM_BASE_URL` | `TDAI_LLM_BASE_URL` | +| `MEMORY_TENCENTDB_LLM_MODEL` | `TDAI_LLM_MODEL` | -> ⚠️ Only `MEMORY_TENCENTDB_*` env vars are honored by this provider for the -> Gateway location and LLM credentials. Data-directory resolution is -> deliberately delegated to the Gateway via `TDAI_DATA_DIR` (see above) so -> the provider and the Gateway can never disagree about where L0~L3 live. +> Data-directory resolution is deliberately delegated to the Gateway via +> `TDAI_DATA_DIR` (see above) so the provider and the Gateway can never +> disagree about where L0~L3 live. ## LLM Tools diff --git a/hermes-plugin/memory/memory_tencentdb/__init__.py b/hermes-plugin/memory/memory_tencentdb/__init__.py index 86350fad..483bacb8 100644 --- a/hermes-plugin/memory/memory_tencentdb/__init__.py +++ b/hermes-plugin/memory/memory_tencentdb/__init__.py @@ -186,14 +186,14 @@ def _discover_gateway_cmd() -> Optional[str]: ``~/tdai-memory-openclaw-plugin`` and ``~/.hermes/plugins/tdai-memory-openclaw-plugin``). - Returns a ready-to-``Popen`` command string wrapping a ``sh -c`` that - ``cd``-s into the plugin root before exec-ing ``pnpm exec tsx - src/gateway/server.ts``. The ``cd`` is required because ``tsx`` is - installed under ``/node_modules`` and Node's ESM resolver - searches ``package.json`` from the cwd upward — if we launched ``tsx`` - with the hermes-agent cwd, resolution would fail with - ``ERR_MODULE_NOT_FOUND``. Using ``sh -c`` keeps the supervisor's - ``shlex.split`` + ``Popen(argv)`` contract intact (no ``shell=True``). + Returns a ready-to-``Popen`` command string that ``cd``-s into the plugin + root before exec-ing the Gateway. The ``cd`` is required because ``tsx`` + is installed under ``/node_modules`` and Node's ESM resolver + searches ``package.json`` from the cwd upward — if we launched from the + hermes-agent cwd, resolution would fail with ``ERR_MODULE_NOT_FOUND``. + POSIX uses ``sh -c`` so the supervisor can keep its ``shlex.split`` + + ``Popen(argv)`` path. Windows returns an explicit ``cmd /d /s /c ...`` + command and the supervisor starts it with ``shell=True``. Returns ``None`` if no ``server.ts`` candidate exists. The function never raises: supervisor-side validation will surface a friendly warning if the @@ -231,6 +231,18 @@ def _discover_gateway_cmd() -> Optional[str]: "(override with MEMORY_TENCENTDB_GATEWAY_CMD)", candidate, ) + if os.name == "nt": + # cmd.exe needs /d so autorun hooks do not mutate the + # environment, /s for predictable quote stripping, and + # /c so Hermes can supervise the process lifetime. + # `node --import tsx/esm` mirrors the packaged Windows + # bootstrap script and avoids relying on npx/pnpm. + inner = ( + f'cd /d "{str(plugin_root)}" && ' + "node --import tsx/esm src\\gateway\\server.ts" + ) + return f'cmd /d /s /c "{inner}"' + # shlex.quote guards against spaces / shell metachars in paths. # The inner command mirrors start-memory-tencentdb-gateway.sh: # cd && exec pnpm exec tsx src/gateway/server.ts @@ -971,15 +983,19 @@ def shutdown(self) -> None: except Exception as e: logger.debug("memory-tencentdb session end failed: %s", e) - # Note: do NOT shut down the supervisor/Gateway here — it may serve - # other sessions. The Gateway manages its own lifecycle. - # We *do* drop our reference to the supervisor so any in-flight - # _try_recover_gateway() call sees self._supervisor is None and - # bails out instead of resurrecting a released provider. + # Shut down only the Gateway process this supervisor started itself. + # External Gateways are safe: GatewaySupervisor.shutdown() is a no-op + # when it never spawned a child (self._process is None). + supervisor = self._supervisor self._client = None self._gateway_available = False self._initialized = False self._supervisor = None + if supervisor is not None: + try: + supervisor.shutdown() + except Exception as e: + logger.warning("memory-tencentdb supervisor shutdown failed: %s", e) # -- Tools ---------------------------------------------------------------- diff --git a/hermes-plugin/memory/memory_tencentdb/supervisor.py b/hermes-plugin/memory/memory_tencentdb/supervisor.py index 1b025cc4..7dc94463 100644 --- a/hermes-plugin/memory/memory_tencentdb/supervisor.py +++ b/hermes-plugin/memory/memory_tencentdb/supervisor.py @@ -11,6 +11,7 @@ import logging import os import shlex +import signal import subprocess import time from typing import IO, Optional @@ -168,6 +169,21 @@ def ensure_running(self) -> bool: env = os.environ.copy() env["MEMORY_TENCENTDB_GATEWAY_PORT"] = str(self._port) env["MEMORY_TENCENTDB_GATEWAY_HOST"] = self._host + # The Node Gateway reads TDAI_GATEWAY_{HOST,PORT}. Keep the + # MEMORY_TENCENTDB_* names for the Python provider contract, but + # also populate the Gateway-native names unless the operator set + # them explicitly. + env.setdefault("TDAI_GATEWAY_PORT", str(self._port)) + env.setdefault("TDAI_GATEWAY_HOST", self._host) + # Hermes-facing LLM env names predate the Gateway's TDAI_LLM_* + # names. Mirror them into the child process so Windows-native + # Hermes installs do not have to set both sets by hand. + if not env.get("TDAI_LLM_API_KEY") and env.get("MEMORY_TENCENTDB_LLM_API_KEY"): + env["TDAI_LLM_API_KEY"] = env["MEMORY_TENCENTDB_LLM_API_KEY"] + if not env.get("TDAI_LLM_BASE_URL") and env.get("MEMORY_TENCENTDB_LLM_BASE_URL"): + env["TDAI_LLM_BASE_URL"] = env["MEMORY_TENCENTDB_LLM_BASE_URL"] + if not env.get("TDAI_LLM_MODEL") and env.get("MEMORY_TENCENTDB_LLM_MODEL"): + env["TDAI_LLM_MODEL"] = env["MEMORY_TENCENTDB_LLM_MODEL"] # Note: we deliberately do NOT inject TDAI_GATEWAY_API_KEY into # the child's env from here. Whether the Gateway enforces auth is # the operator's call — they configure it on the Gateway side @@ -203,13 +219,26 @@ def ensure_running(self) -> bool: stdout_target = subprocess.DEVNULL stderr_target = subprocess.DEVNULL - self._process = subprocess.Popen( - shlex.split(self._gateway_cmd), - env=env, - stdout=stdout_target, - stderr=stderr_target, - start_new_session=True, # Detach from parent process group - ) + if os.name == "nt": + creationflags = 0 + if hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"): + creationflags |= subprocess.CREATE_NEW_PROCESS_GROUP + self._process = subprocess.Popen( + self._gateway_cmd, + shell=True, + env=env, + stdout=stdout_target, + stderr=stderr_target, + creationflags=creationflags, + ) + else: + self._process = subprocess.Popen( + shlex.split(self._gateway_cmd), + env=env, + stdout=stdout_target, + stderr=stderr_target, + start_new_session=True, # Detach from parent process group + ) except Exception as e: logger.error("Failed to start memory-tencentdb Gateway: %s", e) self._close_log_handles() @@ -309,13 +338,19 @@ def shutdown(self) -> None: logger.info("Shutting down memory-tencentdb Gateway...") try: - # Send SIGTERM for graceful shutdown - self._process.terminate() + if os.name == "nt": + self._terminate_windows_process_tree(self._process.pid) + else: + # Send SIGTERM for graceful shutdown. + self._process.terminate() try: self._process.wait(timeout=10) except subprocess.TimeoutExpired: logger.warning("memory-tencentdb Gateway did not exit in 10s, sending SIGKILL") - self._process.kill() + if os.name == "nt": + self._kill_windows_process_tree(self._process.pid) + else: + self._process.kill() self._process.wait(timeout=5) except Exception as e: logger.warning("Error shutting down memory-tencentdb Gateway: %s", e) @@ -323,6 +358,28 @@ def shutdown(self) -> None: self._process = None self._close_log_handles() + def _terminate_windows_process_tree(self, pid: int) -> None: + """Terminate the shell-owned Windows process tree for the Gateway.""" + try: + self._process.send_signal(signal.CTRL_BREAK_EVENT) # type: ignore[union-attr] + except Exception: + pass + subprocess.run( + ["taskkill", "/T", "/PID", str(pid)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + + def _kill_windows_process_tree(self, pid: int) -> None: + """Force-kill the shell-owned Windows process tree for the Gateway.""" + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(pid)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + @property def client(self) -> MemoryTencentdbSdkClient: """Get the HTTP client for making API calls.""" diff --git a/hermes-plugin/memory/memory_tencentdb/tests/test_memory_tencentdb_recovery.py b/hermes-plugin/memory/memory_tencentdb/tests/test_memory_tencentdb_recovery.py index 81934aff..9f3c3a0a 100644 --- a/hermes-plugin/memory/memory_tencentdb/tests/test_memory_tencentdb_recovery.py +++ b/hermes-plugin/memory/memory_tencentdb/tests/test_memory_tencentdb_recovery.py @@ -21,6 +21,7 @@ class of failures: import os import pathlib +import subprocess import sys import threading import time @@ -190,10 +191,20 @@ def _factory(*args, **kwargs): class _FakePopen: def __init__(self, returncode: Optional[int] = None) -> None: self._returncode = returncode + self.pid = 12345 + self.signals = [] + self.wait_timeouts = [] def poll(self): return self._returncode + def wait(self, timeout=None): + self.wait_timeouts.append(timeout) + return self._returncode or 0 + + def send_signal(self, sig): + self.signals.append(sig) + @property def returncode(self): return self._returncode @@ -231,6 +242,25 @@ def test_reap_dead_process_keeps_alive_handle(): assert sup._process is alive +def test_windows_shutdown_terminates_process_tree(monkeypatch): + sup = supervisor_module.GatewaySupervisor(gateway_cmd="") + proc = _FakePopen(returncode=None) + sup._process = proc + calls = [] + + def fake_run(args, **kwargs): + calls.append(args) + return subprocess.CompletedProcess(args, 0) + + monkeypatch.setattr(supervisor_module.os, "name", "nt") + monkeypatch.setattr(supervisor_module.subprocess, "run", fake_run) + + sup.shutdown() + + assert ["taskkill", "/T", "/PID", str(proc.pid)] in calls + assert sup._process is None + + # --------------------------------------------------------------------------- # Watchdog: detects death, resurrects, and reattaches # --------------------------------------------------------------------------- @@ -429,6 +459,17 @@ def test_shutdown_drops_supervisor_blocks_recovery(provider_with_fake_supervisor assert fake.ensure_running_calls == before +def test_shutdown_stops_supervisor_owned_gateway(provider_with_fake_supervisor): + provider = provider_with_fake_supervisor + fake = provider._fake + + provider.shutdown() + + assert fake.shutdown_calls == 1 + assert fake.alive is False + assert fake.healthy is False + + def test_shutdown_is_idempotent(provider_with_fake_supervisor): provider = provider_with_fake_supervisor provider.shutdown() diff --git a/package.json b/package.json index 09abe4d8..62c8dd7d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "postinstall": "bash scripts/openclaw-after-tool-call-messages.patch.sh 2>/dev/null || true" + "postinstall": "node scripts/postinstall.mjs" }, "files": [ "dist/", @@ -40,6 +40,8 @@ "scripts/read-local-memory/dist/", "scripts/memory-tencentdb-ctl.sh", "scripts/install_hermes_memory_tencentdb.sh", + "scripts/setup-hermes-memory-tencentdb.bat", + "scripts/postinstall.mjs", "scripts/README.memory-tencentdb-ctl.md", "src", "scripts/openclaw-after-tool-call-messages.patch.sh", diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs new file mode 100644 index 00000000..ddfa8d14 --- /dev/null +++ b/scripts/postinstall.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * Cross-platform postinstall hook. + * + * The OpenClaw runtime patch is a Bash-only convenience. It should never make + * npm install fail, and it is not relevant for Windows-native Hermes installs. + */ + +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.dirname(scriptDir); +const patchScript = path.join(scriptDir, "openclaw-after-tool-call-messages.patch.sh"); + +function log(message) { + console.log(`[memory-tencentdb] postinstall: ${message}`); +} + +function isTruthy(value) { + return /^(1|true|yes|on)$/i.test(String(value || "").trim()); +} + +function skip(message) { + log(`${message}; skipping OpenClaw patch.`); + process.exit(0); +} + +if (process.platform === "win32") { + skip("Windows detected"); +} + +if (!["linux", "darwin"].includes(process.platform)) { + skip(`unsupported platform ${process.platform}`); +} + +if ( + isTruthy(process.env.MEMORY_TENCENTDB_SKIP_OPENCLAW_PATCH) || + process.env.MEMORY_TENCENTDB_MODE === "hermes" || + process.env.HERMES_HOME || + process.env.HERMES_AGENT_DIR +) { + skip("Hermes install context detected"); +} + +if (!existsSync(patchScript)) { + skip(`patch script not found at ${patchScript}`); +} + +const bashCheck = spawnSync("bash", ["--version"], { stdio: "ignore" }); +if (bashCheck.error) { + skip("bash not found"); +} + +log("running OpenClaw after-tool-call patch"); +const result = spawnSync("bash", [patchScript], { + cwd: repoRoot, + env: process.env, + stdio: "inherit", +}); + +if (result.error) { + log(`patch could not start (${result.error.message}); continuing npm install.`); + process.exit(0); +} + +if (result.status !== 0) { + log(`patch exited with status ${result.status}; continuing npm install.`); +} + +process.exit(0); diff --git a/scripts/setup-hermes-memory-tencentdb.bat b/scripts/setup-hermes-memory-tencentdb.bat new file mode 100644 index 00000000..d350771d --- /dev/null +++ b/scripts/setup-hermes-memory-tencentdb.bat @@ -0,0 +1,217 @@ +@echo off +setlocal EnableExtensions + +set "SCRIPT_PATH=%~f0" +set "SCRIPT_DIR=%~dp0" +for %%I in ("%SCRIPT_DIR%..") do set "REPO_ROOT=%%~fI" + +if /I "%~1"=="--gateway-only" goto gateway_only + +echo [memory-tencentdb] Hermes Windows native setup +echo [memory-tencentdb] Repo: %REPO_ROOT% + +if "%USERPROFILE%"=="" ( + echo [ERROR] USERPROFILE is not set. + exit /b 1 +) + +call :require_command node "Node.js" +if errorlevel 1 exit /b 1 +call :require_command npm "npm" +if errorlevel 1 exit /b 1 +call :check_python +if errorlevel 1 exit /b 1 +call :check_hermes + +for /f "delims=" %%V in ('node -p "process.versions.node" 2^>nul') do set "NODE_VERSION=%%V" +node -e "const [M,m,p]=process.versions.node.split('.').map(Number); process.exit(M>22 || (M===22 && (m>16 || (m===16 && p>=0))) ? 0 : 1)" +if errorlevel 1 ( + echo [ERROR] Node.js 22.16.0 or newer is required. Current: %NODE_VERSION% + exit /b 1 +) +echo [memory-tencentdb] Node.js %NODE_VERSION% + +if not exist "%REPO_ROOT%\package.json" ( + echo [ERROR] package.json not found under %REPO_ROOT% + echo [ERROR] Run this script from the memory-tencentdb package. + exit /b 1 +) + +if "%HERMES_HOME%"=="" set "HERMES_HOME=%USERPROFILE%\.hermes" +if "%MEMORY_TENCENTDB_ROOT%"=="" set "MEMORY_TENCENTDB_ROOT=%USERPROFILE%\.memory-tencentdb" +if "%TDAI_DATA_DIR%"=="" set "TDAI_DATA_DIR=%MEMORY_TENCENTDB_ROOT%\memory-tdai" +if "%MEMORY_TENCENTDB_GATEWAY_HOST%"=="" set "MEMORY_TENCENTDB_GATEWAY_HOST=127.0.0.1" +if "%MEMORY_TENCENTDB_GATEWAY_PORT%"=="" set "MEMORY_TENCENTDB_GATEWAY_PORT=8420" +if "%TDAI_GATEWAY_HOST%"=="" set "TDAI_GATEWAY_HOST=%MEMORY_TENCENTDB_GATEWAY_HOST%" +if "%TDAI_GATEWAY_PORT%"=="" set "TDAI_GATEWAY_PORT=%MEMORY_TENCENTDB_GATEWAY_PORT%" + +if "%TDAI_LLM_API_KEY%"=="" if not "%MEMORY_TENCENTDB_LLM_API_KEY%"=="" set "TDAI_LLM_API_KEY=%MEMORY_TENCENTDB_LLM_API_KEY%" +if "%TDAI_LLM_BASE_URL%"=="" if not "%MEMORY_TENCENTDB_LLM_BASE_URL%"=="" set "TDAI_LLM_BASE_URL=%MEMORY_TENCENTDB_LLM_BASE_URL%" +if "%TDAI_LLM_MODEL%"=="" if not "%MEMORY_TENCENTDB_LLM_MODEL%"=="" set "TDAI_LLM_MODEL=%MEMORY_TENCENTDB_LLM_MODEL%" +if "%TDAI_LLM_BASE_URL%"=="" set "TDAI_LLM_BASE_URL=https://api.openai.com/v1" +if "%TDAI_LLM_MODEL%"=="" set "TDAI_LLM_MODEL=gpt-4o" + +if "%MEMORY_TENCENTDB_LLM_API_KEY%"=="" if not "%TDAI_LLM_API_KEY%"=="" set "MEMORY_TENCENTDB_LLM_API_KEY=%TDAI_LLM_API_KEY%" +if "%MEMORY_TENCENTDB_LLM_BASE_URL%"=="" set "MEMORY_TENCENTDB_LLM_BASE_URL=%TDAI_LLM_BASE_URL%" +if "%MEMORY_TENCENTDB_LLM_MODEL%"=="" set "MEMORY_TENCENTDB_LLM_MODEL=%TDAI_LLM_MODEL%" + +set "MEMORY_TENCENTDB_GATEWAY_CMD=cmd /d /s /c ""%SCRIPT_PATH%"" --gateway-only" +set "HERMES_CONFIG=%HERMES_HOME%\config.yaml" +set "HERMES_ENV=%HERMES_HOME%\.env" +set "HERMES_LOG_DIR=%HERMES_HOME%\logs\memory_tencentdb" + +mkdir "%HERMES_HOME%" 2>nul +mkdir "%HERMES_HOME%\plugins" 2>nul +mkdir "%HERMES_LOG_DIR%" 2>nul +mkdir "%MEMORY_TENCENTDB_ROOT%" 2>nul +mkdir "%TDAI_DATA_DIR%" 2>nul + +pushd "%REPO_ROOT%" >nul +call npm ls --omit=dev --depth=0 >nul 2>nul +set "NPM_LS_RC=%ERRORLEVEL%" +if not "%NPM_LS_RC%"=="0" ( + echo [memory-tencentdb] Gateway dependencies missing or incomplete; running npm install --omit=dev + call npm install --omit=dev + set "NPM_RC=%ERRORLEVEL%" + popd >nul + if not "%NPM_RC%"=="0" ( + echo [ERROR] npm install failed with exit code %NPM_RC%. + exit /b %NPM_RC% + ) +) else ( + popd >nul + echo [memory-tencentdb] Gateway dependencies already installed +) + +set "PLUGIN_SRC=%REPO_ROOT%\hermes-plugin\memory\memory_tencentdb" +set "PLUGIN_DST=%HERMES_HOME%\plugins\memory_tencentdb" +if not exist "%PLUGIN_SRC%\plugin.yaml" ( + echo [ERROR] Hermes provider source not found: %PLUGIN_SRC% + exit /b 1 +) + +echo [memory-tencentdb] Copying Hermes provider to %PLUGIN_DST% +if exist "%PLUGIN_DST%" rmdir /s /q "%PLUGIN_DST%" +xcopy "%PLUGIN_SRC%" "%PLUGIN_DST%\" /E /I /Y >nul +if errorlevel 2 ( + echo [ERROR] Failed to copy Hermes provider to %PLUGIN_DST% + exit /b 1 +) + +call :write_env +if errorlevel 1 ( + echo [WARN] Failed to update %HERMES_ENV%; current process env is still set. +) else ( + echo [memory-tencentdb] Environment written to %HERMES_ENV% +) + +if not exist "%HERMES_CONFIG%" ( + > "%HERMES_CONFIG%" echo memory: + >> "%HERMES_CONFIG%" echo provider: memory_tencentdb + echo [memory-tencentdb] Created %HERMES_CONFIG% with memory.provider=memory_tencentdb +) else ( + findstr /R /C:"^[ ][ ]*provider:[ ][ ]*memory_tencentdb" /C:"^provider:[ ][ ]*memory_tencentdb" "%HERMES_CONFIG%" >nul + if errorlevel 1 ( + echo [memory-tencentdb] Please ensure %HERMES_CONFIG% contains: + echo memory: + echo provider: memory_tencentdb + ) else ( + echo [memory-tencentdb] Hermes config already references memory_tencentdb + ) +) + +call :health_check +if not errorlevel 1 ( + echo [memory-tencentdb] Gateway already healthy at http://%MEMORY_TENCENTDB_GATEWAY_HOST%:%MEMORY_TENCENTDB_GATEWAY_PORT%/health + goto done +) + +echo [memory-tencentdb] Starting Gateway in background +start "memory-tencentdb Gateway" /B cmd /d /s /c call "%SCRIPT_PATH%" --gateway-only 1>> "%HERMES_LOG_DIR%\gateway.stdout.log" 2>> "%HERMES_LOG_DIR%\gateway.stderr.log" + +for /L %%I in (1,1,30) do ( + call :health_check + if not errorlevel 1 goto healthy + timeout /t 1 /nobreak >nul +) + +echo [ERROR] Gateway did not become healthy within 30 seconds. +echo [ERROR] Check logs: +echo %HERMES_LOG_DIR%\gateway.stdout.log +echo %HERMES_LOG_DIR%\gateway.stderr.log +exit /b 1 + +:healthy +echo [memory-tencentdb] Gateway healthy at http://%MEMORY_TENCENTDB_GATEWAY_HOST%:%MEMORY_TENCENTDB_GATEWAY_PORT%/health + +:done +echo. +echo [memory-tencentdb] Done. +echo Data dir: %TDAI_DATA_DIR% +echo Hermes plugin: %PLUGIN_DST% +echo Hermes config: %HERMES_CONFIG% +echo Gateway logs: %HERMES_LOG_DIR% +if "%TDAI_LLM_API_KEY%"=="" ( + echo. + echo [WARN] TDAI_LLM_API_KEY is not set. L1/L2/L3 extraction needs an OpenAI-compatible API key. + echo Set it in this shell or in %HERMES_ENV%, then rerun this script. +) +exit /b 0 + +:gateway_only +if "%USERPROFILE%"=="" exit /b 1 +if "%HERMES_HOME%"=="" set "HERMES_HOME=%USERPROFILE%\.hermes" +if "%MEMORY_TENCENTDB_ROOT%"=="" set "MEMORY_TENCENTDB_ROOT=%USERPROFILE%\.memory-tencentdb" +if "%TDAI_DATA_DIR%"=="" set "TDAI_DATA_DIR=%MEMORY_TENCENTDB_ROOT%\memory-tdai" +if "%MEMORY_TENCENTDB_GATEWAY_HOST%"=="" set "MEMORY_TENCENTDB_GATEWAY_HOST=127.0.0.1" +if "%MEMORY_TENCENTDB_GATEWAY_PORT%"=="" set "MEMORY_TENCENTDB_GATEWAY_PORT=8420" +if "%TDAI_GATEWAY_HOST%"=="" set "TDAI_GATEWAY_HOST=%MEMORY_TENCENTDB_GATEWAY_HOST%" +if "%TDAI_GATEWAY_PORT%"=="" set "TDAI_GATEWAY_PORT=%MEMORY_TENCENTDB_GATEWAY_PORT%" +if "%TDAI_LLM_API_KEY%"=="" if not "%MEMORY_TENCENTDB_LLM_API_KEY%"=="" set "TDAI_LLM_API_KEY=%MEMORY_TENCENTDB_LLM_API_KEY%" +if "%TDAI_LLM_BASE_URL%"=="" if not "%MEMORY_TENCENTDB_LLM_BASE_URL%"=="" set "TDAI_LLM_BASE_URL=%MEMORY_TENCENTDB_LLM_BASE_URL%" +if "%TDAI_LLM_MODEL%"=="" if not "%MEMORY_TENCENTDB_LLM_MODEL%"=="" set "TDAI_LLM_MODEL=%MEMORY_TENCENTDB_LLM_MODEL%" +if "%TDAI_LLM_BASE_URL%"=="" set "TDAI_LLM_BASE_URL=https://api.openai.com/v1" +if "%TDAI_LLM_MODEL%"=="" set "TDAI_LLM_MODEL=gpt-4o" +mkdir "%TDAI_DATA_DIR%" 2>nul +cd /d "%REPO_ROOT%" +node --import tsx/esm src/gateway/server.ts +exit /b %ERRORLEVEL% + +:require_command +where %~1 >nul 2>nul +if errorlevel 1 ( + echo [ERROR] %~2 not found in PATH. + exit /b 1 +) +exit /b 0 + +:check_python +python --version >nul 2>nul +if not errorlevel 1 ( + for /f "delims=" %%V in ('python --version 2^>^&1') do echo [memory-tencentdb] %%V + exit /b 0 +) +py -3 --version >nul 2>nul +if not errorlevel 1 ( + for /f "delims=" %%V in ('py -3 --version 2^>^&1') do echo [memory-tencentdb] %%V + exit /b 0 +) +echo [ERROR] Python 3 not found. Install Python before running Hermes. +exit /b 1 + +:check_hermes +where hermes >nul 2>nul +if errorlevel 1 ( + echo [WARN] hermes command not found in PATH. Provider files will still be installed. + exit /b 0 +) +echo [memory-tencentdb] hermes command found +exit /b 0 + +:write_env +powershell -NoProfile -ExecutionPolicy Bypass -Command "$p=$env:HERMES_ENV; $dir=Split-Path -Parent $p; New-Item -ItemType Directory -Force $dir | Out-Null; $keys=@('TDAI_DATA_DIR','TDAI_GATEWAY_HOST','TDAI_GATEWAY_PORT','TDAI_LLM_BASE_URL','TDAI_LLM_API_KEY','TDAI_LLM_MODEL','MEMORY_TENCENTDB_GATEWAY_CMD','MEMORY_TENCENTDB_GATEWAY_HOST','MEMORY_TENCENTDB_GATEWAY_PORT','MEMORY_TENCENTDB_LLM_BASE_URL','MEMORY_TENCENTDB_LLM_API_KEY','MEMORY_TENCENTDB_LLM_MODEL'); $pattern='^('+(($keys | ForEach-Object {[regex]::Escape($_)}) -join '|')+')='; $lines=@(); if(Test-Path $p){ $lines=Get-Content $p | Where-Object { $_ -notmatch $pattern } }; $dq=[char]34; $bs=[char]92; foreach($k in $keys){ $v=[Environment]::GetEnvironmentVariable($k,'Process'); if($null -ne $v -and $v.Length -gt 0){ $escaped=$v.Replace([string]$bs, ([string]$bs)+([string]$bs)).Replace([string]$dq, ([string]$bs)+([string]$dq)); $lines += ($k + '=' + [string]$dq + $escaped + [string]$dq) } }; Set-Content -Path $p -Value $lines -Encoding UTF8" +exit /b %ERRORLEVEL% + +:health_check +powershell -NoProfile -ExecutionPolicy Bypass -Command "try { $u='http://' + $env:MEMORY_TENCENTDB_GATEWAY_HOST + ':' + $env:MEMORY_TENCENTDB_GATEWAY_PORT + '/health'; $r=Invoke-RestMethod -TimeoutSec 2 -Uri $u; if($r.status -eq 'ok' -or $r.status -eq 'degraded'){ exit 0 }; exit 1 } catch { exit 1 }" +exit /b %ERRORLEVEL%