@x12i/static-memorix 2.0.0

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +431 -0
  3. package/dist/cli.d.ts +17 -0
  4. package/dist/cli.js +278 -0
  5. package/dist/config.d.ts +39 -0
  6. package/dist/config.js +109 -0
  7. package/dist/engine/aliases.d.ts +32 -0
  8. package/dist/engine/aliases.js +74 -0
  9. package/dist/engine/associations.d.ts +26 -0
  10. package/dist/engine/associations.js +60 -0
  11. package/dist/engine/identity.d.ts +22 -0
  12. package/dist/engine/identity.js +50 -0
  13. package/dist/engine/inventory.d.ts +29 -0
  14. package/dist/engine/inventory.js +133 -0
  15. package/dist/engine/lists.d.ts +18 -0
  16. package/dist/engine/lists.js +62 -0
  17. package/dist/engine/narratives.d.ts +16 -0
  18. package/dist/engine/narratives.js +67 -0
  19. package/dist/engine/objectTypes.d.ts +11 -0
  20. package/dist/engine/objectTypes.js +71 -0
  21. package/dist/engine/query.d.ts +38 -0
  22. package/dist/engine/query.js +191 -0
  23. package/dist/engine/records.d.ts +23 -0
  24. package/dist/engine/records.js +152 -0
  25. package/dist/engine/snapshots.d.ts +17 -0
  26. package/dist/engine/snapshots.js +110 -0
  27. package/dist/engine/write.d.ts +30 -0
  28. package/dist/engine/write.js +163 -0
  29. package/dist/routes/helpers.d.ts +4 -0
  30. package/dist/routes/helpers.js +29 -0
  31. package/dist/routes/index.d.ts +2 -0
  32. package/dist/routes/index.js +402 -0
  33. package/dist/server.d.ts +15 -0
  34. package/dist/server.js +130 -0
  35. package/dist/storage/InMemoryStore.d.ts +31 -0
  36. package/dist/storage/InMemoryStore.js +242 -0
  37. package/dist/storage/fs.d.ts +6 -0
  38. package/dist/storage/fs.js +54 -0
  39. package/dist/types.d.ts +88 -0
  40. package/dist/types.js +37 -0
  41. package/guides/demo-app.md +527 -0
  42. package/guides/managing-json-files.md +384 -0
  43. package/guides/running-the-service.md +269 -0
  44. package/guides/using-the-api.md +265 -0
  45. package/mocks/data/admin/snapshots.json +52 -0
  46. package/mocks/data/assets/analysis.json +14 -0
  47. package/mocks/data/assets/decisions.json +14 -0
  48. package/mocks/data/assets/events.json +4 -0
  49. package/mocks/data/assets/snapshots.json +49 -0
  50. package/mocks/data/findings/analysis.json +4 -0
  51. package/mocks/data/findings/decisions.json +4 -0
  52. package/mocks/data/findings/events.json +3 -0
  53. package/mocks/data/findings/snapshots.json +21 -0
  54. package/mocks/data/marketing/snapshots.json +75 -0
  55. package/mocks/data/memory/memory.json +31 -0
  56. package/mocks/data/product/snapshots.json +84 -0
  57. package/mocks/data/system/inventory.json +8 -0
  58. package/mocks/data/users/snapshots.json +5 -0
  59. package/mocks/demo.html +828 -0
  60. package/mocks/metadata/agents.json +7 -0
  61. package/mocks/metadata/lists/assets-default.json +22 -0
  62. package/mocks/metadata/lists/backlog.json +10 -0
  63. package/mocks/metadata/lists/findings-default.json +9 -0
  64. package/mocks/metadata/lists/my-plate.json +10 -0
  65. package/mocks/metadata/lists/this-week.json +10 -0
  66. package/mocks/metadata/narratives/admin.json +7 -0
  67. package/mocks/metadata/narratives/assets.json +17 -0
  68. package/mocks/metadata/narratives/findings.json +7 -0
  69. package/mocks/metadata/narratives/marketing.json +17 -0
  70. package/mocks/metadata/narratives/product.json +12 -0
  71. package/mocks/metadata/object-types/admin.json +6 -0
  72. package/mocks/metadata/object-types/assets.json +9 -0
  73. package/mocks/metadata/object-types/findings.json +8 -0
  74. package/mocks/metadata/object-types/marketing.json +6 -0
  75. package/mocks/metadata/object-types/memory.json +6 -0
  76. package/mocks/metadata/object-types/product.json +6 -0
  77. package/mocks/metadata/object-types/users.json +13 -0
  78. package/mocks/metadata/write-descriptors/admin-task-write.json +20 -0
  79. package/mocks/metadata/write-descriptors/assets-analysis-write.json +14 -0
  80. package/mocks/metadata/write-descriptors/marketing-task-write.json +20 -0
  81. package/mocks/metadata/write-descriptors/memory-write.json +14 -0
  82. package/mocks/metadata/write-descriptors/product-task-write.json +20 -0
  83. package/package.json +67 -0
@@ -0,0 +1,4 @@
1
+ import type { FastifyRequest } from "fastify";
2
+ import type { ExplorerQueryParams } from "../types.js";
3
+ export declare function qp(request: FastifyRequest): ExplorerQueryParams;
4
+ export declare function str(v: unknown, fallback?: string): string;
@@ -0,0 +1,29 @@
1
+ export function qp(request) {
2
+ const q = (request.query || {});
3
+ return {
4
+ filter: q.filter,
5
+ sort: q.sort,
6
+ searchText: q.searchText,
7
+ q: q.q,
8
+ limit: q.limit,
9
+ offset: q.offset,
10
+ skip: q.skip,
11
+ includeTotal: q.includeTotal === "1" || q.includeTotal === "true",
12
+ include: q.include,
13
+ mode: q.mode,
14
+ listId: q.listId,
15
+ entityName: q.entityName,
16
+ sourceLens: q.sourceLens,
17
+ target: q.target,
18
+ includeExactCounts: q.includeExactCounts === "1" || q.includeExactCounts === "true",
19
+ includeInventory: q.includeInventory === "1" || q.includeInventory === "true",
20
+ memoryId: q.memoryId,
21
+ recordId: q.recordId,
22
+ entityId: q.entityId,
23
+ eventId: q.eventId,
24
+ knowledgeId: q.knowledgeId,
25
+ };
26
+ }
27
+ export function str(v, fallback = "") {
28
+ return v === undefined || v === null ? fallback : String(v);
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export declare function registerRoutes(app: FastifyInstance): Promise<void>;
@@ -0,0 +1,402 @@
1
+ import { store } from "../storage/InMemoryStore.js";
2
+ import { buildMemorixDbString, buildCataloxDbString, buildDataFilePrefix, buildMetadataFilePrefix, metadataWritesEnabled, } from "../config.js";
3
+ import { listObjectTypes } from "../engine/inventory.js";
4
+ import { computeInventory, computeInventorySummary, computeInventoryIssues, computeGraph, } from "../engine/inventory.js";
5
+ import { qp, str } from "./helpers.js";
6
+ import { getSnapshotNormalized, getSnapshotAssociated, getSnapshotAssociatedProperty, getAssociatedProperties, } from "../engine/snapshots.js";
7
+ import { getRecordsCollection, getRecordsFull, getRecordsItem, getRawCollection, getRawItem, getWorkspaceRecords, getRecordContent, } from "../engine/records.js";
8
+ import { listListDescriptors, getListDescriptor, executeListDescriptor, suggestListExtensions, } from "../engine/lists.js";
9
+ import { getMergedNarratives, getRawNarratives, queryNarrativeRecords, } from "../engine/narratives.js";
10
+ import { computeRootPropertyCatalog } from "../engine/objectTypes.js";
11
+ import { loadWriteDescriptor, validateInputSchema, executeOperation, } from "../engine/write.js";
12
+ import { planAssociations, applyAssociations, verifyAssociations, } from "../engine/associations.js";
13
+ import { MOCKS_DIR } from "../config.js";
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ export async function registerRoutes(app) {
17
+ // ---------- A. Health & System Diagnostics ----------
18
+ app.get("/health", async () => ({ ok: true }));
19
+ app.get("/api/explorer/health", async (request) => {
20
+ const includeInventory = qp(request).includeInventory;
21
+ const objectTypes = listObjectTypes();
22
+ const base = {
23
+ ok: true,
24
+ mongoUriConfigured: true,
25
+ discoverySample: objectTypes,
26
+ memorixDb: buildMemorixDbString(),
27
+ cataloxDb: buildCataloxDbString(),
28
+ fileRouting: {
29
+ entitiesPrefix: buildDataFilePrefix("snapshots"),
30
+ eventsPrefix: buildDataFilePrefix("events"),
31
+ memoryPrefix: buildDataFilePrefix("memory"),
32
+ metadataPrefix: buildMetadataFilePrefix(),
33
+ },
34
+ };
35
+ if (includeInventory) {
36
+ const rows = computeInventory({ sourceLens: "catalox-first" });
37
+ const summary = computeInventorySummary(rows, { includeExactCounts: false });
38
+ return { ...base, inventorySummary: summary };
39
+ }
40
+ return base;
41
+ });
42
+ // ---------- B. Inventory & Graph Engine ----------
43
+ app.get("/api/explorer/inventory/collections", async (request) => {
44
+ const rows = computeInventory(qp(request));
45
+ return { rows, total: rows.length };
46
+ });
47
+ app.get("/api/explorer/inventory/summary", async (request) => {
48
+ const rows = computeInventory(qp(request));
49
+ const summary = computeInventorySummary(rows, qp(request));
50
+ return { summary, rows };
51
+ });
52
+ app.get("/api/explorer/inventory/issues", async (request) => {
53
+ const rows = computeInventory(qp(request));
54
+ const issues = computeInventoryIssues(rows);
55
+ return { issues, total: issues.length };
56
+ });
57
+ app.get("/api/explorer/inventory/graph", async (request) => {
58
+ const rows = computeInventory(qp(request));
59
+ return computeGraph(rows);
60
+ });
61
+ // ---------- C. Snapshots & Associated Data Pipeline ----------
62
+ app.get("/api/explorer/snapshots/:objectType/:recordId", async (request, reply) => {
63
+ const { objectType, recordId } = request.params;
64
+ const q = qp(request);
65
+ const result = getSnapshotNormalized(objectType, recordId, q.include);
66
+ if (!result)
67
+ return send404(reply, `Snapshot not found: ${objectType}/${recordId}`);
68
+ return result;
69
+ });
70
+ app.get("/api/explorer/snapshots/:objectType/:recordId/associated", async (request, reply) => {
71
+ const { objectType, recordId } = request.params;
72
+ const result = getSnapshotAssociated(objectType, recordId);
73
+ if (!result)
74
+ return send404(reply, `Snapshot not found: ${objectType}/${recordId}`);
75
+ return result;
76
+ });
77
+ app.get("/api/explorer/snapshots/:objectType/:recordId/associated/:propertyName", async (request, reply) => {
78
+ const { objectType, recordId, propertyName } = request.params;
79
+ const result = getSnapshotAssociatedProperty(objectType, recordId, propertyName);
80
+ if (result === undefined)
81
+ return send404(reply, `Associated property not found: ${propertyName}`);
82
+ return result;
83
+ });
84
+ app.get("/api/explorer/snapshots/:objectType/associated-properties", async (request) => {
85
+ const { objectType } = request.params;
86
+ return { objectType, properties: getAssociatedProperties(objectType) };
87
+ });
88
+ app.post("/api/explorer/associations/plan", async (request) => {
89
+ const { objectType } = (request.body || {});
90
+ return { plans: planAssociations(objectType) };
91
+ });
92
+ app.post("/api/explorer/associations/apply", async (request) => {
93
+ const { expectedPlanFingerprint, objectType } = (request.body || {});
94
+ return { results: applyAssociations(expectedPlanFingerprint, objectType) };
95
+ });
96
+ app.post("/api/explorer/associations/verify", async (request) => {
97
+ const { expectedPlanFingerprint, objectType } = (request.body || {});
98
+ return { results: verifyAssociations(expectedPlanFingerprint, objectType) };
99
+ });
100
+ // ---------- D. Records Engine ----------
101
+ app.get("/api/explorer/records/collection", async (request) => {
102
+ const q = qp(request);
103
+ return await getRecordsCollection(q);
104
+ });
105
+ app.get("/api/explorer/records/full", async (request) => {
106
+ const q = qp(request);
107
+ const entityName = str(q.entityName);
108
+ const recordId = str(request.query.recordId);
109
+ if (!entityName || !recordId)
110
+ return { error: "entityName and recordId are required" };
111
+ return getRecordsFull(entityName, recordId);
112
+ });
113
+ app.get("/api/explorer/records/item", async (request, reply) => {
114
+ const q = qp(request);
115
+ const entityName = str(q.entityName);
116
+ const recordId = str(request.query.recordId);
117
+ const itemDescriptor = request.query.itemDescriptor
118
+ ? JSON.parse(request.query.itemDescriptor)
119
+ : undefined;
120
+ const result = getRecordsItem(entityName, recordId, itemDescriptor);
121
+ if (!result)
122
+ return send404(reply, `Item not found: ${entityName}/${recordId}`);
123
+ return result;
124
+ });
125
+ app.get("/api/explorer/records/content", async (request, reply) => {
126
+ const q = qp(request);
127
+ const entityName = str(q.entityName) || (q.target === "memory" ? "memory" : "");
128
+ const contentType = request.query.contentType;
129
+ const identity = {};
130
+ if (q.recordId)
131
+ identity.recordId = q.recordId;
132
+ if (q.entityId)
133
+ identity.entityId = q.entityId;
134
+ if (q.eventId)
135
+ identity.eventId = q.eventId;
136
+ if (q.knowledgeId)
137
+ identity.knowledgeId = q.knowledgeId;
138
+ if (q.memoryId)
139
+ identity.memoryId = q.memoryId;
140
+ if (!contentType || Object.keys(identity).length === 0) {
141
+ return { ok: false, error: "contentType and at least one identity key required" };
142
+ }
143
+ const result = getRecordContent(entityName, contentType, identity);
144
+ if (!result)
145
+ return send404(reply, `Content not found: ${entityName}/${contentType}`);
146
+ return result;
147
+ });
148
+ app.get("/api/explorer/records/raw-collection", async (request) => {
149
+ const { entityName, collectionName } = request.query;
150
+ const name = collectionName || entityName;
151
+ return getRawCollection(str(entityName), str(name));
152
+ });
153
+ app.get("/api/explorer/records/raw-item", async (request, reply) => {
154
+ const { collectionName, recordId } = request.query;
155
+ const result = getRawItem(str(collectionName), str(recordId));
156
+ if (!result)
157
+ return send404(reply, `Raw item not found: ${collectionName}/${recordId}`);
158
+ return result;
159
+ });
160
+ app.get("/api/explorer/records/workspace", async (request) => {
161
+ return await getWorkspaceRecords(qp(request));
162
+ });
163
+ // ---------- E. Lists Subsystem ----------
164
+ app.get("/api/explorer/lists", async (request) => {
165
+ const { entityName } = request.query;
166
+ return { lists: listListDescriptors(entityName) };
167
+ });
168
+ app.get("/api/explorer/lists/:listId", async (request, reply) => {
169
+ const { listId } = request.params;
170
+ const desc = getListDescriptor(listId);
171
+ if (!desc)
172
+ return send404(reply, `List descriptor not found: ${listId}`);
173
+ return desc;
174
+ });
175
+ app.get("/api/explorer/lists/:listId/records", async (request, reply) => {
176
+ const { listId } = request.params;
177
+ const desc = getListDescriptor(listId);
178
+ if (!desc)
179
+ return send404(reply, `List descriptor not found: ${listId}`);
180
+ const result = await executeListDescriptor(desc, qp(request));
181
+ return result;
182
+ });
183
+ app.get("/api/explorer/lists/:listId/records-ws", async (request, reply) => {
184
+ const { listId } = request.params;
185
+ const desc = getListDescriptor(listId);
186
+ if (!desc)
187
+ return send404(reply, `List descriptor not found: ${listId}`);
188
+ const result = await executeListDescriptor(desc, qp(request));
189
+ return result;
190
+ });
191
+ app.get("/api/explorer/lists/suggest", async (request) => {
192
+ const { entityName } = request.query;
193
+ return suggestListExtensions(str(entityName));
194
+ });
195
+ app.post("/api/explorer/lists", async (request) => {
196
+ if (!metadataWritesEnabled()) {
197
+ return { ok: false, error: "Metadata writes are disabled" };
198
+ }
199
+ const body = (request.body || {});
200
+ const listId = body.listId || body.id;
201
+ if (!listId)
202
+ return { error: "listId required" };
203
+ store.setMeta(`lists/${listId}`, { ...body, listId });
204
+ return { ok: true, listId };
205
+ });
206
+ app.patch("/api/explorer/lists/:listId", async (request) => {
207
+ if (!metadataWritesEnabled()) {
208
+ return { ok: false, error: "Metadata writes are disabled" };
209
+ }
210
+ const { listId } = request.params;
211
+ const existing = getListDescriptor(listId) || {};
212
+ const updated = { ...existing, ...(request.body || {}), listId };
213
+ store.setMeta(`lists/${listId}`, updated);
214
+ return { ok: true, listId, descriptor: updated };
215
+ });
216
+ app.put("/api/explorer/lists/:listId/sort", async (request) => {
217
+ if (!metadataWritesEnabled()) {
218
+ return { ok: false, error: "Metadata writes are disabled" };
219
+ }
220
+ const { listId } = request.params;
221
+ const existing = getListDescriptor(listId) || {};
222
+ const updated = { ...existing, sort: (request.body || {}).sort, listId };
223
+ store.setMeta(`lists/${listId}`, updated);
224
+ return { ok: true, listId, descriptor: updated };
225
+ });
226
+ app.delete("/api/explorer/lists/:listId", async (request) => {
227
+ if (!metadataWritesEnabled()) {
228
+ return { ok: false, error: "Metadata writes are disabled" };
229
+ }
230
+ const { listId } = request.params;
231
+ store.deleteMeta(`lists/${listId}`);
232
+ await store.flushAll();
233
+ return { ok: true, deleted: listId };
234
+ });
235
+ // ---------- F. Narratives Engine ----------
236
+ app.get("/api/explorer/narratives", async () => {
237
+ return { narratives: getMergedNarratives() };
238
+ });
239
+ app.get("/api/explorer/narratives/:entity", async (request) => {
240
+ const { entity } = request.params;
241
+ return { entity, narratives: getMergedNarratives(entity) };
242
+ });
243
+ app.get("/api/explorer/narratives/:entity/raw", async (request) => {
244
+ const { entity } = request.params;
245
+ return { entity, raw: getRawNarratives(entity) };
246
+ });
247
+ app.get("/api/explorer/narratives/:entity/:key", async (request, reply) => {
248
+ const { entity, key } = request.params;
249
+ const merged = getMergedNarratives(entity);
250
+ const found = merged.find((n) => n.key === key);
251
+ if (!found)
252
+ return send404(reply, `Narrative not found: ${entity}/${key}`);
253
+ return found;
254
+ });
255
+ app.get("/api/explorer/narratives/:entity/:key/records", async (request) => {
256
+ const { entity, key } = request.params;
257
+ return { entity, key, records: queryNarrativeRecords(entity, key) };
258
+ });
259
+ app.post("/api/explorer/narratives", async (request) => {
260
+ if (!metadataWritesEnabled()) {
261
+ return { ok: false, error: "Metadata writes are disabled" };
262
+ }
263
+ const { entity, key, ...rest } = (request.body || {});
264
+ if (!entity || !key)
265
+ return { error: "entity and key required" };
266
+ const authored = getRawNarratives(entity);
267
+ authored[key] = { key, ...rest };
268
+ store.setMeta(`narratives/${entity}`, authored);
269
+ return { ok: true, entity, key };
270
+ });
271
+ app.patch("/api/explorer/narratives/:entity/:key", async (request) => {
272
+ if (!metadataWritesEnabled()) {
273
+ return { ok: false, error: "Metadata writes are disabled" };
274
+ }
275
+ const { entity, key } = request.params;
276
+ const authored = getRawNarratives(entity);
277
+ authored[key] = { ...authored[key], ...(request.body || {}), key };
278
+ store.setMeta(`narratives/${entity}`, authored);
279
+ return { ok: true, entity, key, descriptor: authored[key] };
280
+ });
281
+ app.delete("/api/explorer/narratives/:entity/:key", async (request) => {
282
+ if (!metadataWritesEnabled()) {
283
+ return { ok: false, error: "Metadata writes are disabled" };
284
+ }
285
+ const { entity, key } = request.params;
286
+ const authored = getRawNarratives(entity);
287
+ delete authored[key];
288
+ store.setMeta(`narratives/${entity}`, authored);
289
+ return { ok: true, entity, key };
290
+ });
291
+ // ---------- G. Object Types & Root Property Catalog ----------
292
+ app.get("/api/explorer/object-types", async () => {
293
+ return { objectTypes: listObjectTypes().map((name) => store.getMeta(`object-types/${name}`)) };
294
+ });
295
+ app.get("/api/explorer/object-types/:name/root-property-catalog", async (request) => {
296
+ const { name } = request.params;
297
+ const meta = store.getMeta(`object-types/${name}`);
298
+ return {
299
+ objectType: name,
300
+ rootPropertyCatalog: meta?.rootPropertyCatalog || [],
301
+ };
302
+ });
303
+ app.post("/api/explorer/object-types/:name/root-property-catalog/compute", async (request) => {
304
+ const { name } = request.params;
305
+ const entries = computeRootPropertyCatalog(name);
306
+ return { objectType: name, computed: true, rootPropertyCatalog: entries };
307
+ });
308
+ // ---------- H. Record Write Engine ----------
309
+ app.post("/api/explorer/records/write", async (request, reply) => {
310
+ const { writeDescriptorId, operation = "add", input, records, dryRun = false, validateOnly = false, } = (request.body || {});
311
+ if (!writeDescriptorId)
312
+ return reply.code(400).send({ error: "writeDescriptorId required", ok: false });
313
+ if (!["add", "upsert", "patch", "replace", "delete"].includes(operation)) {
314
+ return reply.code(400).send({ ok: false, error: `Unsupported operation: ${operation}` });
315
+ }
316
+ const writeSpec = loadWriteDescriptor(writeDescriptorId);
317
+ const targetCollection = writeSpec.targetCollection;
318
+ if (!targetCollection)
319
+ return reply.code(400).send({ error: "writeDescriptor missing targetCollection", ok: false });
320
+ const data = input ?? records;
321
+ if (data === undefined || data === null) {
322
+ return reply.code(400).send({ ok: false, error: "input or records required" });
323
+ }
324
+ const validation = validateInputSchema(writeSpec, data, operation);
325
+ if (!validation.ok) {
326
+ return reply.code(400).send({ ok: false, validated: false, errors: validation.errors });
327
+ }
328
+ if (validateOnly)
329
+ return { ok: true, validated: true };
330
+ const results = executeOperation({
331
+ target: targetCollection,
332
+ op: operation,
333
+ data,
334
+ persist: !dryRun,
335
+ });
336
+ if (!dryRun) {
337
+ await store.flushAll();
338
+ }
339
+ return { ok: true, count: results.length, operation, dryRun, results };
340
+ });
341
+ app.delete("/api/explorer/records/item", async (request, reply) => {
342
+ const { entityName, recordId, writeDescriptorId } = (request.query || {});
343
+ if (!entityName || !recordId || !writeDescriptorId) {
344
+ return reply.code(400).send({
345
+ ok: false,
346
+ error: "entityName, recordId, and writeDescriptorId query params are required",
347
+ });
348
+ }
349
+ const writeSpec = loadWriteDescriptor(writeDescriptorId);
350
+ const expectedTarget = `${entityName}/snapshots`;
351
+ if (writeSpec.targetCollection !== expectedTarget) {
352
+ return reply.code(400).send({
353
+ ok: false,
354
+ error: `Write descriptor targets ${writeSpec.targetCollection}, not ${expectedTarget}`,
355
+ });
356
+ }
357
+ const validation = validateInputSchema(writeSpec, { recordId }, "delete");
358
+ if (!validation.ok) {
359
+ return reply.code(400).send({ ok: false, validated: false, errors: validation.errors });
360
+ }
361
+ const removed = executeOperation({
362
+ target: writeSpec.targetCollection,
363
+ op: "delete",
364
+ data: { recordId },
365
+ });
366
+ await store.flushAll();
367
+ return { ok: true, count: removed.length, removed };
368
+ });
369
+ app.get("/api/explorer/object-types/:name", async (request, reply) => {
370
+ const { name } = request.params;
371
+ const meta = store.getMeta(`object-types/${name}`);
372
+ if (!meta)
373
+ return send404(reply, `Object type not found: ${name}`);
374
+ return meta;
375
+ });
376
+ // ---------- I. Agents Engine ----------
377
+ app.get("/api/explorer/agents", async () => {
378
+ const agents = store.getMeta("agents");
379
+ return agents || { agents: [] };
380
+ });
381
+ // ---- Authix / M2M Auth Shim ----
382
+ app.post("/api/explorer/auth/token", async () => ({
383
+ access_token: "mock-m2m-token",
384
+ token_type: "Bearer",
385
+ expires_in: 3600,
386
+ }));
387
+ // ---- Demo UI ----
388
+ // The demo app served at /demo uses the Explorer API exclusively for its
389
+ // CRUD (records/collection, records/write, DELETE records/item). No separate
390
+ // /api/state flow is needed.
391
+ app.get("/demo", async (_request, reply) => {
392
+ const demoPath = path.join(MOCKS_DIR, "demo.html");
393
+ if (!fs.existsSync(demoPath)) {
394
+ return reply.code(404).send({ ok: false, error: "demo.html not found" });
395
+ }
396
+ const html = fs.readFileSync(demoPath, "utf8");
397
+ return reply.type("text/html").send(html);
398
+ });
399
+ }
400
+ function send404(reply, message) {
401
+ return reply.code(404).send({ ok: false, error: message });
402
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ export interface StartServerOptions {
3
+ port?: number;
4
+ host?: string;
5
+ logger?: boolean;
6
+ installSignalHandlers?: boolean;
7
+ }
8
+ export declare const CLI_HELP = "Memorix Explorer static server\n\nUsage:\n static-memorix [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 static-memorix --port 5030\n";
9
+ export declare function getPackageVersion(): string;
10
+ export declare function parseCliArgs(args: string[]): StartServerOptions;
11
+ export declare function isCliEntrypoint(argvPath: string | undefined, moduleUrl: string): boolean;
12
+ /** Build a loaded Fastify instance without opening a network port. */
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>>;
14
+ /** Build and listen. Options override PORT/HOST environment configuration. */
15
+ export declare function startServer(options?: StartServerOptions): 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>>;
package/dist/server.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ import Fastify from "fastify";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { store } from "./storage/InMemoryStore.js";
7
+ import { registerRoutes } from "./routes/index.js";
8
+ import { PORT, HOST, MOCKS_DIR } from "./config.js";
9
+ export const CLI_HELP = `Memorix Explorer static server
10
+
11
+ Usage:
12
+ static-memorix [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
+ static-memorix --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
+ }
32
+ export function parseCliArgs(args) {
33
+ const options = {};
34
+ for (let index = 0; index < args.length; index++) {
35
+ const arg = args[index];
36
+ if (arg === "--port") {
37
+ const raw = args[++index];
38
+ const port = Number(raw);
39
+ if (!raw || !Number.isInteger(port) || port < 0 || port > 65535) {
40
+ throw Object.assign(new Error("--port must be an integer from 0 to 65535"), {
41
+ statusCode: 400,
42
+ });
43
+ }
44
+ options.port = port;
45
+ }
46
+ else if (arg === "--host") {
47
+ const host = args[++index];
48
+ if (!host)
49
+ throw Object.assign(new Error("--host requires a value"), { statusCode: 400 });
50
+ options.host = host;
51
+ }
52
+ else {
53
+ throw Object.assign(new Error(`Unknown CLI argument: ${arg}`), { statusCode: 400 });
54
+ }
55
+ }
56
+ return options;
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
+ }
68
+ /** Build a loaded Fastify instance without opening a network port. */
69
+ export async function buildServer(options = {}) {
70
+ const app = Fastify({ logger: options.logger ?? false });
71
+ // Global error handler matching predictable error shapes.
72
+ app.setErrorHandler((error, _request, reply) => {
73
+ const normalized = error instanceof Error
74
+ ? error
75
+ : new Error("Internal Server Error");
76
+ const status = normalized.statusCode && normalized.statusCode >= 400
77
+ ? normalized.statusCode
78
+ : 500;
79
+ reply.status(status).send({
80
+ ok: false,
81
+ error: normalized.message || "Internal Server Error",
82
+ });
83
+ });
84
+ await store.load();
85
+ await registerRoutes(app);
86
+ return app;
87
+ }
88
+ /** Build and listen. Options override PORT/HOST environment configuration. */
89
+ export async function startServer(options = {}) {
90
+ const app = await buildServer(options);
91
+ const port = options.port ?? PORT;
92
+ const host = options.host ?? HOST;
93
+ await app.listen({ port, host });
94
+ const address = app.server.address();
95
+ const boundPort = typeof address === "object" && address ? address.port : port;
96
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
97
+ // eslint-disable-next-line no-console
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`);
103
+ if (options.installSignalHandlers ?? true) {
104
+ const shutdown = async () => {
105
+ console.log("\n[static-memorix] flushing + shutting down");
106
+ await store.flushAll();
107
+ await app.close();
108
+ process.exit(0);
109
+ };
110
+ process.once("SIGINT", shutdown);
111
+ process.once("SIGTERM", shutdown);
112
+ }
113
+ return app;
114
+ }
115
+ const isCliEntry = isCliEntrypoint(process.argv[1], import.meta.url);
116
+ if (isCliEntry) {
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
+ });
130
+ }
@@ -0,0 +1,31 @@
1
+ import { type ContentType } from "../types.js";
2
+ export declare class InMemoryStore {
3
+ private collections;
4
+ private metadata;
5
+ private flushTimers;
6
+ private loaded;
7
+ /**
8
+ * Load everything from disk into memory. Idempotent.
9
+ */
10
+ load(): Promise<void>;
11
+ private loadMetadataCatalog;
12
+ private loadDataCollections;
13
+ getMeta<T = any>(key: string): T | undefined;
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;
17
+ getMetaKeys(): string[];
18
+ getCollection(objectType: string, ct: ContentType): any[];
19
+ setCollection(objectType: string, ct: ContentType, data: any[]): void;
20
+ getCollectionKeys(): string[];
21
+ private scheduleCollectionFlush;
22
+ private scheduleMetaFlush;
23
+ flushCollectionNow(key: string): void;
24
+ flushMetaNow(key: string): void;
25
+ /**
26
+ * Flush every dirty file immediately and clear timers.
27
+ */
28
+ flushAll(): Promise<void>;
29
+ get mocksDir(): string;
30
+ }
31
+ export declare const store: InMemoryStore;