recallnest 1.4.1 → 2.2.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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
package/assets/ui/app.js DELETED
@@ -1,807 +0,0 @@
1
- const queryInput = document.getElementById('queryInput');
2
- const profileInput = document.getElementById('profileInput');
3
- const scopeInput = document.getElementById('scopeInput');
4
- const limitInput = document.getElementById('limitInput');
5
- const formatInput = document.getElementById('formatInput');
6
- const resultOutput = document.getElementById('resultOutput');
7
- const resultMeta = document.getElementById('resultMeta');
8
- const resultTitle = document.getElementById('resultTitle');
9
- const artifactBar = document.getElementById('artifactBar');
10
- const resultCards = document.getElementById('resultCards');
11
- const viewToolbar = document.getElementById('viewToolbar');
12
- const viewFilterInput = document.getElementById('viewFilterInput');
13
- const assetTagBar = document.getElementById('assetTagBar');
14
- const assetOpsBar = document.getElementById('assetOpsBar');
15
- const dirtyBriefCount = document.getElementById('dirtyBriefCount');
16
- const statusLine = document.getElementById('statusLine');
17
- const pinMemoryId = document.getElementById('pinMemoryId');
18
- const pinTitle = document.getElementById('pinTitle');
19
- const pinsOutput = document.getElementById('pinsOutput');
20
- const statsOutput = document.getElementById('statsOutput');
21
- const toggleStatsButton = document.getElementById('toggleStatsButton');
22
- const togglePinsButton = document.getElementById('togglePinsButton');
23
- const toggleTraceButton = document.getElementById('toggleTraceButton');
24
- const viewTabs = Array.from(document.querySelectorAll('.view-tab'));
25
- const quickCards = Array.from(document.querySelectorAll('[data-quick-action]'));
26
-
27
- let currentView = 'search';
28
- let lastItems = [];
29
- let lastMode = 'search';
30
- let lastArtifact = null;
31
- let fullStatsText = 'Loading stats...';
32
- let fullPinsText = 'Loading pins...';
33
- let statsExpanded = false;
34
- let pinsExpanded = false;
35
- let traceExpanded = false;
36
- let lastPins = [];
37
- let lastExports = [];
38
- let currentViewFilter = '';
39
- let activeAssetTag = '';
40
- let lastDirtyBriefCount = 0;
41
-
42
- async function api(path, payload) {
43
- const response = await fetch(path, {
44
- method: payload ? 'POST' : 'GET',
45
- headers: payload ? { 'Content-Type': 'application/json' } : undefined,
46
- body: payload ? JSON.stringify(payload) : undefined,
47
- });
48
- const data = await response.json().catch(() => ({}));
49
- if (!response.ok) {
50
- throw new Error(data.output || `Request failed: ${response.status}`);
51
- }
52
- return data;
53
- }
54
-
55
- function currentPayload() {
56
- return {
57
- query: queryInput.value.trim(),
58
- profile: profileInput.value,
59
- scope: scopeInput.value.trim() || undefined,
60
- limit: Number(limitInput.value) || 5,
61
- };
62
- }
63
-
64
- function escapeHtml(text) {
65
- return String(text)
66
- .replace(/&/g, '&')
67
- .replace(/</g, '&lt;')
68
- .replace(/>/g, '&gt;')
69
- .replace(/"/g, '&quot;')
70
- .replace(/'/g, '&#39;');
71
- }
72
-
73
- function collapsedBlockText(text, maxLines) {
74
- const lines = String(text || '').split('\n');
75
- if (lines.length <= maxLines) return text;
76
- return `${lines.slice(0, maxLines).join('\n')}\n\n... (${lines.length - maxLines} more lines)`;
77
- }
78
-
79
- function renderStats() {
80
- statsOutput.textContent = statsExpanded ? fullStatsText : collapsedBlockText(fullStatsText, 12);
81
- statsOutput.classList.toggle('is-collapsed', !statsExpanded);
82
- toggleStatsButton.textContent = statsExpanded ? 'Collapse' : 'Expand';
83
- }
84
-
85
- function renderPinsPanel() {
86
- pinsOutput.textContent = pinsExpanded ? fullPinsText : collapsedBlockText(fullPinsText, 8);
87
- pinsOutput.classList.toggle('is-collapsed', !pinsExpanded);
88
- togglePinsButton.textContent = pinsExpanded ? 'Collapse' : 'Expand';
89
- }
90
-
91
- function renderTrace() {
92
- resultOutput.classList.toggle('is-collapsed', !traceExpanded);
93
- toggleTraceButton.textContent = traceExpanded ? 'Collapse' : 'Expand';
94
- }
95
-
96
- function cardSnippet(item) {
97
- const title = item?.metadata?.title;
98
- if (item?.source === 'asset' && title) {
99
- return title;
100
- }
101
-
102
- const text = String(item?.text || '').replace(/\s+/g, ' ').trim();
103
- if (!text) return '-';
104
-
105
- if (item?.source === 'asset') {
106
- const assetText = text
107
- .replace(/^\[Pinned Asset\]\s*/i, '')
108
- .replace(/Original Scope:.*$/i, '')
109
- .replace(/Snippet:.*$/i, '')
110
- .trim();
111
- return assetText || text;
112
- }
113
-
114
- return text;
115
- }
116
-
117
- function renderArtifactBar() {
118
- if (!lastArtifact) {
119
- artifactBar.innerHTML = '';
120
- return;
121
- }
122
-
123
- artifactBar.innerHTML = `
124
- <div class="artifact-card">
125
- <div class="artifact-copy">
126
- <strong>${escapeHtml(lastArtifact.label)}</strong>
127
- <span>${escapeHtml(lastArtifact.path)}</span>
128
- </div>
129
- <div class="result-card-actions">
130
- <button class="card-chip" id="copyArtifactPath">Copy Path</button>
131
- <button class="card-chip" id="openArtifactPath">Open File</button>
132
- </div>
133
- </div>
134
- `;
135
-
136
- document.getElementById('copyArtifactPath').addEventListener('click', async () => {
137
- await navigator.clipboard.writeText(lastArtifact.path);
138
- statusLine.textContent = 'Artifact path copied.';
139
- });
140
-
141
- document.getElementById('openArtifactPath').addEventListener('click', async () => {
142
- try {
143
- await api('/api/open-path', { path: lastArtifact.path });
144
- statusLine.textContent = 'Artifact opened.';
145
- } catch (error) {
146
- statusLine.textContent = String(error.message || error);
147
- }
148
- });
149
- }
150
-
151
- function setActiveView(view) {
152
- currentView = view;
153
- viewTabs.forEach((tab) => {
154
- tab.classList.toggle('is-active', tab.dataset.view === view);
155
- });
156
- const filterEnabled = view === 'pins' || view === 'exports';
157
- viewToolbar.classList.toggle('is-hidden', !filterEnabled);
158
- assetTagBar.classList.toggle('is-hidden', view !== 'pins');
159
- assetOpsBar.classList.toggle('is-hidden', view !== 'pins');
160
- if (!filterEnabled) {
161
- currentViewFilter = '';
162
- viewFilterInput.value = '';
163
- }
164
- if (view !== 'pins') {
165
- activeAssetTag = '';
166
- assetTagBar.innerHTML = '';
167
- }
168
- }
169
-
170
- function topAssetTags(items, limit = 8) {
171
- const counts = new Map();
172
- for (const item of items) {
173
- for (const tag of item.tags || []) {
174
- counts.set(tag, (counts.get(tag) || 0) + 1);
175
- }
176
- }
177
- return Array.from(counts.entries())
178
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
179
- .slice(0, limit);
180
- }
181
-
182
- function renderAssetTagBar(items) {
183
- if (currentView !== 'pins') {
184
- assetTagBar.innerHTML = '';
185
- assetTagBar.classList.add('is-hidden');
186
- return;
187
- }
188
-
189
- const tags = topAssetTags(items);
190
- if (tags.length === 0) {
191
- assetTagBar.innerHTML = '';
192
- assetTagBar.classList.add('is-hidden');
193
- return;
194
- }
195
-
196
- assetTagBar.classList.remove('is-hidden');
197
- assetTagBar.innerHTML = `
198
- <button class="tag-chip ${activeAssetTag ? '' : 'is-active'}" data-asset-tag="">
199
- All Tags
200
- </button>
201
- ${tags.map(([tag, count]) => `
202
- <button class="tag-chip ${activeAssetTag === tag ? 'is-active' : ''}" data-asset-tag="${escapeHtml(tag)}">
203
- ${escapeHtml(tag)} <span>${count}</span>
204
- </button>
205
- `).join('')}
206
- `;
207
- }
208
-
209
- function renderDirtyBriefOps() {
210
- dirtyBriefCount.textContent = `Dirty briefs: ${lastDirtyBriefCount}`;
211
- }
212
-
213
- function filterPins(items) {
214
- const needle = currentViewFilter.trim().toLowerCase();
215
- return items.filter((item) => {
216
- const matchesText = !needle || (
217
- [item.title, item.summary, item.scope, item.type, item.hits, ...(item.tags || [])]
218
- .filter(Boolean)
219
- .join(' ')
220
- .toLowerCase()
221
- .includes(needle)
222
- );
223
- const matchesTag = !activeAssetTag || (item.tags || []).includes(activeAssetTag);
224
- return matchesText && matchesTag;
225
- });
226
- }
227
-
228
- function filterExports(items) {
229
- const needle = currentViewFilter.trim().toLowerCase();
230
- if (!needle) return items;
231
- return items.filter((item) =>
232
- [item.query, item.profile, item.summary]
233
- .filter(Boolean)
234
- .join(' ')
235
- .toLowerCase()
236
- .includes(needle),
237
- );
238
- }
239
-
240
- function groupBySource(items) {
241
- const groups = new Map();
242
- for (const item of items) {
243
- const key = item.source || 'unknown';
244
- if (!groups.has(key)) groups.set(key, []);
245
- groups.get(key).push(item);
246
- }
247
- return Array.from(groups.entries());
248
- }
249
-
250
- function bindResultCardActions() {
251
- resultCards.querySelectorAll('[data-fill-id]').forEach((button) => {
252
- button.addEventListener('click', () => {
253
- pinMemoryId.value = button.dataset.fillId;
254
- statusLine.textContent = `Loaded ${button.dataset.fillId} into pin panel.`;
255
- });
256
- });
257
-
258
- resultCards.querySelectorAll('[data-pin-id]').forEach((button) => {
259
- button.addEventListener('click', async () => {
260
- pinMemoryId.value = button.dataset.pinId;
261
- await pinMemory();
262
- });
263
- });
264
-
265
- resultCards.querySelectorAll('[data-toggle-id]').forEach((button) => {
266
- button.addEventListener('click', () => {
267
- const card = button.closest('.result-card');
268
- card.classList.toggle('is-open');
269
- button.textContent = card.classList.contains('is-open') ? 'Hide' : 'Details';
270
- });
271
- });
272
-
273
- resultCards.querySelectorAll('[data-copy-text]').forEach((button) => {
274
- button.addEventListener('click', async () => {
275
- const text = decodeURIComponent(button.dataset.copyText);
276
- await navigator.clipboard.writeText(text);
277
- statusLine.textContent = 'Snippet copied to clipboard.';
278
- });
279
- });
280
-
281
- resultCards.querySelectorAll('[data-open-path]').forEach((button) => {
282
- button.addEventListener('click', async () => {
283
- try {
284
- await api('/api/open-path', { path: button.dataset.openPath });
285
- statusLine.textContent = 'File opened.';
286
- } catch (error) {
287
- statusLine.textContent = String(error.message || error);
288
- }
289
- });
290
- });
291
-
292
- resultCards.querySelectorAll('[data-copy-path]').forEach((button) => {
293
- button.addEventListener('click', async () => {
294
- await navigator.clipboard.writeText(button.dataset.copyPath);
295
- statusLine.textContent = 'Path copied.';
296
- });
297
- });
298
-
299
- resultCards.querySelectorAll('[data-run-query]').forEach((button) => {
300
- button.addEventListener('click', async () => {
301
- queryInput.value = decodeURIComponent(button.dataset.runQuery || '');
302
- scopeInput.value = button.dataset.runScope || '';
303
- setActiveView('search');
304
- statusLine.textContent = 'Running evidence search...';
305
- await runMode('search');
306
- });
307
- });
308
-
309
- resultCards.querySelectorAll('[data-asset-tag]').forEach((button) => {
310
- button.addEventListener('click', () => {
311
- activeAssetTag = button.dataset.assetTag || '';
312
- renderMainSurface();
313
- statusLine.textContent = activeAssetTag
314
- ? `Filtered assets by tag: ${activeAssetTag}`
315
- : 'Cleared asset tag filter.';
316
- });
317
- });
318
- }
319
-
320
- function renderSearchCards(items, mode) {
321
- if (!items || items.length === 0) {
322
- resultCards.innerHTML = '<div class="empty-state">No structured results yet. Run a query or broaden the profile.</div>';
323
- return;
324
- }
325
-
326
- const groups = groupBySource(items);
327
- resultCards.innerHTML = groups.map(([source, entries]) => `
328
- <section class="result-group">
329
- <div class="result-group-head">
330
- <strong>${escapeHtml(source)}</strong>
331
- <span>${entries.length} hit${entries.length > 1 ? 's' : ''}</span>
332
- </div>
333
- ${entries.map((item) => `
334
- <article class="result-card ${item.source === 'asset' ? 'is-asset' : ''}" data-card-id="${escapeHtml(item.shortId)}">
335
- <div class="result-card-header">
336
- <div class="result-card-meta">
337
- <span class="result-id">${escapeHtml(item.shortId)}</span>
338
- <span class="result-score">${escapeHtml(item.score)}%</span>
339
- <span>${escapeHtml(item.source)}</span>
340
- <span>${escapeHtml(item.date)}</span>
341
- </div>
342
- </div>
343
- <p class="result-snippet">${escapeHtml(cardSnippet(item).slice(0, 220))}${cardSnippet(item).length > 220 ? '...' : ''}</p>
344
- <div class="result-card-meta">
345
- <span>${escapeHtml(item.scope)}</span>
346
- <span>${escapeHtml(item.retrievalPath)}</span>
347
- <span>${escapeHtml(item.file || '-')}</span>
348
- </div>
349
- <div class="result-card-actions">
350
- <button class="card-chip" data-fill-id="${escapeHtml(item.shortId)}">Use ID</button>
351
- <button class="card-chip" data-pin-id="${escapeHtml(item.shortId)}">Pin</button>
352
- <button class="card-chip" data-toggle-id="${escapeHtml(item.shortId)}">Details</button>
353
- ${mode === 'distill' ? '' : `<button class="card-chip" data-copy-text="${encodeURIComponent(item.text)}">Copy Text</button>`}
354
- </div>
355
- <div class="result-card-detail">
356
- <div class="detail-block">
357
- <strong>Full Text</strong>
358
- <pre>${escapeHtml(item.text)}</pre>
359
- </div>
360
- <div class="detail-block">
361
- <strong>Metadata</strong>
362
- <code>${escapeHtml(JSON.stringify(item.metadata || {}, null, 2))}</code>
363
- </div>
364
- </div>
365
- </article>
366
- `).join('')}
367
- </section>
368
- `).join('');
369
-
370
- bindResultCardActions();
371
- }
372
-
373
- function formatEvidenceList(evidence) {
374
- if (!Array.isArray(evidence) || evidence.length === 0) return 'No evidence yet.';
375
- return evidence.map((item, index) => {
376
- const parts = [
377
- `${index + 1}. ${item.source || '-'}`,
378
- item.date || '-',
379
- item.retrievalPath || '-',
380
- item.scope || '-',
381
- item.snippet || '-',
382
- ];
383
- return parts.join(' | ');
384
- }).join('\n');
385
- }
386
-
387
- function formatSourceSummary(sources) {
388
- if (!Array.isArray(sources) || sources.length === 0) return '-';
389
- return sources.map((item) => `${item.source} (${item.hits})`).join(', ');
390
- }
391
-
392
- function assetDetailMarkup(item) {
393
- if (item.type === 'memory-brief') {
394
- return `
395
- <div class="result-card-detail">
396
- <div class="detail-block">
397
- <strong>Brief Meta</strong>
398
- <code>Query: ${escapeHtml(item.query || '-')}
399
- Profile: ${escapeHtml(item.profile || '-')}
400
- Hits: ${escapeHtml(String(item.hits || 0))}
401
- Sources: ${escapeHtml(formatSourceSummary(item.sources || []))}</code>
402
- <div class="detail-actions">
403
- <button class="card-chip" data-run-query="${encodeURIComponent(item.query || '')}" data-run-scope="">Run Brief Query</button>
404
- </div>
405
- </div>
406
- <div class="detail-block">
407
- <strong>Takeaways</strong>
408
- <pre>${escapeHtml((item.takeaways || []).map((line, index) => `${index + 1}. ${line}`).join('\n') || 'No takeaways yet.')}</pre>
409
- </div>
410
- <div class="detail-block">
411
- <strong>Evidence</strong>
412
- <pre>${escapeHtml(formatEvidenceList(item.evidence || []))}</pre>
413
- <div class="detail-actions">
414
- ${(item.evidence || []).map((evidence, index) => `
415
- <button
416
- class="card-chip evidence-chip"
417
- data-run-query="${encodeURIComponent(evidence.snippet || '')}"
418
- data-run-scope="${escapeHtml(evidence.scope || '')}"
419
- >
420
- ${escapeHtml(`${index + 1}. ${evidence.source || '-'} | ${evidence.date || '-'} | rerun`)}
421
- </button>
422
- `).join('')}
423
- </div>
424
- </div>
425
- <div class="detail-block">
426
- <strong>Reusable Candidates</strong>
427
- <pre>${escapeHtml((item.reusableCandidates || []).map((line, index) => `${index + 1}. ${line}`).join('\n') || 'No reusable candidates yet.')}</pre>
428
- </div>
429
- </div>
430
- `;
431
- }
432
-
433
- return `
434
- <div class="result-card-detail">
435
- <div class="detail-block">
436
- <strong>Pin Meta</strong>
437
- <code>Source Memory: ${escapeHtml(item.sourceMemoryId || '-')}
438
- Source Scope: ${escapeHtml(item.sourceScope || '-')}
439
- Query: ${escapeHtml(item.retrieval?.query || '-')}
440
- Profile: ${escapeHtml(item.retrieval?.profile || '-')}
441
- Path: ${escapeHtml(item.retrieval?.path || '-')}</code>
442
- </div>
443
- <div class="detail-block">
444
- <strong>Snippet</strong>
445
- <pre>${escapeHtml(item.snippet || item.summary || '-')}</pre>
446
- </div>
447
- <div class="detail-block">
448
- <strong>Tags</strong>
449
- <pre>${escapeHtml((item.tags || []).join(', ') || '-')}</pre>
450
- </div>
451
- </div>
452
- `;
453
- }
454
-
455
- function renderPinsView(items) {
456
- if (!items || items.length === 0) {
457
- resultCards.innerHTML = '<div class="empty-state">No memory assets yet.</div>';
458
- return;
459
- }
460
-
461
- resultCards.innerHTML = items.map((item) => `
462
- <article class="list-card result-card is-asset">
463
- <div class="list-card-head">
464
- <strong>${escapeHtml(item.title)}</strong>
465
- </div>
466
- <div class="list-card-meta">
467
- <span>${escapeHtml(item.shortId)}</span>
468
- <span>${escapeHtml(item.type === 'memory-brief' ? 'brief' : 'pin')}</span>
469
- <span>${escapeHtml(item.scope)}</span>
470
- ${item.hits ? `<span>${escapeHtml(String(item.hits))} hits</span>` : ''}
471
- <span>${escapeHtml(item.date)}</span>
472
- </div>
473
- <p class="result-snippet">${escapeHtml(item.summary || '')}</p>
474
- <div class="result-card-meta">
475
- ${(item.tags || []).slice(0, 6).map((tag) => `
476
- <button class="tag-chip inline" data-asset-tag="${escapeHtml(tag)}">${escapeHtml(tag)}</button>
477
- `).join('') || '<span>-</span>'}
478
- </div>
479
- <div class="result-card-actions">
480
- <button class="card-chip" data-toggle-id="${escapeHtml(item.shortId)}">Details</button>
481
- <button class="card-chip" data-copy-path="${escapeHtml(item.path)}">Copy Path</button>
482
- <button class="card-chip" data-open-path="${escapeHtml(item.path)}">Open File</button>
483
- </div>
484
- ${assetDetailMarkup(item)}
485
- </article>
486
- `).join('');
487
-
488
- bindResultCardActions();
489
- }
490
-
491
- function renderExportsView(items) {
492
- if (!items || items.length === 0) {
493
- resultCards.innerHTML = '<div class="empty-state">No exports yet.</div>';
494
- return;
495
- }
496
-
497
- resultCards.innerHTML = items.map((item) => `
498
- <article class="list-card">
499
- <div class="list-card-head">
500
- <strong>${escapeHtml(item.query || item.shortId)}</strong>
501
- </div>
502
- <div class="list-card-meta">
503
- <span>${escapeHtml(item.shortId)}</span>
504
- <span>${escapeHtml(item.profile)}</span>
505
- <span>${escapeHtml(item.format)}</span>
506
- <span>${escapeHtml(item.date)}</span>
507
- </div>
508
- <p class="result-snippet">${escapeHtml((item.summary || '').slice(0, 260))}${item.summary && item.summary.length > 260 ? '...' : ''}</p>
509
- <div class="result-card-actions">
510
- <button class="card-chip" data-copy-path="${escapeHtml(item.path)}">Copy Path</button>
511
- <button class="card-chip" data-open-path="${escapeHtml(item.path)}">Open File</button>
512
- </div>
513
- </article>
514
- `).join('');
515
-
516
- bindResultCardActions();
517
- }
518
-
519
- function renderMainSurface() {
520
- if (currentView === 'pins') {
521
- const filtered = filterPins(lastPins);
522
- resultTitle.textContent = 'Memory Assets';
523
- resultMeta.textContent = `Assets: ${filtered.length}${currentViewFilter || activeAssetTag ? ` / ${lastPins.length} total` : ''}${activeAssetTag ? ` | Tag: ${activeAssetTag}` : ''}`;
524
- renderAssetTagBar(lastPins);
525
- renderPinsView(filtered);
526
- return;
527
- }
528
-
529
- if (currentView === 'exports') {
530
- const filtered = filterExports(lastExports);
531
- resultTitle.textContent = 'Export Artifacts';
532
- resultMeta.textContent = `Exports: ${filtered.length}${currentViewFilter ? ` / ${lastExports.length} total` : ''}`;
533
- renderExportsView(filtered);
534
- return;
535
- }
536
-
537
- resultTitle.textContent = 'Result Surface';
538
- resultMeta.textContent = `Mode: ${lastMode} | Profile: ${profileInput.value} | Query: ${queryInput.value.trim() || '-'} | Hits: ${lastItems.length}`;
539
- renderSearchCards(lastItems, lastMode);
540
- }
541
-
542
- async function runMode(mode) {
543
- const payload = currentPayload();
544
- if (!payload.query) {
545
- statusLine.textContent = 'Enter a query first.';
546
- return;
547
- }
548
- statusLine.textContent = `Running ${mode}...`;
549
- resultOutput.textContent = 'Loading...';
550
- setActiveView('search');
551
- try {
552
- const data = await api(`/api/${mode}`, payload);
553
- lastItems = data.items || [];
554
- lastMode = mode;
555
- lastArtifact = null;
556
- resultOutput.textContent = data.output;
557
- renderArtifactBar();
558
- renderMainSurface();
559
- statusLine.textContent = `${mode} completed.`;
560
- } catch (error) {
561
- resultOutput.textContent = String(error.message || error);
562
- lastArtifact = null;
563
- lastItems = [];
564
- renderArtifactBar();
565
- renderMainSurface();
566
- statusLine.textContent = `${mode} failed.`;
567
- }
568
- }
569
-
570
- async function loadPins() {
571
- try {
572
- const data = await api('/api/pins');
573
- lastPins = data.items || [];
574
- fullPinsText = data.output;
575
- renderPinsPanel();
576
- renderAssetTagBar(lastPins);
577
- await loadDirtyBriefs();
578
- if (currentView === 'pins') renderMainSurface();
579
- } catch (error) {
580
- fullPinsText = String(error.message || error);
581
- renderPinsPanel();
582
- renderAssetTagBar([]);
583
- lastDirtyBriefCount = 0;
584
- renderDirtyBriefOps();
585
- }
586
- }
587
-
588
- async function loadDirtyBriefs() {
589
- try {
590
- const data = await api('/api/dirty-briefs');
591
- lastDirtyBriefCount = Number(data.count || 0);
592
- renderDirtyBriefOps();
593
- } catch {
594
- lastDirtyBriefCount = 0;
595
- renderDirtyBriefOps();
596
- }
597
- }
598
-
599
- async function loadExports() {
600
- try {
601
- const data = await api('/api/exports');
602
- lastExports = data.items || [];
603
- if (currentView === 'exports') renderMainSurface();
604
- } catch (error) {
605
- statusLine.textContent = String(error.message || error);
606
- }
607
- }
608
-
609
- async function loadStats() {
610
- try {
611
- const data = await api('/api/stats');
612
- fullStatsText = data.output;
613
- renderStats();
614
- } catch (error) {
615
- fullStatsText = String(error.message || error);
616
- renderStats();
617
- }
618
- }
619
-
620
- async function pinMemory() {
621
- const memoryId = pinMemoryId.value.trim();
622
- if (!memoryId) {
623
- statusLine.textContent = 'Paste a memory ID first.';
624
- return;
625
- }
626
- statusLine.textContent = 'Pinning memory...';
627
- try {
628
- const data = await api('/api/pin', {
629
- memoryId,
630
- title: pinTitle.value.trim() || undefined,
631
- query: queryInput.value.trim() || undefined,
632
- profile: profileInput.value,
633
- });
634
- lastArtifact = {
635
- label: `Pinned Asset ${data.assetId.slice(0, 8)}`,
636
- path: data.path,
637
- };
638
- resultOutput.textContent = data.output;
639
- renderArtifactBar();
640
- statusLine.textContent = `Memory pinned: ${data.assetId.slice(0, 8)}.`;
641
- await loadPins();
642
- await loadStats();
643
- } catch (error) {
644
- resultOutput.textContent = String(error.message || error);
645
- statusLine.textContent = 'Pin failed.';
646
- }
647
- }
648
-
649
- async function createBrief() {
650
- const payload = currentPayload();
651
- if (!payload.query) {
652
- statusLine.textContent = 'Enter a query first.';
653
- return;
654
- }
655
- statusLine.textContent = 'Creating brief...';
656
- try {
657
- const data = await api('/api/brief', {
658
- ...payload,
659
- title: pinTitle.value.trim() || undefined,
660
- });
661
- lastArtifact = {
662
- label: `Brief ${data.assetId.slice(0, 8)}`,
663
- path: data.path,
664
- };
665
- resultOutput.textContent = data.output;
666
- renderArtifactBar();
667
- await loadPins();
668
- await loadStats();
669
- statusLine.textContent = 'Brief created.';
670
- } catch (error) {
671
- resultOutput.textContent = String(error.message || error);
672
- statusLine.textContent = 'Brief failed.';
673
- }
674
- }
675
-
676
- async function exportMemory() {
677
- const payload = currentPayload();
678
- if (!payload.query) {
679
- statusLine.textContent = 'Enter a query first.';
680
- return;
681
- }
682
- statusLine.textContent = 'Exporting...';
683
- try {
684
- const data = await api('/api/export', {
685
- ...payload,
686
- format: formatInput.value,
687
- });
688
- lastArtifact = {
689
- label: `Export ${data.artifactId.slice(0, 8)} (${data.format})`,
690
- path: data.path,
691
- };
692
- resultOutput.textContent = data.output;
693
- renderArtifactBar();
694
- await loadExports();
695
- statusLine.textContent = `Export completed: ${data.format}.`;
696
- } catch (error) {
697
- resultOutput.textContent = String(error.message || error);
698
- statusLine.textContent = 'Export failed.';
699
- }
700
- }
701
-
702
- async function cleanDirtyBriefs() {
703
- statusLine.textContent = 'Archiving dirty briefs...';
704
- try {
705
- const data = await api('/api/clean-dirty-briefs', {});
706
- resultOutput.textContent = data.output;
707
- await loadPins();
708
- await loadStats();
709
- renderMainSurface();
710
- statusLine.textContent = data.count > 0 ? 'Dirty briefs archived.' : 'No dirty briefs found.';
711
- } catch (error) {
712
- resultOutput.textContent = String(error.message || error);
713
- statusLine.textContent = 'Dirty brief cleanup failed.';
714
- }
715
- }
716
-
717
- async function handleQuickAction(action) {
718
- if (action === 'search') {
719
- await runMode('search');
720
- return;
721
- }
722
- if (action === 'distill') {
723
- await runMode('distill');
724
- return;
725
- }
726
- if (action === 'pin') {
727
- pinMemoryId.focus();
728
- pinMemoryId.scrollIntoView({ behavior: 'smooth', block: 'center' });
729
- statusLine.textContent = 'Pin flow focused.';
730
- }
731
- }
732
-
733
- viewTabs.forEach((tab) => {
734
- tab.addEventListener('click', async () => {
735
- const view = tab.dataset.view;
736
- setActiveView(view);
737
- if (view === 'pins') {
738
- await loadPins();
739
- }
740
- if (view === 'exports') {
741
- await loadExports();
742
- }
743
- renderMainSurface();
744
- });
745
- });
746
-
747
- document.querySelectorAll('[data-mode]').forEach((button) => {
748
- button.addEventListener('click', () => runMode(button.dataset.mode));
749
- });
750
-
751
- document.getElementById('pinButton').addEventListener('click', pinMemory);
752
- document.getElementById('reloadPinsButton').addEventListener('click', async () => {
753
- await loadPins();
754
- await loadExports();
755
- renderMainSurface();
756
- });
757
- document.getElementById('statsButton').addEventListener('click', loadStats);
758
- document.getElementById('briefButton').addEventListener('click', createBrief);
759
- document.getElementById('exportButton').addEventListener('click', exportMemory);
760
- document.getElementById('cleanDirtyBriefsButton').addEventListener('click', cleanDirtyBriefs);
761
- quickCards.forEach((card) => {
762
- card.addEventListener('click', async () => {
763
- await handleQuickAction(card.dataset.quickAction);
764
- });
765
- });
766
- toggleStatsButton.addEventListener('click', () => {
767
- statsExpanded = !statsExpanded;
768
- renderStats();
769
- });
770
- togglePinsButton.addEventListener('click', () => {
771
- pinsExpanded = !pinsExpanded;
772
- renderPinsPanel();
773
- });
774
- toggleTraceButton.addEventListener('click', () => {
775
- traceExpanded = !traceExpanded;
776
- renderTrace();
777
- });
778
- viewFilterInput.addEventListener('input', () => {
779
- currentViewFilter = viewFilterInput.value;
780
- if (currentView === 'pins' || currentView === 'exports') {
781
- renderMainSurface();
782
- }
783
- });
784
-
785
- assetTagBar.addEventListener('click', (event) => {
786
- const button = event.target.closest('[data-asset-tag]');
787
- if (!button) return;
788
- activeAssetTag = button.dataset.assetTag || '';
789
- renderMainSurface();
790
- statusLine.textContent = activeAssetTag
791
- ? `Filtered assets by tag: ${activeAssetTag}`
792
- : 'Cleared asset tag filter.';
793
- });
794
-
795
- queryInput.value = 'telegram bridge';
796
- profileInput.value = 'debug';
797
- setActiveView('search');
798
- renderArtifactBar();
799
- renderStats();
800
- renderPinsPanel();
801
- renderDirtyBriefOps();
802
- renderTrace();
803
- renderMainSurface();
804
- loadPins();
805
- loadExports();
806
- loadStats();
807
- runMode('search');