useathena 0.1.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 (43) hide show
  1. package/README.md +258 -0
  2. package/apps/chrome-extension/README.md +35 -0
  3. package/apps/chrome-extension/background.js +97 -0
  4. package/apps/chrome-extension/gmail.js +107 -0
  5. package/apps/chrome-extension/linkedin.js +123 -0
  6. package/apps/chrome-extension/manifest.json +27 -0
  7. package/apps/chrome-extension/options.html +60 -0
  8. package/apps/chrome-extension/options.js +36 -0
  9. package/apps/chrome-extension/popup.html +37 -0
  10. package/apps/chrome-extension/popup.js +22 -0
  11. package/bin/athena +28 -0
  12. package/dist/api/server.js +145 -0
  13. package/dist/capture/ingest.js +85 -0
  14. package/dist/cli/commands.js +201 -0
  15. package/dist/cli/format.js +76 -0
  16. package/dist/cli/setup.js +316 -0
  17. package/dist/cli.js +291 -0
  18. package/dist/config.js +26 -0
  19. package/dist/core/fixtures.js +65 -0
  20. package/dist/core/ids.js +34 -0
  21. package/dist/core/refs.js +25 -0
  22. package/dist/core/types.js +10 -0
  23. package/dist/engine/engine.js +136 -0
  24. package/dist/engine/parse.js +76 -0
  25. package/dist/engine/prompts.js +64 -0
  26. package/dist/eval/harness.js +123 -0
  27. package/dist/eval/judge.js +75 -0
  28. package/dist/eval/run-eval.js +46 -0
  29. package/dist/eval/scenarios.js +470 -0
  30. package/dist/mcp/server.js +107 -0
  31. package/dist/mcp-server.js +7 -0
  32. package/dist/model/api-model-client.js +99 -0
  33. package/dist/model/cli-model-client.js +111 -0
  34. package/dist/model/model-client.js +28 -0
  35. package/dist/model/registry.js +67 -0
  36. package/dist/sensors/claude-code-hook.js +131 -0
  37. package/dist/serve/brief.js +95 -0
  38. package/dist/serve/outcome.js +56 -0
  39. package/dist/store/open.js +19 -0
  40. package/dist/store/store.js +269 -0
  41. package/docs/schema.md +368 -0
  42. package/package.json +43 -0
  43. package/scripts/prepare.mjs +20 -0
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # Athena
2
+
3
+ Athena captures the tacit knowledge hidden in real work so AI agents can become more autonomous, reliable, and less dependent on repeated human correction.
4
+
5
+ Most agent failures in expert work are not caused by missing public facts. They happen because the agent lacks the unwritten judgment an expert applies without noticing: tone boundaries, account-specific exceptions, when to escalate, what not to assume, what "good enough" looks like, and which correction from last time should generalize to this time.
6
+
7
+ Athena watches the moments where that judgment becomes visible, turns them into evidence-backed hypotheses, validates them, and serves the right rules back to agents before they act.
8
+
9
+ ```text
10
+ work -> corrections -> tacit hypotheses -> validated rules -> agent briefs -> outcomes -> better rules
11
+ ```
12
+
13
+ ```bash
14
+ npx useathena onboard --yes
15
+ ```
16
+
17
+ ## Product thesis
18
+
19
+ Athena is a local-first tacit judgment layer for agents.
20
+
21
+ The user should not have to write a giant operating manual for every assistant. Instead, Athena should learn from the normal feedback loop:
22
+
23
+ - The user edits, rejects, approves, or redirects agent output.
24
+ - Athena captures that judgment moment as immutable evidence.
25
+ - A model-backed engine infers candidate rules with supporting examples and boundaries.
26
+ - The user reviews rules before they become durable.
27
+ - Agents call Athena for a brief before acting.
28
+ - Outcomes feed back into confidence, stale detection, and counterexamples.
29
+
30
+ The north-star metric is a declining correction rate per served brief.
31
+
32
+ ## MVP
33
+
34
+ The MVP is not a broad knowledge base. It is a working closed loop for tacit judgment:
35
+
36
+ 1. Capture high-signal judgment events from real workflows.
37
+ - CLI and MCP capture for manual or agent-recorded events.
38
+ - Claude Code hook for explicit `remember:` / `athena:` notes and redirect-shaped prompts.
39
+ - Chrome extension sensor for LinkedIn and Gmail draft edits: paste a generated draft, edit it, send it, and Athena captures the before/after as a correction or approval.
40
+
41
+ 2. Store evidence locally and safely.
42
+ - SQLite is the source of truth.
43
+ - Raw captured instances are private by default.
44
+ - Every learned rule cites the evidence that produced it.
45
+ - Agents can propose evidence and outcomes, but cannot directly mutate durable rules.
46
+
47
+ 3. Infer tacit rules from evidence.
48
+ - The hypothesis engine clusters examples by domain.
49
+ - It infers cues, expectancies, goals, rules, and boundary conditions.
50
+ - It replay-validates against held-out examples before a rule can be served.
51
+ - Inference is model-backed, not a fake deterministic string matcher.
52
+
53
+ 4. Validate before serving; review is optional.
54
+ - Replay-validated rules are served to agents immediately, flagged with caveats.
55
+ - Rules graduate to confidently-served on their own through repeated upheld
56
+ outcomes — no human in the loop required.
57
+ - `athena review` is an accelerator and audit surface: approving promotes a rule
58
+ instantly, rejecting retires it (kept for audit). It is never a gate.
59
+
60
+ 5. Brief agents before they act.
61
+ - MCP exposes exactly four tools: `athena_brief`, `athena_open`, `athena_record`, and `athena_search`.
62
+ - A brief returns applicable rules, confidence, boundaries, citations, do-not-assume items, open questions, and a readiness verdict.
63
+
64
+ 6. Learn from outcomes.
65
+ - Agents record whether their briefed output was accepted, corrected, abandoned, or unknown.
66
+ - Rules gain trust when upheld and lose trust when overridden.
67
+ - Repeated counterexamples make rules stale.
68
+
69
+ ## What exists now
70
+
71
+ Current build status: early but end-to-end.
72
+
73
+ Implemented:
74
+
75
+ - Core schema and typed domain model.
76
+ - SQLite store with invariants and lexical search.
77
+ - Capture ingestion with structured diffs.
78
+ - LLM-backed hypothesis engine.
79
+ - Model-agnostic providers: `cli:claude`, `cli:codex`, Anthropic API,
80
+ OpenAI-compatible APIs, and local models via Ollama / LM Studio / vLLM.
81
+ - Golden-scenario eval harness.
82
+ - Brief compilation, outcome recording, and autonomous rule promotion
83
+ (validated rules graduate to active through upheld outcomes).
84
+ - MCP server with the four-tool agent surface (`athena mcp`).
85
+ - CLI with a first-run `athena setup` wizard, plus init, status, capture, learn,
86
+ review, brief, rules, open, record, serve, and hook install.
87
+ - Claude Code sensor hook (installed by `athena setup` or `athena hook install`).
88
+ - Localhost API for browser sensors.
89
+ - Chrome extension v0.1 for LinkedIn and Gmail draft-edit capture.
90
+ - One-command onboarding: `npx useathena onboard --yes` (npm package `useathena`,
91
+ binary `athena`; GitHub installs work too).
92
+
93
+ Still rough or planned:
94
+
95
+ - Chrome Web Store extension (today: load-unpacked from the installed package).
96
+ - Chrome extension selector hardening and browser automation smoke tests.
97
+ - Embeddings or semantic search, if lexical search stops being enough.
98
+ - Team/workspace promotion flows for sharing vetted derived rules.
99
+
100
+ ## Architecture
101
+
102
+ ```text
103
+ src/core Types, ids, refs, fixtures
104
+ src/store SQLite store and enforced invariants
105
+ src/capture SensorEvent -> immutable JudgmentInstance ingestion
106
+ src/engine Model-backed hypothesis inference and replay validation
107
+ src/serve Agent brief compilation and outcome recording
108
+ src/eval Golden scenarios, harness, and judges
109
+ src/mcp MCP tools for agents
110
+ src/cli CLI command logic and interactive review loop
111
+ src/api Localhost API for browser sensors
112
+ src/model ModelClient interface and CLI-backed providers
113
+ apps/chrome-extension
114
+ MV3 sensor for LinkedIn and Gmail
115
+ docs Schema and research notes
116
+ ```
117
+
118
+ Important contracts:
119
+
120
+ - [`docs/schema.md`](docs/schema.md) is the product/domain contract.
121
+ - [`docs/research/2026-06-11-tacit-capture-research.md`](docs/research/2026-06-11-tacit-capture-research.md) contains the research and competitive landscape behind the design.
122
+ - [`apps/chrome-extension/README.md`](apps/chrome-extension/README.md) documents the browser sensor capture policy.
123
+
124
+ ## Install
125
+
126
+ Requires Node 22.5+ (Athena uses the built-in `node:sqlite` module).
127
+
128
+ ```bash
129
+ npx useathena onboard # interactive
130
+ npx useathena onboard --yes # accept every default
131
+ ```
132
+
133
+ Working from a branch, or without registry access? The same flow runs straight
134
+ from GitHub for anyone with repo access:
135
+
136
+ ```bash
137
+ npx github:ErikLit005/athena onboard
138
+ ```
139
+
140
+ Onboarding does everything: creates the local store, detects which model providers
141
+ you have (claude CLI, codex CLI, Anthropic or OpenAI API keys, a running Ollama),
142
+ verifies the one you pick with a real inference call, installs itself globally so
143
+ hooks survive the npx cache, wires up Claude Code (sensor hook + MCP registration),
144
+ and prints the browser-extension setup. `--yes` takes the first detected provider
145
+ and says yes to all of it; opt out per-piece with `--no-hook`, `--skip-test`, or
146
+ `--model <spec>`.
147
+
148
+ Model providers are spec strings, set once by `setup` (or per-run via `ATHENA_MODEL`):
149
+
150
+ ```text
151
+ cli:claude[:model] Claude subscription via the claude CLI — no API key
152
+ cli:codex[:model] ChatGPT subscription via the codex CLI — no API key
153
+ anthropic[:model] Anthropic API (ANTHROPIC_API_KEY)
154
+ openai:<model> OpenAI API (OPENAI_API_KEY)
155
+ openai:<model>@<baseUrl> any OpenAI-compatible server (LM Studio, vLLM, …)
156
+ ollama:<model> local Ollama
157
+ ```
158
+
159
+ ## Local development
160
+
161
+ ```bash
162
+ git clone https://github.com/ErikLit005/athena.git && cd athena
163
+ npm install
164
+ npm run verify # typecheck + tests
165
+ ```
166
+
167
+ Useful commands:
168
+
169
+ ```bash
170
+ npm run typecheck
171
+ npm test
172
+ npm run eval
173
+ npm run build # compile to dist/ (what a global install runs)
174
+ npm run athena -- status
175
+ ```
176
+
177
+ The default store path is `~/.athena/athena.db`. Override it with `ATHENA_DB` for tests or isolated local experiments. Config (model choice, optional API keys) lives in `~/.athena/config.json`.
178
+
179
+ ## Running Athena locally
180
+
181
+ Initialize the local store and print MCP setup instructions (or run `athena setup` for the guided version):
182
+
183
+ ```bash
184
+ npm run athena -- init
185
+ ```
186
+
187
+ Capture an example correction:
188
+
189
+ ```bash
190
+ npm run athena -- capture correction \
191
+ --summary "edited a LinkedIn connection note to be warmer and less salesy" \
192
+ --domain linkedin.outreach \
193
+ --before "Would love to connect and discuss how we can help your team." \
194
+ --after "Saw your post on GTM hiring. Would be glad to connect."
195
+ ```
196
+
197
+ Learn candidate rules from captured evidence:
198
+
199
+ ```bash
200
+ npm run athena -- learn --domain linkedin.outreach
201
+ ```
202
+
203
+ Review inferred rules:
204
+
205
+ ```bash
206
+ npm run athena -- review
207
+ ```
208
+
209
+ Ask for the kind of brief an agent should get before acting:
210
+
211
+ ```bash
212
+ npm run athena -- brief "draft a LinkedIn connection note to a GTM leader" --domain linkedin.outreach
213
+ ```
214
+
215
+ Start the localhost API for the Chrome extension:
216
+
217
+ ```bash
218
+ npm run athena -- serve
219
+ ```
220
+
221
+ ## Design principles
222
+
223
+ - Instances are immutable evidence; hypotheses are revisable views over evidence.
224
+ - Behavior is ground truth; rationale is labeled hypothesis.
225
+ - Everything cites.
226
+ - Privacy fields are mandatory on capture-derived records.
227
+ - SQLite is the source of truth; markdown is a projection.
228
+ - The engine is model-backed and model-agnostic.
229
+ - The agent surface stays small and auditable.
230
+ - Rules earn trust autonomously — replay validation to be served, upheld outcomes
231
+ to be served confidently. Human review accelerates and audits; it never gates.
232
+ - Human interaction concentrates at critical moments: briefs say `ask_human` when
233
+ athena's knowledge is thin or contradictory, and counterexamples surface for audit.
234
+
235
+ ## Where to help
236
+
237
+ Good near-term contribution areas:
238
+
239
+ - Hardening the Chrome extension against LinkedIn and Gmail DOM changes.
240
+ - Adding browser smoke tests for the extension capture flow.
241
+ - Publishing the Chrome extension to the Web Store.
242
+ - Expanding eval scenarios from real corrections, especially cases with subtle boundaries or counterexamples.
243
+
244
+ Before changing core behavior, read [`docs/schema.md`](docs/schema.md). Before changing product direction, read the research notes and keep the north-star metric in mind: fewer repeated corrections per served brief.
245
+
246
+ ## Broader vision
247
+
248
+ The first wedge is personal: Athena should make one person's agents learn their standards, preferences, and judgment over time.
249
+
250
+ The broader vision is an evidence-backed judgment layer for teams and organizations:
251
+
252
+ - Personal tacit rules stay private by default.
253
+ - Vetted derived rules can be promoted to a workspace.
254
+ - Agents can be briefed with context-specific judgment before they write, decide, escalate, or act.
255
+ - Experts spend less time repeating corrections and more time reviewing high-leverage rules.
256
+ - Organizations accumulate operational judgment without pretending every important rule can be written down in advance.
257
+
258
+ If Athena works, an agent should not merely remember what happened. It should learn how the expert decided.
@@ -0,0 +1,35 @@
1
+ # athena sensor (Chrome extension)
2
+
3
+ Captures the judgment behind your edits of drafted messages and sends it to **your own
4
+ local athena** — nothing leaves your machine.
5
+
6
+ ## Capture policy (the contract)
7
+
8
+ The extension captures in exactly two situations:
9
+
10
+ 1. **Draft edits.** You paste a substantial draft (≥ 80 chars — e.g. agent output) into a
11
+ LinkedIn note/message or Gmail compose body. The pasted text is the "before". When you
12
+ send (button or Cmd/Ctrl+Enter), the final text is the "after". Edited → `correction`,
13
+ unchanged → `approval`.
14
+ 2. **Explicit saves.** Right-click a selection → "athena: remember this" → `manual_note`.
15
+
16
+ Everything else is out of scope by construction: no keystroke logging, no page scraping,
17
+ no capture of messages you typed from scratch, only whitelisted domains
18
+ (linkedin.com, mail.google.com), and the only network destination is `http://127.0.0.1`.
19
+ A pause toggle lives in the toolbar popup.
20
+
21
+ All captures land as `user_private_raw` instances — they become rules only through the
22
+ engine and your review (`athena review`).
23
+
24
+ ## Install
25
+
26
+ 1. Start the local API: `athena serve` (prints the token).
27
+ 2. `chrome://extensions` → enable Developer mode → "Load unpacked" → select this folder.
28
+ 3. Extension options → paste the token → Save → Test connection.
29
+ 4. Work normally. Check captures with `athena status`, learn with `athena learn`.
30
+
31
+ ## Known limitations (v0.1)
32
+
33
+ - LinkedIn/Gmail DOM selectors are best-effort and will need maintenance as those UIs change.
34
+ - Drafts typed from scratch are deliberately not captured (no "before" to learn from).
35
+ - No browser-automation smoke test yet — port the memodis harness when this stabilizes.
@@ -0,0 +1,97 @@
1
+ // athena sensor — service worker.
2
+ // Routes events from content scripts to the local athena API (127.0.0.1 only).
3
+ // The capture policy lives in the content scripts; this file only transports.
4
+
5
+ const DEFAULTS = { athenaPort: 4517, athenaToken: "", paused: false };
6
+
7
+ async function settings() {
8
+ return chrome.storage.local.get(DEFAULTS);
9
+ }
10
+
11
+ async function postEvent(event) {
12
+ const { athenaPort, athenaToken, paused } = await settings();
13
+ if (paused) return { ok: false, error: "paused" };
14
+ if (!athenaToken) return { ok: false, error: "no token — open the options page" };
15
+ try {
16
+ const response = await fetch(`http://127.0.0.1:${athenaPort}/events`, {
17
+ method: "POST",
18
+ headers: {
19
+ "content-type": "application/json",
20
+ authorization: `Bearer ${athenaToken}`,
21
+ },
22
+ body: JSON.stringify(event),
23
+ });
24
+ if (!response.ok) {
25
+ const body = await response.json().catch(() => ({}));
26
+ return { ok: false, error: body.error || `api ${response.status}` };
27
+ }
28
+ return { ok: true, body: await response.json() };
29
+ } catch (error) {
30
+ return { ok: false, error: `athena not reachable — is "athena serve" running? (${error})` };
31
+ }
32
+ }
33
+
34
+ async function health() {
35
+ const { athenaPort } = await settings();
36
+ try {
37
+ const response = await fetch(`http://127.0.0.1:${athenaPort}/health`);
38
+ return { ok: response.ok };
39
+ } catch {
40
+ return { ok: false };
41
+ }
42
+ }
43
+
44
+ function flashBadge(ok) {
45
+ chrome.action.setBadgeBackgroundColor({ color: ok ? "#2da44e" : "#cf222e" });
46
+ chrome.action.setBadgeText({ text: ok ? "✓" : "!" });
47
+ setTimeout(() => chrome.action.setBadgeText({ text: "" }), 2500);
48
+ }
49
+
50
+ function domainForUrl(url) {
51
+ try {
52
+ const host = new URL(url).hostname;
53
+ if (host === "mail.google.com") return "email";
54
+ if (host.endsWith("linkedin.com")) return "linkedin";
55
+ return "web";
56
+ } catch {
57
+ return "web";
58
+ }
59
+ }
60
+
61
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
62
+ if (message && message.type === "athena-event") {
63
+ postEvent(message.event).then((result) => {
64
+ flashBadge(result.ok);
65
+ sendResponse(result);
66
+ });
67
+ return true;
68
+ }
69
+ if (message && message.type === "athena-health") {
70
+ health().then(sendResponse);
71
+ return true;
72
+ }
73
+ return false;
74
+ });
75
+
76
+ chrome.runtime.onInstalled.addListener(() => {
77
+ chrome.contextMenus.create({
78
+ id: "athena-remember",
79
+ title: "athena: remember this",
80
+ contexts: ["selection"],
81
+ });
82
+ });
83
+
84
+ chrome.contextMenus.onClicked.addListener((info, tab) => {
85
+ if (info.menuItemId !== "athena-remember" || !info.selectionText) return;
86
+ const event = {
87
+ kind: "manual_note",
88
+ situation: {
89
+ summary: `remember from ${tab && tab.title ? tab.title.slice(0, 80) : "page"}`,
90
+ domain: domainForUrl(tab ? tab.url : ""),
91
+ app: "chrome",
92
+ },
93
+ after: info.selectionText.slice(0, 8000),
94
+ raw: { url: tab ? tab.url : undefined },
95
+ };
96
+ postEvent(event).then((result) => flashBadge(result.ok));
97
+ });
@@ -0,0 +1,107 @@
1
+ // athena sensor — Gmail content script.
2
+ //
3
+ // Same capture policy as LinkedIn: track ONLY pasted drafts (>= 80 chars)
4
+ // into a compose body; capture on send (click or Cmd/Ctrl+Enter);
5
+ // edited -> correction, unchanged -> approval. Untracked mail is never read.
6
+
7
+ (() => {
8
+ const MIN_DRAFT_CHARS = 80;
9
+ const tracked = new WeakMap();
10
+ let lastTracked = null;
11
+
12
+ function composeBodyOf(target) {
13
+ if (!target || !target.closest) return null;
14
+ return target.closest("div[role='textbox'][contenteditable='true']");
15
+ }
16
+
17
+ document.addEventListener(
18
+ "paste",
19
+ (event) => {
20
+ try {
21
+ const body = composeBodyOf(event.target);
22
+ if (!body) return;
23
+ setTimeout(() => {
24
+ const value = body.innerText || "";
25
+ if (value.trim().length >= MIN_DRAFT_CHARS) {
26
+ tracked.set(body, { before: value, at: Date.now() });
27
+ lastTracked = body;
28
+ }
29
+ }, 80);
30
+ } catch {
31
+ // sensor must never break the page
32
+ }
33
+ },
34
+ true,
35
+ );
36
+
37
+ function containerOf(node) {
38
+ return node && node.closest ? node.closest("[role='dialog'], .iN, table") : null;
39
+ }
40
+
41
+ function subjectFor(node) {
42
+ const container = containerOf(node);
43
+ const input = (container || document).querySelector("input[name='subjectbox']");
44
+ return input && input.value ? input.value.trim().slice(0, 80) : "";
45
+ }
46
+
47
+ function emitIfTracked(body) {
48
+ try {
49
+ const state = body && tracked.get(body);
50
+ if (!state) return;
51
+ tracked.delete(body);
52
+ if (body === lastTracked) lastTracked = null;
53
+ const after = body.innerText || "";
54
+ if (after.trim().length === 0) return;
55
+ const unchanged = after.trim() === state.before.trim();
56
+ const subject = subjectFor(body);
57
+ chrome.runtime.sendMessage({
58
+ type: "athena-event",
59
+ event: {
60
+ kind: unchanged ? "approval" : "correction",
61
+ situation: {
62
+ summary:
63
+ (unchanged ? "sent pasted draft unchanged" : "edited pasted draft before sending") +
64
+ (subject ? ` (email: ${subject})` : " (email)"),
65
+ domain: "email.compose",
66
+ app: "gmail",
67
+ },
68
+ before: state.before,
69
+ after,
70
+ raw: { url: location.href },
71
+ },
72
+ });
73
+ } catch {
74
+ // sensor must never break the page
75
+ }
76
+ }
77
+
78
+ document.addEventListener(
79
+ "click",
80
+ (event) => {
81
+ try {
82
+ const button =
83
+ event.target && event.target.closest ? event.target.closest("div[role='button'], button") : null;
84
+ if (!button) return;
85
+ const label = (button.getAttribute("aria-label") || button.textContent || "").trim();
86
+ if (!/^send\b/i.test(label)) return;
87
+ const container = containerOf(button);
88
+ const body = container ? container.querySelector("div[role='textbox'][contenteditable='true']") : null;
89
+ emitIfTracked(body || lastTracked);
90
+ } catch {
91
+ // sensor must never break the page
92
+ }
93
+ },
94
+ true,
95
+ );
96
+
97
+ document.addEventListener(
98
+ "keydown",
99
+ (event) => {
100
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
101
+ const body = composeBodyOf(event.target);
102
+ if (body) emitIfTracked(body);
103
+ }
104
+ },
105
+ true,
106
+ );
107
+ })();
@@ -0,0 +1,123 @@
1
+ // athena sensor — LinkedIn content script.
2
+ //
3
+ // Capture policy (the whole point — read before editing):
4
+ // - We track a field ONLY when the user pastes a substantial draft (>= 80 chars)
5
+ // into a note/message field. The pasted text is the "before".
6
+ // - On send (button click or Cmd/Ctrl+Enter) we capture the final text:
7
+ // edited -> correction, unchanged -> approval. Tracking is then cleared.
8
+ // - Untracked typing is NEVER captured. Nothing else on the page is read.
9
+
10
+ (() => {
11
+ const MIN_DRAFT_CHARS = 80;
12
+ const tracked = new WeakMap();
13
+ let lastTracked = null;
14
+
15
+ function fieldOf(target) {
16
+ if (!target || !target.closest) return null;
17
+ return target.closest("textarea, [contenteditable='true']");
18
+ }
19
+
20
+ function valueOf(field) {
21
+ if (!field) return "";
22
+ return field.value !== undefined ? field.value : field.innerText || "";
23
+ }
24
+
25
+ document.addEventListener(
26
+ "paste",
27
+ (event) => {
28
+ try {
29
+ const field = fieldOf(event.target);
30
+ if (!field) return;
31
+ setTimeout(() => {
32
+ const value = valueOf(field);
33
+ if (value.trim().length >= MIN_DRAFT_CHARS) {
34
+ tracked.set(field, { before: value, at: Date.now() });
35
+ lastTracked = field;
36
+ }
37
+ }, 80);
38
+ } catch {
39
+ // sensor must never break the page
40
+ }
41
+ },
42
+ true,
43
+ );
44
+
45
+ function dialogOf(field) {
46
+ return field && field.closest ? field.closest("[role='dialog']") : null;
47
+ }
48
+
49
+ function isConnectionNote(field) {
50
+ const dialog = dialogOf(field);
51
+ if (!dialog) return false;
52
+ return /add a note|invitation|invite|connect/i.test(dialog.textContent || "");
53
+ }
54
+
55
+ function recipientOf(field) {
56
+ const dialog = dialogOf(field);
57
+ if (!dialog) return "";
58
+ const heading = dialog.querySelector("h2, h1, strong");
59
+ return heading ? (heading.textContent || "").trim().slice(0, 60) : "";
60
+ }
61
+
62
+ function emitIfTracked(field) {
63
+ try {
64
+ const state = field && tracked.get(field);
65
+ if (!state) return;
66
+ tracked.delete(field);
67
+ if (field === lastTracked) lastTracked = null;
68
+ const after = valueOf(field);
69
+ if (after.trim().length === 0) return;
70
+ const unchanged = after.trim() === state.before.trim();
71
+ const note = isConnectionNote(field);
72
+ const recipient = recipientOf(field);
73
+ chrome.runtime.sendMessage({
74
+ type: "athena-event",
75
+ event: {
76
+ kind: unchanged ? "approval" : "correction",
77
+ situation: {
78
+ summary:
79
+ (unchanged ? "sent pasted draft unchanged" : "edited pasted draft before sending") +
80
+ (note ? " (LinkedIn connection note" : " (LinkedIn message") +
81
+ (recipient ? ` to ${recipient})` : ")"),
82
+ domain: note ? "linkedin.connection_note" : "linkedin.message",
83
+ app: "linkedin",
84
+ },
85
+ before: state.before,
86
+ after,
87
+ raw: { url: location.href },
88
+ },
89
+ });
90
+ } catch {
91
+ // sensor must never break the page
92
+ }
93
+ }
94
+
95
+ document.addEventListener(
96
+ "click",
97
+ (event) => {
98
+ try {
99
+ const button = event.target && event.target.closest ? event.target.closest("button") : null;
100
+ if (!button) return;
101
+ const label = (button.getAttribute("aria-label") || button.textContent || "").trim();
102
+ if (!/^send\b/i.test(label)) return;
103
+ const dialog = button.closest("[role='dialog']");
104
+ const field = dialog ? fieldOf(dialog.querySelector("textarea, [contenteditable='true']")) : lastTracked;
105
+ if (field) emitIfTracked(field);
106
+ } catch {
107
+ // sensor must never break the page
108
+ }
109
+ },
110
+ true,
111
+ );
112
+
113
+ document.addEventListener(
114
+ "keydown",
115
+ (event) => {
116
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
117
+ const field = fieldOf(event.target);
118
+ if (field) emitIfTracked(field);
119
+ }
120
+ },
121
+ true,
122
+ );
123
+ })();
@@ -0,0 +1,27 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "athena sensor",
4
+ "version": "0.1.0",
5
+ "description": "Captures the judgment behind your edits of drafted messages — locally, to your own athena. No cloud, no telemetry.",
6
+ "permissions": ["storage", "contextMenus"],
7
+ "host_permissions": [
8
+ "https://www.linkedin.com/*",
9
+ "https://mail.google.com/*",
10
+ "http://127.0.0.1/*"
11
+ ],
12
+ "background": { "service_worker": "background.js" },
13
+ "content_scripts": [
14
+ {
15
+ "matches": ["https://www.linkedin.com/*"],
16
+ "js": ["linkedin.js"],
17
+ "run_at": "document_idle"
18
+ },
19
+ {
20
+ "matches": ["https://mail.google.com/*"],
21
+ "js": ["gmail.js"],
22
+ "run_at": "document_idle"
23
+ }
24
+ ],
25
+ "action": { "default_popup": "popup.html", "default_title": "athena" },
26
+ "options_page": "options.html"
27
+ }