@x12i/static-memorix-explorer-api 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -10,6 +10,27 @@ See [guides/demo-app.md](./guides/demo-app.md) for the full walkthrough.
10
10
 
11
11
  ## Quick start
12
12
 
13
+ Install once, then run one command:
14
+
15
+ ```bash
16
+ npm install --global @x12i/static-memorix-explorer-api
17
+ memorix-explorer
18
+ ```
19
+
20
+ Then open **http://localhost:5030/demo**. The command prints the Demo, API,
21
+ Health, and JSON paths when it starts. No database or additional configuration
22
+ is required.
23
+
24
+ Useful options:
25
+
26
+ ```bash
27
+ memorix-explorer --port 8080
28
+ memorix-explorer --help
29
+ memorix-explorer --version
30
+ ```
31
+
32
+ ### Run from this repository
33
+
13
34
  ```bash
14
35
  npm install
15
36
  npm run build
@@ -38,7 +59,7 @@ npx @x12i/static-memorix-explorer-api
38
59
  Or, after a global install:
39
60
 
40
61
  ```bash
41
- static-memorix-explorer-api --port 5030 --host 127.0.0.1
62
+ memorix-explorer
42
63
  ```
43
64
 
44
65
  Override the port from the shell:
@@ -121,7 +142,9 @@ The mock server accepts the following identity keys across fetch/write engines:
121
142
  - `memoryId` — memory
122
143
 
123
144
  Endpoints like `/records/full` and `/records/content` require exactly one identity key
124
- per request. Memory collections are keyed by `memoryId` and stored in `./mocks/data/memory/memory.json`.
145
+ per request. Memory collections are keyed by `memoryId`; the unprefixed
146
+ `./mocks/data/memory/memory.json` is the seed and routed writes use the active
147
+ Catalox-prefixed filename.
125
148
 
126
149
  ## Snapshot alias translation
127
150
 
@@ -184,7 +207,7 @@ Full implementation walkthrough: [guides/demo-app.md](./guides/demo-app.md).
184
207
  |-----|---------|-------|
185
208
  | `PORT` | `5030` | Listen port |
186
209
  | `HOST` | `0.0.0.0` | Listen host |
187
- | `MOCKS_DIR` | `<project>/mocks` | Fixture root |
210
+ | `MOCKS_DIR` | bundled `<package>/mocks` | Fixture root |
188
211
  | `DISK_FLUSH_DEBOUNCE_MS` | `300` | Debounce before disk flush |
189
212
  | `MEMORIX_ORG_ID` | `memorix` | Organization prefix for DB routing |
190
213
  | `MEMORIX_AGENT_ID` | `default-agent` | Agent prefix for catalox DB routing |
@@ -193,6 +216,11 @@ Full implementation walkthrough: [guides/demo-app.md](./guides/demo-app.md).
193
216
  | `MEMORIX_EXPLORER_ENABLE_PIPELINE_WRITES` | `false` | Allow pipeline writes |
194
217
  | `MEMORIX_EXPLORER_ENABLE_REGISTRY_WRITES` | `false` | Allow registry writes |
195
218
 
219
+ For mutation-capable installed deployments, set `MOCKS_DIR` to a writable,
220
+ persistent directory. Routing configuration is process-scoped and resolved on
221
+ first import; use one process per organization/agent context and one writer per
222
+ prefix.
223
+
196
224
  ## DB Router Simulation
197
225
 
198
226
  The mock server does not connect to MongoDB, but it **tricks the frontend** into
@@ -253,6 +281,8 @@ The mock's querying and writing engines now support `target=memory` and
253
281
  and `knowledgeId`.
254
282
 
255
283
  - Folder structure: `./mocks/data/memory/memory.json` (seeded with example memories).
284
+ - Runtime memory writes persist to the active
285
+ `<agent>-memorix-catalox--memory.json` route.
256
286
  - Inventory engine accepts `target=memory` and includes memory collections in
257
287
  the simulated `inventorySummary`.
258
288
  - Records engine accepts `target=memory` and supports `memoryId` for fetching.
@@ -269,6 +299,22 @@ and `knowledgeId`.
269
299
 
270
300
  ## Release history
271
301
 
302
+ ### v1.3.2
303
+ - Added the short `memorix-explorer` CLI command.
304
+ - Added `--help`/`-h` and `--version`/`-v` commands.
305
+ - Startup now prints clickable Demo, API, Health, and JSON locations.
306
+ - Reworked Quick Start documentation around install-once, run-one-command use.
307
+
308
+ ### v1.3.1
309
+ - Rejected unsafe routing IDs, object types, and metadata keys before path use.
310
+ - Changed malformed and empty JSON handling from silent fallback to explicit
311
+ startup failure, preventing accidental data loss.
312
+ - Made JSON persistence atomic through same-directory temporary-file renames.
313
+ - Made list deletion restart-safe with routed tombstones.
314
+ - Fixed installed npm CLI execution through symlink-aware entry detection.
315
+ - Added packed-install, restart, traversal, corruption, and atomic-write checks.
316
+ - Documented writable `MOCKS_DIR`, process isolation, and single-writer limits.
317
+
272
318
  ### v1.3.0
273
319
  - Added database-routing parity through deterministic JSON filename prefixes.
274
320
  - Added separate entity, event, and Catalox/metadata prefix resolution.
package/dist/config.d.ts CHANGED
@@ -5,6 +5,7 @@ export declare const DATA_DIR: string;
5
5
  export declare const PORT: number;
6
6
  export declare const HOST: string;
7
7
  export declare const DISK_FLUSH_DEBOUNCE_MS: number;
8
+ export declare function validateRouteId(value: string, variableName?: string): string;
8
9
  export declare const MEMORIX_ORG_ID: string;
9
10
  export declare const MEMORIX_AGENT_ID: string;
10
11
  export declare const MEMORIX_DEPLOYMENT_PROFILE: string;
package/dist/config.js CHANGED
@@ -20,13 +20,23 @@ function resolveOrgId() {
20
20
  if ((process.env.MEMORIX_DEPLOYMENT_PROFILE || "").toLowerCase() === "ebook") {
21
21
  return "ebooks";
22
22
  }
23
- return process.env.MEMORIX_ORG_ID || "memorix";
23
+ return validateRouteId(process.env.MEMORIX_ORG_ID || "memorix", "MEMORIX_ORG_ID");
24
24
  }
25
25
  function resolveAgentId() {
26
26
  if ((process.env.MEMORIX_DEPLOYMENT_PROFILE || "").toLowerCase() === "ebook") {
27
27
  return "ebooks";
28
28
  }
29
- return process.env.MEMORIX_AGENT_ID || "default-agent";
29
+ return validateRouteId(process.env.MEMORIX_AGENT_ID || "default-agent", "MEMORIX_AGENT_ID");
30
+ }
31
+ export function validateRouteId(value, variableName = "route ID") {
32
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) ||
33
+ value.includes("--") ||
34
+ value === "." ||
35
+ value === "..") {
36
+ throw Object.assign(new Error(`${variableName} must contain only letters, numbers, dot, underscore, or hyphen; ` +
37
+ `it must not contain path separators or the reserved "--" delimiter`), { statusCode: 400 });
38
+ }
39
+ return value;
30
40
  }
31
41
  export const MEMORIX_ORG_ID = resolveOrgId();
32
42
  export const MEMORIX_AGENT_ID = resolveAgentId();
@@ -62,6 +72,12 @@ export function buildMetadataFilePrefix() {
62
72
  return buildCataloxDbString();
63
73
  }
64
74
  export function prefixedJsonFilename(prefix, basename) {
75
+ validateRouteId(prefix, "file routing prefix");
76
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(basename) || basename.includes("--")) {
77
+ throw Object.assign(new Error(`Unsafe JSON fixture basename: ${basename}`), {
78
+ statusCode: 400,
79
+ });
80
+ }
65
81
  return `${prefix}--${basename}.json`;
66
82
  }
67
83
  // ---------------------------------------------------------------------------
@@ -228,7 +228,8 @@ export async function registerRoutes(app) {
228
228
  return { ok: false, error: "Metadata writes are disabled" };
229
229
  }
230
230
  const { listId } = request.params;
231
- store.setMeta(`lists/${listId}`, undefined, false);
231
+ store.deleteMeta(`lists/${listId}`);
232
+ await store.flushAll();
232
233
  return { ok: true, deleted: listId };
233
234
  });
234
235
  // ---------- F. Narratives Engine ----------
package/dist/server.d.ts CHANGED
@@ -5,7 +5,10 @@ export interface StartServerOptions {
5
5
  logger?: boolean;
6
6
  installSignalHandlers?: boolean;
7
7
  }
8
+ export declare const CLI_HELP = "Memorix Explorer static server\n\nUsage:\n memorix-explorer [options]\n\nOptions:\n --port <number> Port to listen on (default: 5030)\n --host <address> Host to bind (default: 0.0.0.0)\n -h, --help Show this help\n -v, --version Show the installed version\n\nEnvironment:\n MOCKS_DIR Writable JSON fixture directory\n MEMORIX_ORG_ID Organization routing ID\n MEMORIX_AGENT_ID Agent/Catalox routing ID\n\nExample:\n memorix-explorer --port 5030\n";
9
+ export declare function getPackageVersion(): string;
8
10
  export declare function parseCliArgs(args: string[]): StartServerOptions;
11
+ export declare function isCliEntrypoint(argvPath: string | undefined, moduleUrl: string): boolean;
9
12
  /** Build a loaded Fastify instance without opening a network port. */
10
13
  export declare function buildServer(options?: Pick<StartServerOptions, "logger">): Promise<import("fastify").FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
11
14
  /** Build and listen. Options override PORT/HOST environment configuration. */
package/dist/server.js CHANGED
@@ -1,10 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import Fastify from "fastify";
3
+ import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { store } from "./storage/InMemoryStore.js";
6
7
  import { registerRoutes } from "./routes/index.js";
7
8
  import { PORT, HOST, MOCKS_DIR } from "./config.js";
9
+ export const CLI_HELP = `Memorix Explorer static server
10
+
11
+ Usage:
12
+ memorix-explorer [options]
13
+
14
+ Options:
15
+ --port <number> Port to listen on (default: 5030)
16
+ --host <address> Host to bind (default: 0.0.0.0)
17
+ -h, --help Show this help
18
+ -v, --version Show the installed version
19
+
20
+ Environment:
21
+ MOCKS_DIR Writable JSON fixture directory
22
+ MEMORIX_ORG_ID Organization routing ID
23
+ MEMORIX_AGENT_ID Agent/Catalox routing ID
24
+
25
+ Example:
26
+ memorix-explorer --port 5030
27
+ `;
28
+ export function getPackageVersion() {
29
+ const packageFile = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
30
+ return JSON.parse(fs.readFileSync(packageFile, "utf8")).version;
31
+ }
8
32
  export function parseCliArgs(args) {
9
33
  const options = {};
10
34
  for (let index = 0; index < args.length; index++) {
@@ -31,6 +55,16 @@ export function parseCliArgs(args) {
31
55
  }
32
56
  return options;
33
57
  }
58
+ export function isCliEntrypoint(argvPath, moduleUrl) {
59
+ if (!argvPath)
60
+ return false;
61
+ try {
62
+ return fs.realpathSync(path.resolve(argvPath)) === fileURLToPath(moduleUrl);
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
34
68
  /** Build a loaded Fastify instance without opening a network port. */
35
69
  export async function buildServer(options = {}) {
36
70
  const app = Fastify({ logger: options.logger ?? false });
@@ -59,9 +93,13 @@ export async function startServer(options = {}) {
59
93
  await app.listen({ port, host });
60
94
  const address = app.server.address();
61
95
  const boundPort = typeof address === "object" && address ? address.port : port;
96
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
62
97
  // eslint-disable-next-line no-console
63
- console.log(`[mock-memorix-explorer-api] listening on ${host}:${boundPort}`);
64
- console.log(`[mock-memorix-explorer-api] mocks dir: ${MOCKS_DIR}`);
98
+ console.log(`\nMemorix Explorer is ready`);
99
+ console.log(` Demo: http://${displayHost}:${boundPort}/demo`);
100
+ console.log(` API: http://${displayHost}:${boundPort}/api/explorer`);
101
+ console.log(` Health: http://${displayHost}:${boundPort}/health`);
102
+ console.log(` JSON: ${MOCKS_DIR}\n`);
65
103
  if (options.installSignalHandlers ?? true) {
66
104
  const shutdown = async () => {
67
105
  console.log("\n[mock-memorix-explorer-api] flushing + shutting down");
@@ -74,13 +112,19 @@ export async function startServer(options = {}) {
74
112
  }
75
113
  return app;
76
114
  }
77
- const isCliEntry = process.argv[1]
78
- ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
79
- : false;
115
+ const isCliEntry = isCliEntrypoint(process.argv[1], import.meta.url);
80
116
  if (isCliEntry) {
81
- Promise.resolve().then(() => startServer(parseCliArgs(process.argv.slice(2)))).catch((err) => {
82
- // eslint-disable-next-line no-console
83
- console.error(err);
84
- process.exit(1);
85
- });
117
+ const args = process.argv.slice(2);
118
+ if (args.includes("--help") || args.includes("-h")) {
119
+ console.log(CLI_HELP);
120
+ }
121
+ else if (args.includes("--version") || args.includes("-v")) {
122
+ console.log(getPackageVersion());
123
+ }
124
+ else
125
+ Promise.resolve().then(() => startServer(parseCliArgs(args))).catch((err) => {
126
+ // eslint-disable-next-line no-console
127
+ console.error(err);
128
+ process.exit(1);
129
+ });
86
130
  }
@@ -12,6 +12,8 @@ export declare class InMemoryStore {
12
12
  private loadDataCollections;
13
13
  getMeta<T = any>(key: string): T | undefined;
14
14
  setMeta(key: string, data: any, persist?: boolean): void;
15
+ /** Persist a routed tombstone so an unprefixed seed does not reappear. */
16
+ deleteMeta(key: string): void;
15
17
  getMetaKeys(): string[];
16
18
  getCollection(objectType: string, ct: ContentType): any[];
17
19
  setCollection(objectType: string, ct: ContentType, data: any[]): void;
@@ -1,11 +1,28 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { CONTENT_TYPES } from "../types.js";
4
- import { MOCKS_DIR, METADATA_DIR, DATA_DIR, buildDataFilePrefix, buildMetadataFilePrefix, prefixedJsonFilename, } from "../config.js";
4
+ import { MOCKS_DIR, METADATA_DIR, DATA_DIR, buildDataFilePrefix, buildMetadataFilePrefix, prefixedJsonFilename, validateRouteId, } from "../config.js";
5
5
  import { validateRecordArray } from "../engine/identity.js";
6
6
  import { isDir, listJsonFiles, readJson, writeJson, basenameNoExt, } from "./fs.js";
7
7
  import { DISK_FLUSH_DEBOUNCE_MS } from "../config.js";
8
8
  const DIRTY = Symbol("dirty");
9
+ function routedMetadataPath(key) {
10
+ const parts = key.split("/");
11
+ if (parts.length === 1 && parts[0] === "agents") {
12
+ return path.join(METADATA_DIR, prefixedJsonFilename(buildMetadataFilePrefix(), "agents"));
13
+ }
14
+ if (parts.length !== 2) {
15
+ throw Object.assign(new Error(`Unsafe metadata key: ${key}`), { statusCode: 400 });
16
+ }
17
+ const [section, name] = parts;
18
+ if (!["object-types", "lists", "write-descriptors", "narratives"].includes(section)) {
19
+ throw Object.assign(new Error(`Unsupported metadata section: ${section}`), {
20
+ statusCode: 400,
21
+ });
22
+ }
23
+ validateRouteId(name, "metadata fixture name");
24
+ return path.join(METADATA_DIR, section, prefixedJsonFilename(buildMetadataFilePrefix(), name));
25
+ }
9
26
  export class InMemoryStore {
10
27
  collections = new Map(); // key: `${objectType}/${contentType}`
11
28
  metadata = new Map();
@@ -108,7 +125,7 @@ export class InMemoryStore {
108
125
  }
109
126
  else {
110
127
  this.metadata.set(key, {
111
- path: path.join(METADATA_DIR, `${path.dirname(key)}/${prefixedJsonFilename(buildMetadataFilePrefix(), path.basename(key))}`),
128
+ path: routedMetadataPath(key),
112
129
  data,
113
130
  [DIRTY]: true,
114
131
  });
@@ -116,11 +133,28 @@ export class InMemoryStore {
116
133
  if (persist)
117
134
  this.scheduleMetaFlush(key);
118
135
  }
136
+ /** Persist a routed tombstone so an unprefixed seed does not reappear. */
137
+ deleteMeta(key) {
138
+ const existing = this.metadata.get(key);
139
+ if (existing) {
140
+ existing.data = null;
141
+ existing[DIRTY] = true;
142
+ }
143
+ else {
144
+ this.metadata.set(key, {
145
+ path: routedMetadataPath(key),
146
+ data: null,
147
+ [DIRTY]: true,
148
+ });
149
+ }
150
+ this.scheduleMetaFlush(key);
151
+ }
119
152
  getMetaKeys() {
120
153
  return [...this.metadata.keys()];
121
154
  }
122
155
  // ---- Collection access ----
123
156
  getCollection(objectType, ct) {
157
+ validateRouteId(objectType, "object type");
124
158
  const key = `${objectType}/${ct}`;
125
159
  const c = this.collections.get(key);
126
160
  if (!c) {
@@ -134,6 +168,7 @@ export class InMemoryStore {
134
168
  return c.data;
135
169
  }
136
170
  setCollection(objectType, ct, data) {
171
+ validateRouteId(objectType, "object type");
137
172
  const key = `${objectType}/${ct}`;
138
173
  const c = this.collections.get(key);
139
174
  if (c) {
@@ -15,16 +15,31 @@ export function readJson(p, fallback) {
15
15
  try {
16
16
  const raw = fs.readFileSync(p, "utf8");
17
17
  if (!raw.trim())
18
- return fallback;
18
+ throw new Error("file is empty");
19
19
  return JSON.parse(raw);
20
20
  }
21
- catch {
22
- return fallback;
21
+ catch (error) {
22
+ if (error.code === "ENOENT")
23
+ return fallback;
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ throw new Error(`Failed to read JSON fixture ${p}: ${message}`);
23
26
  }
24
27
  }
25
28
  export function writeJson(p, data) {
26
29
  ensureDir(path.dirname(p));
27
- fs.writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf8");
30
+ const serialized = JSON.stringify(data, null, 2);
31
+ if (serialized === undefined) {
32
+ throw new Error(`Cannot serialize undefined JSON fixture: ${p}`);
33
+ }
34
+ const temp = path.join(path.dirname(p), `.${path.basename(p)}.${process.pid}.tmp`);
35
+ try {
36
+ fs.writeFileSync(temp, serialized + "\n", "utf8");
37
+ fs.renameSync(temp, p);
38
+ }
39
+ finally {
40
+ if (fs.existsSync(temp))
41
+ fs.unlinkSync(temp);
42
+ }
28
43
  }
29
44
  export function listJsonFiles(dir) {
30
45
  if (!isDir(dir))
@@ -324,7 +324,9 @@ with fields for title, area, status, priority, and assignee. On submit:
324
324
  descriptor for the chosen area (`product-task-write`, etc.).
325
325
  3. The server validates the payload against the descriptor's JSON schema.
326
326
  4. On success, the record is appended to the in-memory collection and the
327
- debounced flush writes it to `mocks/data/<area>/snapshots.json` within 300 ms.
327
+ flush writes it to
328
+ `mocks/data/<area>/<org>-memorix-entities--snapshots.json`. If that routed
329
+ file does not exist yet, the unprefixed `snapshots.json` is used as seed data.
328
330
  5. The UI unshifts the returned record into `state.tasks`, selects it, and
329
331
  re-renders.
330
332
 
@@ -385,8 +387,9 @@ scale. For higher volume you'd model comments as `events` records keyed by
385
387
  ## Why each area is a separate collection
386
388
 
387
389
  We could have put all tasks in one `tasks` collection with an `area` field.
388
- The demo deliberately splits them across `product/snapshots.json`,
389
- `marketing/snapshots.json`, and `admin/snapshots.json` to demonstrate:
390
+ The demo deliberately splits the seed fixtures across `product/snapshots.json`,
391
+ `marketing/snapshots.json`, and `admin/snapshots.json`; runtime mutations go to
392
+ their routed, prefixed counterparts. This demonstrates:
390
393
 
391
394
  1. **Multi-entity applications** — typical real apps have more than one object
392
395
  type. Showing 3 object types is more representative than 1.
@@ -29,6 +29,10 @@ the routing matrix is:
29
29
  4. Unprefixed seed files are never overwritten while routed mode is active.
30
30
  5. Files belonging to a different prefix are ignored by the active process.
31
31
 
32
+ Deleting a metadata document writes JSON `null` as a routed tombstone. This is
33
+ intentional: removing only the routed file would cause the unprefixed seed to
34
+ reappear on the next restart.
35
+
32
36
  This makes onboarding migration-safe: existing files such as
33
37
  `product/snapshots.json` continue to seed a new tenant, while the first write
34
38
  creates `product/neo-memorix-entities--snapshots.json`. Starting with another
@@ -37,6 +41,20 @@ organization ID selects a different file and therefore different state.
37
41
  The delimiter `--` is reserved. Do not use it inside ordinary, unprefixed
38
42
  fixture basenames.
39
43
 
44
+ Routing IDs are validated before any files are opened. Organization IDs,
45
+ agent IDs, object types, and metadata names may contain letters, numbers,
46
+ dot, underscore, and hyphen, but cannot contain path separators, `..`, or the
47
+ reserved `--` delimiter.
48
+
49
+ Malformed or empty JSON stops startup with the affected path in the error;
50
+ it is never silently converted to an empty collection. Writes use a temporary
51
+ file and same-directory rename so readers never observe a partially written
52
+ JSON document.
53
+
54
+ Routing is process-scoped. Run one server process per organization/agent route,
55
+ and use only one writer process for a given prefix. Two processes writing the
56
+ same routed file concurrently use last-writer-wins semantics.
57
+
40
58
  ### Inspect the active route
41
59
 
42
60
  ```bash
@@ -12,9 +12,9 @@ from in-memory state backed by JSON files on disk.
12
12
  npm install -g @x12i/static-memorix-explorer-api
13
13
  ```
14
14
 
15
- This makes the `mock-memorix-explorer-api` command available globally.
16
- The preferred command name is `static-memorix-explorer-api`; the older
17
- `mock-memorix-explorer-api` name remains as a compatibility alias.
15
+ This makes the short `memorix-explorer` command available globally.
16
+ `static-memorix-explorer-api` and `mock-memorix-explorer-api` remain available
17
+ as compatibility aliases.
18
18
 
19
19
  ### From source (git clone)
20
20
 
@@ -45,11 +45,19 @@ iteration, but not suitable for distribution.
45
45
  ### As a global CLI
46
46
 
47
47
  ```bash
48
- static-memorix-explorer-api
48
+ memorix-explorer
49
49
  ```
50
50
 
51
51
  After `npm install -g`, the CLI starts the server with default settings. The
52
- mocks directory defaults to a `mocks/` folder relative to the installed package.
52
+ mocks directory defaults to the bundled fixtures, and the CLI prints the URLs
53
+ to open:
54
+
55
+ ```text
56
+ Memorix Explorer is ready
57
+ Demo: http://localhost:5030/demo
58
+ API: http://localhost:5030/api/explorer
59
+ Health: http://localhost:5030/health
60
+ ```
53
61
 
54
62
  ### With `npx` (no global install)
55
63
 
@@ -61,8 +69,15 @@ Choose a port with a CLI option or environment variable. CLI options take
61
69
  precedence:
62
70
 
63
71
  ```bash
64
- static-memorix-explorer-api --port 5030 --host 127.0.0.1
65
- PORT=5030 HOST=127.0.0.1 static-memorix-explorer-api
72
+ memorix-explorer --port 5030 --host 127.0.0.1
73
+ PORT=5030 HOST=127.0.0.1 memorix-explorer
74
+ ```
75
+
76
+ Built-in help and version output do not start the server:
77
+
78
+ ```bash
79
+ memorix-explorer --help
80
+ memorix-explorer --version
66
81
  ```
67
82
 
68
83
  ### From application code
@@ -104,7 +119,19 @@ All configuration is via environment variables. Defaults work out of the box.
104
119
  | --------- | ----------- | ------------------------------ |
105
120
  | `PORT` | `5030` | HTTP listen port |
106
121
  | `HOST` | `0.0.0.0` | HTTP listen host |
107
- | `MOCKS_DIR` | `<project>/mocks` | Root directory for JSON fixtures |
122
+ | `MOCKS_DIR` | bundled `<package>/mocks` | Root directory for JSON fixtures |
123
+
124
+ `MOCKS_DIR` must be writable if you use mutation endpoints. For installed or
125
+ containerized deployments, point it at a persistent application-owned volume
126
+ rather than relying on the package's bundled seed directory:
127
+
128
+ ```bash
129
+ MOCKS_DIR=/var/lib/my-app/memorix-mocks memorix-explorer --port 5030
130
+ ```
131
+
132
+ Configuration is resolved when the package is imported. In programmatic usage,
133
+ set `process.env.MOCKS_DIR`, `MEMORIX_ORG_ID`, and `MEMORIX_AGENT_ID` before the
134
+ first dynamic import, and run separate processes for separate routing contexts.
108
135
 
109
136
  ### DB Router Simulation
110
137
 
@@ -150,7 +177,7 @@ default.
150
177
  Set to `true` or `1` to enable:
151
178
 
152
179
  ```bash
153
- MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true mock-memorix-explorer-api
180
+ MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true memorix-explorer
154
181
  ```
155
182
 
156
183
  ### Disk flush
@@ -168,7 +195,7 @@ MOCKS_DIR=/path/to/my/fixtures \
168
195
  MEMORIX_ORG_ID=neo \
169
196
  MEMORIX_AGENT_ID=neo-agent \
170
197
  MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true \
171
- mock-memorix-explorer-api
198
+ memorix-explorer
172
199
  ```
173
200
 
174
201
  ## Health checks
@@ -261,5 +261,5 @@ when the corresponding feature flag is not enabled. Set the environment
261
261
  variable to `true` or `1` to allow writes:
262
262
 
263
263
  ```bash
264
- MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true mock-memorix-explorer-api
264
+ MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true memorix-explorer
265
265
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/static-memorix-explorer-api",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Static mock server providing full API parity for the Memorix Explorer API.",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -15,6 +15,7 @@
15
15
  }
16
16
  },
17
17
  "bin": {
18
+ "memorix-explorer": "dist/server.js",
18
19
  "static-memorix-explorer-api": "dist/server.js",
19
20
  "mock-memorix-explorer-api": "dist/server.js"
20
21
  },