total-recall-brain 3.28.2 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/bin/total-recall.mjs +24 -1
  2. package/docs/ARCHITECTURE.md +26 -18
  3. package/extension/background.js +259 -84
  4. package/extension/content-script.css +7 -13
  5. package/extension/content-script.js +252 -477
  6. package/extension/icons/icon-128.png +0 -0
  7. package/extension/icons/icon-16.png +0 -0
  8. package/extension/icons/icon-32.png +0 -0
  9. package/extension/icons/icon-48.png +0 -0
  10. package/extension/icons/mark.svg +6 -0
  11. package/extension/lib/brain-client.js +160 -73
  12. package/extension/lib/tokens.css +187 -0
  13. package/extension/lib/ui-helpers.js +229 -0
  14. package/extension/lib/ui-helpers.spec.mjs +94 -0
  15. package/extension/manifest.json +20 -5
  16. package/extension/options/options.css +64 -218
  17. package/extension/options/options.html +91 -43
  18. package/extension/options/options.js +208 -85
  19. package/extension/sidepanel/sidepanel.css +268 -970
  20. package/extension/sidepanel/sidepanel.html +200 -166
  21. package/extension/sidepanel/sidepanel.js +788 -942
  22. package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
  23. package/frontend/dist/index.html +1 -1
  24. package/frontend/src/api/plugins.ts +124 -92
  25. package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
  26. package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
  27. package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
  28. package/frontend/src/pages/PluginsPage.tsx +454 -1218
  29. package/package.json +2 -1
  30. package/plugins/code-quality/README.md +25 -0
  31. package/plugins/code-quality/cli.mjs +25 -0
  32. package/plugins/code-quality/plugin.json +17 -0
  33. package/plugins/git-sentinel/README.md +27 -0
  34. package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
  35. package/plugins/git-sentinel/plugin.json +18 -0
  36. package/plugins/system-monitor/README.md +26 -0
  37. package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
  38. package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
  39. package/plugins/system-monitor/plugin.json +29 -0
  40. package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
  41. package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
  42. package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
  43. package/src/cli/agent.mjs +1 -1
  44. package/src/cli/app/index.mjs +493 -0
  45. package/src/cli/brain.mjs +33 -3
  46. package/src/cli/command.mjs +80 -41
  47. package/src/cli/harness.mjs +1 -1
  48. package/src/cli/init.mjs +3 -1
  49. package/src/cli/mesh.mjs +118 -12
  50. package/src/cli/plugin/create.mjs +144 -25
  51. package/src/cli/plugin/index.mjs +83 -53
  52. package/src/cli/plugin/install.mjs +10 -127
  53. package/src/cli/plugin/list.mjs +44 -44
  54. package/src/cli/plugin/remove.mjs +23 -30
  55. package/src/cli/plugin/scaffold-cmd.mjs +30 -0
  56. package/src/cli/plugin/search.mjs +108 -23
  57. package/src/cli/rebuild.mjs +37 -1
  58. package/src/cli/recall.mjs +17 -2
  59. package/src/cli/research.mjs +4 -6
  60. package/src/cli/share.mjs +2 -0
  61. package/src/core/app-deploy/apply.mjs +386 -0
  62. package/src/core/app-deploy/design-tokens.mjs +217 -0
  63. package/src/core/app-deploy/plan.mjs +249 -0
  64. package/src/core/app-deploy/resolve.mjs +295 -0
  65. package/src/core/app-deploy/source.mjs +337 -0
  66. package/src/core/app-deploy/standalone.mjs +248 -0
  67. package/src/core/app-deploy/upgrade.mjs +256 -0
  68. package/src/core/app-deploy/verify.mjs +213 -0
  69. package/src/core/bound-hosts.mjs +1 -1
  70. package/src/core/brain-registry.mjs +146 -0
  71. package/src/core/brain-state.json +1 -0
  72. package/src/core/daemon-loop.mjs +30 -26
  73. package/src/core/dream.mjs +5 -17
  74. package/src/core/fact-seeker.mjs +53 -368
  75. package/src/core/lan-discovery.mjs +3 -3
  76. package/src/core/mesh-access-sync.mjs +134 -0
  77. package/src/core/mesh-access.mjs +18 -3
  78. package/src/core/mesh-discover.mjs +173 -0
  79. package/src/core/mesh.mjs +131 -32
  80. package/src/core/optimizer.mjs +4 -41
  81. package/src/core/plugin-bundle.mjs +167 -0
  82. package/src/core/plugin-context.mjs +8 -13
  83. package/src/core/plugin-loader.mjs +272 -68
  84. package/src/core/plugin-peers.mjs +163 -0
  85. package/src/core/plugin-public.mjs +85 -0
  86. package/src/core/plugin-runner-child.mjs +24 -0
  87. package/src/core/plugin-runner.mjs +102 -0
  88. package/src/core/plugin-store.mjs +384 -0
  89. package/src/core/plugin-tasks.mjs +212 -0
  90. package/src/core/post-mortem.mjs +40 -9
  91. package/src/core/research-gate.mjs +182 -0
  92. package/src/core/research-queue.mjs +47 -7
  93. package/src/core/research-surface.mjs +211 -0
  94. package/src/core/research.mjs +25 -32
  95. package/src/core/scheduler.mjs +67 -153
  96. package/src/core/schema.mjs +129 -0
  97. package/src/core/search.mjs +12 -1
  98. package/src/core/secret-integration-research.mjs +12 -8
  99. package/src/core/secrets-store.mjs +32 -2
  100. package/src/core/session-watcher.mjs +6 -0
  101. package/src/core/surface.mjs +64 -3
  102. package/src/core/task-executors.mjs +8 -45
  103. package/src/core/vault-cache.mjs +70 -4
  104. package/src/server/api.mjs +21 -0
  105. package/src/server/rest.mjs +2 -0
  106. package/src/server/route-manifest.json +21 -5
  107. package/src/server/routes/collab.mjs +38 -11
  108. package/src/server/routes/extension.mjs +3 -2
  109. package/src/server/routes/memory.mjs +24 -2
  110. package/src/server/routes/mesh.mjs +7 -7
  111. package/src/server/routes/plugins-mesh.mjs +49 -0
  112. package/src/server/routes/plugins.mjs +91 -426
  113. package/src/server/routes/research.mjs +5 -2
  114. package/src/server/routes/share.mjs +2 -0
  115. package/src/server/tools.mjs +29 -2
  116. package/extension/icons/README.md +0 -27
  117. package/extension/lib/preconfigured.js +0 -5
  118. package/extension/popup/popup.css +0 -227
  119. package/extension/popup/popup.html +0 -50
  120. package/extension/popup/popup.js +0 -175
  121. package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
  122. package/scaffold/.agent/plugins/.gitkeep +0 -0
  123. package/scaffold/.agent/plugins/code-quality/README.md +0 -16
  124. package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
  125. package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
  126. package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
  127. package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
  128. package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
  129. package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
  130. package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
  131. package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
  132. package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
@@ -40,6 +40,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
40
40
  const CLI_DIR = path.join(__dirname, '..', 'src', 'cli');
41
41
 
42
42
  const COMMANDS = {
43
+ app: 'app/index.mjs',
43
44
  init: 'init.mjs',
44
45
  setup: 'setup.mjs',
45
46
  start: 'start.mjs',
@@ -98,6 +99,8 @@ const COMMANDS = {
98
99
  agents: 'agent.mjs',
99
100
  plugin: 'plugin/index.mjs',
100
101
  plugins: 'plugin/index.mjs',
102
+ scaffold: 'plugin/scaffold-cmd.mjs',
103
+ 'scaffold-plugin': 'plugin/scaffold-cmd.mjs',
101
104
  };
102
105
  function printHelp() {
103
106
  console.log(`
@@ -188,6 +191,26 @@ async function main() {
188
191
  }
189
192
  }
190
193
 
194
+ // Check for custom global-level commands next
195
+ let globalAgentDir = null;
196
+ try {
197
+ const gBrain = resolveBrainLayer('global');
198
+ globalAgentDir = gBrain.agentDir;
199
+ } catch (e) { /* ignore */ }
200
+
201
+ if (globalAgentDir) {
202
+ const customCmdPath = path.join(globalAgentDir, 'commands', `${command}.mjs`);
203
+ if (fs.existsSync(customCmdPath)) {
204
+ const handler = await import(customCmdPath);
205
+ if (handler.run) {
206
+ await handler.run(process.argv);
207
+ } else {
208
+ await handler.default(process.argv.slice(3));
209
+ }
210
+ process.exit(0);
211
+ }
212
+ }
213
+
191
214
  const integrationPath = path.join(agentDir, 'skills', 'total-recall', 'integrations', `${command}.md`);
192
215
 
193
216
  if (fs.existsSync(integrationPath)) {
@@ -235,7 +258,7 @@ async function main() {
235
258
  } else if (handler.default) {
236
259
  await handler.default(process.argv.slice(3));
237
260
  }
238
- process.exit(0);
261
+ process.exit(process.exitCode ?? 0);
239
262
  }
240
263
  }
241
264
  }
@@ -236,27 +236,35 @@ Total Recall features a native terminal-based chat REPL (`npx total-recall chat`
236
236
 
237
237
  ---
238
238
 
239
- ## 🧩 Plugin System & Extension Architecture
239
+ ## 🧩 Plugin System — Peer-to-Peer Customization
240
240
 
241
- Total Recall features a decentralized, filesystem-native plugin system that allows modular capability suites, background scholarly daemons, and custom SSSS graph schemas to extend the memory kernel without modifying core packages.
241
+ Plugins shape one Total Recall install for a particular use (software development, research, operations, …). They can be shared **directly with another user through a hash-pinned HTTPS link**, with private mesh sharing retained for one user's own nodes. There is no central catalog, and nothing about a plugin is shown that the node cannot verify: no ratings, reviews, install counts or "verified" badges.
242
242
 
243
- ### 1. Plugin Manifest Specification (`plugin.json`)
244
- Every plugin is a self-contained directory under `.agent/plugins/<id>/` (or global `~/.agent/plugins/<id>/`) containing a validated `plugin.json` conforming to `metadata.plugin.schema.json`:
245
- - **Metadata**: Unique lowercase kebab-case `id`, semantic `version`, `name`, and `description`.
246
- - **Custom SSSS Categories**: Declares domain-specific memory node categories (`ssss_schemas.categories`) with schemas and templates automatically registered during vault builds.
247
- - **Surface Compilation Hook**: Defines a custom context generator script (`compile.generator`) and watch triggers that assemble prioritized evolving context surfaces (up to 500k tokens).
248
- - **Background Tasks & Daemons**: Registers recurring cron envelopes (`tasks`) and execution entrypoints (`entrypoint`) automatically picked up by the daemon loop.
249
- - **Pure Unix CLI Commands**: Exposes first-class CLI commands (`cli.command` and `cli.handler`) seamlessly routed under `total-recall <command>`.
243
+ ### 1. Manifest (`plugin.json`, schema: `metadata.plugin.schema.json`)
244
+ - **Metadata**: kebab-case `id`, semver `version`, `name`, `description`, optional `author`, `license`, `homepage`.
245
+ - **`use_cases`**: kebab-case tags saying what the plugin is for; list/search/UI filter on them.
246
+ - **`ssss_schemas.categories`**: memory categories the plugin adds.
247
+ - **`compile.generator` / `compile.watch`**: a module whose `generateContext()` output is added to compiled instruction surfaces.
248
+ - **`cli`**: a command routed as `total-recall <command>`.
249
+ - **`tasks`**: `{ intent, schedule (5-field cron), command }` — the plugin's own CLI subcommand, run on schedule by the daemon in a child process. Every run is a `plugin.task_run` event.
250
+ - **`openwiki_hubs`**: markdown hubs ingested into OpenWiki.
250
251
 
251
- ### 2. Plugin Discovery & Lifecycle (`src/core/plugin-loader.mjs`)
252
- - **Layered Discovery**: Scans project-level (`.agent/plugins/`) and global-level (`~/.agent/plugins/`) directories. Project plugins take precedence over global plugins with identical IDs.
253
- - **Strict Schema Validation**: Validates all manifests against the plugin schema prior to runtime execution, flagging errors without breaking host brain stability.
254
- - **Symlink & Development Linking**: Developers can link in-progress plugins using `total-recall plugin install <path> --link` (or via the web UI) to enable real-time edits without reinstallation.
252
+ ### 2. Where plugins live
253
+ - Project: `<project>/.agent/skills/total-recall/plugins/<id>/`; machine-wide: `~/.agent/skills/total-recall/plugins/<id>/`. Project shadows global by id. Installed plugins are per-brain state (`src/core/brain-state.json`) and never ship in the scaffold.
254
+ - Bundled: `plugins/<id>/` in the package, installable on any node by id.
255
255
 
256
- ### 3. Web Dashboard & Rating System (`frontend/src/pages/PluginsPage.tsx`)
257
- - **Plugin Management UI**: Dedicated `/plugins` dashboard view for browsing installed extensions, inspecting raw manifests, exploring documentation, and one-click uninstallation.
258
- - **Curated Discovery Catalog**: Browsable catalog of ecosystem plugins (Scientific Frontiers Engine, Meta-Harness, Code Quality & SSSS Conformance, Chrome DevTools) with single-click installation.
259
- - **Interactive 5-Star Rating System**: Persistent local ratings and reviews (`.agent/config/plugin-ratings.json`), verified badges, and sorting by rating, reviews, or alphabetical order.
256
+ ### 3. Install sources (`src/core/plugin-store.mjs`)
257
+ `<bundled id>` · direct HTTPS share link · `peer:<host>/<id>` · git URL · local path (`--link` for development). Copies are written to a dot-prefixed staging directory and renamed into place. Each install writes a `plugin_record` (`system/plugins/<id>.md`: scope, source, sha256, shared, public_shared, task_runs) through the Core Contract plus a `plugin.installed` event.
258
+
259
+ ### 4. Peer-to-peer sharing
260
+ - `total-recall plugin share <id>` marks a plugin shared in its record.
261
+ - Public sharing requires a fresh `public_shared: true` opt-in. Legacy mesh-only `shared: true` records do not become public. `TR_PUBLIC_BASE_URL` supplies the externally reachable HTTPS origin; the sender gives the recipient a direct URL with `#sha256=<hash>`. The installer pins DNS to a public address and verifies the bundle hash before writing files. `GET /api/public/plugins/:id/bundle` exposes only opted-in bundles and has no listing route.
262
+ - Peers query `GET /api/mesh/plugins` and fetch `GET /api/mesh/plugins/:id/bundle` (`src/server/routes/plugins-mesh.mjs`), guarded by `requireMeshSyncAuth` (mesh/loopback source + `TR_MESH_SYNC_TOKEN`). Read-only; only shared plugins are visible.
263
+ - Bundles are JSON (`tr-plugin-bundle/1`) with a SHA-256 over a canonical file list (`src/core/plugin-bundle.mjs`). The installer verifies the bytes against both the bundle header and the hash the peer advertised; a mismatch installs nothing. Paths are checked for traversal; size/file-count capped.
264
+ - `GET /api/plugins/peers` / `total-recall plugin peers` report each peer as observed: `ok`, `offline`, `unreachable`, `not_configured`, `unsupported` (older version), or `error`.
265
+
266
+ ### 5. Execution
267
+ Plugin code never runs inside the brain server: the dashboard runner (`POST /api/plugins/:id/run`, `config:write`) and scheduled tasks use `src/core/plugin-runner.mjs` — a child `node` process with a timeout, an output cap, and `TR_PACKAGE_ROOT` / `TR_PLUGIN_DIR` in its environment.
260
268
 
261
269
  ---
262
270
 
@@ -285,7 +293,7 @@ The Meta-Harness treats external AI CLI tools and local neural models as pluggab
285
293
  | `ollama` | Ollama Local LLM | `pipe_stdin` | Zero-token-cost local neural inference |
286
294
 
287
295
  - **Multi-Harness Council (`total-recall harness council "<task>"`)**: Runs concurrent deliberative queries across all available local harnesses, compiling multiple perspectives into a structured comparative report.
288
- - **Cross-Node Harness Dispatch (`total-recall harness dispatch <id> --node <node> "<task>"`)**: Dispatches tasks to a specific harness located on another mesh node (e.g. running `ollama` models on a dedicated local machine like `macmini`).
296
+ - **Cross-Node Harness Dispatch (`total-recall harness dispatch <id> --node <node> "<task>"`)**: Dispatches tasks to a specific harness located on another mesh node (e.g. running `ollama` models on a dedicated local machine like `build-box`).
289
297
 
290
298
  ### 3. Agent Process Management (`src/core/agent-manager.mjs`)
291
299
 
@@ -1,115 +1,290 @@
1
- // Total Recall — Background Service Worker
2
- // Handles context menus, message routing, and passive browsing buffer.
1
+ // Total Recall — service worker
2
+ // Context menus, keyboard commands, omnibox search, per-tab "related memories"
3
+ // badge, and capture. The side panel and content script talk to the brain
4
+ // through here or through BrainClient directly.
3
5
 
4
- importScripts('lib/preconfigured.js', 'lib/brain-client.js');
6
+ importScripts('lib/brain-client.js', 'lib/ui-helpers.js');
5
7
 
6
- // Destructure for convenience after importScripts populates self.BrainClient
7
- const { share, search, healthCheck } = self.BrainClient;
8
+ const Brain = self.BrainClient;
9
+ const UI = self.TRUi;
10
+
11
+ const SETTINGS_DEFAULTS = { pageRecall: false, showPill: true, blocklist: [] };
12
+
13
+ async function getSettings() {
14
+ return chrome.storage.sync.get(SETTINGS_DEFAULTS);
15
+ }
8
16
 
9
17
  // ---------------------------------------------------------------------------
10
- // Context menu registration
18
+ // Install / startup
11
19
  // ---------------------------------------------------------------------------
12
- chrome.runtime.onInstalled.addListener(() => {
13
- if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
14
- chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error);
20
+
21
+ function registerPanelBehavior() {
22
+ chrome.sidePanel?.setPanelBehavior?.({ openPanelOnActionClick: true }).catch(() => {});
23
+ }
24
+ registerPanelBehavior();
25
+
26
+ chrome.runtime.onInstalled.addListener(async (details) => {
27
+ registerPanelBehavior();
28
+ await chrome.contextMenus.removeAll();
29
+ const menus = [
30
+ { id: 'remember-page', title: 'Remember this page', contexts: ['page'] },
31
+ { id: 'remember-selection', title: 'Remember selection', contexts: ['selection'] },
32
+ { id: 'ask-selection', title: 'Ask Total Recall about “%s”', contexts: ['selection'] },
33
+ { id: 'research-selection', title: 'Research “%s”', contexts: ['selection'] },
34
+ { id: 'remember-link', title: 'Remember link', contexts: ['link'] },
35
+ ];
36
+ for (const menu of menus) {
37
+ chrome.contextMenus.create({ ...menu, documentUrlPatterns: ['http://*/*', 'https://*/*'] });
15
38
  }
39
+ if (details.reason === 'install') chrome.runtime.openOptionsPage();
40
+ });
16
41
 
17
- chrome.contextMenus.create({
18
- id: 'send-to-brain',
19
- title: 'Send to Brain',
20
- contexts: ['page']
21
- });
22
- chrome.contextMenus.create({
23
- id: 'remember-this',
24
- title: 'Remember This',
25
- contexts: ['selection']
26
- });
27
- chrome.contextMenus.create({
28
- id: 'research-this',
29
- title: 'Research This',
30
- contexts: ['selection']
42
+ // ---------------------------------------------------------------------------
43
+ // Page context + feedback via the content script
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function sendToTab(tabId, message, timeoutMs = 1500) {
47
+ return new Promise((resolve) => {
48
+ const timer = setTimeout(() => resolve(null), timeoutMs);
49
+ try {
50
+ chrome.tabs.sendMessage(tabId, message, (response) => {
51
+ clearTimeout(timer);
52
+ // No content script (tab predates install, or a restricted page).
53
+ if (chrome.runtime.lastError) return resolve(null);
54
+ resolve(response || null);
55
+ });
56
+ } catch {
57
+ clearTimeout(timer);
58
+ resolve(null);
59
+ }
31
60
  });
32
- });
61
+ }
62
+
63
+ async function pageContext(tab) {
64
+ const ctx = await sendToTab(tab.id, { type: 'GET_PAGE_CONTEXT' });
65
+ return ctx || { url: tab.url, title: tab.title, description: '', selection: '', text: '' };
66
+ }
67
+
68
+ function flashBadge(tabId, ok) {
69
+ chrome.action.setBadgeBackgroundColor({ tabId, color: ok ? '#34d399' : '#f87171' });
70
+ chrome.action.setBadgeText({ tabId, text: ok ? '✓' : '!' });
71
+ setTimeout(() => refreshRelatedBadge(tabId), 2500);
72
+ }
73
+
74
+ function notifyTab(tabId, text, tone) {
75
+ sendToTab(tabId, { type: 'TR_TOAST', text, tone });
76
+ }
33
77
 
34
78
  // ---------------------------------------------------------------------------
35
- // Context menu click handler
79
+ // Capture
36
80
  // ---------------------------------------------------------------------------
37
- chrome.contextMenus.onClicked.addListener(async (info, tab) => {
81
+
82
+ /**
83
+ * @param {chrome.tabs.Tab} tab
84
+ * @param {{action?: 'remember'|'research', selection?: string, linkUrl?: string, linkText?: string, note?: string}} opts
85
+ */
86
+ async function capture(tab, opts = {}) {
87
+ const action = opts.action || 'remember';
88
+ let payload;
89
+
90
+ if (opts.linkUrl) {
91
+ if (!UI.isCapturableUrl(opts.linkUrl)) throw new Error('Only web links can be remembered.');
92
+ payload = {
93
+ url: opts.linkUrl,
94
+ title: (opts.linkText || '').trim() || opts.linkUrl,
95
+ excerpt: UI.buildCaptureExcerpt({ url: opts.linkUrl, note: opts.note || (tab?.title ? `Linked from “${tab.title}”` : '') }),
96
+ tags: ['web', 'link', UI.hostnameOf(opts.linkUrl)].filter(Boolean),
97
+ };
98
+ } else {
99
+ if (!tab || !UI.isCapturableUrl(tab.url)) throw new Error('This page can’t be captured.');
100
+ const ctx = await pageContext(tab);
101
+ const selection = opts.selection !== undefined ? opts.selection : ctx.selection;
102
+ const url = ctx.url || tab.url;
103
+ payload = {
104
+ url,
105
+ title: ctx.title || tab.title || url,
106
+ excerpt: action === 'research'
107
+ ? (selection || ctx.description || '').trim()
108
+ : UI.buildCaptureExcerpt({ url, description: ctx.description, selection, text: ctx.text, note: opts.note }),
109
+ tags: ['web', UI.hostnameOf(url)].filter(Boolean),
110
+ };
111
+ if (action === 'research' && selection && selection.trim()) {
112
+ // Researching a selection: the selection is the topic, the page is context.
113
+ payload.title = UI.clip(selection, 140);
114
+ payload.excerpt = `From ${url}`;
115
+ }
116
+ }
117
+
118
+ const result = await Brain.share({ ...payload, action, source: 'chrome-extension' });
119
+ chrome.runtime.sendMessage({ type: 'CAPTURED', action, result }).catch(() => {});
120
+ return result;
121
+ }
122
+
123
+ async function captureWithFeedback(tab, opts) {
124
+ const verb = opts.action === 'research' ? 'Research queued' : 'Saved to your brain';
38
125
  try {
39
- if (info.menuItemId === 'send-to-brain') {
40
- await share({
41
- url: tab.url,
42
- title: tab.title,
43
- action: 'remember',
44
- source: 'chrome-extension'
45
- });
46
- showBadge('\u2713', '#22c55e');
47
- } else if (info.menuItemId === 'remember-this') {
48
- await share({
49
- url: tab.url,
50
- title: tab.title,
51
- excerpt: info.selectionText,
52
- action: 'remember',
53
- source: 'chrome-extension'
54
- });
55
- showBadge('\u2713', '#22c55e');
56
- } else if (info.menuItemId === 'research-this') {
57
- await share({
58
- excerpt: info.selectionText,
59
- action: 'research',
60
- source: 'chrome-extension'
61
- });
62
- showBadge('\u2713', '#22c55e');
126
+ await capture(tab, opts);
127
+ if (tab?.id) {
128
+ flashBadge(tab.id, true);
129
+ notifyTab(tab.id, verb, 'success');
63
130
  }
64
131
  } catch (err) {
65
- console.error('Brain API error:', err);
66
- showBadge('!', '#ef4444');
132
+ console.warn('[Total Recall] capture failed:', err);
133
+ if (tab?.id) {
134
+ flashBadge(tab.id, false);
135
+ notifyTab(tab.id, err.message || 'Capture failed', 'error');
136
+ }
137
+ }
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Context menus + commands
142
+ // ---------------------------------------------------------------------------
143
+
144
+ chrome.contextMenus.onClicked.addListener((info, tab) => {
145
+ switch (info.menuItemId) {
146
+ case 'remember-page':
147
+ captureWithFeedback(tab, { action: 'remember', selection: '' });
148
+ break;
149
+ case 'remember-selection':
150
+ captureWithFeedback(tab, { action: 'remember', selection: info.selectionText || '' });
151
+ break;
152
+ case 'research-selection':
153
+ captureWithFeedback(tab, { action: 'research', selection: info.selectionText || '' });
154
+ break;
155
+ case 'remember-link':
156
+ captureWithFeedback(tab, { action: 'remember', linkUrl: info.linkUrl, linkText: info.selectionText });
157
+ break;
158
+ case 'ask-selection':
159
+ // sidePanel.open must run inside the user gesture, before any await.
160
+ if (tab?.id) chrome.sidePanel.open({ tabId: tab.id }).catch(() => {});
161
+ chrome.storage.session.set({
162
+ pendingAsk: { text: info.selectionText || '', url: tab?.url || '', title: tab?.title || '', at: Date.now() },
163
+ });
164
+ break;
67
165
  }
68
166
  });
69
167
 
168
+ chrome.commands.onCommand.addListener(async (command, tab) => {
169
+ if (command !== 'remember-page') return;
170
+ const target = tab || (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
171
+ captureWithFeedback(target, { action: 'remember' });
172
+ });
173
+
70
174
  // ---------------------------------------------------------------------------
71
- // Badge helper
175
+ // Omnibox: `tr <query>` searches the brain from the address bar
72
176
  // ---------------------------------------------------------------------------
73
- function showBadge(text, color) {
74
- chrome.action.setBadgeText({ text });
75
- chrome.action.setBadgeBackgroundColor({ color });
76
- setTimeout(() => chrome.action.setBadgeText({ text: '' }), 3000);
177
+
178
+ function xmlEscape(text) {
179
+ return String(text || '').replace(/[<>&"']/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&apos;' }[c]));
77
180
  }
78
181
 
182
+ chrome.omnibox.setDefaultSuggestion({ description: 'Search your Total Recall brain for <match>%s</match>' });
183
+
184
+ let omniboxTimer = null;
185
+ chrome.omnibox.onInputChanged.addListener((text, suggest) => {
186
+ clearTimeout(omniboxTimer);
187
+ const query = text.trim();
188
+ if (query.length < 2) return;
189
+ omniboxTimer = setTimeout(async () => {
190
+ try {
191
+ const results = await Brain.search(query, { topK: 6 });
192
+ suggest(results
193
+ .filter((r) => r.type !== 'session' && r.slug && r.title)
194
+ .map((r) => ({
195
+ content: `slug:${r.slug}`,
196
+ description: `${xmlEscape(r.title)} <dim>— ${xmlEscape(r.category || 'memory')}</dim>`,
197
+ })));
198
+ } catch {
199
+ suggest([]);
200
+ }
201
+ }, 250);
202
+ });
203
+
204
+ chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
205
+ const slug = text.startsWith('slug:') ? text.slice(5) : null;
206
+ const url = await Brain.dashboardUrl(slug ? `/memory?slug=${encodeURIComponent(slug)}` : '/memory');
207
+ if (disposition === 'currentTab') chrome.tabs.update({ url });
208
+ else chrome.tabs.create({ url, active: disposition !== 'newBackgroundTab' });
209
+ });
210
+
79
211
  // ---------------------------------------------------------------------------
80
- // Message listener (popup, content-script, sidepanel → background)
212
+ // Related memories (page recall) + per-tab badge
81
213
  // ---------------------------------------------------------------------------
82
- chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
83
- if (msg.type === 'QUERY_BRAIN') {
84
- search(msg.query, msg.topK || 3)
85
- .then(data => sendResponse({ memories: data.results || [] }))
86
- .catch(err => sendResponse({ memories: [], error: err.message }));
87
- return true; // keep channel open for async response
88
- }
89
214
 
90
- if (msg.type === 'SHARE') {
91
- share(msg.data)
92
- .then(data => sendResponse({ success: true, ...data }))
93
- .catch(err => sendResponse({ success: false, error: err.message }));
94
- return true;
215
+ const relatedByTab = new Map(); // tabId -> count
216
+
217
+ function refreshRelatedBadge(tabId) {
218
+ const count = relatedByTab.get(tabId) || 0;
219
+ chrome.action.setBadgeBackgroundColor({ tabId, color: '#3b82f6' });
220
+ chrome.action.setBadgeText({ tabId, text: count ? String(count) : '' }).catch(() => {});
221
+ }
222
+
223
+ async function queryRelated({ url, title, description }, tabId) {
224
+ const settings = await getSettings();
225
+ if (!settings.pageRecall || !UI.isCapturableUrl(url) || UI.isBlocked(url, settings.blocklist)) return [];
226
+ const query = [title, description].filter(Boolean).join(' — ').slice(0, 300);
227
+ if (!query.trim()) return [];
228
+ const results = await Brain.search(query, { topK: 10 });
229
+ const related = UI.filterRelated(results, { excludeUrl: url, limit: 5 });
230
+ if (tabId !== undefined) {
231
+ relatedByTab.set(tabId, related.length);
232
+ refreshRelatedBadge(tabId);
95
233
  }
234
+ return related;
235
+ }
96
236
 
97
- if (msg.type === 'HEALTH_CHECK') {
98
- healthCheck()
99
- .then(data => sendResponse({ connected: true, ...data }))
100
- .catch(() => sendResponse({ connected: false }));
101
- return true;
237
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
238
+ if (changeInfo.status === 'loading' && changeInfo.url) {
239
+ relatedByTab.delete(tabId);
240
+ refreshRelatedBadge(tabId);
102
241
  }
242
+ });
243
+ chrome.tabs.onRemoved.addListener((tabId) => relatedByTab.delete(tabId));
103
244
 
104
- if (msg.type === 'OPEN_SIDE_PANEL') {
105
- const tabId = sender.tab?.id;
106
- if (!chrome.sidePanel?.open || !tabId) {
107
- sendResponse({ success: false, error: 'Side panel is unavailable for this tab.' });
108
- return false;
245
+ // ---------------------------------------------------------------------------
246
+ // Messages from the content script and side panel
247
+ // ---------------------------------------------------------------------------
248
+
249
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
250
+ switch (msg && msg.type) {
251
+ case 'QUERY_RELATED':
252
+ queryRelated(msg.page || {}, sender.tab?.id)
253
+ .then((memories) => sendResponse({ memories }))
254
+ .catch((err) => sendResponse({ memories: [], error: err.message }));
255
+ return true;
256
+
257
+ case 'CAPTURE': {
258
+ const run = async () => {
259
+ const tab = msg.tabId
260
+ ? await chrome.tabs.get(msg.tabId)
261
+ : sender.tab || (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
262
+ return capture(tab, msg.options || {});
263
+ };
264
+ run()
265
+ .then((result) => sendResponse({ success: true, result }))
266
+ .catch((err) => sendResponse({ success: false, error: err.message, code: err.code }));
267
+ return true;
268
+ }
269
+
270
+ case 'OPEN_SIDE_PANEL': {
271
+ const tabId = sender.tab?.id;
272
+ if (!tabId || !chrome.sidePanel?.open) {
273
+ sendResponse({ success: false });
274
+ return false;
275
+ }
276
+ chrome.sidePanel.open({ tabId })
277
+ .then(() => sendResponse({ success: true }))
278
+ .catch((err) => sendResponse({ success: false, error: err.message }));
279
+ return true;
109
280
  }
110
- chrome.sidePanel.open({ tabId })
111
- .then(() => sendResponse({ success: true }))
112
- .catch(err => sendResponse({ success: false, error: err.message }));
113
- return true;
281
+
282
+ case 'OPEN_MEMORY':
283
+ Brain.dashboardUrl(`/memory?slug=${encodeURIComponent(msg.slug || '')}`)
284
+ .then((url) => chrome.tabs.create({ url }));
285
+ return false;
286
+
287
+ default:
288
+ return false;
114
289
  }
115
290
  });
@@ -1,16 +1,10 @@
1
- /* Total Recall — Content Script Styles
2
- * The floating pill uses Shadow DOM for style isolation,
3
- * so this file only contains the host-level positioning.
4
- * Keeping it minimal to avoid conflicts with host pages. */
5
-
6
- #total-recall-pill-host {
7
- /* Position is handled inline via Shadow DOM :host styles.
8
- * This rule serves as a fallback and ensures the element
9
- * doesn't inherit unintended styles from the host page. */
10
- all: initial;
1
+ /* Total Recall — the overlay host element only. Everything visible lives in a
2
+ * closed shadow root styled from content-script.js; these rules just keep the
3
+ * host itself from inheriting page styles. */
4
+ total-recall-overlay#total-recall-overlay {
5
+ all: initial !important;
11
6
  position: fixed !important;
12
- bottom: 20px !important;
13
7
  right: 20px !important;
14
- z-index: 2147483647 !important;
15
- pointer-events: auto !important;
8
+ bottom: 20px !important;
9
+ z-index: 2147483646 !important;
16
10
  }