neotoma 0.5.0 → 0.5.1

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 (33) hide show
  1. package/dist/actions.d.ts.map +1 -1
  2. package/dist/actions.js +140 -46
  3. package/dist/actions.js.map +1 -1
  4. package/dist/cli/index.d.ts +19 -0
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +211 -23
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/server.d.ts.map +1 -1
  9. package/dist/server.js +4 -9
  10. package/dist/server.js.map +1 -1
  11. package/dist/services/entity_queries.d.ts +17 -0
  12. package/dist/services/entity_queries.d.ts.map +1 -1
  13. package/dist/services/entity_queries.js +28 -0
  14. package/dist/services/entity_queries.js.map +1 -1
  15. package/dist/services/recent_conversations.d.ts +31 -0
  16. package/dist/services/recent_conversations.d.ts.map +1 -0
  17. package/dist/services/recent_conversations.js +214 -0
  18. package/dist/services/recent_conversations.js.map +1 -0
  19. package/dist/services/recent_record_activity.d.ts +26 -0
  20. package/dist/services/recent_record_activity.d.ts.map +1 -1
  21. package/dist/services/recent_record_activity.js +145 -7
  22. package/dist/services/recent_record_activity.js.map +1 -1
  23. package/dist/shared/action_schemas.d.ts +6 -0
  24. package/dist/shared/action_schemas.d.ts.map +1 -1
  25. package/dist/shared/action_schemas.js +15 -6
  26. package/dist/shared/action_schemas.js.map +1 -1
  27. package/dist/shared/contract_mappings.d.ts.map +1 -1
  28. package/dist/shared/contract_mappings.js +14 -2
  29. package/dist/shared/contract_mappings.js.map +1 -1
  30. package/dist/shared/openapi_types.d.ts +191 -2
  31. package/dist/shared/openapi_types.d.ts.map +1 -1
  32. package/openapi.yaml +300 -0
  33. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAsF9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AA2M7B,yGAAyG;AACzG,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAK5D;AA2gKD,wBAAsB,eAAe;;;eAgCpC"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAuF9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AA2M7B,yGAAyG;AACzG,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAK5D;AAomKD,wBAAsB,eAAe;;;eAgCpC"}
package/dist/actions.js CHANGED
@@ -33,6 +33,7 @@ import { prepareEntitySnapshotWithEmbedding, upsertEntitySnapshotWithEmbedding,
33
33
  import { readOpenApiActionsFile, readOpenApiFile } from "./shared/openapi_file.js";
34
34
  import { buildSmitheryServerCard } from "./mcp_server_card.js";
35
35
  import { listRecentRecordActivity } from "./services/recent_record_activity.js";
36
+ import { listRecentConversations } from "./services/recent_conversations.js";
36
37
  export const app = express();
37
38
  // Trust proxy headers (required for express-rate-limit when X-Forwarded-For is present)
38
39
  app.set("trust proxy", true);
@@ -1845,30 +1846,99 @@ app.get("/entities/:id/relationships", async (req, res) => {
1845
1846
  const idsArr = Array.from(relatedIds);
1846
1847
  const [snapshotsResult, entitiesResult] = await Promise.all([
1847
1848
  db.from("entity_snapshots").select("*").in("entity_id", idsArr).eq("user_id", userId),
1848
- db.from("entities").select("id, canonical_name").in("id", idsArr).eq("user_id", userId),
1849
+ db
1850
+ .from("entities")
1851
+ .select("id, canonical_name, entity_type")
1852
+ .in("id", idsArr)
1853
+ .eq("user_id", userId),
1849
1854
  ]);
1850
- if (!snapshotsResult.error && snapshotsResult.data) {
1851
- const canonicalNames = new Map();
1852
- if (!entitiesResult.error && entitiesResult.data) {
1853
- for (const ent of entitiesResult.data) {
1854
- if (ent.canonical_name)
1855
- canonicalNames.set(ent.id, ent.canonical_name);
1856
- }
1855
+ // Merge snapshot + entity rows so callers always get canonical_name
1856
+ // and entity_type even when the snapshot row is missing.
1857
+ const canonicalNames = new Map();
1858
+ const entityTypes = new Map();
1859
+ if (!entitiesResult.error && entitiesResult.data) {
1860
+ for (const ent of entitiesResult.data) {
1861
+ if (ent.canonical_name)
1862
+ canonicalNames.set(ent.id, ent.canonical_name);
1863
+ if (ent.entity_type)
1864
+ entityTypes.set(ent.id, ent.entity_type);
1857
1865
  }
1858
- relatedEntities = {};
1866
+ }
1867
+ relatedEntities = {};
1868
+ if (!snapshotsResult.error && snapshotsResult.data) {
1859
1869
  for (const e of snapshotsResult.data) {
1860
1870
  if (!e.canonical_name && canonicalNames.has(e.entity_id)) {
1861
1871
  e.canonical_name = canonicalNames.get(e.entity_id);
1862
1872
  }
1873
+ if (!e.entity_type && entityTypes.has(e.entity_id)) {
1874
+ e.entity_type = entityTypes.get(e.entity_id);
1875
+ }
1863
1876
  relatedEntities[e.entity_id] = e;
1864
1877
  }
1865
1878
  }
1879
+ // Fill in entities that have no snapshot row yet.
1880
+ for (const rid of idsArr) {
1881
+ if (relatedEntities[rid])
1882
+ continue;
1883
+ relatedEntities[rid] = {
1884
+ entity_id: rid,
1885
+ canonical_name: canonicalNames.get(rid) ?? null,
1886
+ entity_type: entityTypes.get(rid) ?? null,
1887
+ };
1888
+ }
1889
+ // Resolve schema-derived entity_type_label via the registry once per
1890
+ // distinct type (best effort; non-fatal).
1891
+ try {
1892
+ const { SchemaRegistryService } = await import("./services/schema_registry.js");
1893
+ const registry = new SchemaRegistryService();
1894
+ const distinctTypes = [...new Set(Array.from(entityTypes.values()).filter(Boolean))];
1895
+ const labelByType = new Map();
1896
+ for (const t of distinctTypes) {
1897
+ const schema = await registry.loadActiveSchema(t, userId);
1898
+ if (schema?.metadata?.label)
1899
+ labelByType.set(t, schema.metadata.label);
1900
+ }
1901
+ for (const rid of Object.keys(relatedEntities)) {
1902
+ const type = relatedEntities[rid]?.entity_type;
1903
+ if (type && labelByType.has(type)) {
1904
+ relatedEntities[rid].entity_type_label = labelByType.get(type);
1905
+ }
1906
+ }
1907
+ }
1908
+ catch (err) {
1909
+ // Ignore — expansions without labels are still useful.
1910
+ console.warn("Failed to attach entity_type_label to related entities:", err instanceof Error ? err.message : err);
1911
+ }
1866
1912
  }
1867
1913
  }
1914
+ // Decorate relationship rows with top-level convenience fields so clients
1915
+ // don't have to look up `related_entities` per row. Only populated when
1916
+ // `expand_entities=true`.
1917
+ const decorateRelationship = (rel) => {
1918
+ if (!relatedEntities)
1919
+ return rel;
1920
+ const src = relatedEntities[rel.source_entity_id];
1921
+ const tgt = relatedEntities[rel.target_entity_id];
1922
+ return {
1923
+ ...rel,
1924
+ source_entity_name: src?.canonical_name ?? null,
1925
+ source_entity_type: src?.entity_type ?? null,
1926
+ source_entity_type_label: src?.entity_type_label ?? null,
1927
+ target_entity_name: tgt?.canonical_name ?? null,
1928
+ target_entity_type: tgt?.entity_type ?? null,
1929
+ target_entity_type_label: tgt?.entity_type_label ?? null,
1930
+ };
1931
+ };
1932
+ const decoratedOutgoing = expandEntities
1933
+ ? formattedOutgoing.map(decorateRelationship)
1934
+ : formattedOutgoing;
1935
+ const decoratedIncoming = expandEntities
1936
+ ? formattedIncoming.map(decorateRelationship)
1937
+ : formattedIncoming;
1868
1938
  const responseBody = {
1869
- outgoing: formattedOutgoing,
1870
- incoming: formattedIncoming,
1871
- relationships: [...formattedOutgoing, ...formattedIncoming],
1939
+ outgoing: decoratedOutgoing,
1940
+ incoming: decoratedIncoming,
1941
+ relationships: [...decoratedOutgoing, ...decoratedIncoming],
1872
1942
  };
1873
1943
  if (relatedEntities) {
1874
1944
  responseBody.related_entities = relatedEntities;
@@ -2126,8 +2196,8 @@ app.post("/relationships/snapshot", async (req, res) => {
2126
2196
  return sendValidationError(res, parsed.error.issues);
2127
2197
  }
2128
2198
  try {
2129
- const userId = await getAuthenticatedUserId(req, undefined);
2130
- const { relationship_type, source_entity_id, target_entity_id } = parsed.data;
2199
+ const { relationship_type, source_entity_id, target_entity_id, user_id } = parsed.data;
2200
+ const userId = await getAuthenticatedUserId(req, user_id);
2131
2201
  const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
2132
2202
  const { data: snapshot, error: snapshotError } = await db
2133
2203
  .from("relationship_snapshots")
@@ -2236,9 +2306,7 @@ app.get("/timeline", async (req, res) => {
2236
2306
  }
2237
2307
  // Enrich events with entity canonical names and types
2238
2308
  const events = data || [];
2239
- const entityIds = [
2240
- ...new Set(events.map((e) => e.entity_id).filter(Boolean)),
2241
- ];
2309
+ const entityIds = [...new Set(events.map((e) => e.entity_id).filter(Boolean))];
2242
2310
  const entityLookup = new Map();
2243
2311
  if (entityIds.length > 0) {
2244
2312
  const { data: entities } = await db
@@ -2341,6 +2409,19 @@ app.get("/record_activity", async (req, res) => {
2341
2409
  return handleApiError(req, res, error, "Failed to list recent record activity", "DB_QUERY_FAILED", "APIError:record_activity");
2342
2410
  }
2343
2411
  });
2412
+ // GET /recent_conversations — Inspector: conversations with nested messages (SQLite)
2413
+ app.get("/recent_conversations", async (req, res) => {
2414
+ try {
2415
+ const userId = await getAuthenticatedUserId(req, req.query.user_id);
2416
+ const limit = parseInt(String(req.query.limit ?? "25"), 10) || 25;
2417
+ const offset = parseInt(String(req.query.offset ?? "0"), 10) || 0;
2418
+ const result = listRecentConversations(userId, limit, offset);
2419
+ return res.json(result);
2420
+ }
2421
+ catch (error) {
2422
+ return handleApiError(req, res, error, "Failed to list recent conversations", "DB_QUERY_FAILED", "APIError:recent_conversations");
2423
+ }
2424
+ });
2344
2425
  // GET /api/sources - Get source list (FU-301)
2345
2426
  app.get("/sources", async (req, res) => {
2346
2427
  try {
@@ -2660,7 +2741,7 @@ async function storeStructuredForApi(params) {
2660
2741
  const { userId, entities, sourcePriority, idempotencyKey, originalFilename, relationships, commit: commitInput, strict: strictInput, } = params;
2661
2742
  const commit = commitInput !== false;
2662
2743
  const strict = strictInput === true;
2663
- const { resolveEntityWithTrace, CanonicalNameUnresolvedError, MergeRefusedError, } = await import("./services/entity_resolution.js");
2744
+ const { resolveEntityWithTrace, CanonicalNameUnresolvedError, MergeRefusedError } = await import("./services/entity_resolution.js");
2664
2745
  const { detectFlatPackedRows, FlatPackedRowsError } = await import("./services/flat_packed_detection.js");
2665
2746
  // Reject flat-packed rows (whole tables smuggled into a single entity as
2666
2747
  // `<prefix>_<index>_<suffix>` keys). These cannot produce per-row snapshots
@@ -2689,10 +2770,15 @@ async function storeStructuredForApi(params) {
2689
2770
  entity_type: obs.entity_type,
2690
2771
  observation_id: obs.id,
2691
2772
  }));
2773
+ // Idempotency replay: the source row already exists for
2774
+ // (user_id, idempotency_key). No new observations or entities were
2775
+ // written. Callers distinguish this from a zero-commit fresh write via the
2776
+ // `replayed: true` flag (v0.5.1+). See docs/architecture/idempotence_pattern.md.
2692
2777
  return {
2693
2778
  success: true,
2779
+ replayed: true,
2694
2780
  source_id: existingSource.id,
2695
- entities_created: existingEntities.length,
2781
+ entities_created: 0,
2696
2782
  observations_created: existingEntities.length,
2697
2783
  entities: existingEntities,
2698
2784
  };
@@ -2717,6 +2803,24 @@ async function storeStructuredForApi(params) {
2717
2803
  source_priority: sourcePriority,
2718
2804
  },
2719
2805
  });
2806
+ // v0.5.1: structured guidance for the v0.5.0 breaking change where callers
2807
+ // nested fields under `attributes`. If resolution failed and the only
2808
+ // observed top-level keys are `attributes` (plus optionally `entity_type`),
2809
+ // surface a hint pointing callers at the flat-payload convention.
2810
+ function buildAttributesHint(seenFields) {
2811
+ if (!Array.isArray(seenFields))
2812
+ return undefined;
2813
+ const keys = seenFields.filter((k) => typeof k === "string");
2814
+ if (keys.length === 0)
2815
+ return undefined;
2816
+ const nonMetaKeys = keys.filter((k) => k !== "entity_type");
2817
+ if (nonMetaKeys.length === 1 && nonMetaKeys[0] === "attributes") {
2818
+ return ("Payload nests fields under `attributes`. Since v0.5.0, /store expects " +
2819
+ "fields at the top level of each entity object (e.g. `{ entity_type, " +
2820
+ "title, canonical_name, ... }`). Move keys out of `attributes` and retry.");
2821
+ }
2822
+ return undefined;
2823
+ }
2720
2824
  const resolved = [];
2721
2825
  const issues = [];
2722
2826
  for (let observation_index = 0; observation_index < entities.length; observation_index++) {
@@ -2784,6 +2888,7 @@ async function storeStructuredForApi(params) {
2784
2888
  }
2785
2889
  catch (err) {
2786
2890
  if (err instanceof CanonicalNameUnresolvedError) {
2891
+ const hint = buildAttributesHint(err.seenFields);
2787
2892
  issues.push({
2788
2893
  observation_index,
2789
2894
  entity_type,
@@ -2793,6 +2898,7 @@ async function storeStructuredForApi(params) {
2793
2898
  seen_fields: err.seenFields,
2794
2899
  attempted_value: err.attemptedValue,
2795
2900
  },
2901
+ ...(hint ? { hint } : {}),
2796
2902
  });
2797
2903
  }
2798
2904
  else if (err instanceof MergeRefusedError) {
@@ -2843,8 +2949,7 @@ async function storeStructuredForApi(params) {
2843
2949
  const { recomputeSnapshot } = await import("./services/snapshot_computation.js");
2844
2950
  const snap = await recomputeSnapshot(r.entity_id, userId);
2845
2951
  snapshotAfter =
2846
- snap
2847
- ?.snapshot ?? null;
2952
+ snap?.snapshot ?? null;
2848
2953
  }
2849
2954
  catch (snapshotErr) {
2850
2955
  logger.warn(`Snapshot recompute failed for ${r.entity_id}: ${snapshotErr}`);
@@ -2920,11 +3025,11 @@ async function storeStructuredForApi(params) {
2920
3025
  }
2921
3026
  return {
2922
3027
  success: true,
3028
+ replayed: false,
2923
3029
  commit,
2924
3030
  source_id: commit ? storageResult.sourceId : null,
2925
3031
  entities_created: commit
2926
- ? createdEntities.filter((e) => e.action === "created" || e.action === "extended")
2927
- .length
3032
+ ? createdEntities.filter((e) => e.action === "created" || e.action === "extended").length
2928
3033
  : 0,
2929
3034
  observations_created: commit ? createdEntities.length : 0,
2930
3035
  entities: createdEntities,
@@ -2932,7 +3037,7 @@ async function storeStructuredForApi(params) {
2932
3037
  };
2933
3038
  }
2934
3039
  async function storeUnstructuredForApi(params) {
2935
- const { fileContent, fileBuffer, mimeType, idempotencyKey, originalFilename, userId, } = params;
3040
+ const { fileContent, fileBuffer, mimeType, idempotencyKey, originalFilename, userId } = params;
2936
3041
  const resolvedFileBuffer = fileBuffer ?? (fileContent !== undefined ? Buffer.from(fileContent, "base64") : undefined);
2937
3042
  if (!resolvedFileBuffer) {
2938
3043
  throw new Error("fileContent or fileBuffer is required for unstructured storage");
@@ -3012,7 +3117,8 @@ app.post("/store", async (req, res) => {
3012
3117
  fileContent,
3013
3118
  fileBuffer: resolvedFileBuffer,
3014
3119
  mimeType,
3015
- idempotencyKey: parsed.data.file_idempotency_key ?? (!hasEntities ? parsed.data.idempotency_key : undefined),
3120
+ idempotencyKey: parsed.data.file_idempotency_key ??
3121
+ (!hasEntities ? parsed.data.idempotency_key : undefined),
3016
3122
  originalFilename,
3017
3123
  });
3018
3124
  }
@@ -3031,18 +3137,13 @@ app.post("/store", async (req, res) => {
3031
3137
  if (error instanceof Error && error.message.includes("Not authenticated")) {
3032
3138
  return sendError(res, 401, "AUTH_REQUIRED", error.message);
3033
3139
  }
3034
- const errCode = error && typeof error === "object"
3035
- ? error.code
3036
- : undefined;
3037
- if (errCode === "ERR_FORBIDDEN_ENTITY_TYPE" ||
3038
- errCode === "ERR_PLURAL_ENTITY_TYPE") {
3140
+ const errCode = error && typeof error === "object" ? error.code : undefined;
3141
+ if (errCode === "ERR_FORBIDDEN_ENTITY_TYPE" || errCode === "ERR_PLURAL_ENTITY_TYPE") {
3039
3142
  const message = error instanceof Error ? error.message : String(error);
3040
3143
  logWarn("EntityTypeGuardError:store", req, { code: errCode, message });
3041
3144
  return sendError(res, 400, errCode, message);
3042
3145
  }
3043
- if (error &&
3044
- typeof error === "object" &&
3045
- errCode === "ERR_STORE_RESOLUTION_FAILED") {
3146
+ if (error && typeof error === "object" && errCode === "ERR_STORE_RESOLUTION_FAILED") {
3046
3147
  const err = error;
3047
3148
  logWarn("StoreResolutionError:store", req, {
3048
3149
  issue_count: err.issues?.length ?? 0,
@@ -3055,9 +3156,7 @@ app.post("/store", async (req, res) => {
3055
3156
  },
3056
3157
  });
3057
3158
  }
3058
- if (error &&
3059
- typeof error === "object" &&
3060
- errCode === "ERR_FLAT_PACKED_ROWS") {
3159
+ if (error && typeof error === "object" && errCode === "ERR_FLAT_PACKED_ROWS") {
3061
3160
  const err = error;
3062
3161
  logWarn("FlatPackedRowsError:store", req, {
3063
3162
  prefix: err.detection.prefix,
@@ -3280,10 +3379,7 @@ app.post("/list_observations", async (req, res) => {
3280
3379
  return sendValidationError(res, parsed.error.issues);
3281
3380
  }
3282
3381
  const { entity_id, limit = 100, offset = 0, updated_since, created_since } = parsed.data;
3283
- let query = db
3284
- .from("observations")
3285
- .select("*")
3286
- .eq("entity_id", entity_id);
3382
+ let query = db.from("observations").select("*").eq("entity_id", entity_id);
3287
3383
  if (updated_since) {
3288
3384
  query = query.gte("observed_at", updated_since);
3289
3385
  }
@@ -3366,10 +3462,10 @@ app.post("/create_relationship", async (req, res) => {
3366
3462
  });
3367
3463
  return sendValidationError(res, parsed.error.issues);
3368
3464
  }
3369
- const { relationship_type, source_entity_id, target_entity_id, source_id, metadata } = parsed.data;
3370
- const userId = "00000000-0000-0000-0000-000000000000"; // v0.1.0 single-user
3465
+ const { relationship_type, source_entity_id, target_entity_id, source_id, metadata, user_id } = parsed.data;
3371
3466
  const { relationshipsService } = await import("./services/relationships.js");
3372
3467
  try {
3468
+ const userId = await getAuthenticatedUserId(req, user_id);
3373
3469
  const relationship = await relationshipsService.createRelationship({
3374
3470
  relationship_type,
3375
3471
  source_entity_id,
@@ -3889,8 +3985,7 @@ app.post("/update_schema_incremental", async (req, res) => {
3889
3985
  }
3890
3986
  catch (err) {
3891
3987
  const code = err?.code;
3892
- if (code === "ERR_FORBIDDEN_ENTITY_TYPE" ||
3893
- code === "ERR_PLURAL_ENTITY_TYPE") {
3988
+ if (code === "ERR_FORBIDDEN_ENTITY_TYPE" || code === "ERR_PLURAL_ENTITY_TYPE") {
3894
3989
  return sendError(res, 400, code, err.message);
3895
3990
  }
3896
3991
  throw err;
@@ -3963,8 +4058,7 @@ app.post("/register_schema", async (req, res) => {
3963
4058
  const code = err?.code;
3964
4059
  const message = err instanceof Error ? err.message : String(err);
3965
4060
  logWarn("ValidationError:register_schema", req, { error: message });
3966
- if (code === "ERR_FORBIDDEN_ENTITY_TYPE" ||
3967
- code === "ERR_PLURAL_ENTITY_TYPE") {
4061
+ if (code === "ERR_FORBIDDEN_ENTITY_TYPE" || code === "ERR_PLURAL_ENTITY_TYPE") {
3968
4062
  return sendError(res, 400, code, message);
3969
4063
  }
3970
4064
  return sendError(res, 400, "SCHEMA_VALIDATION_FAILED", message);