enigma-memory 0.1.1 → 0.1.2
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/apps/browser-extension/manifest.json +41 -0
- package/apps/browser-extension/src/background.js +88 -0
- package/apps/browser-extension/src/content-script.js +602 -0
- package/apps/browser-extension/src/native-bridge.js +289 -0
- package/apps/cli/bin/enigma.mjs +209 -2
- package/apps/desktop/src/tray.js +231 -0
- package/docs/browser-extension-install.md +169 -0
- package/docs/developer-ecosystem.md +74 -0
- package/docs/hosted-cloud-product.md +68 -0
- package/docs/installers-and-desktop.md +76 -0
- package/docs/memory-benchmarks.md +51 -0
- package/docs/sdk-api.md +181 -0
- package/examples/ci/github-actions.yml +63 -0
- package/examples/node-basic-memory.mjs +84 -0
- package/package.json +20 -1
- package/packages/connectors/src/index.js +274 -39
- package/packages/hosted-cloud/src/index.js +538 -0
- package/packages/mcp-server/src/index.js +1 -1
- package/scripts/build-installer-assets.mjs +273 -0
- package/scripts/package-browser-extension.mjs +473 -0
- package/scripts/run-memory-benchmarks.mjs +585 -0
- package/scripts/verify-registry-install.mjs +6 -1
- package/templates/mcp-client-config.json +10 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const PROTOCOL = 'enigma-browser-extension';
|
|
5
|
+
const SOURCE = 'enigma-content-script';
|
|
6
|
+
const ROOT_ID = 'enigma-local-memory-root';
|
|
7
|
+
const MAX_INSERT_CHARS = 32000;
|
|
8
|
+
|
|
9
|
+
const PROVIDERS = Object.freeze({
|
|
10
|
+
chatgpt: Object.freeze({
|
|
11
|
+
id: 'chatgpt',
|
|
12
|
+
label: 'ChatGPT',
|
|
13
|
+
hosts: Object.freeze(['chatgpt.com', 'chat.openai.com']),
|
|
14
|
+
promptSelectors: Object.freeze([
|
|
15
|
+
'textarea[data-id="root"]',
|
|
16
|
+
'textarea',
|
|
17
|
+
'div[contenteditable="true"][data-id="root"]',
|
|
18
|
+
'div[contenteditable="true"].ProseMirror',
|
|
19
|
+
'div[contenteditable="true"][role="textbox"]',
|
|
20
|
+
'div[contenteditable]:not([contenteditable="false"])'
|
|
21
|
+
])
|
|
22
|
+
}),
|
|
23
|
+
claude: Object.freeze({
|
|
24
|
+
id: 'claude',
|
|
25
|
+
label: 'Claude',
|
|
26
|
+
hosts: Object.freeze(['claude.ai']),
|
|
27
|
+
promptSelectors: Object.freeze([
|
|
28
|
+
'div[contenteditable="true"][aria-label]',
|
|
29
|
+
'div[contenteditable="true"].ProseMirror',
|
|
30
|
+
'div[contenteditable="true"][role="textbox"]',
|
|
31
|
+
'textarea',
|
|
32
|
+
'div[contenteditable]:not([contenteditable="false"])'
|
|
33
|
+
])
|
|
34
|
+
}),
|
|
35
|
+
kimi: Object.freeze({
|
|
36
|
+
id: 'kimi',
|
|
37
|
+
label: 'Kimi',
|
|
38
|
+
hosts: Object.freeze(['kimi.com', 'www.kimi.com', 'kimi.moonshot.cn']),
|
|
39
|
+
promptSelectors: Object.freeze([
|
|
40
|
+
'textarea',
|
|
41
|
+
'div[contenteditable="true"][role="textbox"]',
|
|
42
|
+
'div[contenteditable="true"].ProseMirror',
|
|
43
|
+
'div[contenteditable]:not([contenteditable="false"])'
|
|
44
|
+
])
|
|
45
|
+
}),
|
|
46
|
+
perplexity: Object.freeze({
|
|
47
|
+
id: 'perplexity',
|
|
48
|
+
label: 'Perplexity',
|
|
49
|
+
hosts: Object.freeze(['perplexity.ai', 'www.perplexity.ai']),
|
|
50
|
+
promptSelectors: Object.freeze([
|
|
51
|
+
'textarea',
|
|
52
|
+
'div[contenteditable="true"][role="textbox"]',
|
|
53
|
+
'div[contenteditable="true"][aria-label]',
|
|
54
|
+
'div[contenteditable="true"].ProseMirror',
|
|
55
|
+
'div[contenteditable]:not([contenteditable="false"])'
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const provider = detectProvider(location.href);
|
|
61
|
+
if (!provider) return;
|
|
62
|
+
|
|
63
|
+
const state = {
|
|
64
|
+
root: undefined,
|
|
65
|
+
shadow: undefined,
|
|
66
|
+
context: undefined,
|
|
67
|
+
approvedTarget: undefined,
|
|
68
|
+
pendingSelection: undefined,
|
|
69
|
+
currentTarget: undefined,
|
|
70
|
+
busy: false,
|
|
71
|
+
observer: undefined,
|
|
72
|
+
targetStatus: 'Looking for focused prompt box…'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
mount();
|
|
76
|
+
sendMessage({ kind: 'enigma.provider.detected' }).catch(() => undefined);
|
|
77
|
+
|
|
78
|
+
function mount() {
|
|
79
|
+
if (document.getElementById(ROOT_ID)) return;
|
|
80
|
+
|
|
81
|
+
state.root = document.createElement('div');
|
|
82
|
+
state.root.id = ROOT_ID;
|
|
83
|
+
state.root.setAttribute('data-provider', provider.id);
|
|
84
|
+
state.shadow = state.root.attachShadow({ mode: 'closed' });
|
|
85
|
+
document.documentElement.appendChild(state.root);
|
|
86
|
+
|
|
87
|
+
renderCollapsed();
|
|
88
|
+
state.observer = new MutationObserver(updateTargetStatus);
|
|
89
|
+
state.observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
90
|
+
document.addEventListener('focusin', handleFocusIn, true);
|
|
91
|
+
updateTargetStatus();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderCollapsed() {
|
|
95
|
+
resetShadow();
|
|
96
|
+
const button = el('button', 'enigma-launch', `Enigma context for ${provider.label}`);
|
|
97
|
+
button.type = 'button';
|
|
98
|
+
button.addEventListener('click', renderPanel);
|
|
99
|
+
state.shadow.append(styleNode(), button);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderPanel() {
|
|
103
|
+
resetShadow();
|
|
104
|
+
|
|
105
|
+
const panel = el('section', 'enigma-panel');
|
|
106
|
+
const header = el('div', 'enigma-header');
|
|
107
|
+
const title = el('strong', '', 'Enigma local memory');
|
|
108
|
+
const close = el('button', 'enigma-close', '×');
|
|
109
|
+
close.type = 'button';
|
|
110
|
+
close.setAttribute('aria-label', 'Close Enigma panel');
|
|
111
|
+
close.addEventListener('click', () => {
|
|
112
|
+
state.context = undefined;
|
|
113
|
+
state.approvedTarget = undefined;
|
|
114
|
+
state.pendingSelection = undefined;
|
|
115
|
+
renderCollapsed();
|
|
116
|
+
});
|
|
117
|
+
header.append(title, close);
|
|
118
|
+
|
|
119
|
+
const body = el('div', 'enigma-body');
|
|
120
|
+
body.append(
|
|
121
|
+
el('p', 'enigma-copy', `Detected ${provider.label}. Enigma never injects automatically. Focus the prompt, request context, review the receipt boundary, then approve insertion.`),
|
|
122
|
+
el('p', 'enigma-status', state.targetStatus)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const selectionLabel = el('label', 'enigma-check');
|
|
126
|
+
const selectionCheckbox = document.createElement('input');
|
|
127
|
+
selectionCheckbox.type = 'checkbox';
|
|
128
|
+
selectionCheckbox.checked = false;
|
|
129
|
+
selectionLabel.append(selectionCheckbox, document.createTextNode(` Include selected page text after a local-only warning showing character count and ${location.origin}`));
|
|
130
|
+
body.append(selectionLabel);
|
|
131
|
+
|
|
132
|
+
if (state.context) body.append(receiptBoundarySummary(state.context.receipt));
|
|
133
|
+
|
|
134
|
+
const preview = el('pre', 'enigma-preview', state.context ? previewText(state.context.context.text) : 'No context requested yet.');
|
|
135
|
+
body.append(preview);
|
|
136
|
+
|
|
137
|
+
const actions = el('div', 'enigma-actions');
|
|
138
|
+
const request = el('button', 'enigma-primary', state.context ? 'Refresh context' : 'Request context');
|
|
139
|
+
request.type = 'button';
|
|
140
|
+
request.disabled = state.busy;
|
|
141
|
+
request.addEventListener('click', async () => {
|
|
142
|
+
await withBusy(async () => {
|
|
143
|
+
const target = findPromptTarget();
|
|
144
|
+
if (!target) throw new Error(`Focus the ${provider.label} prompt box before requesting Enigma context.`);
|
|
145
|
+
const approvedTarget = capturePromptTarget(target);
|
|
146
|
+
if (selectionCheckbox.checked) {
|
|
147
|
+
const selection = selectedPageText();
|
|
148
|
+
if (!selection) throw new Error('Select page text first, then approve sending that selection to the local Enigma host.');
|
|
149
|
+
state.pendingSelection = { selection, target: approvedTarget };
|
|
150
|
+
renderSelectionApproval(state.pendingSelection);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
state.pendingSelection = undefined;
|
|
154
|
+
state.approvedTarget = approvedTarget;
|
|
155
|
+
state.context = await requestContext(undefined);
|
|
156
|
+
renderPanel();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const insert = el('button', 'enigma-insert', 'Approve and insert');
|
|
161
|
+
insert.type = 'button';
|
|
162
|
+
insert.disabled = state.busy || !state.context;
|
|
163
|
+
insert.addEventListener('click', async () => {
|
|
164
|
+
await withBusy(async () => {
|
|
165
|
+
if (!state.context) throw new Error('Request context before insertion.');
|
|
166
|
+
const target = requireApprovedTarget();
|
|
167
|
+
const contextPack = state.context;
|
|
168
|
+
const result = insertPlainText(target.element, contextPack.context.text);
|
|
169
|
+
state.context = undefined;
|
|
170
|
+
state.approvedTarget = undefined;
|
|
171
|
+
state.pendingSelection = undefined;
|
|
172
|
+
await recordInsertion(contextPack.receipt, result);
|
|
173
|
+
renderNotice('Inserted Enigma context. The receipt commitment and local record were saved by the local host.');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
actions.append(request, insert);
|
|
178
|
+
body.append(actions);
|
|
179
|
+
panel.append(header, body);
|
|
180
|
+
state.shadow.append(styleNode(), panel);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderSelectionApproval(snapshot) {
|
|
184
|
+
resetShadow();
|
|
185
|
+
const panel = el('section', 'enigma-panel');
|
|
186
|
+
const header = el('div', 'enigma-header');
|
|
187
|
+
header.append(el('strong', '', 'Selected text warning'), closeButton());
|
|
188
|
+
const summary = el(
|
|
189
|
+
'p',
|
|
190
|
+
'enigma-copy',
|
|
191
|
+
`${snapshot.selection.charCount} selected page characters from ${snapshot.selection.origin} are selected; ${snapshot.selection.includedCharCount} characters will be sent only to the local Enigma host. No browser sync storage is used.`
|
|
192
|
+
);
|
|
193
|
+
const target = el('p', 'enigma-status', `Prompt target locked: ${snapshot.target.label}. If focus changes before insertion, Enigma will stop.`);
|
|
194
|
+
const actions = el('div', 'enigma-actions');
|
|
195
|
+
const cancel = el('button', 'enigma-primary', 'Cancel');
|
|
196
|
+
cancel.type = 'button';
|
|
197
|
+
cancel.addEventListener('click', () => {
|
|
198
|
+
state.pendingSelection = undefined;
|
|
199
|
+
renderPanel();
|
|
200
|
+
});
|
|
201
|
+
const approve = el('button', 'enigma-insert', 'Send selected text to local host');
|
|
202
|
+
approve.type = 'button';
|
|
203
|
+
approve.disabled = state.busy;
|
|
204
|
+
approve.addEventListener('click', async () => {
|
|
205
|
+
await withBusy(async () => {
|
|
206
|
+
ensureCurrentTarget(snapshot.target);
|
|
207
|
+
state.approvedTarget = snapshot.target;
|
|
208
|
+
state.context = await requestContext(snapshot.selection);
|
|
209
|
+
state.pendingSelection = undefined;
|
|
210
|
+
renderPanel();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
actions.append(cancel, approve);
|
|
214
|
+
panel.append(header, summary, target, actions);
|
|
215
|
+
state.shadow.append(styleNode(), panel);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderNotice(message) {
|
|
219
|
+
resetShadow();
|
|
220
|
+
const panel = el('section', 'enigma-panel');
|
|
221
|
+
panel.append(el('div', 'enigma-header', 'Enigma local memory'), el('p', 'enigma-copy', message));
|
|
222
|
+
const close = el('button', 'enigma-primary', 'Done');
|
|
223
|
+
close.type = 'button';
|
|
224
|
+
close.addEventListener('click', renderCollapsed);
|
|
225
|
+
panel.append(close);
|
|
226
|
+
state.shadow.append(styleNode(), panel);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderError(message) {
|
|
230
|
+
resetShadow();
|
|
231
|
+
const panel = el('section', 'enigma-panel');
|
|
232
|
+
const header = el('div', 'enigma-header');
|
|
233
|
+
header.append(el('strong', '', 'Enigma local memory'), closeButton());
|
|
234
|
+
panel.append(header, el('p', 'enigma-error', message));
|
|
235
|
+
const retry = el('button', 'enigma-primary', 'Back');
|
|
236
|
+
retry.type = 'button';
|
|
237
|
+
retry.addEventListener('click', renderPanel);
|
|
238
|
+
panel.append(retry);
|
|
239
|
+
state.shadow.append(styleNode(), panel);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function closeButton() {
|
|
243
|
+
const close = el('button', 'enigma-close', '×');
|
|
244
|
+
close.type = 'button';
|
|
245
|
+
close.setAttribute('aria-label', 'Close Enigma panel');
|
|
246
|
+
close.addEventListener('click', () => {
|
|
247
|
+
state.context = undefined;
|
|
248
|
+
state.approvedTarget = undefined;
|
|
249
|
+
state.pendingSelection = undefined;
|
|
250
|
+
renderCollapsed();
|
|
251
|
+
});
|
|
252
|
+
return close;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function withBusy(operation) {
|
|
256
|
+
if (state.busy) return;
|
|
257
|
+
state.busy = true;
|
|
258
|
+
try {
|
|
259
|
+
await operation();
|
|
260
|
+
} catch (error) {
|
|
261
|
+
renderError(error instanceof Error ? error.message : 'Enigma action failed.');
|
|
262
|
+
} finally {
|
|
263
|
+
state.busy = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function receiptBoundarySummary(receipt) {
|
|
268
|
+
const summary = el('pre', 'enigma-preview');
|
|
269
|
+
summary.textContent = [
|
|
270
|
+
`Receipt ID: ${receipt.id}`,
|
|
271
|
+
`Commitment: ${receipt.commitment}`,
|
|
272
|
+
'Proof boundary: local receipt record only; raw memory and selected page text are forbidden in receipt, relay, witness, SIEM, and public proof artifacts.'
|
|
273
|
+
].join('\n');
|
|
274
|
+
return summary;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function capturePromptTarget(target) {
|
|
278
|
+
return Object.freeze({ element: target.element, label: target.label });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function requireApprovedTarget() {
|
|
282
|
+
if (!state.approvedTarget) throw new Error('Request context again from the focused prompt before approving insertion.');
|
|
283
|
+
return ensureCurrentTarget(state.approvedTarget);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ensureCurrentTarget(expected) {
|
|
287
|
+
const current = findPromptTarget();
|
|
288
|
+
if (!current || current.element !== expected.element) {
|
|
289
|
+
throw new Error('The focused prompt target changed before approval. Refocus the original prompt and request context again.');
|
|
290
|
+
}
|
|
291
|
+
return current;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function requestContext(selection) {
|
|
295
|
+
const response = await sendMessage({
|
|
296
|
+
kind: 'enigma.context.request',
|
|
297
|
+
selection,
|
|
298
|
+
page: {
|
|
299
|
+
origin: location.origin,
|
|
300
|
+
hostname: location.hostname,
|
|
301
|
+
titlePresent: document.title.length > 0,
|
|
302
|
+
titleCharCount: document.title.length
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
if (!response.payload?.context?.text || !response.payload?.receipt) throw new Error('Local host returned an incomplete context pack.');
|
|
306
|
+
return response.payload;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function recordInsertion(receipt, result) {
|
|
310
|
+
await sendMessage({
|
|
311
|
+
kind: 'enigma.insertion.record',
|
|
312
|
+
receipt,
|
|
313
|
+
insertedCharCount: result.insertedCharCount,
|
|
314
|
+
mode: result.mode,
|
|
315
|
+
target: result.target
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function sendMessage(payload) {
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
chrome.runtime.sendMessage(
|
|
322
|
+
{
|
|
323
|
+
protocol: PROTOCOL,
|
|
324
|
+
source: SOURCE,
|
|
325
|
+
provider: provider.id,
|
|
326
|
+
...payload
|
|
327
|
+
},
|
|
328
|
+
(response) => {
|
|
329
|
+
const lastError = chrome.runtime.lastError;
|
|
330
|
+
if (lastError) {
|
|
331
|
+
reject(new Error('Unable to contact Enigma extension background.'));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!response?.ok) {
|
|
335
|
+
reject(new Error(safeBackgroundErrorMessage(response?.error)));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
resolve(response);
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function safeBackgroundErrorMessage(message) {
|
|
345
|
+
if (typeof message !== 'string') return 'Enigma background rejected the request without exposing local memory.';
|
|
346
|
+
const trimmed = message.trim();
|
|
347
|
+
if (!trimmed || trimmed.length > 160 || looksUnsafeDisplayError(trimmed) || !isKnownSafeBackgroundError(trimmed)) {
|
|
348
|
+
return 'Enigma background rejected the request without exposing local memory.';
|
|
349
|
+
}
|
|
350
|
+
return trimmed;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isKnownSafeBackgroundError(message) {
|
|
354
|
+
switch (message) {
|
|
355
|
+
case 'Unsupported or unknown AI provider page.':
|
|
356
|
+
case 'Message provider does not match the active page.':
|
|
357
|
+
case 'Unsupported Enigma extension message.':
|
|
358
|
+
case 'Native host rejected context request.':
|
|
359
|
+
case 'Native host rejected insertion receipt.':
|
|
360
|
+
case 'Unable to reach Enigma native host.':
|
|
361
|
+
case 'Timed out waiting for Enigma native host.':
|
|
362
|
+
case 'Native host returned an invalid Enigma context response.':
|
|
363
|
+
case 'Native host returned an invalid insertion receipt acknowledgement.':
|
|
364
|
+
case 'Native host response id did not match the request id.':
|
|
365
|
+
case 'Native host did not return insertable context.':
|
|
366
|
+
case 'Enigma native host must return a receipt object.':
|
|
367
|
+
case 'Receipt contains a forbidden plaintext-like field.':
|
|
368
|
+
case 'Selected page text exceeds 4000 characters.':
|
|
369
|
+
case 'selection.source exceeds 80 characters.':
|
|
370
|
+
return true;
|
|
371
|
+
default:
|
|
372
|
+
return /^(?:Native host context exceeds|Missing [A-Za-z0-9_.]+|[A-Za-z0-9_.]+ exceeds) \d+ characters\.$/.test(message) ||
|
|
373
|
+
/^Missing [A-Za-z0-9_.]+\.$/.test(message);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function looksUnsafeDisplayError(message) {
|
|
377
|
+
return /https?:\/\/|[?&#](?:token|key|prompt|q)=|\b(selectedText|selected_text|plaintext|rawMemory|raw_memory|contextText|context_text)\b|["'](?:text|content|memory|raw|prompt|receipt)["']\s*:/i.test(message);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function updateTargetStatus() {
|
|
381
|
+
const target = findPromptTarget();
|
|
382
|
+
state.targetStatus = target ? `Focused prompt target ready: ${target.label}.` : `No focused ${provider.label} prompt box found. Focus the composer before requesting or inserting Enigma context.`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function findPromptTarget() {
|
|
386
|
+
const active = document.activeElement;
|
|
387
|
+
if (active && active !== state.root) {
|
|
388
|
+
const activeTarget = targetFromElement(active);
|
|
389
|
+
if (activeTarget) {
|
|
390
|
+
state.currentTarget = capturePromptTarget(activeTarget);
|
|
391
|
+
return state.currentTarget;
|
|
392
|
+
}
|
|
393
|
+
const nestedEditable = active.closest?.('[contenteditable="true"], textarea, input');
|
|
394
|
+
const nestedTarget = targetFromElement(nestedEditable);
|
|
395
|
+
if (nestedTarget) {
|
|
396
|
+
state.currentTarget = capturePromptTarget(nestedTarget);
|
|
397
|
+
return state.currentTarget;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (state.currentTarget?.element?.isConnected && isVisible(state.currentTarget.element)) return state.currentTarget;
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function handleFocusIn(event) {
|
|
405
|
+
const node = event.target;
|
|
406
|
+
if (!(node instanceof HTMLElement) || node === state.root || node.closest(`#${ROOT_ID}`)) return;
|
|
407
|
+
const directTarget = targetFromElement(node);
|
|
408
|
+
const nestedTarget = directTarget || targetFromElement(node.closest?.('[contenteditable="true"], textarea, input'));
|
|
409
|
+
if (nestedTarget) state.currentTarget = capturePromptTarget(nestedTarget);
|
|
410
|
+
updateTargetStatus();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function targetFromElement(node) {
|
|
414
|
+
if (!(node instanceof HTMLElement)) return undefined;
|
|
415
|
+
if (!isVisible(node) || node.closest(`#${ROOT_ID}`)) return undefined;
|
|
416
|
+
|
|
417
|
+
const tag = node.localName;
|
|
418
|
+
if (tag === 'textarea') return { element: node, label: 'textarea' };
|
|
419
|
+
if (tag === 'input') {
|
|
420
|
+
const type = node.getAttribute('type') || 'text';
|
|
421
|
+
if (['text', 'search'].includes(type)) return { element: node, label: 'text input' };
|
|
422
|
+
}
|
|
423
|
+
if (node.isContentEditable) return { element: node, label: 'contenteditable composer' };
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function insertPlainText(target, rawText) {
|
|
428
|
+
const text = normalizeInsertionText(rawText);
|
|
429
|
+
if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) {
|
|
430
|
+
return insertIntoTextControl(target, text);
|
|
431
|
+
}
|
|
432
|
+
if (target instanceof HTMLElement && target.isContentEditable) {
|
|
433
|
+
return insertIntoContentEditable(target, text);
|
|
434
|
+
}
|
|
435
|
+
throw new Error('Unsupported prompt target.');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function insertIntoTextControl(target, text) {
|
|
439
|
+
target.focus();
|
|
440
|
+
const start = Number.isInteger(target.selectionStart) ? target.selectionStart : target.value.length;
|
|
441
|
+
const end = Number.isInteger(target.selectionEnd) ? target.selectionEnd : start;
|
|
442
|
+
const nextValue = `${target.value.slice(0, start)}${text}${target.value.slice(end)}`;
|
|
443
|
+
const prototype = target instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
444
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
445
|
+
if (!descriptor?.set) throw new Error('Prompt target does not support safe value insertion.');
|
|
446
|
+
descriptor.set.call(target, nextValue);
|
|
447
|
+
const caret = start + text.length;
|
|
448
|
+
target.setSelectionRange(caret, caret);
|
|
449
|
+
target.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
450
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
451
|
+
return { insertedCharCount: text.length, mode: start === end ? 'insert-at-cursor' : 'replace-selection', target: target.localName };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function insertIntoContentEditable(target, text) {
|
|
455
|
+
target.focus();
|
|
456
|
+
const selection = window.getSelection();
|
|
457
|
+
const selectedInTarget = selection && selection.rangeCount > 0 && target.contains(selection.anchorNode) && target.contains(selection.focusNode);
|
|
458
|
+
const replacedSelection = Boolean(selectedInTarget && !selection.isCollapsed);
|
|
459
|
+
let inserted = false;
|
|
460
|
+
if (selectedInTarget && document.queryCommandSupported?.('insertText')) {
|
|
461
|
+
inserted = document.execCommand('insertText', false, text);
|
|
462
|
+
}
|
|
463
|
+
if (!inserted) {
|
|
464
|
+
const range = selectedInTarget ? selection.getRangeAt(0).cloneRange() : document.createRange();
|
|
465
|
+
if (!selectedInTarget) {
|
|
466
|
+
range.selectNodeContents(target);
|
|
467
|
+
range.collapse(false);
|
|
468
|
+
}
|
|
469
|
+
range.deleteContents();
|
|
470
|
+
const textNode = document.createTextNode(text);
|
|
471
|
+
range.insertNode(textNode);
|
|
472
|
+
range.setStartAfter(textNode);
|
|
473
|
+
range.collapse(true);
|
|
474
|
+
selection?.removeAllRanges();
|
|
475
|
+
selection?.addRange(range);
|
|
476
|
+
}
|
|
477
|
+
target.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
478
|
+
return { insertedCharCount: text.length, mode: replacedSelection ? 'replace-selection' : 'insert-at-cursor', target: 'contenteditable' };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function normalizeInsertionText(text) {
|
|
482
|
+
if (typeof text !== 'string' || text.length === 0) throw new Error('No Enigma context is available to insert.');
|
|
483
|
+
const normalized = text.replace(/\r\n?/g, '\n');
|
|
484
|
+
if (normalized.length > MAX_INSERT_CHARS) throw new Error(`Enigma context exceeds ${MAX_INSERT_CHARS} characters.`);
|
|
485
|
+
return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function selectedPageText() {
|
|
489
|
+
const selection = window.getSelection();
|
|
490
|
+
const text = selection?.toString().trim();
|
|
491
|
+
if (!text) return undefined;
|
|
492
|
+
if (text.length > 4000) throw new Error('Selected page text exceeds 4000 characters. Select a smaller passage before local transfer.');
|
|
493
|
+
return {
|
|
494
|
+
text,
|
|
495
|
+
source: 'window-selection',
|
|
496
|
+
origin: location.origin,
|
|
497
|
+
charCount: text.length,
|
|
498
|
+
includedCharCount: text.length
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function detectProvider(url) {
|
|
503
|
+
let parsed;
|
|
504
|
+
try {
|
|
505
|
+
parsed = new URL(url);
|
|
506
|
+
} catch {
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
if (parsed.protocol !== 'https:') return undefined;
|
|
510
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
511
|
+
return Object.values(PROVIDERS).find((candidate) => candidate.hosts.includes(hostname));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isVisible(node) {
|
|
515
|
+
const rect = node.getBoundingClientRect();
|
|
516
|
+
if (rect.width < 20 || rect.height < 20) return false;
|
|
517
|
+
const style = window.getComputedStyle(node);
|
|
518
|
+
return style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity) !== 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function previewText(text) {
|
|
522
|
+
const normalized = normalizeInsertionText(text);
|
|
523
|
+
if (normalized.length <= 1200) return normalized;
|
|
524
|
+
return `${normalized.slice(0, 1200)}\n… ${normalized.length - 1200} more characters available for insertion.`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function resetShadow() {
|
|
528
|
+
while (state.shadow.firstChild) state.shadow.firstChild.remove();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function el(tag, className, text) {
|
|
532
|
+
const node = document.createElement(tag);
|
|
533
|
+
if (className) node.className = className;
|
|
534
|
+
if (text !== undefined) node.textContent = text;
|
|
535
|
+
return node;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function styleNode() {
|
|
539
|
+
const style = document.createElement('style');
|
|
540
|
+
style.textContent = `
|
|
541
|
+
:host { all: initial; color-scheme: light dark; }
|
|
542
|
+
.enigma-launch {
|
|
543
|
+
position: fixed;
|
|
544
|
+
right: 18px;
|
|
545
|
+
bottom: 18px;
|
|
546
|
+
z-index: 2147483647;
|
|
547
|
+
border: 1px solid rgba(120, 120, 120, 0.35);
|
|
548
|
+
border-radius: 999px;
|
|
549
|
+
padding: 10px 14px;
|
|
550
|
+
background: #111827;
|
|
551
|
+
color: #ffffff;
|
|
552
|
+
font: 600 13px/1.2 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
553
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
|
554
|
+
cursor: pointer;
|
|
555
|
+
}
|
|
556
|
+
.enigma-panel {
|
|
557
|
+
position: fixed;
|
|
558
|
+
right: 18px;
|
|
559
|
+
bottom: 18px;
|
|
560
|
+
z-index: 2147483647;
|
|
561
|
+
width: min(420px, calc(100vw - 36px));
|
|
562
|
+
border: 1px solid rgba(120, 120, 120, 0.35);
|
|
563
|
+
border-radius: 18px;
|
|
564
|
+
padding: 16px;
|
|
565
|
+
background: Canvas;
|
|
566
|
+
color: CanvasText;
|
|
567
|
+
font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
568
|
+
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.32);
|
|
569
|
+
}
|
|
570
|
+
.enigma-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
|
|
571
|
+
.enigma-close { border: 0; border-radius: 999px; width: 28px; height: 28px; background: color-mix(in srgb, CanvasText 10%, transparent); color: CanvasText; cursor: pointer; font-size: 18px; line-height: 1; }
|
|
572
|
+
.enigma-body { display: grid; gap: 12px; }
|
|
573
|
+
.enigma-copy, .enigma-status, .enigma-error { margin: 0; }
|
|
574
|
+
.enigma-status { color: color-mix(in srgb, CanvasText 72%, transparent); }
|
|
575
|
+
.enigma-error { color: #b42318; }
|
|
576
|
+
.enigma-check { display: flex; align-items: flex-start; gap: 8px; user-select: none; }
|
|
577
|
+
.enigma-preview {
|
|
578
|
+
max-height: 220px;
|
|
579
|
+
overflow: auto;
|
|
580
|
+
margin: 0;
|
|
581
|
+
padding: 12px;
|
|
582
|
+
border-radius: 12px;
|
|
583
|
+
background: color-mix(in srgb, CanvasText 7%, transparent);
|
|
584
|
+
white-space: pre-wrap;
|
|
585
|
+
word-break: break-word;
|
|
586
|
+
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
587
|
+
}
|
|
588
|
+
.enigma-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
589
|
+
.enigma-primary, .enigma-insert {
|
|
590
|
+
border: 1px solid rgba(120, 120, 120, 0.35);
|
|
591
|
+
border-radius: 10px;
|
|
592
|
+
padding: 9px 12px;
|
|
593
|
+
font: 600 13px/1 ui-sans-serif, system-ui, sans-serif;
|
|
594
|
+
cursor: pointer;
|
|
595
|
+
}
|
|
596
|
+
.enigma-primary { background: Canvas; color: CanvasText; }
|
|
597
|
+
.enigma-insert { background: #111827; color: #ffffff; }
|
|
598
|
+
button:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
599
|
+
`;
|
|
600
|
+
return style;
|
|
601
|
+
}
|
|
602
|
+
})();
|