mem-os-python 1.6.27__py3-none-any.whl

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 (253) hide show
  1. mem_os_python-1.6.27.dist-info/METADATA +1032 -0
  2. mem_os_python-1.6.27.dist-info/RECORD +253 -0
  3. mem_os_python-1.6.27.dist-info/WHEEL +4 -0
  4. mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
  5. mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
  6. memos/__init__.py +16 -0
  7. memos/_js/adapters/anthropic.d.ts +279 -0
  8. memos/_js/adapters/anthropic.d.ts.map +1 -0
  9. memos/_js/adapters/anthropic.js +348 -0
  10. memos/_js/adapters/anthropic.js.map +1 -0
  11. memos/_js/adapters/base.d.ts +10 -0
  12. memos/_js/adapters/base.d.ts.map +1 -0
  13. memos/_js/adapters/base.js +2 -0
  14. memos/_js/adapters/base.js.map +1 -0
  15. memos/_js/adapters/openai.d.ts +264 -0
  16. memos/_js/adapters/openai.d.ts.map +1 -0
  17. memos/_js/adapters/openai.js +342 -0
  18. memos/_js/adapters/openai.js.map +1 -0
  19. memos/_js/apps/explorer.d.ts +244 -0
  20. memos/_js/apps/explorer.d.ts.map +1 -0
  21. memos/_js/apps/explorer.js +738 -0
  22. memos/_js/apps/explorer.js.map +1 -0
  23. memos/_js/citations.d.ts +65 -0
  24. memos/_js/citations.d.ts.map +1 -0
  25. memos/_js/citations.js +143 -0
  26. memos/_js/citations.js.map +1 -0
  27. memos/_js/cli-browse.d.ts +3 -0
  28. memos/_js/cli-browse.d.ts.map +1 -0
  29. memos/_js/cli-browse.js +129 -0
  30. memos/_js/cli-browse.js.map +1 -0
  31. memos/_js/cli-completion.d.ts +31 -0
  32. memos/_js/cli-completion.d.ts.map +1 -0
  33. memos/_js/cli-completion.js +340 -0
  34. memos/_js/cli-completion.js.map +1 -0
  35. memos/_js/cli-help.d.ts +18 -0
  36. memos/_js/cli-help.d.ts.map +1 -0
  37. memos/_js/cli-help.js +357 -0
  38. memos/_js/cli-help.js.map +1 -0
  39. memos/_js/cli.d.ts +28 -0
  40. memos/_js/cli.d.ts.map +1 -0
  41. memos/_js/cli.js +2880 -0
  42. memos/_js/cli.js.map +1 -0
  43. memos/_js/compression.d.ts +25 -0
  44. memos/_js/compression.d.ts.map +1 -0
  45. memos/_js/compression.js +319 -0
  46. memos/_js/compression.js.map +1 -0
  47. memos/_js/confidence-machine.d.ts +99 -0
  48. memos/_js/confidence-machine.d.ts.map +1 -0
  49. memos/_js/confidence-machine.js +222 -0
  50. memos/_js/confidence-machine.js.map +1 -0
  51. memos/_js/context-pack.d.ts +506 -0
  52. memos/_js/context-pack.d.ts.map +1 -0
  53. memos/_js/context-pack.js +1435 -0
  54. memos/_js/context-pack.js.map +1 -0
  55. memos/_js/contradictions.d.ts +160 -0
  56. memos/_js/contradictions.d.ts.map +1 -0
  57. memos/_js/contradictions.js +255 -0
  58. memos/_js/contradictions.js.map +1 -0
  59. memos/_js/dashboard.d.ts +30 -0
  60. memos/_js/dashboard.d.ts.map +1 -0
  61. memos/_js/dashboard.js +302 -0
  62. memos/_js/dashboard.js.map +1 -0
  63. memos/_js/ebbinghaus.d.ts +29 -0
  64. memos/_js/ebbinghaus.d.ts.map +1 -0
  65. memos/_js/ebbinghaus.js +32 -0
  66. memos/_js/ebbinghaus.js.map +1 -0
  67. memos/_js/embedding-queue.d.ts +190 -0
  68. memos/_js/embedding-queue.d.ts.map +1 -0
  69. memos/_js/embedding-queue.js +316 -0
  70. memos/_js/embedding-queue.js.map +1 -0
  71. memos/_js/embeddings.d.ts +187 -0
  72. memos/_js/embeddings.d.ts.map +1 -0
  73. memos/_js/embeddings.js +633 -0
  74. memos/_js/embeddings.js.map +1 -0
  75. memos/_js/entity-extraction.d.ts +54 -0
  76. memos/_js/entity-extraction.d.ts.map +1 -0
  77. memos/_js/entity-extraction.js +325 -0
  78. memos/_js/entity-extraction.js.map +1 -0
  79. memos/_js/entity-index.d.ts +93 -0
  80. memos/_js/entity-index.d.ts.map +1 -0
  81. memos/_js/entity-index.js +165 -0
  82. memos/_js/entity-index.js.map +1 -0
  83. memos/_js/event-memory.d.ts +147 -0
  84. memos/_js/event-memory.d.ts.map +1 -0
  85. memos/_js/event-memory.js +290 -0
  86. memos/_js/event-memory.js.map +1 -0
  87. memos/_js/external-import.d.ts +98 -0
  88. memos/_js/external-import.d.ts.map +1 -0
  89. memos/_js/external-import.js +669 -0
  90. memos/_js/external-import.js.map +1 -0
  91. memos/_js/fidelity.d.ts +367 -0
  92. memos/_js/fidelity.d.ts.map +1 -0
  93. memos/_js/fidelity.js +911 -0
  94. memos/_js/fidelity.js.map +1 -0
  95. memos/_js/graph-expansion.d.ts +119 -0
  96. memos/_js/graph-expansion.d.ts.map +1 -0
  97. memos/_js/graph-expansion.js +213 -0
  98. memos/_js/graph-expansion.js.map +1 -0
  99. memos/_js/graph-mermaid.d.ts +13 -0
  100. memos/_js/graph-mermaid.d.ts.map +1 -0
  101. memos/_js/graph-mermaid.js +32 -0
  102. memos/_js/graph-mermaid.js.map +1 -0
  103. memos/_js/graph.d.ts +170 -0
  104. memos/_js/graph.d.ts.map +1 -0
  105. memos/_js/graph.js +482 -0
  106. memos/_js/graph.js.map +1 -0
  107. memos/_js/harness.d.ts +63 -0
  108. memos/_js/harness.d.ts.map +1 -0
  109. memos/_js/harness.js +93 -0
  110. memos/_js/harness.js.map +1 -0
  111. memos/_js/importance.d.ts +118 -0
  112. memos/_js/importance.d.ts.map +1 -0
  113. memos/_js/importance.js +144 -0
  114. memos/_js/importance.js.map +1 -0
  115. memos/_js/index.d.ts +51 -0
  116. memos/_js/index.d.ts.map +1 -0
  117. memos/_js/index.js +53 -0
  118. memos/_js/index.js.map +1 -0
  119. memos/_js/mcp.d.ts +58 -0
  120. memos/_js/mcp.d.ts.map +1 -0
  121. memos/_js/mcp.js +1853 -0
  122. memos/_js/mcp.js.map +1 -0
  123. memos/_js/memory.d.ts +1762 -0
  124. memos/_js/memory.d.ts.map +1 -0
  125. memos/_js/memory.js +6283 -0
  126. memos/_js/memory.js.map +1 -0
  127. memos/_js/procedural.d.ts +74 -0
  128. memos/_js/procedural.d.ts.map +1 -0
  129. memos/_js/procedural.js +125 -0
  130. memos/_js/procedural.js.map +1 -0
  131. memos/_js/project-config.d.ts +68 -0
  132. memos/_js/project-config.d.ts.map +1 -0
  133. memos/_js/project-config.js +59 -0
  134. memos/_js/project-config.js.map +1 -0
  135. memos/_js/proof-of-forgetting.d.ts +57 -0
  136. memos/_js/proof-of-forgetting.d.ts.map +1 -0
  137. memos/_js/proof-of-forgetting.js +80 -0
  138. memos/_js/proof-of-forgetting.js.map +1 -0
  139. memos/_js/provenance.d.ts +122 -0
  140. memos/_js/provenance.d.ts.map +1 -0
  141. memos/_js/provenance.js +182 -0
  142. memos/_js/provenance.js.map +1 -0
  143. memos/_js/quarantine-commands.d.ts +23 -0
  144. memos/_js/quarantine-commands.d.ts.map +1 -0
  145. memos/_js/quarantine-commands.js +42 -0
  146. memos/_js/quarantine-commands.js.map +1 -0
  147. memos/_js/quarantine.d.ts +120 -0
  148. memos/_js/quarantine.d.ts.map +1 -0
  149. memos/_js/quarantine.js +873 -0
  150. memos/_js/quarantine.js.map +1 -0
  151. memos/_js/redact.d.ts +12 -0
  152. memos/_js/redact.d.ts.map +1 -0
  153. memos/_js/redact.js +26 -0
  154. memos/_js/redact.js.map +1 -0
  155. memos/_js/repl.d.ts +57 -0
  156. memos/_js/repl.d.ts.map +1 -0
  157. memos/_js/repl.js +295 -0
  158. memos/_js/repl.js.map +1 -0
  159. memos/_js/rerank-local.d.ts +61 -0
  160. memos/_js/rerank-local.d.ts.map +1 -0
  161. memos/_js/rerank-local.js +210 -0
  162. memos/_js/rerank-local.js.map +1 -0
  163. memos/_js/retain-filter.d.ts +98 -0
  164. memos/_js/retain-filter.d.ts.map +1 -0
  165. memos/_js/retain-filter.js +232 -0
  166. memos/_js/retain-filter.js.map +1 -0
  167. memos/_js/retrieval.d.ts +265 -0
  168. memos/_js/retrieval.d.ts.map +1 -0
  169. memos/_js/retrieval.js +1018 -0
  170. memos/_js/retrieval.js.map +1 -0
  171. memos/_js/revert.d.ts +77 -0
  172. memos/_js/revert.d.ts.map +1 -0
  173. memos/_js/revert.js +252 -0
  174. memos/_js/revert.js.map +1 -0
  175. memos/_js/scope.d.ts +30 -0
  176. memos/_js/scope.d.ts.map +1 -0
  177. memos/_js/scope.js +71 -0
  178. memos/_js/scope.js.map +1 -0
  179. memos/_js/semantic-screen.d.ts +152 -0
  180. memos/_js/semantic-screen.d.ts.map +1 -0
  181. memos/_js/semantic-screen.js +290 -0
  182. memos/_js/semantic-screen.js.map +1 -0
  183. memos/_js/session-hooks.d.ts +127 -0
  184. memos/_js/session-hooks.d.ts.map +1 -0
  185. memos/_js/session-hooks.js +482 -0
  186. memos/_js/session-hooks.js.map +1 -0
  187. memos/_js/storage/sqlite.d.ts +427 -0
  188. memos/_js/storage/sqlite.d.ts.map +1 -0
  189. memos/_js/storage/sqlite.js +2428 -0
  190. memos/_js/storage/sqlite.js.map +1 -0
  191. memos/_js/sync.d.ts +213 -0
  192. memos/_js/sync.d.ts.map +1 -0
  193. memos/_js/sync.js +389 -0
  194. memos/_js/sync.js.map +1 -0
  195. memos/_js/tamper-log.d.ts +98 -0
  196. memos/_js/tamper-log.d.ts.map +1 -0
  197. memos/_js/tamper-log.js +327 -0
  198. memos/_js/tamper-log.js.map +1 -0
  199. memos/_js/temporal.d.ts +92 -0
  200. memos/_js/temporal.d.ts.map +1 -0
  201. memos/_js/temporal.js +536 -0
  202. memos/_js/temporal.js.map +1 -0
  203. memos/_js/time-travel.d.ts +38 -0
  204. memos/_js/time-travel.d.ts.map +1 -0
  205. memos/_js/time-travel.js +46 -0
  206. memos/_js/time-travel.js.map +1 -0
  207. memos/_js/timestamp-format.d.ts +18 -0
  208. memos/_js/timestamp-format.d.ts.map +1 -0
  209. memos/_js/timestamp-format.js +52 -0
  210. memos/_js/timestamp-format.js.map +1 -0
  211. memos/_js/triple-extraction.d.ts +31 -0
  212. memos/_js/triple-extraction.d.ts.map +1 -0
  213. memos/_js/triple-extraction.js +116 -0
  214. memos/_js/triple-extraction.js.map +1 -0
  215. memos/_js/types.d.ts +2474 -0
  216. memos/_js/types.d.ts.map +1 -0
  217. memos/_js/types.js +20 -0
  218. memos/_js/types.js.map +1 -0
  219. memos/_js/updater.d.ts +95 -0
  220. memos/_js/updater.d.ts.map +1 -0
  221. memos/_js/updater.js +268 -0
  222. memos/_js/updater.js.map +1 -0
  223. memos/_js/version.d.ts +17 -0
  224. memos/_js/version.d.ts.map +1 -0
  225. memos/_js/version.js +34 -0
  226. memos/_js/version.js.map +1 -0
  227. memos/_js/vision/clip-query-cache.d.ts +58 -0
  228. memos/_js/vision/clip-query-cache.d.ts.map +1 -0
  229. memos/_js/vision/clip-query-cache.js +79 -0
  230. memos/_js/vision/clip-query-cache.js.map +1 -0
  231. memos/_js/vision/clip.d.ts +113 -0
  232. memos/_js/vision/clip.d.ts.map +1 -0
  233. memos/_js/vision/clip.js +224 -0
  234. memos/_js/vision/clip.js.map +1 -0
  235. memos/_js/vision/fake-clip.d.ts +55 -0
  236. memos/_js/vision/fake-clip.d.ts.map +1 -0
  237. memos/_js/vision/fake-clip.js +122 -0
  238. memos/_js/vision/fake-clip.js.map +1 -0
  239. memos/_js/vision/index.d.ts +15 -0
  240. memos/_js/vision/index.d.ts.map +1 -0
  241. memos/_js/vision/index.js +13 -0
  242. memos/_js/vision/index.js.map +1 -0
  243. memos/_js/vision/store.d.ts +137 -0
  244. memos/_js/vision/store.d.ts.map +1 -0
  245. memos/_js/vision/store.js +383 -0
  246. memos/_js/vision/store.js.map +1 -0
  247. memos/adapters/__init__.py +0 -0
  248. memos/adapters/crewai.py +385 -0
  249. memos/adapters/langchain.py +301 -0
  250. memos/adapters/ollama.py +396 -0
  251. memos/server/__init__.py +0 -0
  252. memos/server/main.py +547 -0
  253. memos/server/routes.py +360 -0
@@ -0,0 +1,738 @@
1
+ /**
2
+ * MCP Apps memory explorer — an interactive graph UI served as a `ui://`
3
+ * resource (MCP Apps, ratified Jan 2026).
4
+ *
5
+ * The explorer renders the MemOS entity/memory graph as a self-contained,
6
+ * CSP-locked HTML payload: no external network fetches, no CDN scripts —
7
+ * the SVG renderer, pan/zoom, and filters are all hand-rolled in the
8
+ * payload. The bitemporal "as-of" scrubber re-queries the graph at any past
9
+ * timestamp through the host: each scrub position sends a structured `read`
10
+ * request over the `postMessage` bridge, the host performs a server-side
11
+ * `resources/read` at that timestamp (the existing as-of read path), and the
12
+ * app swaps in the returned snapshot. Nodes are colored by provenance
13
+ * tier, edges labeled by relation type.
14
+ *
15
+ * Consent model (the important part): the app itself is UNPRIVILEGED. It
16
+ * cannot touch the database. Mutating actions (forget, link, release from
17
+ * quarantine) are expressed as structured *intents* posted to the host via
18
+ * `postMessage`; the host translates each intent into a normal MCP
19
+ * `tools/call` (see {@link intentToToolCall}) so the HOST's consent UI
20
+ * applies exactly as if the agent had called the tool itself. Read-path
21
+ * interactions (pan/zoom/filter/scrub) need no consent.
22
+ *
23
+ * Where the host has no MCP Apps support the same resource serves a
24
+ * text/markdown fallback (mermaid diagram + summary) instead of the HTML
25
+ * app — see {@link resolveExplorerMode}.
26
+ *
27
+ * Everything here is deterministic and LLM-free.
28
+ *
29
+ * @module @mem-os/apps-explorer
30
+ */
31
+ import { z } from "zod";
32
+ import { UriTemplate } from "@modelcontextprotocol/server";
33
+ import { citationToken } from "../citations.js";
34
+ import { graphToMermaid } from "../graph-mermaid.js";
35
+ import { PROVENANCE_TIER_ORDER } from "../provenance.js";
36
+ // ---------------------------------------------------------------------------
37
+ // Identity & caps
38
+ // ---------------------------------------------------------------------------
39
+ /** Canonical resource URI for the explorer app. */
40
+ export const EXPLORER_APP_URI = "ui://memos/explorer";
41
+ /**
42
+ * URI template registered on the MCP server. Query variables:
43
+ * - `asOf` — unix ms; render the graph as of a past timestamp
44
+ * (bitemporal time-travel) instead of live.
45
+ * - `mode` — `app` forces the HTML app, `text` forces the markdown
46
+ * fallback. Unset: auto-detect from the host signal,
47
+ * defaulting to the safe `text` fallback.
48
+ * - `nodeLimit` — override the node cap (clamped to 1..1000).
49
+ * - `edgeLimit` — override the edge cap (clamped to 1..2000).
50
+ */
51
+ export const EXPLORER_URI_TEMPLATE = "ui://memos/explorer{?asOf,mode,nodeLimit,edgeLimit}";
52
+ /**
53
+ * URI template for the explorer resource with correct optional-query
54
+ * matching.
55
+ *
56
+ * The SDK's built-in `UriTemplate.match()` compiles `{?a,b,...}` into a
57
+ * regex that requires *every* variable to be present, in declaration order —
58
+ * so `ui://memos/explorer?mode=app` would never match
59
+ * `ui://memos/explorer{?asOf,mode,nodeLimit,edgeLimit}`. This subclass keeps
60
+ * the standard template string (so `resources/templates/list` advertises the
61
+ * documented template) but overrides matching: the path must be exactly
62
+ * `ui://memos/explorer`, and any subset of the documented query parameters
63
+ * may be present, in any order. Unknown query parameters are ignored.
64
+ */
65
+ export class ExplorerUriTemplate extends UriTemplate {
66
+ constructor() {
67
+ super(EXPLORER_URI_TEMPLATE);
68
+ }
69
+ match(uri) {
70
+ let parsed;
71
+ try {
72
+ parsed = new URL(uri);
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ if (parsed.protocol !== "ui:" ||
78
+ parsed.host !== "memos" ||
79
+ parsed.pathname !== "/explorer") {
80
+ return null;
81
+ }
82
+ const variables = {};
83
+ for (const name of this.variableNames) {
84
+ const value = parsed.searchParams.get(name);
85
+ if (value !== null)
86
+ variables[name] = value;
87
+ }
88
+ return variables;
89
+ }
90
+ }
91
+ /** Default cap on nodes embedded in one payload (keeps it small). */
92
+ export const EXPLORER_NODE_CAP = 150;
93
+ /** Default cap on edges embedded in one payload. */
94
+ export const EXPLORER_EDGE_CAP = 400;
95
+ /** Hard ceiling for client-overridden caps. */
96
+ export const EXPLORER_NODE_CAP_MAX = 1000;
97
+ export const EXPLORER_EDGE_CAP_MAX = 2000;
98
+ /** Characters of node content embedded per node. */
99
+ export const EXPLORER_CONTENT_CHARS = 280;
100
+ /**
101
+ * postMessage protocol identifier. The app posts
102
+ * `{ protocol: "memos-explorer/1", kind: "intent", intent, params, requestId }`
103
+ * to `window.parent`; hosts translate intents via {@link intentToToolCall}
104
+ * and reply with `{ protocol: "memos-explorer/1", kind: "intent-result", ... }`.
105
+ *
106
+ * Read-path time travel uses a second message pair so scrubbing the as-of
107
+ * slider re-queries the server instead of filtering the embedded snapshot
108
+ * client-side: the app posts
109
+ * `{ protocol: "memos-explorer/1", kind: "read", requestId, asOf: number|null }`
110
+ * (`asOf: null` = live graph) and the host performs an MCP `resources/read`
111
+ * of `ui://memos/explorer?mode=app` (plus `&asOf=<ms>` when set) and replies
112
+ * with `{ protocol: "memos-explorer/1", kind: "snapshot", requestId, ok, html?, error? }`
113
+ * where `html` is the raw resource text — the app extracts the new snapshot
114
+ * from its `<script id="memos-snapshot">` tag and re-renders. Reads need no
115
+ * consent. If the host never answers (no bridge support) the app falls back
116
+ * to filtering its embedded snapshot locally.
117
+ */
118
+ export const EXPLORER_PROTOCOL = "memos-explorer/1";
119
+ /** Node fill color per provenance tier (highest trust → greenest). */
120
+ export const PROVENANCE_TIER_COLORS = {
121
+ "user-verified": "#2f9e44",
122
+ user: "#1971c2",
123
+ "tool-output": "#e67700",
124
+ chat: "#7048e8",
125
+ imported: "#868e96",
126
+ };
127
+ /** Edge stroke color per relation type. */
128
+ export const EDGE_RELATION_COLORS = {
129
+ relates_to: "#adb5bd",
130
+ supports: "#2f9e44",
131
+ contradicts: "#e03131",
132
+ derived_from: "#1971c2",
133
+ part_of: "#7048e8",
134
+ temporal_precedes: "#e67700",
135
+ custom: "#868e96",
136
+ };
137
+ /** Intents the app may emit (all go through host consent). */
138
+ export const SUPPORTED_INTENTS = [
139
+ "forget",
140
+ "link",
141
+ "release-quarantine",
142
+ ];
143
+ function clampCap(value, def, max) {
144
+ if (value === undefined || !Number.isFinite(value))
145
+ return def;
146
+ return Math.max(1, Math.min(max, Math.floor(value)));
147
+ }
148
+ function projectNode(node) {
149
+ const content = node.content.length > EXPLORER_CONTENT_CHARS
150
+ ? node.content.slice(0, EXPLORER_CONTENT_CHARS) + "…"
151
+ : node.content;
152
+ return {
153
+ id: node.id,
154
+ content,
155
+ contentTruncated: node.content.length > EXPLORER_CONTENT_CHARS,
156
+ type: node.type,
157
+ importance: node.importance,
158
+ trustScore: node.trustScore,
159
+ provenance: node.provenance ?? "unknown",
160
+ quarantined: node.quarantined ?? false,
161
+ quarantineReason: node.quarantineReason ?? null,
162
+ validFrom: node.validFrom ?? null,
163
+ validTo: node.validTo ?? null,
164
+ createdAt: node.createdAt,
165
+ citation: citationToken(node.id),
166
+ };
167
+ }
168
+ function projectEdge(edge) {
169
+ return {
170
+ id: edge.id,
171
+ sourceId: edge.sourceId,
172
+ targetId: edge.targetId,
173
+ relation: edge.relation,
174
+ weight: edge.weight,
175
+ validFrom: edge.validFrom ?? null,
176
+ validTo: edge.validTo ?? null,
177
+ };
178
+ }
179
+ /**
180
+ * Build the capped, JSON-safe snapshot the app embeds. Nodes are taken in
181
+ * importance order (most important first); edges are kept only when both
182
+ * endpoints survived the node cap, in weight order. Pass `asOf` to
183
+ * pre-filter to a bitemporal timestamp (same predicates as the server
184
+ * as-of read path).
185
+ */
186
+ export function buildExplorerSnapshot(graph, opts = {}) {
187
+ const nodeCap = clampCap(opts.nodeCap, EXPLORER_NODE_CAP, EXPLORER_NODE_CAP_MAX);
188
+ const edgeCap = clampCap(opts.edgeCap, EXPLORER_EDGE_CAP, EXPLORER_EDGE_CAP_MAX);
189
+ let nodes = graph.nodes;
190
+ let edges = graph.edges;
191
+ if (opts.asOf !== undefined) {
192
+ nodes = nodes.filter((n) => isNodeValidAt(n, opts.asOf));
193
+ edges = edges.filter((e) => isEdgeValidAt(e, opts.asOf));
194
+ }
195
+ const totalNodes = nodes.length;
196
+ const totalEdges = edges.length;
197
+ const kept = [...nodes]
198
+ .sort((a, b) => b.importance - a.importance || (a.id < b.id ? -1 : 1))
199
+ .slice(0, nodeCap);
200
+ const keptIds = new Set(kept.map((n) => n.id));
201
+ const keptEdges = edges
202
+ .filter((e) => keptIds.has(e.sourceId) && keptIds.has(e.targetId))
203
+ .sort((a, b) => b.weight - a.weight)
204
+ .slice(0, edgeCap);
205
+ return {
206
+ nodes: kept.map(projectNode),
207
+ edges: keptEdges.map(projectEdge),
208
+ totalNodes,
209
+ totalEdges,
210
+ truncated: totalNodes > kept.length || totalEdges > keptEdges.length,
211
+ generatedAt: Date.now(),
212
+ asOf: opts.asOf ?? null,
213
+ };
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // Bitemporal as-of predicates (mirror the server read path exactly)
217
+ // ---------------------------------------------------------------------------
218
+ /**
219
+ * Node as-of predicate — identical to the SQLite as-of read path
220
+ * (`valid_from <= t AND (valid_to IS NULL OR valid_to >= t)`).
221
+ */
222
+ export function isNodeValidAt(node, atTime) {
223
+ const from = node.validFrom ?? null;
224
+ const to = node.validTo ?? null;
225
+ return (from === null || from <= atTime) && (to === null || to >= atTime);
226
+ }
227
+ /**
228
+ * Edge as-of predicate — identical to the SQLite edge as-of read path,
229
+ * which uses strict `valid_to > t` (see `queryEdges({ validAt })`): an
230
+ * edge closed in the same millisecond as the query is already historical.
231
+ */
232
+ export function isEdgeValidAt(edge, atTime) {
233
+ const from = edge.validFrom ?? null;
234
+ const to = edge.validTo ?? null;
235
+ return (from === null || from <= atTime) && (to === null || to > atTime);
236
+ }
237
+ /**
238
+ * The as-of scrubber's filter: re-evaluate a snapshot at a past timestamp.
239
+ * This is the graceful-degradation path for the time slider — the same
240
+ * predicates the server uses, applied client-side to the embedded
241
+ * snapshot — used only when the host does not implement the `read` bridge
242
+ * (see {@link EXPLORER_PROTOCOL}). With a supporting host, every scrub
243
+ * position re-queries the server via `resources/read?asOf=` instead.
244
+ */
245
+ export function filterSnapshotAsOf(snapshot, atTime) {
246
+ const nodes = snapshot.nodes.filter((n) => isNodeValidAt(n, atTime));
247
+ const ids = new Set(nodes.map((n) => n.id));
248
+ const edges = snapshot.edges.filter((e) => ids.has(e.sourceId) && ids.has(e.targetId) && isEdgeValidAt(e, atTime));
249
+ return { ...snapshot, nodes, edges, asOf: atTime };
250
+ }
251
+ /**
252
+ * `_meta` keys probed for an explicit host MCP Apps signal, in priority
253
+ * order. `memos.uiApps` is the documented MemOS convention; the others
254
+ * are tolerated aliases.
255
+ */
256
+ const APPS_SIGNAL_KEYS = [
257
+ "memos.uiApps",
258
+ "io.modelcontextprotocol/ui",
259
+ "ui",
260
+ ];
261
+ /**
262
+ * Probe a `resources/read` request `_meta` for host MCP Apps support.
263
+ * Returns true/false on an explicit signal, null when the host says
264
+ * nothing (the common case for older clients).
265
+ */
266
+ export function probeHostAppsSupport(meta) {
267
+ if (meta === null || meta === undefined || typeof meta !== "object") {
268
+ return null;
269
+ }
270
+ const record = meta;
271
+ for (const key of APPS_SIGNAL_KEYS) {
272
+ const value = record[key];
273
+ if (value === true)
274
+ return true;
275
+ if (value === false)
276
+ return false;
277
+ }
278
+ return null;
279
+ }
280
+ /**
281
+ * Decide whether to serve the HTML app or the markdown fallback.
282
+ *
283
+ * 1. An explicit `?mode=app|text` always wins.
284
+ * 2. Otherwise an explicit host Apps signal in `_meta` wins.
285
+ * 3. Otherwise fail closed to the `text` fallback: a host that cannot
286
+ * render the app must never receive a raw HTML blob it would dump as
287
+ * markup into the conversation.
288
+ */
289
+ export function resolveExplorerMode(opts) {
290
+ const param = (opts.modeParam ?? "").trim().toLowerCase();
291
+ if (param === "app")
292
+ return "app";
293
+ if (param === "text" || param === "markdown")
294
+ return "text";
295
+ return probeHostAppsSupport(opts.meta) === true ? "app" : "text";
296
+ }
297
+ // ---------------------------------------------------------------------------
298
+ // Consent bridge: UI intents → MCP tool calls
299
+ // ---------------------------------------------------------------------------
300
+ const intentPayloadSchemas = {
301
+ forget: z.object({ id: z.string().min(1) }),
302
+ link: z.object({
303
+ sourceId: z.string().min(1),
304
+ targetId: z.string().min(1),
305
+ relation: z.enum([
306
+ "relates_to",
307
+ "contradicts",
308
+ "supports",
309
+ "derived_from",
310
+ "part_of",
311
+ "temporal_precedes",
312
+ "custom",
313
+ ]),
314
+ weight: z.number().min(0).max(1).optional(),
315
+ }),
316
+ "release-quarantine": z.object({ id: z.string().min(1) }),
317
+ };
318
+ /**
319
+ * Translate a UI intent (posted by the app via `postMessage`) into the MCP
320
+ * tool call the host must issue. Pure and zod-validated: unknown intents
321
+ * and malformed params are rejected here, before any host consent UI.
322
+ *
323
+ * The host — never this module, never the app — performs the actual
324
+ * `tools/call`, so the host's consent model applies to every mutation.
325
+ * The app itself has no credentials and no database access.
326
+ */
327
+ export function intentToToolCall(raw) {
328
+ if (raw === null || raw === undefined || typeof raw !== "object") {
329
+ return { ok: false, error: "intent must be an object" };
330
+ }
331
+ const { intent, params } = raw;
332
+ if (typeof intent !== "string" ||
333
+ !SUPPORTED_INTENTS.includes(intent)) {
334
+ return {
335
+ ok: false,
336
+ error: `unknown intent ${JSON.stringify(intent)}; supported: ${SUPPORTED_INTENTS.join(", ")}`,
337
+ };
338
+ }
339
+ const name = intent;
340
+ const parsed = intentPayloadSchemas[name].safeParse(params ?? {});
341
+ if (!parsed.success) {
342
+ return { ok: false, error: `invalid params for intent "${name}"` };
343
+ }
344
+ const p = parsed.data;
345
+ switch (name) {
346
+ case "forget":
347
+ return {
348
+ ok: true,
349
+ intent: name,
350
+ call: { tool: "memos_forget", args: { id: p.id } },
351
+ };
352
+ case "link":
353
+ return {
354
+ ok: true,
355
+ intent: name,
356
+ call: {
357
+ tool: "memos_link",
358
+ args: {
359
+ sourceId: p.sourceId,
360
+ targetId: p.targetId,
361
+ relation: p.relation,
362
+ ...(p.weight !== undefined ? { weight: p.weight } : {}),
363
+ },
364
+ },
365
+ };
366
+ case "release-quarantine":
367
+ return {
368
+ ok: true,
369
+ intent: name,
370
+ call: { tool: "memos_quarantine_release", args: { id: p.id } },
371
+ };
372
+ }
373
+ }
374
+ /**
375
+ * Build the graceful-degradation payload: the mermaid diagram (reusing
376
+ * `graphToMermaid`, the same renderer as `memos://graph`) plus a summary.
377
+ * Served when {@link resolveExplorerMode} picks `"text"`.
378
+ */
379
+ export function buildExplorerFallbackMarkdown(snapshot, opts = {}) {
380
+ // Reuse the canonical mermaid renderer over the (capped) snapshot.
381
+ const graph = {
382
+ nodes: snapshot.nodes.map((n) => ({
383
+ id: n.id,
384
+ content: n.content,
385
+ type: n.type,
386
+ })),
387
+ edges: snapshot.edges.map((e) => ({
388
+ id: e.id,
389
+ sourceId: e.sourceId,
390
+ targetId: e.targetId,
391
+ relation: e.relation,
392
+ })),
393
+ };
394
+ const mermaid = graphToMermaid(graph);
395
+ const asOf = opts.asOf ?? snapshot.asOf;
396
+ const lines = [
397
+ "# MemOS memory explorer — text fallback",
398
+ "",
399
+ "> Your host does not support MCP Apps interactive UIs, so this is a",
400
+ "> static snapshot. Open `" +
401
+ EXPLORER_APP_URI +
402
+ "?mode=app` in an MCP Apps host",
403
+ "> (Claude, ChatGPT, Goose, or VS Code) for the interactive graph with",
404
+ "> the bitemporal time-travel scrubber.",
405
+ "",
406
+ asOf !== null && asOf !== undefined
407
+ ? `*As of ${new Date(asOf).toISOString()} (bitemporal time-travel view)*`
408
+ : "*Live view (current memory state)*",
409
+ "",
410
+ `Showing ${snapshot.nodes.length} of ${snapshot.totalNodes} memories and ` +
411
+ `${snapshot.edges.length} of ${snapshot.totalEdges} relations` +
412
+ (snapshot.truncated
413
+ ? ` (capped at ${EXPLORER_NODE_CAP} nodes / ${EXPLORER_EDGE_CAP} edges — see docs/mcp-apps-explorer.md)`
414
+ : "") +
415
+ ".",
416
+ "",
417
+ "## Graph (Mermaid)",
418
+ "",
419
+ "```mermaid",
420
+ mermaid,
421
+ "```",
422
+ "",
423
+ "## Memories (by importance)",
424
+ "",
425
+ ];
426
+ const top = [...snapshot.nodes]
427
+ .sort((a, b) => b.importance - a.importance)
428
+ .slice(0, 25);
429
+ for (const n of top) {
430
+ const flags = [
431
+ n.provenance,
432
+ `trust ${n.trustScore.toFixed(2)}`,
433
+ n.quarantined ? "QUARANTINED" : null,
434
+ n.validTo !== null ? "superseded" : null,
435
+ ]
436
+ .filter(Boolean)
437
+ .join(", ");
438
+ lines.push(`- [${n.type}] ${n.content} _(${flags})_ ${n.citation}`);
439
+ }
440
+ if (snapshot.nodes.length > top.length) {
441
+ lines.push("", `_…and ${snapshot.nodes.length - top.length} more._`);
442
+ }
443
+ lines.push("", "## Node color key (interactive app)", "", ...PROVENANCE_TIER_ORDER.map((t) => `- ${PROVENANCE_TIER_COLORS[t]} — \`${t}\``), "- red dashed outline — quarantined (pending review)", "", "Mutating actions from the interactive app (forget, link, release from", "quarantine) always go through the host's MCP tool-call consent flow —", "the app itself cannot write to memory.");
444
+ return lines.join("\n");
445
+ }
446
+ /** Minimal HTML/XML escaping for interpolated strings. */
447
+ function escapeHtml(s) {
448
+ return s
449
+ .replace(/&/g, "&amp;")
450
+ .replace(/</g, "&lt;")
451
+ .replace(/>/g, "&gt;")
452
+ .replace(/"/g, "&quot;");
453
+ }
454
+ /** Escape a JSON payload for embedding in <script type="application/json">. */
455
+ function embedJson(value) {
456
+ return JSON.stringify(value).replace(/</g, "\\u003c");
457
+ }
458
+ /**
459
+ * Build the single-file HTML app payload.
460
+ *
461
+ * Security properties (asserted by tests):
462
+ * - `<meta http-equiv="Content-Security-Policy">` locks the document down:
463
+ * no network, no plugins, inline scripts/styles only.
464
+ * - No external URLs anywhere in the payload (no CDNs, fonts, images).
465
+ * - An HTML comment states the host-side requirement:
466
+ * `<iframe sandbox="allow-scripts">` (no `allow-same-origin`).
467
+ * - The app is unprivileged: the only outbound channel is `postMessage`
468
+ * intents to the host; there is no database access from this layer.
469
+ */
470
+ export function buildExplorerHtml(snapshot, opts = {}) {
471
+ const snapJson = embedJson(snapshot);
472
+ const tierColorsJson = embedJson(PROVENANCE_TIER_COLORS);
473
+ const relColorsJson = embedJson(EDGE_RELATION_COLORS);
474
+ const tierOrderJson = embedJson([...PROVENANCE_TIER_ORDER, "unknown"]);
475
+ const asOf = opts.asOf ?? snapshot.asOf;
476
+ const title = asOf !== null && asOf !== undefined
477
+ ? "MemOS memory explorer — as of " + new Date(asOf).toISOString()
478
+ : "MemOS memory explorer";
479
+ // NOTE: client JS below deliberately avoids template literals and ${}
480
+ // so it can live inside this TypeScript template literal unescaped.
481
+ return ("<!DOCTYPE html>\n" +
482
+ "<!-- MemOS memory explorer (MCP App, " +
483
+ EXPLORER_PROTOCOL +
484
+ ").\n" +
485
+ ' HOST REQUIREMENT: render inside <iframe sandbox="allow-scripts">.\n' +
486
+ " Do NOT add allow-same-origin or allow-top-navigation: the app must\n" +
487
+ " stay opaque to the host origin. Mutating actions are intents posted\n" +
488
+ " via postMessage; the host turns them into MCP tool calls under its\n" +
489
+ " own consent UI. This payload makes zero network requests. -->\n" +
490
+ '<html lang="en">\n<head>\n<meta charset="utf-8">\n' +
491
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n' +
492
+ "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'none'; font-src 'none'; connect-src 'none'; base-uri 'none'; form-action 'none'\">\n" +
493
+ "<title>" +
494
+ escapeHtml(title) +
495
+ "</title>\n" +
496
+ "<style>\n" +
497
+ ":root{color-scheme:dark}\n" +
498
+ "body{margin:0;font:13px/1.45 system-ui,-apple-system,sans-serif;background:#14171c;color:#e7eaef}\n" +
499
+ "header{display:flex;align-items:center;gap:12px;padding:8px 12px;background:#1c2128;border-bottom:1px solid #2d333b;flex-wrap:wrap}\n" +
500
+ "header h1{font-size:14px;margin:0;font-weight:600}\n" +
501
+ ".badge{font-size:11px;padding:2px 8px;border-radius:10px;background:#0d419d;color:#fff}\n" +
502
+ ".badge.past{background:#9c6d00}\n" +
503
+ "#toolbar{display:flex;gap:10px;align-items:center;padding:8px 12px;background:#1c2128;border-bottom:1px solid #2d333b;flex-wrap:wrap}\n" +
504
+ "#toolbar input[type=text]{background:#0d1117;border:1px solid #2d333b;color:#e7eaef;border-radius:6px;padding:5px 8px;min-width:180px}\n" +
505
+ "#toolbar select{background:#0d1117;border:1px solid #2d333b;color:#e7eaef;border-radius:6px;padding:5px}\n" +
506
+ ".tiers{display:flex;gap:8px;flex-wrap:wrap;font-size:12px}\n" +
507
+ ".tiers label{display:flex;gap:4px;align-items:center;cursor:pointer}\n" +
508
+ ".dot{display:inline-block;width:10px;height:10px;border-radius:50%}\n" +
509
+ "#main{display:flex;height:calc(100vh - 148px);min-height:320px}\n" +
510
+ "#canvaswrap{flex:1;position:relative;min-width:0}\n" +
511
+ "#graph{width:100%;height:100%;display:block;cursor:grab;touch-action:none}\n" +
512
+ "#graph:active{cursor:grabbing}\n" +
513
+ "#scrub{position:absolute;left:12px;right:12px;bottom:10px;display:flex;gap:8px;align-items:center;background:rgba(28,33,40,.92);border:1px solid #2d333b;border-radius:8px;padding:6px 10px}\n" +
514
+ "#scrub input[type=range]{flex:1;accent-color:#1f6feb}\n" +
515
+ "#scrub button{background:#1f6feb;border:0;color:#fff;border-radius:6px;padding:4px 10px;cursor:pointer}\n" +
516
+ "#tlabel{font-size:11px;color:#9aa4b2;white-space:nowrap;min-width:210px}\n" +
517
+ "#panel{width:300px;border-left:1px solid #2d333b;background:#161b22;padding:12px;overflow-y:auto}\n" +
518
+ "#panel h2{font-size:13px;margin:0 0 8px}\n" +
519
+ "#panel .meta{font-size:12px;color:#9aa4b2;margin:4px 0}\n" +
520
+ "#panel .content{font-size:12px;background:#0d1117;border:1px solid #2d333b;border-radius:6px;padding:8px;margin:8px 0;white-space:pre-wrap;word-break:break-word}\n" +
521
+ "#panel button.action{display:block;width:100%;margin:6px 0;padding:7px;border-radius:6px;border:1px solid #2d333b;background:#21262d;color:#e7eaef;cursor:pointer}\n" +
522
+ "#panel button.action:hover{background:#2d333b}\n" +
523
+ "#panel button.action.danger{border-color:#7a2e2e}\n" +
524
+ "#panel button.action:disabled{opacity:.45;cursor:not-allowed}\n" +
525
+ "#panel details{font-size:11px;color:#9aa4b2;margin-top:8px}\n" +
526
+ "#panel pre{background:#0d1117;padding:8px;border-radius:6px;overflow-x:auto;font-size:11px}\n" +
527
+ "#panel label.f{display:block;font-size:11px;color:#9aa4b2;margin:8px 0 2px}\n" +
528
+ "#panel select,#panel input[type=text],#panel input[type=number]{width:100%;background:#0d1117;border:1px solid #2d333b;color:#e7eaef;border-radius:6px;padding:5px;box-sizing:border-box}\n" +
529
+ "#legend{font-size:11px;color:#9aa4b2}\n" +
530
+ "#legend .row{display:flex;gap:6px;align-items:center;margin:2px 0}\n" +
531
+ "#toast{position:fixed;left:50%;bottom:64px;transform:translateX(-50%);background:#1c2128;border:1px solid #2d333b;border-radius:8px;padding:8px 14px;font-size:12px;max-width:80%;display:none;z-index:10}\n" +
532
+ ".hint{font-size:11px;color:#9aa4b2}\n" +
533
+ "</style>\n</head>\n<body>\n" +
534
+ "<header>\n<h1>MemOS memory explorer</h1>\n" +
535
+ '<span class="badge" id="modebadge">live</span>\n' +
536
+ '<span class="hint" id="counts"></span>\n' +
537
+ '<span class="hint">nodes = provenance tier &middot; edge labels = relation &middot; drag to pan, wheel to zoom</span>\n' +
538
+ "</header>\n" +
539
+ '<div id="toolbar">\n' +
540
+ '<input type="text" id="q" placeholder="filter text\u2026" aria-label="filter text">\n' +
541
+ '<select id="typef" aria-label="filter type"><option value="all">all types</option></select>\n' +
542
+ '<div class="tiers" id="tierf"></div>\n' +
543
+ "</div>\n" +
544
+ '<div id="main">\n<div id="canvaswrap">\n' +
545
+ '<svg id="graph" role="img" aria-label="memory graph"></svg>\n' +
546
+ '<div id="scrub">\n<button id="livebtn" title="jump back to the live graph">Live</button>\n' +
547
+ '<input type="range" id="time" aria-label="as-of time scrubber">\n' +
548
+ '<span id="tlabel"></span>\n</div>\n</div>\n' +
549
+ '<aside id="panel">\n<h2>Details</h2>\n' +
550
+ '<div id="pbody"><p class="hint">Select a node to inspect it. Mutating actions ask the host for approval first.</p></div>\n' +
551
+ '<h2 style="margin-top:16px">Legend</h2>\n<div id="legend"></div>\n' +
552
+ "</aside>\n</div>\n" +
553
+ '<div id="toast" role="status"></div>\n' +
554
+ '<script type="application/json" id="memos-snapshot">' +
555
+ snapJson +
556
+ "</script>\n" +
557
+ "<script>\n" +
558
+ '"use strict";\n' +
559
+ 'var SNAP=JSON.parse(document.getElementById("memos-snapshot").textContent);\n' +
560
+ "var PROTOCOL=" +
561
+ JSON.stringify(EXPLORER_PROTOCOL) +
562
+ ";\n" +
563
+ "var TIER_COLORS=" +
564
+ tierColorsJson +
565
+ ";\n" +
566
+ "var REL_COLORS=" +
567
+ relColorsJson +
568
+ ";\n" +
569
+ "var TIER_ORDER=" +
570
+ tierOrderJson +
571
+ ";\n" +
572
+ 'var state={t:SNAP.asOf!==null?SNAP.asOf:SNAP.generatedAt,live:SNAP.asOf===null,q:"",tiers:{},type:"all",sel:null,tx:0,ty:0,k:1};\n' +
573
+ "TIER_ORDER.forEach(function(t){state.tiers[t]=true;});\n" +
574
+ "var $=function(id){return document.getElementById(id);};\n" +
575
+ 'function esc(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}\n' +
576
+ 'function toast(m){var t=$("toast");t.textContent=m;t.style.display="block";clearTimeout(t._h);t._h=setTimeout(function(){t.style.display="none";},4200);}\n' +
577
+ // as-of predicates (mirror the server read path)
578
+ "function validNode(n,t){return (n.validFrom===null||n.validFrom<=t)&&(n.validTo===null||n.validTo>=t);}\n" +
579
+ "function validEdge(e,t){return (e.validFrom===null||e.validFrom<=t)&&(e.validTo===null||e.validTo>t);}\n" +
580
+ // deterministic ring layout, computed once so nodes hold still while scrubbing
581
+ "function computeLayout(){\n" +
582
+ "var adj={};SNAP.nodes.forEach(function(n){adj[n.id]=[];});\n" +
583
+ "SNAP.edges.forEach(function(e){if(adj[e.sourceId]&&adj[e.targetId]){adj[e.sourceId].push(e.targetId);adj[e.targetId].push(e.sourceId);}});\n" +
584
+ "var order=SNAP.nodes.slice().sort(function(a,b){return b.importance-a.importance;});\n" +
585
+ "var depth={},q=[],maxD=0,i;\n" +
586
+ "if(order.length){q.push(order[0].id);depth[order[0].id]=0;}\n" +
587
+ "while(q.length){var id=q.shift(),d=depth[id];var nbs=adj[id]||[];for(i=0;i<nbs.length;i++){var nb=nbs[i];if(depth[nb]===undefined){depth[nb]=d+1;if(d+1>maxD)maxD=d+1;q.push(nb);}}}\n" +
588
+ "var rings={};SNAP.nodes.forEach(function(n){var dd=depth[n.id];if(dd===undefined)dd=maxD+1;if(!rings[dd])rings[dd]=[];rings[dd].push(n.id);});\n" +
589
+ "var pos={};Object.keys(rings).forEach(function(dk){var dd=+dk,ring=rings[dk];if(dd===0){pos[ring[0]]={x:0,y:0};return;}var R=150*dd;for(var j=0;j<ring.length;j++){var a=(2*Math.PI*j/ring.length)+(dd*0.37);pos[ring[j]]={x:Math.round(R*Math.cos(a)),y:Math.round(R*Math.sin(a))};}});\n" +
590
+ "return pos;}\n" +
591
+ "var POS=computeLayout();\n" +
592
+ // keep positions for nodes the app has already seen so the graph does not
593
+ // jump when a re-queried snapshot arrives; brand-new nodes go on an outer ring
594
+ "function ensureLayout(){var maxR=0,seen={},fresh=[],i;\n" +
595
+ "Object.keys(POS).forEach(function(id){seen[id]=1;var p=POS[id];var r=Math.sqrt(p.x*p.x+p.y*p.y);if(r>maxR)maxR=r;});\n" +
596
+ "SNAP.nodes.forEach(function(n){if(!seen[n.id])fresh.push(n);});\n" +
597
+ "for(i=0;i<fresh.length;i++){var a=2*Math.PI*i/Math.max(1,fresh.length);var R=maxR+150;POS[fresh[i].id]={x:Math.round(R*Math.cos(a)),y:Math.round(R*Math.sin(a))};}}\n" +
598
+ "function currentView(){\n" +
599
+ "var t=state.t;\n" +
600
+ "var nodes=SNAP.nodes.filter(function(n){\n" +
601
+ "if(!validNode(n,t))return false;\n" +
602
+ 'if(state.q){var hay=(n.content+" "+n.type+" "+n.id).toLowerCase();if(hay.indexOf(state.q)<0)return false;}\n' +
603
+ "if(!state.tiers[n.provenance])return false;\n" +
604
+ 'if(state.type!=="all"&&n.type!==state.type)return false;\n' +
605
+ "return true;});\n" +
606
+ "var ids={};nodes.forEach(function(n){ids[n.id]=1;});\n" +
607
+ "var edges=SNAP.edges.filter(function(e){return ids[e.sourceId]&&ids[e.targetId]&&validEdge(e,t);});\n" +
608
+ "return {nodes:nodes,edges:edges};}\n" +
609
+ // render
610
+ 'function nodeColor(n){return TIER_COLORS[n.provenance]||"#adb5bd";}\n' +
611
+ 'function relColor(r){return REL_COLORS[r]||"#868e96";}\n' +
612
+ 'function shortId(id){return id.replace(/-/g,"").slice(0,8);}\n' +
613
+ "function render(){\n" +
614
+ "var v=currentView(),svg=$('graph'),s='';\n" +
615
+ "var i,e,n,a,b,mx,my,lx,ly;\n" +
616
+ "for(i=0;i<v.edges.length;i++){e=v.edges[i];a=POS[e.sourceId];b=POS[e.targetId];if(!a||!b)continue;\n" +
617
+ "mx=(a.x+b.x)/2;my=(a.y+b.y)/2;\n" +
618
+ "s+='<line x1=\"'+a.x+'\" y1=\"'+a.y+'\" x2=\"'+b.x+'\" y2=\"'+b.y+'\" stroke=\"'+relColor(e.relation)+'\" stroke-width=\"'+(0.8+e.weight*1.6).toFixed(1)+'\" opacity=\"0.75\"/>';\n" +
619
+ 's+=\'<text x="\'+mx+\'" y="\'+(my-4)+\'" font-size="9" fill="#9aa4b2" text-anchor="middle">\'+esc(e.relation)+\'</text>\';}\n' +
620
+ "for(i=0;i<v.nodes.length;i++){n=v.nodes[i];var p=POS[n.id];if(!p)continue;var r=13+Math.min(8,n.importance*8);\n" +
621
+ 'var dash=n.quarantined?\' stroke-dasharray="5,3"\':"";\n' +
622
+ 'var stroke=n.quarantined?"#e03131":(n.validTo!==null?"#868e96":"#0d1117");\n' +
623
+ "var sw=n.quarantined?2.5:1.5;\n" +
624
+ "var op=(n.validTo!==null&&state.live)?0.55:1;\n" +
625
+ "s+='<g class=\"node\" data-id=\"'+esc(n.id)+'\" opacity=\"'+op+'\"><title>'+esc(n.type+\": \"+n.content)+'</title>';\n" +
626
+ "s+='<circle cx=\"'+p.x+'\" cy=\"'+p.y+'\" r=\"'+r.toFixed(1)+'\" fill=\"'+nodeColor(n)+'\" stroke=\"'+stroke+'\" stroke-width=\"'+sw+'\"'+dash+'/>';\n" +
627
+ // (keep building: label)
628
+ "s+='<text x=\"'+p.x+'\" y=\"'+(p.y+r+12)+'\" font-size=\"9\" fill=\"#c9d1d9\" text-anchor=\"middle\">'+esc(n.type)+' · '+esc(shortId(n.id))+'</text></g>';}\n" +
629
+ "svg.innerHTML='<g id=\"vp\" transform=\"translate('+state.tx+' '+state.ty+') scale('+state.k+')\">'+s+'</g>';\n" +
630
+ 'var c=$("counts");c.textContent=v.nodes.length+" of "+SNAP.totalNodes+" memories \u00b7 "+v.edges.length+" of "+SNAP.totalEdges+" relations"+(SNAP.truncated?" (capped)":"");\n' +
631
+ 'var live=state.live;$("modebadge").textContent=live?"live":"as-of";$("modebadge").className="badge"+(live?"":" past");\n' +
632
+ 'var lab=$("tlabel");lab.textContent=new Date(state.t).toLocaleString()+(live?" (live)":" (time travel)");\n' +
633
+ "}\n" +
634
+ // pan & zoom
635
+ 'var svg=$("graph"),drag=null;\n' +
636
+ 'svg.addEventListener("pointerdown",function(ev){drag={x:ev.clientX,y:ev.clientY,tx:state.tx,ty:state.ty};svg.setPointerCapture(ev.pointerId);});\n' +
637
+ 'svg.addEventListener("pointermove",function(ev){if(!drag)return;state.tx=drag.tx+(ev.clientX-drag.x);state.ty=drag.ty+(ev.clientY-drag.y);var vp=$("vp");if(vp)vp.setAttribute("transform","translate("+state.tx+" "+state.ty+") scale("+state.k+")");});\n' +
638
+ 'svg.addEventListener("pointerup",function(){drag=null;});\n' +
639
+ 'svg.addEventListener("wheel",function(ev){ev.preventDefault();var f=ev.deltaY<0?1.15:1/1.15;state.k=Math.min(6,Math.max(0.2,state.k*f));var vp=$("vp");if(vp)vp.setAttribute("transform","translate("+state.tx+" "+state.ty+") scale("+state.k+")");},{passive:false});\n' +
640
+ 'svg.addEventListener("click",function(ev){var g=ev.target.closest?ev.target.closest("g.node"):null;if(!g)return;selectNode(g.getAttribute("data-id"));});\n' +
641
+ // selection + panel
642
+ "function findNode(id){for(var i=0;i<SNAP.nodes.length;i++)if(SNAP.nodes[i].id===id)return SNAP.nodes[i];return null;}\n" +
643
+ "function selectNode(id){state.sel=id;renderPanel();}\n" +
644
+ 'function fmtT(t){return t===null?"\u2014":new Date(t).toLocaleString();}\n' +
645
+ "function renderPanel(){\n" +
646
+ 'var body=$("pbody"),n=state.sel?findNode(state.sel):null;\n' +
647
+ "if(!n){body.innerHTML='<p class=\"hint\">Select a node to inspect it. Mutating actions ask the host for approval first.</p>';return;}\n" +
648
+ "var h='';\n" +
649
+ "h+='<div class=\"meta\"><span class=\"dot\" style=\"background:'+nodeColor(n)+'\"></span> <b>'+esc(n.type)+'</b> · '+esc(n.provenance)+'</div>';\n" +
650
+ 'h+=\'<div class="content" id="pc"></div>\';\n' +
651
+ "h+='<div class=\"meta\">trust '+n.trustScore.toFixed(2)+' · importance '+n.importance.toFixed(2)+'</div>';\n" +
652
+ "h+='<div class=\"meta\">citation <code>'+esc(n.citation)+'</code></div>';\n" +
653
+ "h+='<div class=\"meta\">valid: '+esc(fmtT(n.validFrom))+' → '+esc(fmtT(n.validTo))+'</div>';\n" +
654
+ 'if(n.quarantined)h+=\'<div class="meta" style="color:#ff8787">quarantined: \'+esc(n.quarantineReason||"unspecified")+\'</div>\';\n' +
655
+ 'h+=\'<button class="action danger" id="a-forget">Forget this memory</button>\';\n' +
656
+ 'if(n.quarantined)h+=\'<button class="action" id="a-release">Release from quarantine</button>\';\n' +
657
+ // link form
658
+ 'h+=\'<label class="f">Link to memory</label><select id="link-target"></select>\';\n' +
659
+ 'h+=\'<label class="f">Relation</label><select id="link-rel">\'+\n' +
660
+ "Object.keys(REL_COLORS).map(function(r){return '<option value=\"'+r+'\">'+r+'</option>';}).join(\"\")+'</select>';\n" +
661
+ 'h+=\'<button class="action" id="a-link">Create link</button>\';\n' +
662
+ 'h+=\'<details><summary>Equivalent MCP tool call (what the host will run)</summary><pre id="toolpreview"></pre><button class="action" id="a-copy">Copy tool call JSON</button></details>\';\n' +
663
+ "h+='<p class=\"hint\">Actions are sent to the host as intents and need your approval there. Nothing is written directly.</p>';\n" +
664
+ "body.innerHTML=h;\n" +
665
+ '$("pc").textContent=n.content;\n' +
666
+ // target options
667
+ 'var ts=$("link-target");SNAP.nodes.forEach(function(m){if(m.id===n.id)return;var o=document.createElement("option");o.value=m.id;o.textContent=m.type+" \u00b7 "+shortId(m.id);ts.appendChild(o);});\n' +
668
+ 'var preview=$("toolpreview");\n' +
669
+ 'function showPreview(intent,params){var tc=toolCallFor(intent,params);preview.textContent=tc?JSON.stringify(tc,null,2):"unsupported intent";}\n' +
670
+ 'showPreview("forget",{id:n.id});\n' +
671
+ '$("a-forget").addEventListener("click",function(){var p={id:n.id};showPreview("forget",p);sendIntent("forget",p);});\n' +
672
+ 'var rel=$("a-release");if(rel)rel.addEventListener("click",function(){var p={id:n.id};showPreview("release-quarantine",p);sendIntent("release-quarantine",p);});\n' +
673
+ '$("a-link").addEventListener("click",function(){var p={sourceId:n.id,targetId:ts.value,relation:$("link-rel").value,weight:0.5};showPreview("link",p);sendIntent("link",p);});\n' +
674
+ '$("a-copy").addEventListener("click",function(){var t=preview.textContent;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).then(function(){toast("Copied tool call JSON");},function(){toast("Copy failed");});}else{toast("Clipboard unavailable");}});\n' +
675
+ "}\n" +
676
+ // intent bridge
677
+ "function toolCallFor(intent,params){\n" +
678
+ 'if(intent==="forget")return {tool:"memos_forget",args:{id:params.id}};\n' +
679
+ 'if(intent==="link")return {tool:"memos_link",args:{sourceId:params.sourceId,targetId:params.targetId,relation:params.relation,weight:params.weight}};\n' +
680
+ 'if(intent==="release-quarantine")return {tool:"memos_quarantine_release",args:{id:params.id}};\n' +
681
+ "return null;}\n" +
682
+ "var seq=0,pending={};\n" +
683
+ "function sendIntent(intent,params){\n" +
684
+ 'var id="req-"+(++seq)+"-"+Date.now();\n' +
685
+ 'var msg={protocol:PROTOCOL,kind:"intent",intent:intent,params:params,requestId:id};\n' +
686
+ "pending[id]=intent;\n" +
687
+ 'try{window.parent.postMessage(msg,"*");}catch(e){toast("postMessage failed");return;}\n' +
688
+ 'toast("Sent \'"+intent+"\' to the host \u2014 approve it there to apply.");\n' +
689
+ "}\n" +
690
+ 'window.addEventListener("message",function(ev){\n' +
691
+ "var d=ev.data;if(!d||d.protocol!==PROTOCOL)return;\n" +
692
+ 'if(d.kind==="snapshot"){onSnapshot(d);return;}\n' +
693
+ 'if(d.kind!=="intent-result")return;\n' +
694
+ 'var what=pending[d.requestId]||"intent";delete pending[d.requestId];\n' +
695
+ 'toast(d.ok?("Host applied \'"+what+"\'. Refresh the resource to see it."):("Host did not apply \'"+what+"\': "+(d.error||"declined")));\n' +
696
+ "});\n" +
697
+ // toolbar wiring
698
+ 'var qi=$("q");qi.addEventListener("input",function(){state.q=qi.value.trim().toLowerCase();render();});\n' +
699
+ 'var tf=$("typef");var types={};SNAP.nodes.forEach(function(n){types[n.type]=1;});\n' +
700
+ 'Object.keys(types).sort().forEach(function(t){var o=document.createElement("option");o.value=t;o.textContent=t;tf.appendChild(o);});\n' +
701
+ 'tf.addEventListener("change",function(){state.type=tf.value;render();});\n' +
702
+ 'var tierf=$("tierf");TIER_ORDER.forEach(function(t){\n' +
703
+ 'var lab=document.createElement("label");var cb=document.createElement("input");cb.type="checkbox";cb.checked=true;\n' +
704
+ 'cb.addEventListener("change",function(){state.tiers[t]=cb.checked;render();});\n' +
705
+ 'var dot=document.createElement("span");dot.className="dot";dot.style.background=TIER_COLORS[t]||"#adb5bd";\n' +
706
+ "lab.appendChild(cb);lab.appendChild(dot);lab.appendChild(document.createTextNode(t));tierf.appendChild(lab);});\n" +
707
+ // legend
708
+ 'var lg=$("legend");TIER_ORDER.forEach(function(t){var d=document.createElement("div");d.className="row";var s=document.createElement("span");s.className="dot";s.style.background=TIER_COLORS[t]||"#adb5bd";d.appendChild(s);d.appendChild(document.createTextNode(t));lg.appendChild(d);});\n' +
709
+ 'var qd=document.createElement("div");qd.className="row";qd.innerHTML=\'<span class="dot" style="background:transparent;border:2px dashed #e03131"></span>\';qd.appendChild(document.createTextNode("quarantined"));lg.appendChild(qd);\n' +
710
+ // scrubber: every position re-queries the server through the host bridge
711
+ // (read-only, no consent needed). When the host does not implement the
712
+ // bridge, scrubbing degrades to filtering the embedded snapshot locally.
713
+ 'var tr=$("time");\n' +
714
+ "function scrubBounds(){var tmin=SNAP.generatedAt;SNAP.nodes.forEach(function(n){if(n.createdAt<tmin)tmin=n.createdAt;});tr.min=tmin;tr.max=Math.max(SNAP.generatedAt,state.t);tr.step=60000;}\n" +
715
+ "scrubBounds();tr.value=state.t;\n" +
716
+ "var bridge={mode:0,wait:0,timer:0,debounce:0};\n" +
717
+ 'function extractSnapshot(html){try{var doc=new DOMParser().parseFromString(html,"text/html");var el=doc.getElementById("memos-snapshot");return el?JSON.parse(el.textContent):null;}catch(e){return null;}}\n' +
718
+ "function applySnapshot(snap){SNAP=snap;state.t=snap.asOf!==null?snap.asOf:snap.generatedAt;state.live=snap.asOf===null;ensureLayout();scrubBounds();tr.value=state.t;render();}\n" +
719
+ "function fallbackScrub(t){state.t=t;state.live=t>=SNAP.generatedAt;tr.value=state.t;render();}\n" +
720
+ "function fallbackLive(){state.t=SNAP.generatedAt;state.live=true;tr.value=state.t;render();}\n" +
721
+ "function onSnapshot(d){if(d.requestId!==bridge.wait)return;bridge.wait=0;clearTimeout(bridge.timer);\n" +
722
+ "if(d.ok&&d.html){var snap=extractSnapshot(d.html);if(snap){bridge.mode=1;applySnapshot(snap);return;}}\n" +
723
+ 'bridge.mode=2;toast("Host could not re-read the graph; scrubbing locally.");if(bridge.wantLive)fallbackLive();else fallbackScrub(bridge.wantT);}\n' +
724
+ "function requestSnapshot(t,live){\n" +
725
+ "var go=function(){if(live)fallbackLive();else fallbackScrub(t);};\n" +
726
+ "if(bridge.mode===2){go();return;}\n" +
727
+ 'var id="read-"+(++seq)+"-"+Date.now();bridge.wait=id;bridge.wantT=t;bridge.wantLive=live;\n' +
728
+ 'try{window.parent.postMessage({protocol:PROTOCOL,kind:"read",requestId:id,asOf:live?null:t},"*");}\n' +
729
+ "catch(e){bridge.mode=2;go();return;}\n" +
730
+ "clearTimeout(bridge.timer);\n" +
731
+ "bridge.timer=setTimeout(function(){if(bridge.wait===id){bridge.mode=2;bridge.wait=0;go();} },2000);\n" +
732
+ "}\n" +
733
+ 'tr.addEventListener("input",function(){var t=+tr.value;clearTimeout(bridge.debounce);bridge.debounce=setTimeout(function(){requestSnapshot(t,false);},300);});\n' +
734
+ '$("livebtn").addEventListener("click",function(){requestSnapshot(0,true);});\n' +
735
+ "render();\n" +
736
+ "</script>\n</body>\n</html>");
737
+ }
738
+ //# sourceMappingURL=explorer.js.map