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.
- package/README.md +258 -0
- package/apps/chrome-extension/README.md +35 -0
- package/apps/chrome-extension/background.js +97 -0
- package/apps/chrome-extension/gmail.js +107 -0
- package/apps/chrome-extension/linkedin.js +123 -0
- package/apps/chrome-extension/manifest.json +27 -0
- package/apps/chrome-extension/options.html +60 -0
- package/apps/chrome-extension/options.js +36 -0
- package/apps/chrome-extension/popup.html +37 -0
- package/apps/chrome-extension/popup.js +22 -0
- package/bin/athena +28 -0
- package/dist/api/server.js +145 -0
- package/dist/capture/ingest.js +85 -0
- package/dist/cli/commands.js +201 -0
- package/dist/cli/format.js +76 -0
- package/dist/cli/setup.js +316 -0
- package/dist/cli.js +291 -0
- package/dist/config.js +26 -0
- package/dist/core/fixtures.js +65 -0
- package/dist/core/ids.js +34 -0
- package/dist/core/refs.js +25 -0
- package/dist/core/types.js +10 -0
- package/dist/engine/engine.js +136 -0
- package/dist/engine/parse.js +76 -0
- package/dist/engine/prompts.js +64 -0
- package/dist/eval/harness.js +123 -0
- package/dist/eval/judge.js +75 -0
- package/dist/eval/run-eval.js +46 -0
- package/dist/eval/scenarios.js +470 -0
- package/dist/mcp/server.js +107 -0
- package/dist/mcp-server.js +7 -0
- package/dist/model/api-model-client.js +99 -0
- package/dist/model/cli-model-client.js +111 -0
- package/dist/model/model-client.js +28 -0
- package/dist/model/registry.js +67 -0
- package/dist/sensors/claude-code-hook.js +131 -0
- package/dist/serve/brief.js +95 -0
- package/dist/serve/outcome.js +56 -0
- package/dist/store/open.js +19 -0
- package/dist/store/store.js +269 -0
- package/docs/schema.md +368 -0
- package/package.json +43 -0
- 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
|
+
}
|