Skip to content

Commit 940ba2e

Browse files
committed
Replace prompt injection with priming message architecture
Instead of stuffing memory context into the visible input (like Mem0), the extension now sends an invisible priming message at the start of each new conversation. The AI processes the context and the exchange is hidden from view. By the time the user types, the AI genuinely already knows their context. Made-with: Cursor
1 parent 3df92aa commit 940ba2e

7 files changed

Lines changed: 352 additions & 192 deletions

File tree

extensions/chrome/README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ Your AI tools share one memory. Tell one, they all know.
1212

1313
## How It Works
1414

15-
The extension runs invisibly in the background on supported AI chat sites:
15+
The extension uses a "Priming Message" architecture that makes the AI genuinely already know your context, rather than injecting text into your prompts.
1616

17-
1. **Memory injection**: When you send a message, the extension searches your Reflect Memory for relevant context and silently prepends it to your prompt. The AI sees your full context without you doing anything.
17+
1. **When you open a new conversation** on any supported AI site, the extension detects the empty chat and sends a brief priming message containing your Reflect Memory context. The AI processes it and responds with "Ready."
1818

19-
2. **Memory capture**: After the AI responds, the extension captures the key exchange and writes it back to Reflect Memory. Other AI tools will have this context next time.
19+
2. **Both the priming message and response are hidden** from view by the extension. By the time you start typing, the AI already has your context in its conversation history. It knows your projects, preferences, and recent work without you saying a word.
2020

21-
No buttons to click. No commands to type. No approval prompts. It just works.
21+
3. **As you chat**, the extension passively captures meaningful exchanges and writes them back to Reflect Memory. Next time you open a different AI tool, that context is already there.
22+
23+
No buttons. No commands. No visible injection. The AI just knows.
2224

2325
## Install (Development)
2426

@@ -34,9 +36,9 @@ No buttons to click. No commands to type. No approval prompts. It just works.
3436
chrome/
3537
manifest.json - MV3 manifest with per-vendor content scripts
3638
background.js - Service worker handling all API calls
37-
popup.html / popup.js - Setup UI for entering agent key
39+
popup.html / popup.js - One-time setup UI for agent key
3840
content-scripts/
39-
shared.js - Core logic: memory injection, capture, vendor adapter interface
41+
shared.js - Core: priming message, memory capture, vendor adapter interface
4042
chatgpt.js - ChatGPT DOM adapter
4143
claude.js - Claude DOM adapter
4244
gemini.js - Gemini DOM adapter
@@ -51,4 +53,4 @@ chrome/
5153
- API calls go only to api.reflectmemory.com
5254
- No analytics, no tracking, no third-party services
5355
- Content scripts only run on supported AI chat sites
54-
- No page content is sent anywhere except conversation messages to your own Reflect Memory account
56+
- The priming message is hidden from view and never stored in your conversation exports

extensions/chrome/content-scripts/chatgpt.js

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
/**
22
* ChatGPT content script adapter.
33
*
4-
* ChatGPT uses a contenteditable div (#prompt-textarea) for input
5-
* and renders messages in [data-message-author-role] elements.
6-
*
74
* NOTE: ChatGPT already has full API integration via the Custom GPT
8-
* with the isConsequential fix. This content script is a fallback
9-
* for users who haven't set up the Custom GPT, providing passive
10-
* memory capture from regular ChatGPT conversations.
5+
* with the isConsequential fix. This content script provides the
6+
* invisible priming experience for users on regular chatgpt.com
7+
* who haven't set up the Custom GPT.
118
*/
129

1310
(() => {
@@ -28,9 +25,14 @@
2825
const el = this.getInputElement();
2926
if (!el) return;
3027
if (el.tagName === "TEXTAREA") {
31-
el.value = text;
28+
const setter = Object.getOwnPropertyDescriptor(
29+
HTMLTextAreaElement.prototype, "value"
30+
)?.set;
31+
if (setter) setter.call(el, text);
32+
else el.value = text;
3233
el.dispatchEvent(new Event("input", { bubbles: true }));
3334
} else {
35+
el.focus();
3436
el.innerText = text;
3537
el.dispatchEvent(new Event("input", { bubbles: true }));
3638
}
@@ -49,19 +51,42 @@
4951
return messages;
5052
},
5153

52-
onNewMessage(callback) {
53-
const container =
54-
document.querySelector("main") || document.body;
54+
isNewConversation() {
55+
return this.getMessages().length === 0;
56+
},
57+
58+
triggerSend() {
59+
const btn = document.querySelector(
60+
"[data-testid='send-button'], button[aria-label='Send prompt']"
61+
);
62+
if (btn) {
63+
btn.click();
64+
return;
65+
}
66+
const el = this.getInputElement();
67+
if (el) {
68+
el.dispatchEvent(new KeyboardEvent("keydown", {
69+
key: "Enter", code: "Enter", keyCode: 13, bubbles: true,
70+
}));
71+
}
72+
},
73+
74+
hideLastExchange() {
75+
const allMsgs = document.querySelectorAll("[data-message-author-role]");
76+
const toHide = [...allMsgs].slice(-2);
77+
toHide.forEach((el) => {
78+
const container = el.closest("[data-testid^='conversation-turn']") || el.parentElement;
79+
if (container) container.style.display = "none";
80+
});
81+
},
5582

83+
onNewMessage(callback) {
84+
const container = document.querySelector("main") || document.body;
5685
const observer = new MutationObserver(() => {
5786
const messages = this.getMessages();
5887
if (messages.length > 0) callback(messages);
5988
});
60-
61-
observer.observe(container, {
62-
childList: true,
63-
subtree: true,
64-
});
89+
observer.observe(container, { childList: true, subtree: true });
6590
},
6691
};
6792

extensions/chrome/content-scripts/claude.js

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* Claude content script adapter.
33
*
4-
* Claude uses a contenteditable div with class "ProseMirror" for input
5-
* and renders conversation turns in structured containers.
4+
* Claude uses a ProseMirror contenteditable for input and renders
5+
* conversation turns in structured containers.
66
*/
77

88
(() => {
@@ -11,7 +11,8 @@
1111
const adapter = {
1212
getInputElement() {
1313
return document.querySelector(
14-
"[contenteditable='true'].ProseMirror, div[contenteditable='true'][data-placeholder]"
14+
"[contenteditable='true'].ProseMirror, " +
15+
"div[contenteditable='true'][data-placeholder]"
1516
);
1617
},
1718

@@ -31,53 +32,64 @@
3132

3233
getMessages() {
3334
const messages = [];
34-
35-
const humanTurns = document.querySelectorAll(
36-
"[data-testid='human-turn'], .font-user-message"
37-
);
38-
const aiTurns = document.querySelectorAll(
39-
"[data-testid='ai-turn'], .font-claude-message"
35+
const turns = document.querySelectorAll(
36+
"[data-testid='human-turn'], [data-testid='ai-turn'], " +
37+
".font-user-message, .font-claude-message"
4038
);
41-
42-
humanTurns.forEach((el) => {
39+
turns.forEach((el) => {
40+
const isUser =
41+
el.matches("[data-testid='human-turn']") ||
42+
el.matches(".font-user-message");
4343
const text = el.innerText?.trim();
44-
if (text) messages.push({ role: "user", text });
44+
if (text) {
45+
messages.push({ role: isUser ? "user" : "assistant", text });
46+
}
4547
});
48+
return messages;
49+
},
4650

47-
aiTurns.forEach((el) => {
48-
const text = el.innerText?.trim();
49-
if (text) messages.push({ role: "assistant", text });
50-
});
51+
isNewConversation() {
52+
return this.getMessages().length === 0;
53+
},
5154

52-
messages.sort((a, b) => {
53-
const aEl =
54-
a.role === "user"
55-
? [...humanTurns].find((e) => e.innerText?.trim() === a.text)
56-
: [...aiTurns].find((e) => e.innerText?.trim() === a.text);
57-
const bEl =
58-
b.role === "user"
59-
? [...humanTurns].find((e) => e.innerText?.trim() === b.text)
60-
: [...aiTurns].find((e) => e.innerText?.trim() === b.text);
61-
if (!aEl || !bEl) return 0;
62-
return aEl.compareDocumentPosition(bEl) & Node.DOCUMENT_POSITION_FOLLOWING
63-
? -1
64-
: 1;
65-
});
55+
triggerSend() {
56+
const btn = document.querySelector(
57+
"button[aria-label='Send Message'], " +
58+
"button[data-testid='send-message']"
59+
);
60+
if (btn) {
61+
btn.click();
62+
return;
63+
}
64+
const el = this.getInputElement();
65+
if (el) {
66+
el.dispatchEvent(new KeyboardEvent("keydown", {
67+
key: "Enter", code: "Enter", keyCode: 13, bubbles: true,
68+
}));
69+
}
70+
},
6671

67-
return messages;
72+
hideLastExchange() {
73+
const turns = document.querySelectorAll(
74+
"[data-testid='human-turn'], [data-testid='ai-turn'], " +
75+
".font-user-message, .font-claude-message"
76+
);
77+
const toHide = [...turns].slice(-2);
78+
toHide.forEach((el) => {
79+
const container = el.closest("[class*='turn']") || el.parentElement;
80+
if (container) container.style.display = "none";
81+
});
6882
},
6983

7084
onNewMessage(callback) {
7185
const container =
7286
document.querySelector("[data-testid='conversation']") ||
7387
document.querySelector("main") ||
7488
document.body;
75-
7689
const observer = new MutationObserver(() => {
7790
const messages = this.getMessages();
7891
if (messages.length > 0) callback(messages);
7992
});
80-
8193
observer.observe(container, { childList: true, subtree: true });
8294
},
8395
};

extensions/chrome/content-scripts/gemini.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* Gemini content script adapter.
33
*
4-
* Gemini uses a rich text input area and renders conversation
5-
* in message-content containers with model/user attribution.
4+
* Gemini uses a rich text editor (Quill-based) for input and renders
5+
* conversation in message containers.
66
*/
77

88
(() => {
@@ -33,39 +33,66 @@
3333

3434
getMessages() {
3535
const messages = [];
36-
3736
const turns = document.querySelectorAll(
38-
"message-content, .conversation-container .message"
37+
"message-content, .conversation-container .message, " +
38+
"[data-message-author]"
3939
);
40-
4140
turns.forEach((el) => {
4241
const isUser =
4342
el.closest("[data-message-author='user']") ||
4443
el.closest(".user-message") ||
4544
el.querySelector(".query-text");
4645
const text = el.innerText?.trim();
4746
if (text) {
48-
messages.push({
49-
role: isUser ? "user" : "assistant",
50-
text,
51-
});
47+
messages.push({ role: isUser ? "user" : "assistant", text });
5248
}
5349
});
54-
5550
return messages;
5651
},
5752

53+
isNewConversation() {
54+
return this.getMessages().length === 0;
55+
},
56+
57+
triggerSend() {
58+
const btn = document.querySelector(
59+
"button[aria-label='Send message'], " +
60+
"button.send-button, " +
61+
"[data-test-id='send-button']"
62+
);
63+
if (btn) {
64+
btn.click();
65+
return;
66+
}
67+
const el = this.getInputElement();
68+
if (el) {
69+
el.dispatchEvent(new KeyboardEvent("keydown", {
70+
key: "Enter", code: "Enter", keyCode: 13, bubbles: true,
71+
}));
72+
}
73+
},
74+
75+
hideLastExchange() {
76+
const turns = document.querySelectorAll(
77+
"message-content, .conversation-container .message, " +
78+
"[data-message-author]"
79+
);
80+
const toHide = [...turns].slice(-2);
81+
toHide.forEach((el) => {
82+
const container = el.closest(".message-wrapper") || el.parentElement;
83+
if (container) container.style.display = "none";
84+
});
85+
},
86+
5887
onNewMessage(callback) {
5988
const container =
6089
document.querySelector(".conversation-container") ||
6190
document.querySelector("main") ||
6291
document.body;
63-
6492
const observer = new MutationObserver(() => {
6593
const messages = this.getMessages();
6694
if (messages.length > 0) callback(messages);
6795
});
68-
6996
observer.observe(container, { childList: true, subtree: true });
7097
},
7198
};

0 commit comments

Comments
 (0)