total-recall-brain 3.28.1 → 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.
- package/bin/total-recall.mjs +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/connect.mjs +5 -7
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +7 -5
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/cli/skill-projection.mjs +13 -13
- package/src/cli/status.mjs +42 -31
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/embeddings.mjs +14 -4
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +32 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
package/bin/total-recall.mjs
CHANGED
|
@@ -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
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
239
|
+
## 🧩 Plugin System — Peer-to-Peer Customization
|
|
240
240
|
|
|
241
|
-
Total Recall
|
|
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.
|
|
244
|
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
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.
|
|
252
|
-
-
|
|
253
|
-
-
|
|
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.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 `
|
|
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
|
|
package/extension/background.js
CHANGED
|
@@ -1,115 +1,290 @@
|
|
|
1
|
-
// Total Recall —
|
|
2
|
-
//
|
|
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/
|
|
6
|
+
importScripts('lib/brain-client.js', 'lib/ui-helpers.js');
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const
|
|
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
|
-
//
|
|
18
|
+
// Install / startup
|
|
11
19
|
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
//
|
|
79
|
+
// Capture
|
|
36
80
|
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
66
|
-
|
|
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
|
-
//
|
|
175
|
+
// Omnibox: `tr <query>` searches the brain from the address bar
|
|
72
176
|
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 3000);
|
|
177
|
+
|
|
178
|
+
function xmlEscape(text) {
|
|
179
|
+
return String(text || '').replace(/[<>&"']/g, (c) => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[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
|
-
//
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
.
|
|
113
|
-
|
|
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 —
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
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
|
-
|
|
15
|
-
|
|
8
|
+
bottom: 20px !important;
|
|
9
|
+
z-index: 2147483646 !important;
|
|
16
10
|
}
|