@remnic/server 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joshua Warren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -44,6 +44,17 @@ function envOverrides() {
44
44
  if (memoryDir) remnic.memoryDir = memoryDir;
45
45
  return { ...overrides, ...Object.keys(remnic).length > 0 ? { remnic } : {} };
46
46
  }
47
+ function abortableDelay(ms, signal) {
48
+ if (signal.aborted) return Promise.resolve();
49
+ return new Promise((resolve) => {
50
+ const timer = setTimeout(resolve, ms);
51
+ const onAbort = () => {
52
+ clearTimeout(timer);
53
+ resolve();
54
+ };
55
+ signal.addEventListener("abort", onAbort, { once: true });
56
+ });
57
+ }
47
58
  async function startServer(options) {
48
59
  initLogger();
49
60
  const configPath = resolveConfigPath(options?.configPath);
@@ -59,6 +70,7 @@ async function startServer(options) {
59
70
  };
60
71
  const config = parseConfig(remnicConfig);
61
72
  const orchestrator = new Orchestrator(config);
73
+ await orchestrator.initialize();
62
74
  const service = new EngramAccessService(orchestrator);
63
75
  const authToken = serverConfig.authToken ?? readCompatEnv("REMNIC_AUTH_TOKEN", "ENGRAM_AUTH_TOKEN") ?? "";
64
76
  if (!authToken && getAllValidTokens().length === 0) {
@@ -72,10 +84,54 @@ async function startServer(options) {
72
84
  authTokensGetter: () => getAllValidTokensCached(),
73
85
  principal: serverConfig.principal,
74
86
  maxBodyBytes: serverConfig.maxBodyBytes,
75
- adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false
87
+ adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false,
88
+ citationsEnabled: config.citationsEnabled,
89
+ citationsAutoDetect: config.citationsAutoDetect
76
90
  });
77
91
  const { host, port } = await httpServer.start();
78
- return { config, service, httpServer, host, port };
92
+ const startupSyncAbort = new AbortController();
93
+ const originalStop = httpServer.stop.bind(httpServer);
94
+ httpServer.stop = async () => {
95
+ startupSyncAbort.abort();
96
+ return originalStop();
97
+ };
98
+ orchestrator.deferredReady.then(() => {
99
+ if (!config.qmdEnabled || orchestrator.qmd.debugStatus() === "backend=noop") {
100
+ log.debug("QMD startup-sync: search disabled or noop backend, skipping retries");
101
+ return;
102
+ }
103
+ const needsRetry = !orchestrator.qmd.isAvailable() || !orchestrator.deferredSyncSucceeded;
104
+ if (!needsRetry) {
105
+ log.debug("QMD startup-sync: deferred init completed successfully, no retries needed");
106
+ return;
107
+ }
108
+ const RETRY_DELAYS_MS = [5e3, 15e3, 3e4, 6e4, 12e4];
109
+ (async () => {
110
+ for (const delay of RETRY_DELAYS_MS) {
111
+ await abortableDelay(delay, startupSyncAbort.signal);
112
+ if (startupSyncAbort.signal.aborted) {
113
+ log.debug("QMD startup-sync retry: cancelled by shutdown");
114
+ return;
115
+ }
116
+ const synced = await orchestrator.startupSearchSync(startupSyncAbort.signal);
117
+ if (!synced) {
118
+ if (orchestrator.qmd.debugStatus() === "backend=noop") {
119
+ log.debug("QMD startup-sync retry: search intentionally disabled; stopping retries");
120
+ return;
121
+ }
122
+ log.debug(`QMD startup-sync retry: not available yet (next retry in ${RETRY_DELAYS_MS[RETRY_DELAYS_MS.indexOf(delay) + 1] ?? "n/a"}ms)`);
123
+ continue;
124
+ }
125
+ return;
126
+ }
127
+ log.warn("QMD startup-sync retry: exhausted all retries; search index may be stale");
128
+ })().catch((err) => {
129
+ log.warn(`QMD startup-sync retry: unexpected error: ${err}`);
130
+ });
131
+ }).catch((err) => {
132
+ log.warn(`Deferred init error: ${err}`);
133
+ });
134
+ return { config, service, httpServer, host, port, cancelStartupSync: () => startupSyncAbort.abort(), abortDeferredInit: () => orchestrator.abortDeferredInit() };
79
135
  }
80
136
  function parseCliArgs(argv) {
81
137
  const args = {};
@@ -130,6 +186,8 @@ Environment:
130
186
  const shutdown = async (signal) => {
131
187
  console.log(`
132
188
  Received ${signal}, shutting down...`);
189
+ result.cancelStartupSync();
190
+ result.abortDeferredInit();
133
191
  await result.httpServer.stop();
134
192
  process.exit(0);
135
193
  };
@@ -147,4 +205,4 @@ if (process.argv[1] && (/remnic-server[\\/](?:dist|src)[\\/]index\.[jt]s$/.test(
147
205
  export {
148
206
  cliMain
149
207
  };
150
- //# sourceMappingURL=chunk-XDNRJ5ON.js.map
208
+ //# sourceMappingURL=chunk-FFTS3VKE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/index.ts"],"sourcesContent":["/**\n * @remnic/server\n *\n * Standalone Remnic memory server.\n *\n * Loads config from `remnic.config.json` (or env vars), creates an Orchestrator,\n * and starts the HTTP access server with MCP endpoint — no OpenClaw required.\n *\n * Usage:\n * npx remnic-server\n * npx remnic-server --config ./my-remnic.json\n * npx remnic-server --port 4320\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseConfig, Orchestrator, EngramAccessService, EngramAccessHttpServer, initLogger, log, getAllValidTokens, getAllValidTokensCached, type PluginConfig } from \"@remnic/core\";\n\n// ── Config loading ──────────────────────────────────────────────────────────\n\nexport interface ServerConfig {\n remnic: Record<string, unknown>;\n server: {\n host?: string;\n port?: number;\n authToken?: string;\n principal?: string;\n maxBodyBytes?: number;\n adminConsoleEnabled?: boolean;\n };\n}\n\nfunction readCompatEnv(primary: string, legacy: string): string | undefined {\n return process.env[primary] ?? process.env[legacy];\n}\n\nfunction resolveConfigPath(cliPath?: string): string {\n if (cliPath) return path.resolve(cliPath);\n\n const envPath = readCompatEnv(\"REMNIC_CONFIG_PATH\", \"ENGRAM_CONFIG_PATH\");\n if (envPath) return path.resolve(envPath);\n\n const homeDir = process.env.HOME ?? \"~\";\n const candidates = [\n path.join(process.cwd(), \"remnic.config.json\"),\n path.join(process.cwd(), \"engram.config.json\"),\n path.join(homeDir, \".config\", \"remnic\", \"config.json\"),\n path.join(homeDir, \".config\", \"engram\", \"config.json\"),\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n return path.join(homeDir, \".config\", \"remnic\", \"config.json\");\n}\n\nfunction loadConfigFile(configPath: string): ServerConfig {\n const raw = JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n return {\n remnic: raw.remnic ?? raw.engram ?? raw ?? {},\n server: raw.server ?? {},\n };\n}\n\nfunction envOverrides(): Partial<ServerConfig[\"server\"]> & { remnic?: Record<string, unknown> } {\n const overrides: Record<string, unknown> = {};\n const remnic: Record<string, unknown> = {};\n\n const port = readCompatEnv(\"REMNIC_PORT\", \"ENGRAM_PORT\");\n const host = readCompatEnv(\"REMNIC_HOST\", \"ENGRAM_HOST\");\n const authToken = readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\");\n if (port) overrides.port = parseInt(port, 10);\n if (host) overrides.host = host;\n if (authToken) overrides.authToken = authToken;\n\n if (process.env.OPENAI_API_KEY) remnic.openaiApiKey = process.env.OPENAI_API_KEY;\n const memoryDir = readCompatEnv(\"REMNIC_MEMORY_DIR\", \"ENGRAM_MEMORY_DIR\");\n if (memoryDir) remnic.memoryDir = memoryDir;\n\n return { ...overrides, ...(Object.keys(remnic).length > 0 ? { remnic } : {}) };\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Like `setTimeout` wrapped in a Promise, but respects an `AbortSignal`.\n * Resolves immediately (without throwing) when the signal fires so the\n * caller can check `signal.aborted` and exit cleanly.\n */\nfunction abortableDelay(ms: number, signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n return new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n// ── Server startup ──────────────────────────────────────────────────────────\n\nexport interface ServerResult {\n config: PluginConfig;\n service: EngramAccessService;\n httpServer: EngramAccessHttpServer;\n host: string;\n port: number;\n /** Cancel any pending startup-sync retry timers. Called automatically on shutdown. */\n cancelStartupSync: () => void;\n /** Abort deferred orchestrator initialization (QMD sync, warmup, cache). */\n abortDeferredInit: () => void;\n}\n\nexport async function startServer(options?: {\n configPath?: string;\n host?: string;\n port?: number;\n authToken?: string;\n}): Promise<ServerResult> {\n initLogger();\n\n const configPath = resolveConfigPath(options?.configPath);\n const fileConfig = fs.existsSync(configPath)\n ? loadConfigFile(configPath)\n : { remnic: {}, server: {} };\n\n const env = envOverrides();\n\n // Merge: file < env < cli flags\n const remnicConfig = { ...fileConfig.remnic, ...(env.remnic ?? {}) };\n const serverConfig = {\n ...fileConfig.server,\n ...env,\n ...(options?.host ? { host: options.host } : {}),\n ...(options?.port ? { port: options.port } : {}),\n ...(options?.authToken ? { authToken: options.authToken } : {}),\n };\n\n const config = parseConfig(remnicConfig);\n const orchestrator = new Orchestrator(config);\n await orchestrator.initialize();\n\n // Start the HTTP server immediately so health checks, MCP handshakes,\n // and liveness probes can connect while deferred init is still running.\n const service = new EngramAccessService(orchestrator);\n\n const authToken = serverConfig.authToken ?? readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\") ?? \"\";\n\n // Connector tokens are loaded dynamically per request via authTokensGetter\n // so that token generate/revoke takes effect without server restart\n if (!authToken && getAllValidTokens().length === 0) {\n log.warn(\"No auth token set — server will reject all requests. Set REMNIC_AUTH_TOKEN, server.authToken in config, or generate tokens with 'remnic token generate'.\");\n }\n\n const httpServer = new EngramAccessHttpServer({\n service,\n host: serverConfig.host ?? \"127.0.0.1\",\n port: serverConfig.port ?? 4318,\n authToken: authToken || undefined,\n authTokensGetter: () => getAllValidTokensCached(),\n principal: serverConfig.principal,\n maxBodyBytes: serverConfig.maxBodyBytes,\n adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false,\n citationsEnabled: config.citationsEnabled,\n citationsAutoDetect: config.citationsAutoDetect,\n });\n\n const { host, port } = await httpServer.start();\n\n // Fire-and-forget: wait for deferred init (QMD probe, collection setup,\n // warmup) then check QMD availability and retry if needed. This does NOT\n // block the server listener — connections are accepted immediately above.\n // An AbortController allows the shutdown handler to cancel pending retries.\n const startupSyncAbort = new AbortController();\n\n // Wrap httpServer.stop() so that stopping the HTTP server also cancels any\n // in-flight startup-sync retry timers. This ensures callers that only have\n // a reference to httpServer (e.g. test harnesses) don't leave dangling timers\n // even if they never call cancelStartupSync() directly.\n const originalStop = httpServer.stop.bind(httpServer);\n httpServer.stop = async (): Promise<void> => {\n startupSyncAbort.abort();\n return originalStop();\n };\n\n orchestrator.deferredReady.then(() => {\n // Skip retries when search is explicitly disabled via config or when the\n // orchestrator already resolved to a noop backend (e.g. missing collection\n // detected during deferredInitialize). Both cases mean no sync should ever\n // run; scheduling retries would create misleading operational noise and\n // unnecessary background work on every server start.\n if (!config.qmdEnabled || orchestrator.qmd.debugStatus() === \"backend=noop\") {\n log.debug(\"QMD startup-sync: search disabled or noop backend, skipping retries\");\n return;\n }\n\n // Retry when either: (a) QMD is not available yet (cold-start race), or\n // (b) QMD is available but the deferred init sync step failed silently\n // (e.g., update errors swallowed by backend, throttle skip, transient\n // network failure). Without (b), the daemon permanently serves stale\n // recall after a failed sync despite healthy QMD probe.\n const needsRetry = !orchestrator.qmd.isAvailable() || !orchestrator.deferredSyncSucceeded;\n if (!needsRetry) {\n log.debug(\"QMD startup-sync: deferred init completed successfully, no retries needed\");\n return;\n }\n\n const RETRY_DELAYS_MS = [5_000, 15_000, 30_000, 60_000, 120_000];\n (async () => {\n for (const delay of RETRY_DELAYS_MS) {\n await abortableDelay(delay, startupSyncAbort.signal);\n\n if (startupSyncAbort.signal.aborted) {\n log.debug(\"QMD startup-sync retry: cancelled by shutdown\");\n return;\n }\n\n const synced = await orchestrator.startupSearchSync(startupSyncAbort.signal);\n if (!synced) {\n if (orchestrator.qmd.debugStatus() === \"backend=noop\") {\n log.debug(\"QMD startup-sync retry: search intentionally disabled; stopping retries\");\n return;\n }\n log.debug(`QMD startup-sync retry: not available yet (next retry in ${RETRY_DELAYS_MS[RETRY_DELAYS_MS.indexOf(delay) + 1] ?? \"n/a\"}ms)`);\n continue;\n }\n\n return; // sync succeeded, stop retrying\n }\n\n log.warn(\"QMD startup-sync retry: exhausted all retries; search index may be stale\");\n })().catch((err) => {\n log.warn(`QMD startup-sync retry: unexpected error: ${err}`);\n });\n }).catch((err) => {\n log.warn(`Deferred init error: ${err}`);\n });\n\n return { config, service, httpServer, host, port, cancelStartupSync: () => startupSyncAbort.abort(), abortDeferredInit: () => orchestrator.abortDeferredInit() };\n}\n\n// ── CLI entry point ──────────────────────────────────────────────────────────\n\nfunction parseCliArgs(argv: string[]): Record<string, string | undefined> {\n const args: Record<string, string | undefined> = {};\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n if (token.startsWith(\"--\")) {\n const key = token.slice(2);\n const next = argv[i + 1];\n if (next && !next.startsWith(\"--\")) {\n args[key] = next;\n i++;\n } else {\n args[key] = \"true\";\n }\n }\n }\n return args;\n}\n\nexport async function cliMain(argv: string[] = process.argv.slice(2)): Promise<void> {\n const args = parseCliArgs(argv);\n\n if (args.help) {\n console.log(`\nremnic-server — Standalone Remnic memory server\n\nUsage:\n remnic-server [options]\n\nOptions:\n --config <path> Path to config file (default: remnic.config.json)\n --host <addr> Bind address (default: 127.0.0.1)\n --port <number> Port number (default: 4318)\n --auth-token <tok> Bearer token for auth (or set REMNIC_AUTH_TOKEN)\n --help Show this help\n\nEnvironment:\n REMNIC_CONFIG_PATH Config file path (ENGRAM_CONFIG_PATH also supported)\n REMNIC_PORT Server port (ENGRAM_PORT also supported)\n REMNIC_HOST Bind address (ENGRAM_HOST also supported)\n REMNIC_AUTH_TOKEN Auth bearer token (ENGRAM_AUTH_TOKEN also supported)\n REMNIC_MEMORY_DIR Override memory directory (ENGRAM_MEMORY_DIR also supported)\n OPENAI_API_KEY OpenAI API key for extraction\n`);\n process.exit(0);\n }\n\n const result = await startServer({\n configPath: args.config,\n host: args.host,\n port: args.port ? parseInt(args.port, 10) : undefined,\n authToken: args[\"auth-token\"],\n });\n\n console.log(`Remnic server listening on http://${result.host}:${result.port}`);\n\n // Graceful shutdown\n const shutdown = async (signal: string) => {\n console.log(`\\nReceived ${signal}, shutting down...`);\n result.cancelStartupSync();\n result.abortDeferredInit();\n await result.httpServer.stop();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n}\n\n// Auto-run when executed directly\n// Matches: `node .../remnic-server/dist/index.js`, `node .../remnic-server/src/index.ts`,\n// `npx remnic-server`, `npx engram-server`, but NOT test files under those directories\nif (\n process.argv[1] &&\n (/remnic-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n /engram-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n process.argv[1].endsWith(\"remnic-server\") ||\n process.argv[1].endsWith(\"engram-server\"))\n) {\n cliMain().catch((err) => {\n process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n });\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@
2
2
  // openclaw-engram: Local-first memory plugin
3
3
  import {
4
4
  cliMain
5
- } from "./chunk-XDNRJ5ON.js";
5
+ } from "./chunk-FFTS3VKE.js";
6
6
 
7
7
  // bin/engram-server.ts
8
8
  cliMain().catch((err) => {
@@ -2,7 +2,7 @@
2
2
  // openclaw-engram: Local-first memory plugin
3
3
  import {
4
4
  cliMain
5
- } from "./chunk-XDNRJ5ON.js";
5
+ } from "./chunk-FFTS3VKE.js";
6
6
 
7
7
  // bin/remnic-server.ts
8
8
  cliMain().catch((err) => {
package/dist/index.js CHANGED
@@ -44,6 +44,17 @@ function envOverrides() {
44
44
  if (memoryDir) remnic.memoryDir = memoryDir;
45
45
  return { ...overrides, ...Object.keys(remnic).length > 0 ? { remnic } : {} };
46
46
  }
47
+ function abortableDelay(ms, signal) {
48
+ if (signal.aborted) return Promise.resolve();
49
+ return new Promise((resolve) => {
50
+ const timer = setTimeout(resolve, ms);
51
+ const onAbort = () => {
52
+ clearTimeout(timer);
53
+ resolve();
54
+ };
55
+ signal.addEventListener("abort", onAbort, { once: true });
56
+ });
57
+ }
47
58
  async function startServer(options) {
48
59
  initLogger();
49
60
  const configPath = resolveConfigPath(options?.configPath);
@@ -59,6 +70,7 @@ async function startServer(options) {
59
70
  };
60
71
  const config = parseConfig(remnicConfig);
61
72
  const orchestrator = new Orchestrator(config);
73
+ await orchestrator.initialize();
62
74
  const service = new EngramAccessService(orchestrator);
63
75
  const authToken = serverConfig.authToken ?? readCompatEnv("REMNIC_AUTH_TOKEN", "ENGRAM_AUTH_TOKEN") ?? "";
64
76
  if (!authToken && getAllValidTokens().length === 0) {
@@ -72,10 +84,54 @@ async function startServer(options) {
72
84
  authTokensGetter: () => getAllValidTokensCached(),
73
85
  principal: serverConfig.principal,
74
86
  maxBodyBytes: serverConfig.maxBodyBytes,
75
- adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false
87
+ adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false,
88
+ citationsEnabled: config.citationsEnabled,
89
+ citationsAutoDetect: config.citationsAutoDetect
76
90
  });
77
91
  const { host, port } = await httpServer.start();
78
- return { config, service, httpServer, host, port };
92
+ const startupSyncAbort = new AbortController();
93
+ const originalStop = httpServer.stop.bind(httpServer);
94
+ httpServer.stop = async () => {
95
+ startupSyncAbort.abort();
96
+ return originalStop();
97
+ };
98
+ orchestrator.deferredReady.then(() => {
99
+ if (!config.qmdEnabled || orchestrator.qmd.debugStatus() === "backend=noop") {
100
+ log.debug("QMD startup-sync: search disabled or noop backend, skipping retries");
101
+ return;
102
+ }
103
+ const needsRetry = !orchestrator.qmd.isAvailable() || !orchestrator.deferredSyncSucceeded;
104
+ if (!needsRetry) {
105
+ log.debug("QMD startup-sync: deferred init completed successfully, no retries needed");
106
+ return;
107
+ }
108
+ const RETRY_DELAYS_MS = [5e3, 15e3, 3e4, 6e4, 12e4];
109
+ (async () => {
110
+ for (const delay of RETRY_DELAYS_MS) {
111
+ await abortableDelay(delay, startupSyncAbort.signal);
112
+ if (startupSyncAbort.signal.aborted) {
113
+ log.debug("QMD startup-sync retry: cancelled by shutdown");
114
+ return;
115
+ }
116
+ const synced = await orchestrator.startupSearchSync(startupSyncAbort.signal);
117
+ if (!synced) {
118
+ if (orchestrator.qmd.debugStatus() === "backend=noop") {
119
+ log.debug("QMD startup-sync retry: search intentionally disabled; stopping retries");
120
+ return;
121
+ }
122
+ log.debug(`QMD startup-sync retry: not available yet (next retry in ${RETRY_DELAYS_MS[RETRY_DELAYS_MS.indexOf(delay) + 1] ?? "n/a"}ms)`);
123
+ continue;
124
+ }
125
+ return;
126
+ }
127
+ log.warn("QMD startup-sync retry: exhausted all retries; search index may be stale");
128
+ })().catch((err) => {
129
+ log.warn(`QMD startup-sync retry: unexpected error: ${err}`);
130
+ });
131
+ }).catch((err) => {
132
+ log.warn(`Deferred init error: ${err}`);
133
+ });
134
+ return { config, service, httpServer, host, port, cancelStartupSync: () => startupSyncAbort.abort(), abortDeferredInit: () => orchestrator.abortDeferredInit() };
79
135
  }
80
136
  function parseCliArgs(argv) {
81
137
  const args = {};
@@ -130,6 +186,8 @@ Environment:
130
186
  const shutdown = async (signal) => {
131
187
  console.log(`
132
188
  Received ${signal}, shutting down...`);
189
+ result.cancelStartupSync();
190
+ result.abortDeferredInit();
133
191
  await result.httpServer.stop();
134
192
  process.exit(0);
135
193
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @remnic/server\n *\n * Standalone Remnic memory server.\n *\n * Loads config from `remnic.config.json` (or env vars), creates an Orchestrator,\n * and starts the HTTP access server with MCP endpoint — no OpenClaw required.\n *\n * Usage:\n * npx remnic-server\n * npx remnic-server --config ./my-remnic.json\n * npx remnic-server --port 4320\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseConfig, Orchestrator, EngramAccessService, EngramAccessHttpServer, initLogger, log, getAllValidTokens, getAllValidTokensCached, type PluginConfig } from \"@remnic/core\";\n\n// ── Config loading ──────────────────────────────────────────────────────────\n\nexport interface ServerConfig {\n remnic: Record<string, unknown>;\n server: {\n host?: string;\n port?: number;\n authToken?: string;\n principal?: string;\n maxBodyBytes?: number;\n adminConsoleEnabled?: boolean;\n };\n}\n\nfunction readCompatEnv(primary: string, legacy: string): string | undefined {\n return process.env[primary] ?? process.env[legacy];\n}\n\nfunction resolveConfigPath(cliPath?: string): string {\n if (cliPath) return path.resolve(cliPath);\n\n const envPath = readCompatEnv(\"REMNIC_CONFIG_PATH\", \"ENGRAM_CONFIG_PATH\");\n if (envPath) return path.resolve(envPath);\n\n const homeDir = process.env.HOME ?? \"~\";\n const candidates = [\n path.join(process.cwd(), \"remnic.config.json\"),\n path.join(process.cwd(), \"engram.config.json\"),\n path.join(homeDir, \".config\", \"remnic\", \"config.json\"),\n path.join(homeDir, \".config\", \"engram\", \"config.json\"),\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n return path.join(homeDir, \".config\", \"remnic\", \"config.json\");\n}\n\nfunction loadConfigFile(configPath: string): ServerConfig {\n const raw = JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n return {\n remnic: raw.remnic ?? raw.engram ?? raw ?? {},\n server: raw.server ?? {},\n };\n}\n\nfunction envOverrides(): Partial<ServerConfig[\"server\"]> & { remnic?: Record<string, unknown> } {\n const overrides: Record<string, unknown> = {};\n const remnic: Record<string, unknown> = {};\n\n const port = readCompatEnv(\"REMNIC_PORT\", \"ENGRAM_PORT\");\n const host = readCompatEnv(\"REMNIC_HOST\", \"ENGRAM_HOST\");\n const authToken = readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\");\n if (port) overrides.port = parseInt(port, 10);\n if (host) overrides.host = host;\n if (authToken) overrides.authToken = authToken;\n\n if (process.env.OPENAI_API_KEY) remnic.openaiApiKey = process.env.OPENAI_API_KEY;\n const memoryDir = readCompatEnv(\"REMNIC_MEMORY_DIR\", \"ENGRAM_MEMORY_DIR\");\n if (memoryDir) remnic.memoryDir = memoryDir;\n\n return { ...overrides, ...(Object.keys(remnic).length > 0 ? { remnic } : {}) };\n}\n\n// ── Server startup ──────────────────────────────────────────────────────────\n\nexport interface ServerResult {\n config: PluginConfig;\n service: EngramAccessService;\n httpServer: EngramAccessHttpServer;\n host: string;\n port: number;\n}\n\nexport async function startServer(options?: {\n configPath?: string;\n host?: string;\n port?: number;\n authToken?: string;\n}): Promise<ServerResult> {\n initLogger();\n\n const configPath = resolveConfigPath(options?.configPath);\n const fileConfig = fs.existsSync(configPath)\n ? loadConfigFile(configPath)\n : { remnic: {}, server: {} };\n\n const env = envOverrides();\n\n // Merge: file < env < cli flags\n const remnicConfig = { ...fileConfig.remnic, ...(env.remnic ?? {}) };\n const serverConfig = {\n ...fileConfig.server,\n ...env,\n ...(options?.host ? { host: options.host } : {}),\n ...(options?.port ? { port: options.port } : {}),\n ...(options?.authToken ? { authToken: options.authToken } : {}),\n };\n\n const config = parseConfig(remnicConfig);\n const orchestrator = new Orchestrator(config);\n const service = new EngramAccessService(orchestrator);\n\n const authToken = serverConfig.authToken ?? readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\") ?? \"\";\n\n // Connector tokens are loaded dynamically per request via authTokensGetter\n // so that token generate/revoke takes effect without server restart\n if (!authToken && getAllValidTokens().length === 0) {\n log.warn(\"No auth token set — server will reject all requests. Set REMNIC_AUTH_TOKEN, server.authToken in config, or generate tokens with 'remnic token generate'.\");\n }\n\n const httpServer = new EngramAccessHttpServer({\n service,\n host: serverConfig.host ?? \"127.0.0.1\",\n port: serverConfig.port ?? 4318,\n authToken: authToken || undefined,\n authTokensGetter: () => getAllValidTokensCached(),\n principal: serverConfig.principal,\n maxBodyBytes: serverConfig.maxBodyBytes,\n adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false,\n });\n\n const { host, port } = await httpServer.start();\n\n return { config, service, httpServer, host, port };\n}\n\n// ── CLI entry point ──────────────────────────────────────────────────────────\n\nfunction parseCliArgs(argv: string[]): Record<string, string | undefined> {\n const args: Record<string, string | undefined> = {};\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n if (token.startsWith(\"--\")) {\n const key = token.slice(2);\n const next = argv[i + 1];\n if (next && !next.startsWith(\"--\")) {\n args[key] = next;\n i++;\n } else {\n args[key] = \"true\";\n }\n }\n }\n return args;\n}\n\nexport async function cliMain(argv: string[] = process.argv.slice(2)): Promise<void> {\n const args = parseCliArgs(argv);\n\n if (args.help) {\n console.log(`\nremnic-server — Standalone Remnic memory server\n\nUsage:\n remnic-server [options]\n\nOptions:\n --config <path> Path to config file (default: remnic.config.json)\n --host <addr> Bind address (default: 127.0.0.1)\n --port <number> Port number (default: 4318)\n --auth-token <tok> Bearer token for auth (or set REMNIC_AUTH_TOKEN)\n --help Show this help\n\nEnvironment:\n REMNIC_CONFIG_PATH Config file path (ENGRAM_CONFIG_PATH also supported)\n REMNIC_PORT Server port (ENGRAM_PORT also supported)\n REMNIC_HOST Bind address (ENGRAM_HOST also supported)\n REMNIC_AUTH_TOKEN Auth bearer token (ENGRAM_AUTH_TOKEN also supported)\n REMNIC_MEMORY_DIR Override memory directory (ENGRAM_MEMORY_DIR also supported)\n OPENAI_API_KEY OpenAI API key for extraction\n`);\n process.exit(0);\n }\n\n const result = await startServer({\n configPath: args.config,\n host: args.host,\n port: args.port ? parseInt(args.port, 10) : undefined,\n authToken: args[\"auth-token\"],\n });\n\n console.log(`Remnic server listening on http://${result.host}:${result.port}`);\n\n // Graceful shutdown\n const shutdown = async (signal: string) => {\n console.log(`\\nReceived ${signal}, shutting down...`);\n await result.httpServer.stop();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n}\n\n// Auto-run when executed directly\n// Matches: `node .../remnic-server/dist/index.js`, `node .../remnic-server/src/index.ts`,\n// `npx remnic-server`, `npx engram-server`, but NOT test files under those directories\nif (\n process.argv[1] &&\n (/remnic-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n /engram-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n process.argv[1].endsWith(\"remnic-server\") ||\n process.argv[1].endsWith(\"engram-server\"))\n) {\n cliMain().catch((err) => {\n process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @remnic/server\n *\n * Standalone Remnic memory server.\n *\n * Loads config from `remnic.config.json` (or env vars), creates an Orchestrator,\n * and starts the HTTP access server with MCP endpoint — no OpenClaw required.\n *\n * Usage:\n * npx remnic-server\n * npx remnic-server --config ./my-remnic.json\n * npx remnic-server --port 4320\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseConfig, Orchestrator, EngramAccessService, EngramAccessHttpServer, initLogger, log, getAllValidTokens, getAllValidTokensCached, type PluginConfig } from \"@remnic/core\";\n\n// ── Config loading ──────────────────────────────────────────────────────────\n\nexport interface ServerConfig {\n remnic: Record<string, unknown>;\n server: {\n host?: string;\n port?: number;\n authToken?: string;\n principal?: string;\n maxBodyBytes?: number;\n adminConsoleEnabled?: boolean;\n };\n}\n\nfunction readCompatEnv(primary: string, legacy: string): string | undefined {\n return process.env[primary] ?? process.env[legacy];\n}\n\nfunction resolveConfigPath(cliPath?: string): string {\n if (cliPath) return path.resolve(cliPath);\n\n const envPath = readCompatEnv(\"REMNIC_CONFIG_PATH\", \"ENGRAM_CONFIG_PATH\");\n if (envPath) return path.resolve(envPath);\n\n const homeDir = process.env.HOME ?? \"~\";\n const candidates = [\n path.join(process.cwd(), \"remnic.config.json\"),\n path.join(process.cwd(), \"engram.config.json\"),\n path.join(homeDir, \".config\", \"remnic\", \"config.json\"),\n path.join(homeDir, \".config\", \"engram\", \"config.json\"),\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n return path.join(homeDir, \".config\", \"remnic\", \"config.json\");\n}\n\nfunction loadConfigFile(configPath: string): ServerConfig {\n const raw = JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n return {\n remnic: raw.remnic ?? raw.engram ?? raw ?? {},\n server: raw.server ?? {},\n };\n}\n\nfunction envOverrides(): Partial<ServerConfig[\"server\"]> & { remnic?: Record<string, unknown> } {\n const overrides: Record<string, unknown> = {};\n const remnic: Record<string, unknown> = {};\n\n const port = readCompatEnv(\"REMNIC_PORT\", \"ENGRAM_PORT\");\n const host = readCompatEnv(\"REMNIC_HOST\", \"ENGRAM_HOST\");\n const authToken = readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\");\n if (port) overrides.port = parseInt(port, 10);\n if (host) overrides.host = host;\n if (authToken) overrides.authToken = authToken;\n\n if (process.env.OPENAI_API_KEY) remnic.openaiApiKey = process.env.OPENAI_API_KEY;\n const memoryDir = readCompatEnv(\"REMNIC_MEMORY_DIR\", \"ENGRAM_MEMORY_DIR\");\n if (memoryDir) remnic.memoryDir = memoryDir;\n\n return { ...overrides, ...(Object.keys(remnic).length > 0 ? { remnic } : {}) };\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Like `setTimeout` wrapped in a Promise, but respects an `AbortSignal`.\n * Resolves immediately (without throwing) when the signal fires so the\n * caller can check `signal.aborted` and exit cleanly.\n */\nfunction abortableDelay(ms: number, signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n return new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n// ── Server startup ──────────────────────────────────────────────────────────\n\nexport interface ServerResult {\n config: PluginConfig;\n service: EngramAccessService;\n httpServer: EngramAccessHttpServer;\n host: string;\n port: number;\n /** Cancel any pending startup-sync retry timers. Called automatically on shutdown. */\n cancelStartupSync: () => void;\n /** Abort deferred orchestrator initialization (QMD sync, warmup, cache). */\n abortDeferredInit: () => void;\n}\n\nexport async function startServer(options?: {\n configPath?: string;\n host?: string;\n port?: number;\n authToken?: string;\n}): Promise<ServerResult> {\n initLogger();\n\n const configPath = resolveConfigPath(options?.configPath);\n const fileConfig = fs.existsSync(configPath)\n ? loadConfigFile(configPath)\n : { remnic: {}, server: {} };\n\n const env = envOverrides();\n\n // Merge: file < env < cli flags\n const remnicConfig = { ...fileConfig.remnic, ...(env.remnic ?? {}) };\n const serverConfig = {\n ...fileConfig.server,\n ...env,\n ...(options?.host ? { host: options.host } : {}),\n ...(options?.port ? { port: options.port } : {}),\n ...(options?.authToken ? { authToken: options.authToken } : {}),\n };\n\n const config = parseConfig(remnicConfig);\n const orchestrator = new Orchestrator(config);\n await orchestrator.initialize();\n\n // Start the HTTP server immediately so health checks, MCP handshakes,\n // and liveness probes can connect while deferred init is still running.\n const service = new EngramAccessService(orchestrator);\n\n const authToken = serverConfig.authToken ?? readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\") ?? \"\";\n\n // Connector tokens are loaded dynamically per request via authTokensGetter\n // so that token generate/revoke takes effect without server restart\n if (!authToken && getAllValidTokens().length === 0) {\n log.warn(\"No auth token set — server will reject all requests. Set REMNIC_AUTH_TOKEN, server.authToken in config, or generate tokens with 'remnic token generate'.\");\n }\n\n const httpServer = new EngramAccessHttpServer({\n service,\n host: serverConfig.host ?? \"127.0.0.1\",\n port: serverConfig.port ?? 4318,\n authToken: authToken || undefined,\n authTokensGetter: () => getAllValidTokensCached(),\n principal: serverConfig.principal,\n maxBodyBytes: serverConfig.maxBodyBytes,\n adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false,\n citationsEnabled: config.citationsEnabled,\n citationsAutoDetect: config.citationsAutoDetect,\n });\n\n const { host, port } = await httpServer.start();\n\n // Fire-and-forget: wait for deferred init (QMD probe, collection setup,\n // warmup) then check QMD availability and retry if needed. This does NOT\n // block the server listener — connections are accepted immediately above.\n // An AbortController allows the shutdown handler to cancel pending retries.\n const startupSyncAbort = new AbortController();\n\n // Wrap httpServer.stop() so that stopping the HTTP server also cancels any\n // in-flight startup-sync retry timers. This ensures callers that only have\n // a reference to httpServer (e.g. test harnesses) don't leave dangling timers\n // even if they never call cancelStartupSync() directly.\n const originalStop = httpServer.stop.bind(httpServer);\n httpServer.stop = async (): Promise<void> => {\n startupSyncAbort.abort();\n return originalStop();\n };\n\n orchestrator.deferredReady.then(() => {\n // Skip retries when search is explicitly disabled via config or when the\n // orchestrator already resolved to a noop backend (e.g. missing collection\n // detected during deferredInitialize). Both cases mean no sync should ever\n // run; scheduling retries would create misleading operational noise and\n // unnecessary background work on every server start.\n if (!config.qmdEnabled || orchestrator.qmd.debugStatus() === \"backend=noop\") {\n log.debug(\"QMD startup-sync: search disabled or noop backend, skipping retries\");\n return;\n }\n\n // Retry when either: (a) QMD is not available yet (cold-start race), or\n // (b) QMD is available but the deferred init sync step failed silently\n // (e.g., update errors swallowed by backend, throttle skip, transient\n // network failure). Without (b), the daemon permanently serves stale\n // recall after a failed sync despite healthy QMD probe.\n const needsRetry = !orchestrator.qmd.isAvailable() || !orchestrator.deferredSyncSucceeded;\n if (!needsRetry) {\n log.debug(\"QMD startup-sync: deferred init completed successfully, no retries needed\");\n return;\n }\n\n const RETRY_DELAYS_MS = [5_000, 15_000, 30_000, 60_000, 120_000];\n (async () => {\n for (const delay of RETRY_DELAYS_MS) {\n await abortableDelay(delay, startupSyncAbort.signal);\n\n if (startupSyncAbort.signal.aborted) {\n log.debug(\"QMD startup-sync retry: cancelled by shutdown\");\n return;\n }\n\n const synced = await orchestrator.startupSearchSync(startupSyncAbort.signal);\n if (!synced) {\n if (orchestrator.qmd.debugStatus() === \"backend=noop\") {\n log.debug(\"QMD startup-sync retry: search intentionally disabled; stopping retries\");\n return;\n }\n log.debug(`QMD startup-sync retry: not available yet (next retry in ${RETRY_DELAYS_MS[RETRY_DELAYS_MS.indexOf(delay) + 1] ?? \"n/a\"}ms)`);\n continue;\n }\n\n return; // sync succeeded, stop retrying\n }\n\n log.warn(\"QMD startup-sync retry: exhausted all retries; search index may be stale\");\n })().catch((err) => {\n log.warn(`QMD startup-sync retry: unexpected error: ${err}`);\n });\n }).catch((err) => {\n log.warn(`Deferred init error: ${err}`);\n });\n\n return { config, service, httpServer, host, port, cancelStartupSync: () => startupSyncAbort.abort(), abortDeferredInit: () => orchestrator.abortDeferredInit() };\n}\n\n// ── CLI entry point ──────────────────────────────────────────────────────────\n\nfunction parseCliArgs(argv: string[]): Record<string, string | undefined> {\n const args: Record<string, string | undefined> = {};\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n if (token.startsWith(\"--\")) {\n const key = token.slice(2);\n const next = argv[i + 1];\n if (next && !next.startsWith(\"--\")) {\n args[key] = next;\n i++;\n } else {\n args[key] = \"true\";\n }\n }\n }\n return args;\n}\n\nexport async function cliMain(argv: string[] = process.argv.slice(2)): Promise<void> {\n const args = parseCliArgs(argv);\n\n if (args.help) {\n console.log(`\nremnic-server — Standalone Remnic memory server\n\nUsage:\n remnic-server [options]\n\nOptions:\n --config <path> Path to config file (default: remnic.config.json)\n --host <addr> Bind address (default: 127.0.0.1)\n --port <number> Port number (default: 4318)\n --auth-token <tok> Bearer token for auth (or set REMNIC_AUTH_TOKEN)\n --help Show this help\n\nEnvironment:\n REMNIC_CONFIG_PATH Config file path (ENGRAM_CONFIG_PATH also supported)\n REMNIC_PORT Server port (ENGRAM_PORT also supported)\n REMNIC_HOST Bind address (ENGRAM_HOST also supported)\n REMNIC_AUTH_TOKEN Auth bearer token (ENGRAM_AUTH_TOKEN also supported)\n REMNIC_MEMORY_DIR Override memory directory (ENGRAM_MEMORY_DIR also supported)\n OPENAI_API_KEY OpenAI API key for extraction\n`);\n process.exit(0);\n }\n\n const result = await startServer({\n configPath: args.config,\n host: args.host,\n port: args.port ? parseInt(args.port, 10) : undefined,\n authToken: args[\"auth-token\"],\n });\n\n console.log(`Remnic server listening on http://${result.host}:${result.port}`);\n\n // Graceful shutdown\n const shutdown = async (signal: string) => {\n console.log(`\\nReceived ${signal}, shutting down...`);\n result.cancelStartupSync();\n result.abortDeferredInit();\n await result.httpServer.stop();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n}\n\n// Auto-run when executed directly\n// Matches: `node .../remnic-server/dist/index.js`, `node .../remnic-server/src/index.ts`,\n// `npx remnic-server`, `npx engram-server`, but NOT test files under those directories\nif (\n process.argv[1] &&\n (/remnic-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n /engram-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n process.argv[1].endsWith(\"remnic-server\") ||\n process.argv[1].endsWith(\"engram-server\"))\n) {\n cliMain().catch((err) => {\n process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n });\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/server",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Standalone Remnic memory server — HTTP + MCP without OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,13 +20,8 @@
20
20
  "access": "public",
21
21
  "provenance": true
22
22
  },
23
- "scripts": {
24
- "build": "tsup src/index.ts --format esm --target es2022 --platform node --outDir dist && tsup bin/remnic-server.ts bin/engram-server.ts --format esm --target es2022 --platform node --outDir dist/bin",
25
- "check-types": "tsc --noEmit",
26
- "prepublishOnly": "npm run build"
27
- },
28
23
  "dependencies": {
29
- "@remnic/core": "workspace:^"
24
+ "@remnic/core": "^1.0.3"
30
25
  },
31
26
  "devDependencies": {
32
27
  "tsup": "^8.5.1",
@@ -37,5 +32,9 @@
37
32
  "type": "git",
38
33
  "url": "https://github.com/joshuaswarren/remnic.git",
39
34
  "directory": "packages/remnic-server"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format esm --target es2022 --platform node --outDir dist && tsup bin/remnic-server.ts bin/engram-server.ts --format esm --target es2022 --platform node --outDir dist/bin",
38
+ "check-types": "tsc --noEmit"
40
39
  }
41
- }
40
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/index.ts"],"sourcesContent":["/**\n * @remnic/server\n *\n * Standalone Remnic memory server.\n *\n * Loads config from `remnic.config.json` (or env vars), creates an Orchestrator,\n * and starts the HTTP access server with MCP endpoint — no OpenClaw required.\n *\n * Usage:\n * npx remnic-server\n * npx remnic-server --config ./my-remnic.json\n * npx remnic-server --port 4320\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseConfig, Orchestrator, EngramAccessService, EngramAccessHttpServer, initLogger, log, getAllValidTokens, getAllValidTokensCached, type PluginConfig } from \"@remnic/core\";\n\n// ── Config loading ──────────────────────────────────────────────────────────\n\nexport interface ServerConfig {\n remnic: Record<string, unknown>;\n server: {\n host?: string;\n port?: number;\n authToken?: string;\n principal?: string;\n maxBodyBytes?: number;\n adminConsoleEnabled?: boolean;\n };\n}\n\nfunction readCompatEnv(primary: string, legacy: string): string | undefined {\n return process.env[primary] ?? process.env[legacy];\n}\n\nfunction resolveConfigPath(cliPath?: string): string {\n if (cliPath) return path.resolve(cliPath);\n\n const envPath = readCompatEnv(\"REMNIC_CONFIG_PATH\", \"ENGRAM_CONFIG_PATH\");\n if (envPath) return path.resolve(envPath);\n\n const homeDir = process.env.HOME ?? \"~\";\n const candidates = [\n path.join(process.cwd(), \"remnic.config.json\"),\n path.join(process.cwd(), \"engram.config.json\"),\n path.join(homeDir, \".config\", \"remnic\", \"config.json\"),\n path.join(homeDir, \".config\", \"engram\", \"config.json\"),\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n return path.join(homeDir, \".config\", \"remnic\", \"config.json\");\n}\n\nfunction loadConfigFile(configPath: string): ServerConfig {\n const raw = JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n return {\n remnic: raw.remnic ?? raw.engram ?? raw ?? {},\n server: raw.server ?? {},\n };\n}\n\nfunction envOverrides(): Partial<ServerConfig[\"server\"]> & { remnic?: Record<string, unknown> } {\n const overrides: Record<string, unknown> = {};\n const remnic: Record<string, unknown> = {};\n\n const port = readCompatEnv(\"REMNIC_PORT\", \"ENGRAM_PORT\");\n const host = readCompatEnv(\"REMNIC_HOST\", \"ENGRAM_HOST\");\n const authToken = readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\");\n if (port) overrides.port = parseInt(port, 10);\n if (host) overrides.host = host;\n if (authToken) overrides.authToken = authToken;\n\n if (process.env.OPENAI_API_KEY) remnic.openaiApiKey = process.env.OPENAI_API_KEY;\n const memoryDir = readCompatEnv(\"REMNIC_MEMORY_DIR\", \"ENGRAM_MEMORY_DIR\");\n if (memoryDir) remnic.memoryDir = memoryDir;\n\n return { ...overrides, ...(Object.keys(remnic).length > 0 ? { remnic } : {}) };\n}\n\n// ── Server startup ──────────────────────────────────────────────────────────\n\nexport interface ServerResult {\n config: PluginConfig;\n service: EngramAccessService;\n httpServer: EngramAccessHttpServer;\n host: string;\n port: number;\n}\n\nexport async function startServer(options?: {\n configPath?: string;\n host?: string;\n port?: number;\n authToken?: string;\n}): Promise<ServerResult> {\n initLogger();\n\n const configPath = resolveConfigPath(options?.configPath);\n const fileConfig = fs.existsSync(configPath)\n ? loadConfigFile(configPath)\n : { remnic: {}, server: {} };\n\n const env = envOverrides();\n\n // Merge: file < env < cli flags\n const remnicConfig = { ...fileConfig.remnic, ...(env.remnic ?? {}) };\n const serverConfig = {\n ...fileConfig.server,\n ...env,\n ...(options?.host ? { host: options.host } : {}),\n ...(options?.port ? { port: options.port } : {}),\n ...(options?.authToken ? { authToken: options.authToken } : {}),\n };\n\n const config = parseConfig(remnicConfig);\n const orchestrator = new Orchestrator(config);\n const service = new EngramAccessService(orchestrator);\n\n const authToken = serverConfig.authToken ?? readCompatEnv(\"REMNIC_AUTH_TOKEN\", \"ENGRAM_AUTH_TOKEN\") ?? \"\";\n\n // Connector tokens are loaded dynamically per request via authTokensGetter\n // so that token generate/revoke takes effect without server restart\n if (!authToken && getAllValidTokens().length === 0) {\n log.warn(\"No auth token set — server will reject all requests. Set REMNIC_AUTH_TOKEN, server.authToken in config, or generate tokens with 'remnic token generate'.\");\n }\n\n const httpServer = new EngramAccessHttpServer({\n service,\n host: serverConfig.host ?? \"127.0.0.1\",\n port: serverConfig.port ?? 4318,\n authToken: authToken || undefined,\n authTokensGetter: () => getAllValidTokensCached(),\n principal: serverConfig.principal,\n maxBodyBytes: serverConfig.maxBodyBytes,\n adminConsoleEnabled: serverConfig.adminConsoleEnabled ?? false,\n });\n\n const { host, port } = await httpServer.start();\n\n return { config, service, httpServer, host, port };\n}\n\n// ── CLI entry point ──────────────────────────────────────────────────────────\n\nfunction parseCliArgs(argv: string[]): Record<string, string | undefined> {\n const args: Record<string, string | undefined> = {};\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n if (token.startsWith(\"--\")) {\n const key = token.slice(2);\n const next = argv[i + 1];\n if (next && !next.startsWith(\"--\")) {\n args[key] = next;\n i++;\n } else {\n args[key] = \"true\";\n }\n }\n }\n return args;\n}\n\nexport async function cliMain(argv: string[] = process.argv.slice(2)): Promise<void> {\n const args = parseCliArgs(argv);\n\n if (args.help) {\n console.log(`\nremnic-server — Standalone Remnic memory server\n\nUsage:\n remnic-server [options]\n\nOptions:\n --config <path> Path to config file (default: remnic.config.json)\n --host <addr> Bind address (default: 127.0.0.1)\n --port <number> Port number (default: 4318)\n --auth-token <tok> Bearer token for auth (or set REMNIC_AUTH_TOKEN)\n --help Show this help\n\nEnvironment:\n REMNIC_CONFIG_PATH Config file path (ENGRAM_CONFIG_PATH also supported)\n REMNIC_PORT Server port (ENGRAM_PORT also supported)\n REMNIC_HOST Bind address (ENGRAM_HOST also supported)\n REMNIC_AUTH_TOKEN Auth bearer token (ENGRAM_AUTH_TOKEN also supported)\n REMNIC_MEMORY_DIR Override memory directory (ENGRAM_MEMORY_DIR also supported)\n OPENAI_API_KEY OpenAI API key for extraction\n`);\n process.exit(0);\n }\n\n const result = await startServer({\n configPath: args.config,\n host: args.host,\n port: args.port ? parseInt(args.port, 10) : undefined,\n authToken: args[\"auth-token\"],\n });\n\n console.log(`Remnic server listening on http://${result.host}:${result.port}`);\n\n // Graceful shutdown\n const shutdown = async (signal: string) => {\n console.log(`\\nReceived ${signal}, shutting down...`);\n await result.httpServer.stop();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n}\n\n// Auto-run when executed directly\n// Matches: `node .../remnic-server/dist/index.js`, `node .../remnic-server/src/index.ts`,\n// `npx remnic-server`, `npx engram-server`, but NOT test files under those directories\nif (\n process.argv[1] &&\n (/remnic-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n /engram-server[\\\\/](?:dist|src)[\\\\/]index\\.[jt]s$/.test(process.argv[1]) ||\n process.argv[1].endsWith(\"remnic-server\") ||\n process.argv[1].endsWith(\"engram-server\"))\n) {\n cliMain().catch((err) => {\n process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n });\n}\n"],"mappings":";;;AAcA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,aAAa,cAAc,qBAAqB,wBAAwB,YAAY,KAAK,mBAAmB,+BAAkD;AAgBvK,SAAS,cAAc,SAAiB,QAAoC;AAC1E,SAAO,QAAQ,IAAI,OAAO,KAAK,QAAQ,IAAI,MAAM;AACnD;AAEA,SAAS,kBAAkB,SAA0B;AACnD,MAAI,QAAS,QAAO,KAAK,QAAQ,OAAO;AAExC,QAAM,UAAU,cAAc,sBAAsB,oBAAoB;AACxE,MAAI,QAAS,QAAO,KAAK,QAAQ,OAAO;AAExC,QAAM,UAAU,QAAQ,IAAI,QAAQ;AACpC,QAAM,aAAa;AAAA,IACjB,KAAK,KAAK,QAAQ,IAAI,GAAG,oBAAoB;AAAA,IAC7C,KAAK,KAAK,QAAQ,IAAI,GAAG,oBAAoB;AAAA,IAC7C,KAAK,KAAK,SAAS,WAAW,UAAU,aAAa;AAAA,IACrD,KAAK,KAAK,SAAS,WAAW,UAAU,aAAa;AAAA,EACvD;AACA,aAAW,aAAa,YAAY;AAClC,QAAI,GAAG,WAAW,SAAS,EAAG,QAAO;AAAA,EACvC;AAEA,SAAO,KAAK,KAAK,SAAS,WAAW,UAAU,aAAa;AAC9D;AAEA,SAAS,eAAe,YAAkC;AACxD,QAAM,MAAM,KAAK,MAAM,GAAG,aAAa,YAAY,MAAM,CAAC;AAC1D,SAAO;AAAA,IACL,QAAQ,IAAI,UAAU,IAAI,UAAU,OAAO,CAAC;AAAA,IAC5C,QAAQ,IAAI,UAAU,CAAC;AAAA,EACzB;AACF;AAEA,SAAS,eAAuF;AAC9F,QAAM,YAAqC,CAAC;AAC5C,QAAM,SAAkC,CAAC;AAEzC,QAAM,OAAO,cAAc,eAAe,aAAa;AACvD,QAAM,OAAO,cAAc,eAAe,aAAa;AACvD,QAAM,YAAY,cAAc,qBAAqB,mBAAmB;AACxE,MAAI,KAAM,WAAU,OAAO,SAAS,MAAM,EAAE;AAC5C,MAAI,KAAM,WAAU,OAAO;AAC3B,MAAI,UAAW,WAAU,YAAY;AAErC,MAAI,QAAQ,IAAI,eAAgB,QAAO,eAAe,QAAQ,IAAI;AAClE,QAAM,YAAY,cAAc,qBAAqB,mBAAmB;AACxE,MAAI,UAAW,QAAO,YAAY;AAElC,SAAO,EAAE,GAAG,WAAW,GAAI,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,EAAE,OAAO,IAAI,CAAC,EAAG;AAC/E;AAYA,eAAsB,YAAY,SAKR;AACxB,aAAW;AAEX,QAAM,aAAa,kBAAkB,SAAS,UAAU;AACxD,QAAM,aAAa,GAAG,WAAW,UAAU,IACvC,eAAe,UAAU,IACzB,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE;AAE7B,QAAM,MAAM,aAAa;AAGzB,QAAM,eAAe,EAAE,GAAG,WAAW,QAAQ,GAAI,IAAI,UAAU,CAAC,EAAG;AACnE,QAAM,eAAe;AAAA,IACnB,GAAG,WAAW;AAAA,IACd,GAAG;AAAA,IACH,GAAI,SAAS,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC9C,GAAI,SAAS,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC9C,GAAI,SAAS,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;AAAA,EAC/D;AAEA,QAAM,SAAS,YAAY,YAAY;AACvC,QAAM,eAAe,IAAI,aAAa,MAAM;AAC5C,QAAM,UAAU,IAAI,oBAAoB,YAAY;AAEpD,QAAM,YAAY,aAAa,aAAa,cAAc,qBAAqB,mBAAmB,KAAK;AAIvG,MAAI,CAAC,aAAa,kBAAkB,EAAE,WAAW,GAAG;AAClD,QAAI,KAAK,+JAA0J;AAAA,EACrK;AAEA,QAAM,aAAa,IAAI,uBAAuB;AAAA,IAC5C;AAAA,IACA,MAAM,aAAa,QAAQ;AAAA,IAC3B,MAAM,aAAa,QAAQ;AAAA,IAC3B,WAAW,aAAa;AAAA,IACxB,kBAAkB,MAAM,wBAAwB;AAAA,IAChD,WAAW,aAAa;AAAA,IACxB,cAAc,aAAa;AAAA,IAC3B,qBAAqB,aAAa,uBAAuB;AAAA,EAC3D,CAAC;AAED,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,WAAW,MAAM;AAE9C,SAAO,EAAE,QAAQ,SAAS,YAAY,MAAM,KAAK;AACnD;AAIA,SAAS,aAAa,MAAoD;AACxE,QAAM,OAA2C,CAAC;AAClD,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,MAAM,WAAW,IAAI,GAAG;AAC1B,YAAM,MAAM,MAAM,MAAM,CAAC;AACzB,YAAM,OAAO,KAAK,IAAI,CAAC;AACvB,UAAI,QAAQ,CAAC,KAAK,WAAW,IAAI,GAAG;AAClC,aAAK,GAAG,IAAI;AACZ;AAAA,MACF,OAAO;AACL,aAAK,GAAG,IAAI;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,QAAQ,OAAiB,QAAQ,KAAK,MAAM,CAAC,GAAkB;AACnF,QAAM,OAAO,aAAa,IAAI;AAE9B,MAAI,KAAK,MAAM;AACb,YAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAoBf;AACG,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B,YAAY,KAAK;AAAA,IACjB,MAAM,KAAK;AAAA,IACX,MAAM,KAAK,OAAO,SAAS,KAAK,MAAM,EAAE,IAAI;AAAA,IAC5C,WAAW,KAAK,YAAY;AAAA,EAC9B,CAAC;AAED,UAAQ,IAAI,qCAAqC,OAAO,IAAI,IAAI,OAAO,IAAI,EAAE;AAG7E,QAAM,WAAW,OAAO,WAAmB;AACzC,YAAQ,IAAI;AAAA,WAAc,MAAM,oBAAoB;AACpD,UAAM,OAAO,WAAW,KAAK;AAC7B,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,GAAG,UAAU,MAAM,SAAS,QAAQ,CAAC;AAC7C,UAAQ,GAAG,WAAW,MAAM,SAAS,SAAS,CAAC;AACjD;AAKA,IACE,QAAQ,KAAK,CAAC,MACb,mDAAmD,KAAK,QAAQ,KAAK,CAAC,CAAC,KACvE,mDAAmD,KAAK,QAAQ,KAAK,CAAC,CAAC,KACvE,QAAQ,KAAK,CAAC,EAAE,SAAS,eAAe,KACxC,QAAQ,KAAK,CAAC,EAAE,SAAS,eAAe,IACzC;AACA,UAAQ,EAAE,MAAM,CAAC,QAAQ;AACvB,YAAQ,OAAO,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACnF,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":[]}