neotoma 0.8.0 → 0.9.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 (211) hide show
  1. package/README.md +6 -1
  2. package/dist/actions.d.ts +10 -5
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +306 -43
  5. package/dist/actions.js.map +1 -1
  6. package/dist/cli/bootstrap.js +0 -0
  7. package/dist/cli/hooks.d.ts +4 -4
  8. package/dist/cli/hooks.js +4 -4
  9. package/dist/cli/hooks_detect.d.ts.map +1 -1
  10. package/dist/cli/hooks_detect.js +16 -9
  11. package/dist/cli/hooks_detect.js.map +1 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +91 -19
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/mcp_config_scan.d.ts +10 -0
  16. package/dist/cli/mcp_config_scan.d.ts.map +1 -1
  17. package/dist/cli/mcp_config_scan.js +85 -5
  18. package/dist/cli/mcp_config_scan.js.map +1 -1
  19. package/dist/cli/setup.d.ts +1 -0
  20. package/dist/cli/setup.d.ts.map +1 -1
  21. package/dist/cli/setup.js +71 -1
  22. package/dist/cli/setup.js.map +1 -1
  23. package/dist/cli/triage.d.ts.map +1 -1
  24. package/dist/cli/triage.js +9 -0
  25. package/dist/cli/triage.js.map +1 -1
  26. package/dist/core/operations.d.ts +9 -0
  27. package/dist/core/operations.d.ts.map +1 -1
  28. package/dist/core/operations.js +3 -0
  29. package/dist/core/operations.js.map +1 -1
  30. package/dist/inspector/assets/Combination-BP0-kPZX.js +41 -0
  31. package/dist/inspector/assets/agent_badge-BZT-JO2h.js +1 -0
  32. package/dist/inspector/assets/agent_detail-BGMLF8-n.js +1 -0
  33. package/dist/inspector/assets/agent_filter-DMC4CzhM.js +1 -0
  34. package/dist/inspector/assets/agent_grant_detail-Brqy5K7M.js +1 -0
  35. package/dist/inspector/assets/agent_grant_form-BLDkUh1Y.js +1 -0
  36. package/dist/inspector/assets/agent_grants-B2StunRb.js +1 -0
  37. package/dist/inspector/assets/agents-lYmKs-fG.js +1 -0
  38. package/dist/inspector/assets/arrow-left-D1s7DOns.js +6 -0
  39. package/dist/inspector/assets/attribution_card-D-bp008l.js +1 -0
  40. package/dist/inspector/assets/attribution_summary-Cs9Ccvt3.js +1 -0
  41. package/dist/inspector/assets/card-Btqgzh6p.js +1 -0
  42. package/dist/inspector/assets/check-DAyRtq63.js +6 -0
  43. package/dist/inspector/assets/checkbox-BrpwHaRo.js +1 -0
  44. package/dist/inspector/assets/chevron-down-CCk_jMPN.js +6 -0
  45. package/dist/inspector/assets/chevron-right-C9NbdZtC.js +6 -0
  46. package/dist/inspector/assets/compliance-BHiHtgfg.js +1 -0
  47. package/dist/inspector/assets/confirm-dialog-BcxtVONz.js +6 -0
  48. package/dist/inspector/assets/conversation_common-Dw5j3QuN.js +1 -0
  49. package/dist/inspector/assets/conversation_detail-BR_rBMFV.js +1 -0
  50. package/dist/inspector/assets/copy_id_button-CyjfY7dx.js +6 -0
  51. package/dist/inspector/assets/corrections-DFExbcsm.js +1 -0
  52. package/dist/inspector/assets/dashboard-CxnNRphy.js +73 -0
  53. package/dist/inspector/assets/data-table-CazqdSem.js +22 -0
  54. package/dist/inspector/assets/dialog-X5X7rLah.js +10 -0
  55. package/dist/inspector/assets/dropdown-menu-CmZjxUWM.js +6 -0
  56. package/dist/inspector/assets/entities-l6icu6fc.js +1 -0
  57. package/dist/inspector/assets/entity_detail-daoxB-h1.js +17 -0
  58. package/dist/inspector/assets/entity_link-DtMv__WC.js +1 -0
  59. package/dist/inspector/assets/external-link-BBoTnT-P.js +6 -0
  60. package/dist/inspector/assets/feedback-DeFhdWId.js +35 -0
  61. package/dist/inspector/assets/graph_explorer-BZV40eAE.css +1 -0
  62. package/dist/inspector/assets/graph_explorer-BvicLJEW.js +23 -0
  63. package/dist/inspector/assets/index-B2zHigxN.js +199 -0
  64. package/dist/inspector/assets/index-Czej0Y93.js +1 -0
  65. package/dist/inspector/assets/index-D5i6AEXI.js +1 -0
  66. package/dist/inspector/assets/index-DJuPlRtP.js +1 -0
  67. package/dist/inspector/assets/index-Df569_c9.css +1 -0
  68. package/dist/inspector/assets/interpretations-D9gWqVhy.js +1 -0
  69. package/dist/inspector/assets/interpretations-E0sIBf-l.js +1 -0
  70. package/dist/inspector/assets/json_viewer-ojLDPDtf.js +1 -0
  71. package/dist/inspector/assets/label-DWyQNl4E.js +1 -0
  72. package/dist/inspector/assets/live_relative_time-DzLnsA9y.js +1 -0
  73. package/dist/inspector/assets/observations-Ds0kFmaM.js +1 -0
  74. package/dist/inspector/assets/page_shell-C-4AKr0Y.js +1 -0
  75. package/dist/inspector/assets/pagination-lSg-a95h.js +6 -0
  76. package/dist/inspector/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  77. package/dist/inspector/assets/plus-CQMoR71F.js +6 -0
  78. package/dist/inspector/assets/query_loading-BFETHugg.js +1 -0
  79. package/dist/inspector/assets/query_refresh_indicator-Bewf0Dj1.js +1 -0
  80. package/dist/inspector/assets/recent_activity-Csh_YraY.js +11 -0
  81. package/dist/inspector/assets/recent_conversations-CBengQjb.js +1 -0
  82. package/dist/inspector/assets/recent_conversations-MKmxYevd.js +1 -0
  83. package/dist/inspector/assets/recent_records_feed-CCrBRfkG.js +1 -0
  84. package/dist/inspector/assets/relationship_detail-CUz_GhPI.js +1 -0
  85. package/dist/inspector/assets/relationships-Ulajo16_.js +1 -0
  86. package/dist/inspector/assets/relationships-Z9ALu9Oa.js +1 -0
  87. package/dist/inspector/assets/sandbox-CfD5QvC5.js +1 -0
  88. package/dist/inspector/assets/schema_detail-B1W8rbzV.js +11 -0
  89. package/dist/inspector/assets/schemas-DVlEFPuf.js +5 -0
  90. package/dist/inspector/assets/search-BccQXyTN.js +1 -0
  91. package/dist/inspector/assets/select-Dv1QM6oO.js +6 -0
  92. package/dist/inspector/assets/settings-B4U8tFYI.js +1 -0
  93. package/dist/inspector/assets/source_detail-B1JlZBBx.js +17 -0
  94. package/dist/inspector/assets/source_link-p8KzI1os.js +1 -0
  95. package/dist/inspector/assets/sources-B5ssCN-s.js +9 -0
  96. package/dist/inspector/assets/switch-BPI_y_Z3.js +1 -0
  97. package/dist/inspector/assets/tabs-HUG-sxc2.js +1 -0
  98. package/dist/inspector/assets/textarea-DNz92WE1.js +1 -0
  99. package/dist/inspector/assets/timeline-DRqxyQUF.js +1 -0
  100. package/dist/inspector/assets/timeline-HN2EoMGt.js +1 -0
  101. package/dist/inspector/assets/timeline_event_detail-DbA5EpiN.js +1 -0
  102. package/dist/inspector/assets/trash-2-BAfHKatZ.js +6 -0
  103. package/dist/inspector/assets/turn_detail-BUHzIhWX.js +1 -0
  104. package/dist/inspector/assets/turns-ADRkuqKL.js +1 -0
  105. package/dist/inspector/assets/use_agents-CTZrpsvS.js +1 -0
  106. package/dist/inspector/assets/use_entities-BEhy6HWn.js +1 -0
  107. package/dist/inspector/assets/use_interpretations-CKN63UxX.js +1 -0
  108. package/dist/inspector/assets/use_mutations-B4y1qmV5.js +1 -0
  109. package/dist/inspector/assets/use_recent_conversations-CIBgmz9B.js +1 -0
  110. package/dist/inspector/assets/use_relationships-Cl-o_7u6.js +1 -0
  111. package/dist/inspector/assets/use_schemas-Bl11WNgP.js +1 -0
  112. package/dist/inspector/assets/use_sources-DkJZZBDp.js +1 -0
  113. package/dist/inspector/assets/use_stats-Cn1a3yt-.js +1 -0
  114. package/dist/inspector/assets/use_timeline-DZkbwA-7.js +1 -0
  115. package/dist/inspector/assets/use_turns-BHfaal9v.js +1 -0
  116. package/dist/inspector/assets/value-BTdN53H7.js +1 -0
  117. package/dist/inspector/favicon.svg +10 -0
  118. package/dist/inspector/index.html +14 -0
  119. package/dist/mcp_dev_shim.d.ts +17 -0
  120. package/dist/mcp_dev_shim.d.ts.map +1 -0
  121. package/dist/mcp_dev_shim.js +324 -0
  122. package/dist/mcp_dev_shim.js.map +1 -0
  123. package/dist/mcp_server_card.js +1 -1
  124. package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
  125. package/dist/repositories/sqlite/sqlite_client.js +12 -0
  126. package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
  127. package/dist/scripts/seed_sandbox.js +11 -1
  128. package/dist/scripts/seed_sandbox.js.map +1 -1
  129. package/dist/server.d.ts +11 -0
  130. package/dist/server.d.ts.map +1 -1
  131. package/dist/server.js +118 -37
  132. package/dist/server.js.map +1 -1
  133. package/dist/services/compliance/scorecard.d.ts +35 -73
  134. package/dist/services/compliance/scorecard.d.ts.map +1 -1
  135. package/dist/services/compliance/scorecard.js +234 -270
  136. package/dist/services/compliance/scorecard.js.map +1 -1
  137. package/dist/services/conversation_turn.d.ts +44 -37
  138. package/dist/services/conversation_turn.d.ts.map +1 -1
  139. package/dist/services/conversation_turn.js +248 -362
  140. package/dist/services/conversation_turn.js.map +1 -1
  141. package/dist/services/feedback/admin_proxy.d.ts +21 -7
  142. package/dist/services/feedback/admin_proxy.d.ts.map +1 -1
  143. package/dist/services/feedback/admin_proxy.js +142 -16
  144. package/dist/services/feedback/admin_proxy.js.map +1 -1
  145. package/dist/services/feedback/mirror_local_to_entity.d.ts +20 -53
  146. package/dist/services/feedback/mirror_local_to_entity.d.ts.map +1 -1
  147. package/dist/services/feedback/mirror_local_to_entity.js +46 -76
  148. package/dist/services/feedback/mirror_local_to_entity.js.map +1 -1
  149. package/dist/services/feedback/neotoma_payload.d.ts +72 -33
  150. package/dist/services/feedback/neotoma_payload.d.ts.map +1 -1
  151. package/dist/services/feedback/neotoma_payload.js +12 -24
  152. package/dist/services/feedback/neotoma_payload.js.map +1 -1
  153. package/dist/services/feedback_transport_local.d.ts.map +1 -1
  154. package/dist/services/feedback_transport_local.js +4 -0
  155. package/dist/services/feedback_transport_local.js.map +1 -1
  156. package/dist/services/inspector_mount.d.ts +32 -70
  157. package/dist/services/inspector_mount.d.ts.map +1 -1
  158. package/dist/services/inspector_mount.js +219 -183
  159. package/dist/services/inspector_mount.js.map +1 -1
  160. package/dist/services/recent_conversations.d.ts +9 -0
  161. package/dist/services/recent_conversations.d.ts.map +1 -1
  162. package/dist/services/recent_conversations.js +106 -14
  163. package/dist/services/recent_conversations.js.map +1 -1
  164. package/dist/services/root_landing/harness_snippets.d.ts +2 -0
  165. package/dist/services/root_landing/harness_snippets.d.ts.map +1 -1
  166. package/dist/services/root_landing/harness_snippets.js +15 -8
  167. package/dist/services/root_landing/harness_snippets.js.map +1 -1
  168. package/dist/services/root_landing/html_template.d.ts +9 -0
  169. package/dist/services/root_landing/html_template.d.ts.map +1 -1
  170. package/dist/services/root_landing/html_template.js +66 -1
  171. package/dist/services/root_landing/html_template.js.map +1 -1
  172. package/dist/services/root_landing/index.d.ts +11 -0
  173. package/dist/services/root_landing/index.d.ts.map +1 -1
  174. package/dist/services/root_landing/index.js +26 -8
  175. package/dist/services/root_landing/index.js.map +1 -1
  176. package/dist/services/root_landing/md_template.d.ts.map +1 -1
  177. package/dist/services/root_landing/md_template.js +22 -2
  178. package/dist/services/root_landing/md_template.js.map +1 -1
  179. package/dist/services/sandbox/pack_registry.d.ts +6 -50
  180. package/dist/services/sandbox/pack_registry.d.ts.map +1 -1
  181. package/dist/services/sandbox/pack_registry.js +74 -86
  182. package/dist/services/sandbox/pack_registry.js.map +1 -1
  183. package/dist/services/sandbox/seeder.d.ts +8 -47
  184. package/dist/services/sandbox/seeder.d.ts.map +1 -1
  185. package/dist/services/sandbox/seeder.js +51 -89
  186. package/dist/services/sandbox/seeder.js.map +1 -1
  187. package/dist/services/sandbox/sessions.d.ts +23 -114
  188. package/dist/services/sandbox/sessions.d.ts.map +1 -1
  189. package/dist/services/sandbox/sessions.js +99 -288
  190. package/dist/services/sandbox/sessions.js.map +1 -1
  191. package/dist/services/schema_definitions.d.ts.map +1 -1
  192. package/dist/services/schema_definitions.js +180 -3
  193. package/dist/services/schema_definitions.js.map +1 -1
  194. package/dist/services/schema_registry.d.ts.map +1 -1
  195. package/dist/services/schema_registry.js +28 -2
  196. package/dist/services/schema_registry.js.map +1 -1
  197. package/dist/shared/action_schemas.d.ts +163 -16
  198. package/dist/shared/action_schemas.d.ts.map +1 -1
  199. package/dist/shared/action_schemas.js +28 -10
  200. package/dist/shared/action_schemas.js.map +1 -1
  201. package/dist/shared/contract_mappings.d.ts.map +1 -1
  202. package/dist/shared/contract_mappings.js +23 -0
  203. package/dist/shared/contract_mappings.js.map +1 -1
  204. package/dist/shared/openapi_types.d.ts +245 -16
  205. package/dist/shared/openapi_types.d.ts.map +1 -1
  206. package/dist/tool_definitions.d.ts +1 -1
  207. package/dist/tool_definitions.d.ts.map +1 -1
  208. package/dist/tool_definitions.js +6 -0
  209. package/dist/tool_definitions.js.map +1 -1
  210. package/openapi.yaml +291 -28
  211. package/package.json +26 -9
package/README.md CHANGED
@@ -23,6 +23,8 @@ Neotoma is a deterministic state layer for AI agents. It stores structured recor
23
23
 
24
24
  Not retrieval memory (RAG, vector search, semantic lookup). Neotoma enforces deterministic state evolution: same observations always produce the same entity state, regardless of when or in what order they are processed.
25
25
 
26
+ The **Inspector** — Neotoma's visual control plane for browsing the entity graph, timeline, schema editor, and agent attribution — is bundled and served at `/inspector` by default when the server starts. No separate build or configuration required. Override with `NEOTOMA_INSPECTOR_DISABLE`, `NEOTOMA_PUBLIC_INSPECTOR_URL`, `NEOTOMA_INSPECTOR_STATIC_DIR`, or `NEOTOMA_INSPECTOR_BASE_PATH` (see `.env.example`).
27
+
26
28
  ## Architecture
27
29
 
28
30
  ```mermaid
@@ -186,6 +188,7 @@ Neotoma stores user data and requires secure configuration.
186
188
 
187
189
  ```bash
188
190
  npm run dev # MCP server (stdio)
191
+ npm run dev:mcp:dev-shim # stable stdio shim for MCP source iteration
189
192
  npm run dev:ui # Frontend
190
193
  npm run dev:server # API only (MCP at /mcp)
191
194
  npm run dev:full # API + UI + build watch
@@ -216,7 +219,9 @@ Neotoma exposes state via MCP. Local storage only in preview. Local built-in aut
216
219
 
217
220
  **Setup guides:** [Cursor](https://neotoma.io/neotoma-with-cursor) · [Claude Code](https://neotoma.io/neotoma-with-claude-code) · [Claude](https://neotoma.io/neotoma-with-claude) · [ChatGPT](https://neotoma.io/neotoma-with-chatgpt) · [Codex](https://neotoma.io/neotoma-with-codex) · [OpenClaw](https://neotoma.io/neotoma-with-openclaw)
218
221
 
219
- **Agent behavior contract:** Store first, retrieve before storing, extract entities from user input, create tasks for commitments. Full instructions: [MCP instructions](docs/developer/mcp/instructions.md) and [CLI agent instructions](docs/developer/cli_agent_instructions.md).
222
+ For local source iteration, use the stable dev shim (`scripts/run_neotoma_mcp_stdio_dev_shim.sh` or `npm run dev:mcp:dev-shim`) instead of pointing installed MCP clients at a `tsx watch` stdio process. The shim keeps the client-facing JSON-RPC stream stable and asks clients to refresh or reconnect when the tool interface changes.
223
+
224
+ **Agent behavior contract:** Store first, retrieve before storing, extract entities from user input, create tasks for commitments, and attach bounded host context such as repository name/root scope when available. Full instructions: [MCP instructions](docs/developer/mcp/instructions.md) and [CLI agent instructions](docs/developer/cli_agent_instructions.md).
220
225
 
221
226
  **Representative actions:** `store`, `retrieve_entities`, `retrieve_entity_snapshot`, `merge_entities`, `list_observations`, `create_relationship`, `list_relationships`, `list_timeline_events`, `retrieve_graph_neighborhood`. Full list: [MCP spec](docs/specs/MCP_SPEC.md).
222
227
 
package/dist/actions.d.ts CHANGED
@@ -22,6 +22,14 @@ export declare function isLocalRequest(req: express.Request): boolean;
22
22
  * a different authority. See src/middleware/aauth_verify.ts.
23
23
  */
24
24
  export declare function canonicalAauthAuthority(): string;
25
+ type StoreRelationshipRef = {
26
+ relationship_type: string;
27
+ source_index?: number;
28
+ target_index?: number;
29
+ source_entity_id?: string;
30
+ target_entity_id?: string;
31
+ metadata?: Record<string, unknown>;
32
+ };
25
33
  export declare function storeStructuredForApi(params: {
26
34
  userId: string;
27
35
  entities: Record<string, unknown>[];
@@ -29,11 +37,7 @@ export declare function storeStructuredForApi(params: {
29
37
  observationSource?: import("./shared/action_schemas.js").ObservationSource;
30
38
  idempotencyKey: string;
31
39
  originalFilename?: string;
32
- relationships?: Array<{
33
- relationship_type: string;
34
- source_index: number;
35
- target_index: number;
36
- }>;
40
+ relationships?: StoreRelationshipRef[];
37
41
  commit?: boolean;
38
42
  strict?: boolean;
39
43
  }): Promise<{
@@ -92,4 +96,5 @@ export declare function startHTTPServer(): Promise<{
92
96
  server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
93
97
  port: number;
94
98
  } | undefined>;
99
+ export {};
95
100
  //# sourceMappingURL=actions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAiJ9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AAwZ7B;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAU5D;AA4PD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAmBhD;AA2kHD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,4BAA4B,EAAE,iBAAiB,CAAC;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,KAAK,CAAC;QACpB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;;;;;;;;;;;;;cAkfS,MAAM;gBACJ,MAAM;2BACK,MAAM;qBACZ,MAAM;mBACR,MAAM;wBACD,MAAM;uBACP,MAAM;;;;;;;;;mBA9IV,MAAM;qBACJ,MAAM;wBACH,MAAM,GAAG,IAAI;2BACV,MAAM;;wBAET,MAAM;uBACP,MAAM,EAAE;wBACP,MAAM;uBACP,MAAM;;kBA9NX,MAAM;oBACJ,MAAM;yBACD,MAAM;4BACH,MAAM;2BACP,MAAM;;+BA4NF,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;;;2BAkFlC,MAAM;0BACP,MAAM;0BACN,MAAM;;GA8E3B;AA29DD,wBAAsB,eAAe;;;eA6EpC"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAmK9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AAif7B;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAU5D;AA4PD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAmBhD;AAyrHD,KAAK,oBAAoB,GAAG;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,4BAA4B,EAAE,iBAAiB,CAAC;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;;;;;;;;;;;;;cA+fS,MAAM;gBACJ,MAAM;2BACK,MAAM;qBACZ,MAAM;mBACR,MAAM;wBACD,MAAM;uBACP,MAAM;;;;;;;;;mBA3JV,MAAM;qBACJ,MAAM;wBACH,MAAM,GAAG,IAAI;2BACV,MAAM;;wBAET,MAAM;uBACP,MAAM,EAAE;wBACP,MAAM;uBACP,MAAM;;kBA9NX,MAAM;oBACJ,MAAM;yBACD,MAAM;4BACH,MAAM;2BACP,MAAM;;+BA4NF,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;;;2BAkFlC,MAAM;0BACP,MAAM;0BACN,MAAM;;GA2F3B;AAuhED,wBAAsB,eAAe;;;eA2FpC"}
package/dist/actions.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import express from "express";
2
+ import cookieParser from "cookie-parser";
2
3
  import cors from "cors";
3
4
  import helmet from "helmet";
4
5
  import morgan from "morgan";
@@ -34,13 +35,15 @@ import { logger } from "./utils/logger.js";
34
35
  import { OAuthError } from "./services/mcp_oauth_errors.js";
35
36
  import { ensureLocalDevUser, ensureSandboxAauthUser, ensureSandboxPublicUser, LOCAL_DEV_USER_ID, SANDBOX_PUBLIC_USER_ID, } from "./services/local_auth.js";
36
37
  import { isSandboxMode, sandboxDestructiveGuard, sandboxHeaderMiddleware, } from "./services/sandbox_mode.js";
37
- import { buildLandingContext, buildRootLandingHtml, buildRootLandingJson, buildRootLandingMarkdown, buildRobotsTxt, wantsHtml as acceptWantsHtml, wantsMarkdown as acceptWantsMarkdown, } from "./services/root_landing/index.js";
38
+ import { createSandboxSession, redeemOneTimeCode, resolveSessionFromRequest, revokeSession, purgeSessionUserData, sweepExpiredSessions, SESSION_COOKIE_NAME, } from "./services/sandbox/sessions.js";
39
+ import { buildLandingContext, buildRootLandingHtml, buildRootLandingJson, buildRootLandingMarkdown, buildRobotsTxt, readNeotomaConfigEnvironment, wantsHtml as acceptWantsHtml, wantsMarkdown as acceptWantsMarkdown, } from "./services/root_landing/index.js";
40
+ import { installInspectorMount } from "./services/inspector_mount.js";
38
41
  import { getSandboxTermsResponse } from "./services/sandbox/terms.js";
39
42
  import { resolveSandboxReportTransport } from "./services/sandbox/transport.js";
40
43
  import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
41
44
  import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
42
45
  import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
43
- import { AnalyzeSchemaCandidatesRequestSchema, CorrectEntityRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, StoreRequestSchema, StoreUnstructuredRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
46
+ import { AnalyzeSchemaCandidatesRequestSchema, CorrectEntityRequestSchema, CreateRelationshipsRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, StoreRequestSchema, StoreUnstructuredRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
44
47
  import { getMimeTypeFromExtension } from "./services/file_text_extraction.js";
45
48
  import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
46
49
  import { retrieveEntityByIdentifierWithFallback } from "./shared/action_handlers/entity_identifier_handler.js";
@@ -48,7 +51,9 @@ import { prepareEntitySnapshotWithEmbedding, upsertEntitySnapshotWithEmbedding,
48
51
  import { readOpenApiActionsFile, readOpenApiFile } from "./shared/openapi_file.js";
49
52
  import { buildSmitheryServerCard } from "./mcp_server_card.js";
50
53
  import { listRecentRecordActivity, parseRecordActivityTypesQuery, } from "./services/recent_record_activity.js";
51
- import { listRecentConversations } from "./services/recent_conversations.js";
54
+ import { getRecentConversationById, listRecentConversations } from "./services/recent_conversations.js";
55
+ import { listConversationTurns, getConversationTurn, } from "./services/conversation_turn.js";
56
+ import { buildComplianceScorecard } from "./services/compliance/scorecard.js";
52
57
  import { getAgent, listAgentRecords, listAgents } from "./services/agents_directory.js";
53
58
  export const app = express();
54
59
  // Trust proxy headers (required for express-rate-limit when X-Forwarded-For is present)
@@ -102,6 +107,7 @@ app.use(express.json({
102
107
  },
103
108
  }));
104
109
  app.use(morgan("dev"));
110
+ app.use(cookieParser());
105
111
  app.use(unknownFieldsGuard);
106
112
  // Sandbox-mode response header. Stamped on every response so clients can
107
113
  // detect public-sandbox deployments (sandbox.neotoma.io) without an extra
@@ -110,37 +116,125 @@ if (isSandboxMode()) {
110
116
  app.use(sandboxHeaderMiddleware);
111
117
  logger.info("[Sandbox] NEOTOMA_SANDBOX_MODE=1 — bearer bypass to SANDBOX_PUBLIC_USER_ID, destructive routes gated, weekly reset expected");
112
118
  }
113
- // Inspector SPA mount. When NEOTOMA_INSPECTOR_STATIC_DIR is set, serve the
114
- // pre-built Inspector bundle at NEOTOMA_INSPECTOR_BASE_PATH (default /app).
115
- // Deliberately registered before all auth / rate-limit middleware so the SPA
116
- // shell + assets are reachable without a bearer — the API calls it makes still
117
- // flow through the normal auth stack below.
118
- const inspectorStaticDir = (process.env.NEOTOMA_INSPECTOR_STATIC_DIR || "").trim();
119
- const inspectorBasePath = ((process.env.NEOTOMA_INSPECTOR_BASE_PATH || "/app").trim() || "/app").replace(/\/$/, "");
120
- if (inspectorStaticDir) {
121
- try {
122
- const indexHtmlPath = path.resolve(inspectorStaticDir, "index.html");
123
- // express.static with fallthrough so 404s on unknown files fall into the
124
- // SPA history handler below rather than short-circuiting.
125
- app.use(inspectorBasePath, express.static(inspectorStaticDir, {
126
- index: false,
127
- fallthrough: true,
128
- maxAge: "1h",
129
- }));
130
- app.get([`${inspectorBasePath}`, `${inspectorBasePath}/*`], (req, res, next) => {
131
- // Only respond if the request was headed for the SPA (accepts html).
132
- if (req.method !== "GET")
133
- return next();
134
- res.sendFile(indexHtmlPath, (err) => {
135
- if (err)
136
- next(err);
119
+ // Inspector SPA mount. Deliberately registered before all auth / rate-limit
120
+ // middleware so the SPA shell + assets are reachable without a bearer — the
121
+ // API calls the Inspector makes still flow through the normal auth stack below.
122
+ installInspectorMount(app, process.env, logger);
123
+ // ── Sandbox session endpoints ───────────────────────────────────────────
124
+ // Registered before general auth so the session handshake works for
125
+ // unauthenticated visitors. Routes exist only when NEOTOMA_SANDBOX_MODE=1.
126
+ if (isSandboxMode()) {
127
+ const sessionRateLimit = rateLimit({
128
+ windowMs: 60 * 60 * 1000,
129
+ limit: 10,
130
+ standardHeaders: true,
131
+ legacyHeaders: false,
132
+ keyGenerator: (req) => `ip:${ipKeyGenerator(req.ip || "")}`,
133
+ validate: { trustProxy: false },
134
+ });
135
+ app.post("/sandbox/session/new", sessionRateLimit, (req, res) => {
136
+ try {
137
+ const packId = typeof req.body?.pack_id === "string" ? req.body.pack_id : "generic";
138
+ const session = createSandboxSession(packId);
139
+ res.cookie(SESSION_COOKIE_NAME, session.bearerToken, {
140
+ httpOnly: true,
141
+ sameSite: "lax",
142
+ path: "/",
143
+ expires: new Date(session.expiresAt),
144
+ });
145
+ res.json({
146
+ one_time_code: session.oneTimeCode,
147
+ expires_at: session.expiresAt,
148
+ pack_id: session.packId,
137
149
  });
150
+ }
151
+ catch (err) {
152
+ logger.error(`[Sandbox] session/new failed: ${err.message}`);
153
+ res.status(500).json({ error_code: "SESSION_CREATE_FAILED", message: err.message });
154
+ }
155
+ });
156
+ app.post("/sandbox/session/redeem", (req, res) => {
157
+ try {
158
+ const code = typeof req.body?.code === "string" ? req.body.code : "";
159
+ if (!code) {
160
+ res.status(400).json({ error_code: "MISSING_CODE", message: "code is required" });
161
+ return;
162
+ }
163
+ const result = redeemOneTimeCode(code);
164
+ if (!result) {
165
+ res.status(404).json({ error_code: "INVALID_CODE", message: "Code expired or already redeemed" });
166
+ return;
167
+ }
168
+ res.cookie(SESSION_COOKIE_NAME, result.bearerToken, {
169
+ httpOnly: true,
170
+ sameSite: "lax",
171
+ path: "/",
172
+ expires: new Date(result.expiresAt),
173
+ });
174
+ res.json({
175
+ bearer_token: result.bearerToken,
176
+ user_id: result.userId,
177
+ expires_at: result.expiresAt,
178
+ pack_id: result.packId,
179
+ });
180
+ }
181
+ catch (err) {
182
+ logger.error(`[Sandbox] session/redeem failed: ${err.message}`);
183
+ res.status(500).json({ error_code: "SESSION_REDEEM_FAILED", message: err.message });
184
+ }
185
+ });
186
+ app.get("/sandbox/session", (req, res) => {
187
+ const session = resolveSessionFromRequest(req);
188
+ if (!session) {
189
+ res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
190
+ return;
191
+ }
192
+ res.json({
193
+ user_id: session.userId,
194
+ pack_id: session.packId,
195
+ created_at: session.createdAt,
196
+ expires_at: session.expiresAt,
138
197
  });
139
- logger.info(`[Inspector] Serving SPA from ${inspectorStaticDir} at ${inspectorBasePath}`);
140
- }
141
- catch (err) {
142
- logger.warn(`[Inspector] Failed to mount SPA: ${err.message}`);
143
- }
198
+ });
199
+ app.post("/sandbox/session/reset", (req, res) => {
200
+ const session = resolveSessionFromRequest(req);
201
+ if (!session) {
202
+ res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
203
+ return;
204
+ }
205
+ const packId = typeof req.body?.pack_id === "string" ? req.body.pack_id : undefined;
206
+ purgeSessionUserData(session.userId);
207
+ const newSession = createSandboxSession(packId ?? session.packId);
208
+ res.cookie(SESSION_COOKIE_NAME, newSession.bearerToken, {
209
+ httpOnly: true,
210
+ sameSite: "lax",
211
+ path: "/",
212
+ expires: new Date(newSession.expiresAt),
213
+ });
214
+ res.json({
215
+ user_id: newSession.userId,
216
+ pack_id: newSession.packId,
217
+ expires_at: newSession.expiresAt,
218
+ });
219
+ });
220
+ app.delete("/sandbox/session", (req, res) => {
221
+ const session = resolveSessionFromRequest(req);
222
+ if (!session) {
223
+ res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
224
+ return;
225
+ }
226
+ revokeSession(session.userId);
227
+ purgeSessionUserData(session.userId);
228
+ res.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
229
+ res.json({ ok: true });
230
+ });
231
+ setInterval(() => {
232
+ const purged = sweepExpiredSessions();
233
+ if (purged > 0) {
234
+ logger.info(`[Sandbox] Swept ${purged} expired session(s)`);
235
+ }
236
+ }, 15 * 60 * 1000);
237
+ logger.info("[Sandbox] Session endpoints registered");
144
238
  }
145
239
  // Rate limiters for OAuth endpoints
146
240
  // validate.trustProxy: false — we use trust proxy behind one proxy; skip strict IP check
@@ -399,6 +493,7 @@ app.get("/server-info", (_req, res) => {
399
493
  httpPort,
400
494
  apiBase: config.apiBase,
401
495
  mcpUrl,
496
+ neotoma_env: readNeotomaConfigEnvironment(),
402
497
  });
403
498
  });
404
499
  // ============================================================================
@@ -1803,6 +1898,16 @@ app.use(async (req, res, next) => {
1803
1898
  logger.info(`[Auth] ${req.method} ${req.path} auth_method=local_no_bearer user_id=${devUser.id}`);
1804
1899
  return next();
1805
1900
  }
1901
+ // Sandbox ephemeral session: resolve from cookie or Bearer token before
1902
+ // falling back to the shared public user. Expired/revoked sessions 401.
1903
+ if (isSandboxMode()) {
1904
+ const sessionInfo = resolveSessionFromRequest(req);
1905
+ if (sessionInfo) {
1906
+ req.authenticatedUserId = sessionInfo.userId;
1907
+ logger.info(`[Auth] ${req.method} ${req.path} auth_method=sandbox_session user_id=${sessionInfo.userId}`);
1908
+ return next();
1909
+ }
1910
+ }
1806
1911
  // Sandbox mode: public deployment at sandbox.neotoma.io where anonymous
1807
1912
  // callers are attributed to SANDBOX_PUBLIC_USER_ID without a Bearer. AAuth
1808
1913
  // still runs (earlier in the chain via aauthVerify) so agents exercising the
@@ -3167,6 +3272,21 @@ app.get("/agents/:key/records", async (req, res) => {
3167
3272
  return handleApiError(req, res, error, "Failed to list agent records", "DB_QUERY_FAILED", "APIError:agents_records");
3168
3273
  }
3169
3274
  });
3275
+ // GET /recent_conversations/:conversation_id — Inspector: one conversation with nested messages
3276
+ app.get("/recent_conversations/:conversation_id", async (req, res) => {
3277
+ try {
3278
+ const userId = await getAuthenticatedUserId(req, req.query.user_id);
3279
+ const conversationId = String(req.params.conversation_id ?? "").trim();
3280
+ const item = getRecentConversationById(userId, conversationId);
3281
+ if (!item) {
3282
+ return sendError(res, 404, "RESOURCE_NOT_FOUND", "Conversation not found");
3283
+ }
3284
+ return res.json(item);
3285
+ }
3286
+ catch (error) {
3287
+ return handleApiError(req, res, error, "Failed to load conversation", "DB_QUERY_FAILED", "APIError:recent_conversation_detail");
3288
+ }
3289
+ });
3170
3290
  // GET /recent_conversations — Inspector: conversations with nested messages (SQLite)
3171
3291
  app.get("/recent_conversations", async (req, res) => {
3172
3292
  try {
@@ -3175,7 +3295,9 @@ app.get("/recent_conversations", async (req, res) => {
3175
3295
  const offset = parseInt(String(req.query.offset ?? "0"), 10) || 0;
3176
3296
  const activity_after = typeof req.query.activity_after === "string" ? req.query.activity_after.trim() || null : null;
3177
3297
  const activity_before = typeof req.query.activity_before === "string" ? req.query.activity_before.trim() || null : null;
3178
- const agent_key = typeof req.query.agent_key === "string" ? req.query.agent_key.trim() || null : null;
3298
+ const agentKeyRaw = typeof req.query.agent_key === "string" ? req.query.agent_key.trim() : "";
3299
+ // UI and bookmarks may send agent_key=all; never treat that as a real deriveAgentKey value.
3300
+ const agent_key = agentKeyRaw.length > 0 && agentKeyRaw.toLowerCase() !== "all" ? agentKeyRaw : null;
3179
3301
  const result = listRecentConversations(userId, limit, offset, {
3180
3302
  activity_after,
3181
3303
  activity_before,
@@ -3187,6 +3309,69 @@ app.get("/recent_conversations", async (req, res) => {
3187
3309
  return handleApiError(req, res, error, "Failed to list recent conversations", "DB_QUERY_FAILED", "APIError:recent_conversations");
3188
3310
  }
3189
3311
  });
3312
+ // GET /turns — Inspector: paginated conversation_turn index
3313
+ app.get("/turns", async (req, res) => {
3314
+ try {
3315
+ const userId = await getAuthenticatedUserId(req, req.query.user_id);
3316
+ const limit = parseInt(String(req.query.limit ?? "25"), 10) || 25;
3317
+ const offset = parseInt(String(req.query.offset ?? "0"), 10) || 0;
3318
+ const harness = typeof req.query.harness === "string" ? req.query.harness.trim() || null : null;
3319
+ const status = typeof req.query.status === "string" ? req.query.status.trim() || null : null;
3320
+ const activity_after = typeof req.query.activity_after === "string" ? req.query.activity_after.trim() || null : null;
3321
+ const activity_before = typeof req.query.activity_before === "string" ? req.query.activity_before.trim() || null : null;
3322
+ const result = listConversationTurns(userId, limit, offset, {
3323
+ harness,
3324
+ status,
3325
+ activity_after,
3326
+ activity_before,
3327
+ });
3328
+ return res.json(result);
3329
+ }
3330
+ catch (error) {
3331
+ return handleApiError(req, res, error, "Failed to list turns", "DB_QUERY_FAILED", "APIError:turns");
3332
+ }
3333
+ });
3334
+ // GET /turns/:turn_key — Inspector: conversation_turn detail
3335
+ app.get("/turns/:turn_key", async (req, res) => {
3336
+ try {
3337
+ const userId = await getAuthenticatedUserId(req, req.query.user_id);
3338
+ const turnKey = decodeURIComponent(req.params.turn_key);
3339
+ const result = getConversationTurn(userId, turnKey);
3340
+ if (!result) {
3341
+ return res.status(404).json({ error: "Turn not found", code: "NOT_FOUND" });
3342
+ }
3343
+ return res.json(result);
3344
+ }
3345
+ catch (error) {
3346
+ return handleApiError(req, res, error, "Failed to get turn", "DB_QUERY_FAILED", "APIError:turn_detail");
3347
+ }
3348
+ });
3349
+ // GET /admin/compliance/scorecard — Inspector: aggregated hook compliance (SQLite)
3350
+ app.get("/admin/compliance/scorecard", async (req, res) => {
3351
+ try {
3352
+ const userId = await getAuthenticatedUserId(req, req.query.user_id);
3353
+ const since = typeof req.query.since === "string" ? req.query.since : undefined;
3354
+ const until = typeof req.query.until === "string" ? req.query.until : undefined;
3355
+ const group_by = typeof req.query.group_by === "string" ? req.query.group_by : undefined;
3356
+ const min_turns = parseInt(String(req.query.min_turns ?? ""), 10);
3357
+ const min_backfill_rate = parseFloat(String(req.query.min_backfill_rate ?? ""));
3358
+ const top_missed_steps = parseInt(String(req.query.top_missed_steps ?? ""), 10);
3359
+ const include_synthetic = req.query.include_synthetic === "1" || String(req.query.include_synthetic).toLowerCase() === "true";
3360
+ const result = buildComplianceScorecard(userId, {
3361
+ since,
3362
+ until,
3363
+ group_by,
3364
+ min_turns: Number.isFinite(min_turns) ? min_turns : undefined,
3365
+ min_backfill_rate: Number.isFinite(min_backfill_rate) ? min_backfill_rate : undefined,
3366
+ top_missed_steps: Number.isFinite(top_missed_steps) ? top_missed_steps : undefined,
3367
+ include_synthetic,
3368
+ });
3369
+ return res.json(result);
3370
+ }
3371
+ catch (error) {
3372
+ return handleApiError(req, res, error, "Failed to build compliance scorecard", "DB_QUERY_FAILED", "APIError:compliance_scorecard");
3373
+ }
3374
+ });
3190
3375
  // GET /api/sources - Get source list (FU-301)
3191
3376
  app.get("/sources", async (req, res) => {
3192
3377
  try {
@@ -4044,30 +4229,41 @@ export async function storeStructuredForApi(params) {
4044
4229
  if (commit && relationships && relationships.length > 0) {
4045
4230
  const { relationshipsService } = await import("./services/relationships.js");
4046
4231
  for (const rel of relationships) {
4047
- const source = resolved[rel.source_index];
4048
- const target = resolved[rel.target_index];
4049
- if (!source || !target) {
4050
- logger.warn(`[STORE] Skipping relationship: invalid source_index=${rel.source_index} ` +
4051
- `or target_index=${rel.target_index} (have ${resolved.length} entities).`);
4232
+ const sourceEntityId = typeof rel.source_entity_id === "string"
4233
+ ? rel.source_entity_id
4234
+ : typeof rel.source_index === "number"
4235
+ ? resolved[rel.source_index]?.entity_id
4236
+ : undefined;
4237
+ const targetEntityId = typeof rel.target_entity_id === "string"
4238
+ ? rel.target_entity_id
4239
+ : typeof rel.target_index === "number"
4240
+ ? resolved[rel.target_index]?.entity_id
4241
+ : undefined;
4242
+ if (!sourceEntityId || !targetEntityId) {
4243
+ logger.warn(`[STORE] Skipping relationship: invalid source reference ` +
4244
+ `(source_index=${rel.source_index}, source_entity_id=${rel.source_entity_id}) ` +
4245
+ `or target reference (target_index=${rel.target_index}, ` +
4246
+ `target_entity_id=${rel.target_entity_id}); have ${resolved.length} entities.`);
4052
4247
  continue;
4053
4248
  }
4054
4249
  try {
4055
4250
  await relationshipsService.createRelationship({
4056
- source_entity_id: source.entity_id,
4057
- target_entity_id: target.entity_id,
4251
+ source_entity_id: sourceEntityId,
4252
+ target_entity_id: targetEntityId,
4058
4253
  relationship_type: rel.relationship_type,
4059
4254
  source_id: storageResult.sourceId,
4255
+ metadata: rel.metadata ?? {},
4060
4256
  user_id: userId,
4061
4257
  });
4062
4258
  relationshipsCreated.push({
4063
4259
  relationship_type: rel.relationship_type,
4064
- source_entity_id: source.entity_id,
4065
- target_entity_id: target.entity_id,
4260
+ source_entity_id: sourceEntityId,
4261
+ target_entity_id: targetEntityId,
4066
4262
  });
4067
4263
  }
4068
4264
  catch (relErr) {
4069
4265
  logger.warn(`Failed to create relationship ${rel.relationship_type} ` +
4070
- `${source.entity_id} -> ${target.entity_id}: ${relErr instanceof Error ? relErr.message : String(relErr)}`);
4266
+ `${sourceEntityId} -> ${targetEntityId}: ${relErr instanceof Error ? relErr.message : String(relErr)}`);
4071
4267
  }
4072
4268
  }
4073
4269
  }
@@ -4635,6 +4831,63 @@ app.post("/create_relationship", async (req, res) => {
4635
4831
  return sendError(res, 500, "DB_QUERY_FAILED", error instanceof Error ? error.message : "Failed to create relationship");
4636
4832
  }
4637
4833
  });
4834
+ // Create relationships in batch
4835
+ app.post("/create_relationships", async (req, res) => {
4836
+ const parsed = CreateRelationshipsRequestSchema.safeParse(req.body);
4837
+ if (!parsed.success) {
4838
+ logWarn("ValidationError:create_relationships", req, {
4839
+ issues: parsed.error.issues,
4840
+ });
4841
+ return sendValidationError(res, parsed.error.issues);
4842
+ }
4843
+ const { relationships, source_id, user_id } = parsed.data;
4844
+ const { relationshipsService } = await import("./services/relationships.js");
4845
+ try {
4846
+ const userId = await getAuthenticatedUserId(req, user_id);
4847
+ const created = [];
4848
+ const errors = [];
4849
+ for (const [index, relationship] of relationships.entries()) {
4850
+ try {
4851
+ const snapshot = await relationshipsService.createRelationship({
4852
+ relationship_type: relationship.relationship_type,
4853
+ source_entity_id: relationship.source_entity_id,
4854
+ target_entity_id: relationship.target_entity_id,
4855
+ source_id: relationship.source_id || source_id || null,
4856
+ metadata: relationship.metadata || {},
4857
+ user_id: userId,
4858
+ });
4859
+ created.push({
4860
+ index,
4861
+ ...snapshot,
4862
+ });
4863
+ }
4864
+ catch (error) {
4865
+ errors.push({
4866
+ index,
4867
+ relationship,
4868
+ error: error instanceof Error ? error.message : String(error),
4869
+ });
4870
+ }
4871
+ }
4872
+ logDebug("Success:create_relationships", req, {
4873
+ requested: relationships.length,
4874
+ created: created.length,
4875
+ errors: errors.length,
4876
+ });
4877
+ return res.json({
4878
+ success: errors.length === 0,
4879
+ requested: relationships.length,
4880
+ created_count: created.length,
4881
+ error_count: errors.length,
4882
+ relationships: created,
4883
+ errors,
4884
+ });
4885
+ }
4886
+ catch (error) {
4887
+ logError("RelationshipCreationError:create_relationships", req, error);
4888
+ return sendError(res, 500, "DB_QUERY_FAILED", error instanceof Error ? error.message : "Failed to create relationships");
4889
+ }
4890
+ });
4638
4891
  // List relationships
4639
4892
  app.post("/list_relationships", async (req, res) => {
4640
4893
  const parsed = ListRelationshipsRequestSchema.safeParse(req.body);
@@ -5665,6 +5918,16 @@ export async function startHTTPServer() {
5665
5918
  }
5666
5919
  // Initialize encryption service
5667
5920
  await initServerKeys();
5921
+ // Seed `neotoma_feedback` schema unconditionally so local-only installs
5922
+ // (no AGENT_SITE_BASE_URL) can mirror feedback into the entity graph.
5923
+ try {
5924
+ const { seedNeotomaFeedbackSchema } = await import("./services/feedback/seed_schema.js");
5925
+ await seedNeotomaFeedbackSchema();
5926
+ logger.info("[Feedback] neotoma_feedback schema seeded");
5927
+ }
5928
+ catch (err) {
5929
+ logger.warn(`[Feedback] failed to seed neotoma_feedback schema: ${err.message}`);
5930
+ }
5668
5931
  // Sandbox mode: ensure the `sandbox_abuse_report` entity type is registered
5669
5932
  // before any report comes in so forwarded records can attach cleanly to the
5670
5933
  // entity graph. Non-sandbox deployments still benefit from having the schema