npc-agent 1.0.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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/cdp.d.ts +53 -0
- package/dist/cdp.js +231 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +66 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +173 -0
- package/dist/relay.d.ts +23 -0
- package/dist/relay.js +259 -0
- package/extension/README.md +20 -0
- package/extension/background.js +451 -0
- package/extension/icons/icon.svg +154 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +33 -0
- package/extension/offscreen.html +7 -0
- package/extension/offscreen.js +7 -0
- package/package.json +54 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
const RELAY_URL = 'ws://localhost:7221/extension';
|
|
2
|
+
let ws = null;
|
|
3
|
+
let connectedTabs = new Map();
|
|
4
|
+
let nextSessionId = 1;
|
|
5
|
+
|
|
6
|
+
let offscreenCreating = null;
|
|
7
|
+
async function setupOffscreen() {
|
|
8
|
+
if (offscreenCreating) return offscreenCreating;
|
|
9
|
+
|
|
10
|
+
offscreenCreating = (async () => {
|
|
11
|
+
try {
|
|
12
|
+
const hasDoc = await chrome.offscreen.hasDocument();
|
|
13
|
+
if (!hasDoc) {
|
|
14
|
+
await chrome.offscreen.createDocument({
|
|
15
|
+
url: 'offscreen.html',
|
|
16
|
+
reasons: ['BLOBS'],
|
|
17
|
+
justification: 'Keep service worker alive for NPC browser automation'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.log('[npc] Offscreen setup:', e.message);
|
|
22
|
+
} finally {
|
|
23
|
+
offscreenCreating = null;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
return offscreenCreating;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
31
|
+
if (msg.type === 'keepalive') {
|
|
32
|
+
sendResponse({ ok: true });
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
chrome.runtime.onInstalled.addListener(setupOffscreen);
|
|
38
|
+
chrome.runtime.onStartup.addListener(setupOffscreen);
|
|
39
|
+
|
|
40
|
+
function isAttachable(url) {
|
|
41
|
+
return url && !url.startsWith('chrome://') && !url.startsWith('chrome-extension://') && !url.startsWith('brave://') && !url.startsWith('edge://') && !url.startsWith('about:');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function connect() {
|
|
45
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
ws = new WebSocket(RELAY_URL);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
updateIcon();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ws.onopen = async () => {
|
|
55
|
+
console.log('[npc] WebSocket connected to relay');
|
|
56
|
+
updateIcon();
|
|
57
|
+
await new Promise(r => setTimeout(r, 500));
|
|
58
|
+
await autoAttachActiveTab();
|
|
59
|
+
};
|
|
60
|
+
ws.onerror = () => {};
|
|
61
|
+
ws.onclose = () => {
|
|
62
|
+
ws = null;
|
|
63
|
+
connectedTabs.clear();
|
|
64
|
+
updateIcon();
|
|
65
|
+
setTimeout(connect, 3000);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
ws.onmessage = async (e) => {
|
|
69
|
+
let msg;
|
|
70
|
+
try { msg = JSON.parse(e.data); } catch { return; }
|
|
71
|
+
|
|
72
|
+
if (msg.method === 'ping') {
|
|
73
|
+
ws.send(JSON.stringify({ method: 'pong' }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (msg.method === 'attachActiveTab') {
|
|
78
|
+
await autoAttachActiveTab();
|
|
79
|
+
ws.send(JSON.stringify({ id: msg.id, result: { attached: connectedTabs.size } }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (msg.method === 'corsFetch') {
|
|
84
|
+
const response = { id: msg.id };
|
|
85
|
+
try {
|
|
86
|
+
const { url, options = {} } = msg.params || {};
|
|
87
|
+
|
|
88
|
+
const cookies = await chrome.cookies.getAll({ url: url });
|
|
89
|
+
const cookieString = cookies
|
|
90
|
+
.filter(c => !c.expirationDate || c.expirationDate > Date.now() / 1000)
|
|
91
|
+
.map(c => `${c.name}=${c.value}`)
|
|
92
|
+
.join('; ');
|
|
93
|
+
|
|
94
|
+
const fetchOpts = {
|
|
95
|
+
method: options.method || 'GET',
|
|
96
|
+
headers: {
|
|
97
|
+
...(options.headers || { 'Accept': 'application/json' }),
|
|
98
|
+
'Cookie': cookieString
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
if (options.body) fetchOpts.body = options.body;
|
|
102
|
+
|
|
103
|
+
const resp = await fetch(url, fetchOpts);
|
|
104
|
+
const text = await resp.text();
|
|
105
|
+
let data;
|
|
106
|
+
try { data = JSON.parse(text); } catch { data = text; }
|
|
107
|
+
response.result = { status: resp.status, ok: resp.ok, data };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
response.error = err.message || 'Fetch failed';
|
|
110
|
+
}
|
|
111
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
112
|
+
ws.send(JSON.stringify(response));
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (msg.method === 'forwardCDPCommand') {
|
|
118
|
+
const response = { id: msg.id };
|
|
119
|
+
try {
|
|
120
|
+
response.result = await handleCDP(msg.params);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
response.error = err.message || 'Unknown error';
|
|
123
|
+
}
|
|
124
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
125
|
+
ws.send(JSON.stringify(response));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function handleCDP({ method, params, sessionId }) {
|
|
132
|
+
const browserLevelCommands = [
|
|
133
|
+
'Target.createTarget',
|
|
134
|
+
'Target.closeTarget',
|
|
135
|
+
'Target.activateTarget',
|
|
136
|
+
'Target.getTargets',
|
|
137
|
+
'Target.attachToTarget'
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
if (browserLevelCommands.includes(method)) {
|
|
141
|
+
if (method === 'Target.attachToTarget') {
|
|
142
|
+
const targetId = params?.targetId;
|
|
143
|
+
for (const [tid, info] of connectedTabs) {
|
|
144
|
+
if (info.targetId === targetId) {
|
|
145
|
+
return { sessionId: info.sessionId };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error('Target not found: ' + targetId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (method === 'Target.createTarget') {
|
|
152
|
+
let tab;
|
|
153
|
+
if (params?.newWindow) {
|
|
154
|
+
const win = await chrome.windows.create({ url: params?.url || 'about:blank', focused: false });
|
|
155
|
+
tab = win.tabs[0];
|
|
156
|
+
} else {
|
|
157
|
+
tab = await chrome.tabs.create({ url: params?.url || 'about:blank', active: false });
|
|
158
|
+
}
|
|
159
|
+
await new Promise(r => setTimeout(r, 500));
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await chrome.debugger.detach({ tabId: tab.id });
|
|
163
|
+
} catch (e) {
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { targetInfo } = await attachTab(tab.id);
|
|
167
|
+
return { targetId: targetInfo.targetId };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (method === 'Target.closeTarget') {
|
|
171
|
+
const targetId = params?.targetId;
|
|
172
|
+
let foundTabId = null;
|
|
173
|
+
let foundWindowId = null;
|
|
174
|
+
for (const [tid, info] of connectedTabs) {
|
|
175
|
+
if (info.targetId === targetId) { foundTabId = tid; break; }
|
|
176
|
+
}
|
|
177
|
+
if (foundTabId) {
|
|
178
|
+
try {
|
|
179
|
+
const tab = await chrome.tabs.get(foundTabId);
|
|
180
|
+
foundWindowId = tab.windowId;
|
|
181
|
+
|
|
182
|
+
const win = await chrome.windows.get(foundWindowId, { populate: true });
|
|
183
|
+
const isLastTab = win.tabs.length <= 1;
|
|
184
|
+
|
|
185
|
+
await chrome.debugger.detach({ tabId: foundTabId }).catch(() => {});
|
|
186
|
+
connectedTabs.delete(foundTabId);
|
|
187
|
+
|
|
188
|
+
if (isLastTab) {
|
|
189
|
+
await chrome.windows.remove(foundWindowId);
|
|
190
|
+
} else {
|
|
191
|
+
await chrome.tabs.remove(foundTabId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
195
|
+
ws.send(JSON.stringify({
|
|
196
|
+
method: 'forwardCDPEvent',
|
|
197
|
+
params: {
|
|
198
|
+
method: 'Target.targetDestroyed',
|
|
199
|
+
params: { targetId }
|
|
200
|
+
}
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
return { success: true };
|
|
204
|
+
} catch (e) {
|
|
205
|
+
throw new Error('Could not close tab: ' + e.message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error('Target not found: ' + targetId);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (method === 'Target.activateTarget') {
|
|
212
|
+
const targetId = params?.targetId;
|
|
213
|
+
let foundTabId = null;
|
|
214
|
+
for (const [tid, info] of connectedTabs) {
|
|
215
|
+
if (info.targetId === targetId) { foundTabId = tid; break; }
|
|
216
|
+
}
|
|
217
|
+
if (foundTabId) {
|
|
218
|
+
await chrome.tabs.update(foundTabId, { active: true });
|
|
219
|
+
const tab = await chrome.tabs.get(foundTabId);
|
|
220
|
+
if (tab.windowId) {
|
|
221
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
222
|
+
}
|
|
223
|
+
return { success: true };
|
|
224
|
+
}
|
|
225
|
+
throw new Error('Target not found: ' + targetId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (method === 'Target.getTargets') {
|
|
229
|
+
return {
|
|
230
|
+
targetInfos: Array.from(connectedTabs.values()).map(info => ({
|
|
231
|
+
targetId: info.targetId,
|
|
232
|
+
type: 'page',
|
|
233
|
+
attached: true
|
|
234
|
+
}))
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let tabId = null;
|
|
240
|
+
for (const [tid, info] of connectedTabs) {
|
|
241
|
+
if (info.sessionId === sessionId) { tabId = tid; break; }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!tabId) throw new Error('Session not found');
|
|
245
|
+
return await chrome.debugger.sendCommand({ tabId }, method, params);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function attachTab(tabId) {
|
|
249
|
+
console.log('[npc] Attempting to attach tab:', tabId);
|
|
250
|
+
try {
|
|
251
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
252
|
+
console.log('[npc] Debugger attached to tab:', tabId);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.log('[npc] Attach failed:', e.message);
|
|
255
|
+
throw new Error('Could not attach to tab: ' + e.message);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await chrome.debugger.sendCommand({ tabId }, 'Page.enable');
|
|
260
|
+
} catch {}
|
|
261
|
+
|
|
262
|
+
let targetInfo;
|
|
263
|
+
try {
|
|
264
|
+
const result = await chrome.debugger.sendCommand({ tabId }, 'Target.getTargetInfo');
|
|
265
|
+
targetInfo = result.targetInfo;
|
|
266
|
+
} catch {
|
|
267
|
+
targetInfo = { targetId: `tab-${tabId}`, url: '', type: 'page' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const sessionId = `session-${nextSessionId++}`;
|
|
271
|
+
connectedTabs.set(tabId, { sessionId, targetId: targetInfo.targetId });
|
|
272
|
+
|
|
273
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
274
|
+
ws.send(JSON.stringify({
|
|
275
|
+
method: 'forwardCDPEvent',
|
|
276
|
+
params: {
|
|
277
|
+
method: 'Target.attachedToTarget',
|
|
278
|
+
params: { sessionId, targetInfo: { ...targetInfo, attached: true }, waitingForDebugger: false }
|
|
279
|
+
}
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
updateIcon();
|
|
284
|
+
return { targetInfo, sessionId };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function detachTab(tabId) {
|
|
288
|
+
const info = connectedTabs.get(tabId);
|
|
289
|
+
if (!info) return;
|
|
290
|
+
|
|
291
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
292
|
+
ws.send(JSON.stringify({
|
|
293
|
+
method: 'forwardCDPEvent',
|
|
294
|
+
params: {
|
|
295
|
+
method: 'Target.detachedFromTarget',
|
|
296
|
+
params: { sessionId: info.sessionId, targetId: info.targetId }
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
connectedTabs.delete(tabId);
|
|
302
|
+
chrome.debugger.detach({ tabId }).catch(() => {});
|
|
303
|
+
updateIcon();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function updateIcon() {
|
|
307
|
+
const n = connectedTabs.size;
|
|
308
|
+
const ok = ws?.readyState === WebSocket.OPEN;
|
|
309
|
+
chrome.action.setBadgeText({ text: n > 0 ? String(n) : (ok ? '' : '!') });
|
|
310
|
+
chrome.action.setBadgeBackgroundColor({ color: n > 0 ? '#22c55e' : (ok ? '#64748b' : '#ef4444') });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
chrome.debugger.onEvent.addListener((src, method, params) => {
|
|
314
|
+
const info = connectedTabs.get(src.tabId);
|
|
315
|
+
if (info && ws?.readyState === WebSocket.OPEN) {
|
|
316
|
+
ws.send(JSON.stringify({ method: 'forwardCDPEvent', params: { sessionId: info.sessionId, method, params } }));
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
chrome.debugger.onDetach.addListener((src) => {
|
|
321
|
+
if (connectedTabs.has(src.tabId)) {
|
|
322
|
+
detachTab(src.tabId);
|
|
323
|
+
ensureConnected();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
328
|
+
if (connectedTabs.has(tabId)) {
|
|
329
|
+
detachTab(tabId);
|
|
330
|
+
ensureConnected();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
async function ensureConnected() {
|
|
335
|
+
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
336
|
+
if (connectedTabs.size > 0) return;
|
|
337
|
+
|
|
338
|
+
console.log('[npc] No tabs connected, auto-attaching...');
|
|
339
|
+
await autoAttachActiveTab();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
chrome.action.onClicked.addListener(async (tab) => {
|
|
343
|
+
console.log('[npc] Extension icon clicked, tab:', tab?.id, tab?.url);
|
|
344
|
+
if (!tab.id || !isAttachable(tab.url)) {
|
|
345
|
+
console.log('[npc] Skipping non-attachable page');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (connectedTabs.has(tab.id)) {
|
|
350
|
+
console.log('[npc] Tab already connected, detaching');
|
|
351
|
+
detachTab(tab.id);
|
|
352
|
+
} else {
|
|
353
|
+
console.log('[npc] Connecting to relay and attaching tab');
|
|
354
|
+
connect();
|
|
355
|
+
try {
|
|
356
|
+
await attachTab(tab.id);
|
|
357
|
+
console.log('[npc] Successfully attached tab');
|
|
358
|
+
} catch (e) {
|
|
359
|
+
console.log('[npc] Failed to attach:', e.message);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
connect();
|
|
365
|
+
|
|
366
|
+
setInterval(() => {
|
|
367
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
368
|
+
connect();
|
|
369
|
+
} else if (connectedTabs.size === 0) {
|
|
370
|
+
autoAttachActiveTab();
|
|
371
|
+
}
|
|
372
|
+
}, 3000);
|
|
373
|
+
|
|
374
|
+
chrome.alarms.create('keepalive', { periodInMinutes: 0.5 });
|
|
375
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
376
|
+
if (alarm.name === 'keepalive') {
|
|
377
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
378
|
+
ws.send(JSON.stringify({ method: 'ping' }));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
let autoAttaching = false;
|
|
384
|
+
async function autoAttachActiveTab() {
|
|
385
|
+
if (autoAttaching) return;
|
|
386
|
+
autoAttaching = true;
|
|
387
|
+
console.log('[npc] autoAttachActiveTab called');
|
|
388
|
+
try {
|
|
389
|
+
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
390
|
+
|
|
391
|
+
for (const tab of tabs) {
|
|
392
|
+
if (tab && tab.id && isAttachable(tab.url)) {
|
|
393
|
+
if (!connectedTabs.has(tab.id)) {
|
|
394
|
+
try {
|
|
395
|
+
await attachTab(tab.id);
|
|
396
|
+
console.log('[npc] Auto-attached to tab:', tab.url);
|
|
397
|
+
return;
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.log('[npc] Failed to attach tab:', tab.id, e.message);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
console.log('[npc] No valid tabs found to attach');
|
|
405
|
+
} catch (e) {
|
|
406
|
+
console.log('[npc] Auto-attach failed:', e.message);
|
|
407
|
+
} finally {
|
|
408
|
+
autoAttaching = false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
|
413
|
+
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
414
|
+
try {
|
|
415
|
+
const tab = await chrome.tabs.get(activeInfo.tabId);
|
|
416
|
+
if (tab && isAttachable(tab.url)) {
|
|
417
|
+
if (!connectedTabs.has(tab.id)) {
|
|
418
|
+
await attachTab(tab.id);
|
|
419
|
+
console.log('[npc] Auto-attached on tab switch:', tab.url);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch {}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
chrome.tabs.onCreated.addListener(async (tab) => {
|
|
426
|
+
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
427
|
+
if (connectedTabs.size > 0) return;
|
|
428
|
+
|
|
429
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const updatedTab = await chrome.tabs.get(tab.id);
|
|
433
|
+
if (updatedTab && isAttachable(updatedTab.url)) {
|
|
434
|
+
await attachTab(tab.id);
|
|
435
|
+
console.log('[npc] Auto-attached to new tab:', updatedTab.url);
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|
441
|
+
if (changeInfo.status !== 'complete') return;
|
|
442
|
+
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
443
|
+
if (connectedTabs.size > 0) return;
|
|
444
|
+
|
|
445
|
+
if (tab && isAttachable(tab.url)) {
|
|
446
|
+
try {
|
|
447
|
+
await attachTab(tabId);
|
|
448
|
+
console.log('[npc] Auto-attached on tab load:', tab.url);
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 168 168">
|
|
2
|
+
<defs>
|
|
3
|
+
<filter id="fShadow" x="-25%" y="-15%" width="150%" height="160%">
|
|
4
|
+
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="shadowBlur"/>
|
|
5
|
+
<feOffset dx="4" dy="7" result="shadowOff"/>
|
|
6
|
+
<feFlood flood-color="#000000" flood-opacity="0.65" result="shadowColor"/>
|
|
7
|
+
<feComposite in="shadowColor" in2="shadowOff" operator="in" result="shadow"/>
|
|
8
|
+
<feMerge>
|
|
9
|
+
<feMergeNode in="shadow"/>
|
|
10
|
+
<feMergeNode in="SourceGraphic"/>
|
|
11
|
+
</feMerge>
|
|
12
|
+
</filter>
|
|
13
|
+
|
|
14
|
+
<filter id="f3d" x="-10%" y="-10%" width="120%" height="120%">
|
|
15
|
+
<feGaussianBlur in="SourceAlpha" stdDeviation="2.5" result="blur"/>
|
|
16
|
+
<feSpecularLighting in="blur" surfaceScale="8" specularConstant="1.2" specularExponent="35" result="spec" lighting-color="#e0ffff">
|
|
17
|
+
<fePointLight x="-50" y="-70" z="130"/>
|
|
18
|
+
</feSpecularLighting>
|
|
19
|
+
<feComposite in="spec" in2="SourceAlpha" operator="in" result="specMask"/>
|
|
20
|
+
<feDiffuseLighting in="blur" surfaceScale="5" diffuseConstant="1.0" result="diff" lighting-color="#ccffee">
|
|
21
|
+
<fePointLight x="-40" y="-60" z="110"/>
|
|
22
|
+
</feDiffuseLighting>
|
|
23
|
+
<feComposite in="diff" in2="SourceAlpha" operator="in" result="diffMask"/>
|
|
24
|
+
<feComposite in="SourceGraphic" in2="diffMask" operator="arithmetic" k1="0" k2="0.55" k3="0.45" k4="0" result="lit"/>
|
|
25
|
+
<feComposite in="lit" in2="specMask" operator="arithmetic" k1="0" k2="1" k3="0.5" k4="0"/>
|
|
26
|
+
</filter>
|
|
27
|
+
|
|
28
|
+
<filter id="fGlow" x="-60%" y="-60%" width="220%" height="220%">
|
|
29
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="glow"/>
|
|
30
|
+
<feColorMatrix in="glow" type="saturate" values="2" result="saturated"/>
|
|
31
|
+
<feMerge>
|
|
32
|
+
<feMergeNode in="saturated"/>
|
|
33
|
+
<feMergeNode in="SourceGraphic"/>
|
|
34
|
+
</feMerge>
|
|
35
|
+
</filter>
|
|
36
|
+
|
|
37
|
+
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
38
|
+
<stop offset="0%" stop-color="#0c1929"/>
|
|
39
|
+
<stop offset="100%" stop-color="#050d16"/>
|
|
40
|
+
</linearGradient>
|
|
41
|
+
|
|
42
|
+
<radialGradient id="gBody" cx="0.35" cy="0.3" r="0.7" fx="0.3" fy="0.25">
|
|
43
|
+
<stop offset="0%" stop-color="#b0f5e8"/>
|
|
44
|
+
<stop offset="25%" stop-color="#5dcaa5"/>
|
|
45
|
+
<stop offset="55%" stop-color="#1d9e75"/>
|
|
46
|
+
<stop offset="80%" stop-color="#0f6e56"/>
|
|
47
|
+
<stop offset="100%" stop-color="#04342c"/>
|
|
48
|
+
</radialGradient>
|
|
49
|
+
|
|
50
|
+
<radialGradient id="gHead" cx="0.38" cy="0.32" r="0.68" fx="0.33" fy="0.27">
|
|
51
|
+
<stop offset="0%" stop-color="#c0faf0"/>
|
|
52
|
+
<stop offset="25%" stop-color="#6ee0bb"/>
|
|
53
|
+
<stop offset="55%" stop-color="#2db08a"/>
|
|
54
|
+
<stop offset="80%" stop-color="#157a60"/>
|
|
55
|
+
<stop offset="100%" stop-color="#053d30"/>
|
|
56
|
+
</radialGradient>
|
|
57
|
+
|
|
58
|
+
<linearGradient id="gNeck" x1="0" y1="0" x2="1" y2="0">
|
|
59
|
+
<stop offset="0%" stop-color="#5dcaa5"/>
|
|
60
|
+
<stop offset="30%" stop-color="#1d9e75"/>
|
|
61
|
+
<stop offset="70%" stop-color="#0f6e56"/>
|
|
62
|
+
<stop offset="100%" stop-color="#085041"/>
|
|
63
|
+
</linearGradient>
|
|
64
|
+
|
|
65
|
+
<radialGradient id="gAntenna" cx="0.4" cy="0.3" r="0.55">
|
|
66
|
+
<stop offset="0%" stop-color="#ffffff"/>
|
|
67
|
+
<stop offset="25%" stop-color="#a0ffe0"/>
|
|
68
|
+
<stop offset="55%" stop-color="#22c55e"/>
|
|
69
|
+
<stop offset="100%" stop-color="#0a7a3a"/>
|
|
70
|
+
</radialGradient>
|
|
71
|
+
|
|
72
|
+
<radialGradient id="gSpec" cx="0.5" cy="0.5" r="0.5">
|
|
73
|
+
<stop offset="0%" stop-color="white" stop-opacity="0.75"/>
|
|
74
|
+
<stop offset="100%" stop-color="white" stop-opacity="0"/>
|
|
75
|
+
</radialGradient>
|
|
76
|
+
|
|
77
|
+
<linearGradient id="gAO" x1="0" y1="0" x2="0" y2="1">
|
|
78
|
+
<stop offset="0%" stop-color="black" stop-opacity="0"/>
|
|
79
|
+
<stop offset="100%" stop-color="black" stop-opacity="0.45"/>
|
|
80
|
+
</linearGradient>
|
|
81
|
+
</defs>
|
|
82
|
+
|
|
83
|
+
<rect x="-20" y="-20" width="168" height="168" rx="32" fill="url(#bg)"/>
|
|
84
|
+
<ellipse cx="64" cy="70" rx="45" ry="50" fill="#1d9e75" opacity="0.08"/>
|
|
85
|
+
|
|
86
|
+
<g filter="url(#fShadow)">
|
|
87
|
+
|
|
88
|
+
<!-- EXTRUSION -->
|
|
89
|
+
<g opacity="0.5">
|
|
90
|
+
<g transform="translate(3, 5)">
|
|
91
|
+
<path d="M32 76 Q32 65 41 63 L55 59 Q64 57 73 59 L87 63 Q96 65 96 76 L96 102 Q96 110 87 110 L41 110 Q32 110 32 102 Z" fill="#021a14"/>
|
|
92
|
+
<rect x="38" y="18" width="52" height="38" rx="13" fill="#021a14"/>
|
|
93
|
+
<rect x="55" y="52" width="18" height="14" rx="5" fill="#021a14"/>
|
|
94
|
+
</g>
|
|
95
|
+
<g transform="translate(2, 3.5)">
|
|
96
|
+
<path d="M32 76 Q32 65 41 63 L55 59 Q64 57 73 59 L87 63 Q96 65 96 76 L96 102 Q96 110 87 110 L41 110 Q32 110 32 102 Z" fill="#042e24"/>
|
|
97
|
+
<rect x="38" y="18" width="52" height="38" rx="13" fill="#042e24"/>
|
|
98
|
+
<rect x="55" y="52" width="18" height="14" rx="5" fill="#042e24"/>
|
|
99
|
+
</g>
|
|
100
|
+
<g transform="translate(1, 2)">
|
|
101
|
+
<path d="M32 76 Q32 65 41 63 L55 59 Q64 57 73 59 L87 63 Q96 65 96 76 L96 102 Q96 110 87 110 L41 110 Q32 110 32 102 Z" fill="#064538"/>
|
|
102
|
+
<rect x="38" y="18" width="52" height="38" rx="13" fill="#064538"/>
|
|
103
|
+
<rect x="55" y="52" width="18" height="14" rx="5" fill="#064538"/>
|
|
104
|
+
</g>
|
|
105
|
+
</g>
|
|
106
|
+
|
|
107
|
+
<!-- Dark edge strokes -->
|
|
108
|
+
<path d="M32 76 Q32 65 41 63 L55 59 Q64 57 73 59 L87 63 Q96 65 96 76 L96 102 Q96 110 87 110 L41 110 Q32 110 32 102 Z" fill="none" stroke="#021a14" stroke-width="2.5" opacity="0.6"/>
|
|
109
|
+
<rect x="38" y="18" width="52" height="38" rx="13" fill="none" stroke="#021a14" stroke-width="2.5" opacity="0.6"/>
|
|
110
|
+
|
|
111
|
+
<!-- BODY -->
|
|
112
|
+
<g filter="url(#f3d)">
|
|
113
|
+
<path d="M32 76 Q32 65 41 63 L55 59 Q64 57 73 59 L87 63 Q96 65 96 76 L96 102 Q96 110 87 110 L41 110 Q32 110 32 102 Z" fill="url(#gBody)"/>
|
|
114
|
+
</g>
|
|
115
|
+
<ellipse cx="52" cy="72" rx="18" ry="10" fill="url(#gSpec)" opacity="0.6"/>
|
|
116
|
+
<path d="M32 76 Q32 65 41 63 L55 59 Q64 57 73 59 L87 63 Q96 65 96 76 L96 102 Q96 110 87 110 L41 110 Q32 110 32 102 Z" fill="url(#gAO)" opacity="0.4"/>
|
|
117
|
+
<path d="M87 63 Q96 65 96 76 L96 102 Q96 110 87 110" fill="none" stroke="#b0f5e8" stroke-width="1.5" opacity="0.2"/>
|
|
118
|
+
|
|
119
|
+
<!-- NPC text -->
|
|
120
|
+
<text x="64" y="95" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="800" fill="#042e24" letter-spacing="4" opacity="0.5">NPC</text>
|
|
121
|
+
<line x1="46" y1="79" x2="82" y2="79" stroke="#b0f5e8" stroke-width="0.8" opacity="0.25"/>
|
|
122
|
+
|
|
123
|
+
<!-- NECK -->
|
|
124
|
+
<rect x="55" y="52" width="18" height="14" rx="5" fill="url(#gNeck)"/>
|
|
125
|
+
|
|
126
|
+
<!-- HEAD -->
|
|
127
|
+
<g filter="url(#f3d)">
|
|
128
|
+
<rect x="38" y="18" width="52" height="38" rx="13" fill="url(#gHead)"/>
|
|
129
|
+
</g>
|
|
130
|
+
<ellipse cx="52" cy="27" rx="16" ry="9" fill="url(#gSpec)" opacity="0.6"/>
|
|
131
|
+
<rect x="38" y="18" width="52" height="38" rx="13" fill="url(#gAO)" opacity="0.35"/>
|
|
132
|
+
<path d="M90 31 Q90 18 77 18" fill="none" stroke="#c0faf0" stroke-width="1.2" opacity="0.2"/>
|
|
133
|
+
|
|
134
|
+
<!-- EYES -->
|
|
135
|
+
<circle cx="52" cy="38" r="6" fill="#050d16"/>
|
|
136
|
+
<circle cx="76" cy="38" r="6" fill="#050d16"/>
|
|
137
|
+
<circle cx="52" cy="38" r="6" fill="none" stroke="#22c55e" stroke-width="0.8" opacity="0.3"/>
|
|
138
|
+
<circle cx="76" cy="38" r="6" fill="none" stroke="#22c55e" stroke-width="0.8" opacity="0.3"/>
|
|
139
|
+
<circle cx="52" cy="37.5" r="3.5" fill="#5dcaa5"/>
|
|
140
|
+
<circle cx="76" cy="37.5" r="3.5" fill="#5dcaa5"/>
|
|
141
|
+
<circle cx="54.5" cy="35" r="1.8" fill="white" opacity="0.95"/>
|
|
142
|
+
<circle cx="78.5" cy="35" r="1.8" fill="white" opacity="0.95"/>
|
|
143
|
+
<circle cx="50" cy="39.5" r="0.9" fill="white" opacity="0.5"/>
|
|
144
|
+
<circle cx="74" cy="39.5" r="0.9" fill="white" opacity="0.5"/>
|
|
145
|
+
|
|
146
|
+
<!-- ANTENNA -->
|
|
147
|
+
<line x1="64" y1="18" x2="64" y2="6" stroke="#1d9e75" stroke-width="3" stroke-linecap="round"/>
|
|
148
|
+
<line x1="65.5" y1="18" x2="65.5" y2="6" stroke="#064538" stroke-width="1.2" stroke-linecap="round" opacity="0.5"/>
|
|
149
|
+
<g filter="url(#fGlow)">
|
|
150
|
+
<circle cx="64" cy="5" r="5" fill="url(#gAntenna)"/>
|
|
151
|
+
</g>
|
|
152
|
+
|
|
153
|
+
</g>
|
|
154
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "NPC",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Connect your browser to NPC - handles the side quests.",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"debugger",
|
|
8
|
+
"tabs",
|
|
9
|
+
"activeTab",
|
|
10
|
+
"offscreen",
|
|
11
|
+
"alarms",
|
|
12
|
+
"cookies"
|
|
13
|
+
],
|
|
14
|
+
"host_permissions": [
|
|
15
|
+
"<all_urls>"
|
|
16
|
+
],
|
|
17
|
+
"background": {
|
|
18
|
+
"service_worker": "background.js"
|
|
19
|
+
},
|
|
20
|
+
"action": {
|
|
21
|
+
"default_title": "NPC - connect this tab",
|
|
22
|
+
"default_icon": {
|
|
23
|
+
"16": "icons/icon16.png",
|
|
24
|
+
"48": "icons/icon48.png",
|
|
25
|
+
"128": "icons/icon128.png"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"icons": {
|
|
29
|
+
"16": "icons/icon16.png",
|
|
30
|
+
"48": "icons/icon48.png",
|
|
31
|
+
"128": "icons/icon128.png"
|
|
32
|
+
}
|
|
33
|
+
}
|