@ank1015/llm-extension 0.0.1
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 +214 -0
- package/dist/chrome/background.js +244 -0
- package/dist/chrome/manifest.json +23 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/native/host-wrapper.sh +11 -0
- package/dist/native/host.d.ts +2 -0
- package/dist/native/host.d.ts.map +1 -0
- package/dist/native/host.js +21 -0
- package/dist/native/host.js.map +1 -0
- package/dist/native/server.d.ts +28 -0
- package/dist/native/server.d.ts.map +1 -0
- package/dist/native/server.js +112 -0
- package/dist/native/server.js.map +1 -0
- package/dist/native/stdio.d.ts +21 -0
- package/dist/native/stdio.d.ts.map +1 -0
- package/dist/native/stdio.js +94 -0
- package/dist/native/stdio.js.map +1 -0
- package/dist/protocol/constants.d.ts +26 -0
- package/dist/protocol/constants.d.ts.map +1 -0
- package/dist/protocol/constants.js +26 -0
- package/dist/protocol/constants.js.map +1 -0
- package/dist/protocol/types.d.ts +39 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +3 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/sdk/action/diff.d.ts +7 -0
- package/dist/sdk/action/diff.d.ts.map +1 -0
- package/dist/sdk/action/diff.js +54 -0
- package/dist/sdk/action/diff.js.map +1 -0
- package/dist/sdk/action/helpers.d.ts +23 -0
- package/dist/sdk/action/helpers.d.ts.map +1 -0
- package/dist/sdk/action/helpers.js +93 -0
- package/dist/sdk/action/helpers.js.map +1 -0
- package/dist/sdk/action/index.d.ts +6 -0
- package/dist/sdk/action/index.d.ts.map +1 -0
- package/dist/sdk/action/index.js +4 -0
- package/dist/sdk/action/index.js.map +1 -0
- package/dist/sdk/action/script.d.ts +4 -0
- package/dist/sdk/action/script.d.ts.map +1 -0
- package/dist/sdk/action/script.js +359 -0
- package/dist/sdk/action/script.js.map +1 -0
- package/dist/sdk/action/types.d.ts +47 -0
- package/dist/sdk/action/types.d.ts.map +1 -0
- package/dist/sdk/action/types.js +2 -0
- package/dist/sdk/action/types.js.map +1 -0
- package/dist/sdk/client.d.ts +42 -0
- package/dist/sdk/client.d.ts.map +1 -0
- package/dist/sdk/client.js +118 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/sdk/connect.d.ts +29 -0
- package/dist/sdk/connect.d.ts.map +1 -0
- package/dist/sdk/connect.js +99 -0
- package/dist/sdk/connect.js.map +1 -0
- package/dist/sdk/index.d.ts +17 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +19 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/observe/helpers.d.ts +16 -0
- package/dist/sdk/observe/helpers.d.ts.map +1 -0
- package/dist/sdk/observe/helpers.js +278 -0
- package/dist/sdk/observe/helpers.js.map +1 -0
- package/dist/sdk/observe/index.d.ts +6 -0
- package/dist/sdk/observe/index.d.ts.map +1 -0
- package/dist/sdk/observe/index.js +4 -0
- package/dist/sdk/observe/index.js.map +1 -0
- package/dist/sdk/observe/script.d.ts +7 -0
- package/dist/sdk/observe/script.d.ts.map +1 -0
- package/dist/sdk/observe/script.js +680 -0
- package/dist/sdk/observe/script.js.map +1 -0
- package/dist/sdk/observe/storage.d.ts +39 -0
- package/dist/sdk/observe/storage.d.ts.map +1 -0
- package/dist/sdk/observe/storage.js +83 -0
- package/dist/sdk/observe/storage.js.map +1 -0
- package/dist/sdk/observe/types.d.ts +140 -0
- package/dist/sdk/observe/types.d.ts.map +1 -0
- package/dist/sdk/observe/types.js +2 -0
- package/dist/sdk/observe/types.js.map +1 -0
- package/dist/sdk/window.d.ts +97 -0
- package/dist/sdk/window.d.ts.map +1 -0
- package/dist/sdk/window.js +795 -0
- package/dist/sdk/window.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# @ank1015/llm-extension
|
|
2
|
+
|
|
3
|
+
A Chrome extension that exposes Chrome APIs over a general-purpose RPC bridge. Any process on your machine can connect via TCP and call Chrome APIs like `tabs.query`, `scripting.executeScript`, `storage.local.get`, or subscribe to events like `tabs.onUpdated` — all through a simple `call()` / `subscribe()` interface.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Your Agent ── TCP :9224 ──→ Native Host ── stdin/stdout ──→ Chrome Extension
|
|
9
|
+
│ │
|
|
10
|
+
ChromeServer background.ts
|
|
11
|
+
(TCP proxy) (RPC proxy)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
1. **Chrome extension** installs as a Manifest V3 service worker. On startup, it connects to a native messaging host.
|
|
15
|
+
2. **Native host** is a Node.js process Chrome launches via the [Native Messaging](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging) protocol. It opens a TCP server on port 9224.
|
|
16
|
+
3. **Your agent** connects to the TCP server and sends RPC messages. The native host forwards them to Chrome over stdin/stdout. Chrome executes the API call and sends the result back through the same chain.
|
|
17
|
+
|
|
18
|
+
The protocol uses 6 message types: `call`, `subscribe`, `unsubscribe` (agent → Chrome) and `result`, `error`, `event` (Chrome → agent). All messages are length-prefixed JSON, correlated by UUID.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### Prerequisites
|
|
23
|
+
|
|
24
|
+
- Chrome browser
|
|
25
|
+
- Node.js 18+
|
|
26
|
+
- pnpm
|
|
27
|
+
|
|
28
|
+
### Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Build the extension and native host
|
|
32
|
+
pnpm build
|
|
33
|
+
|
|
34
|
+
# Load the extension in Chrome:
|
|
35
|
+
# 1. Open chrome://extensions
|
|
36
|
+
# 2. Enable "Developer mode"
|
|
37
|
+
# 3. Click "Load unpacked" → select packages/extension/dist/chrome/
|
|
38
|
+
# 4. Copy the extension ID shown on the card
|
|
39
|
+
|
|
40
|
+
# Register the native messaging host
|
|
41
|
+
./manifests/install-host.sh <extension-id>
|
|
42
|
+
|
|
43
|
+
# Quit and reopen Chrome (required for native host registration)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
After this one-time setup, Chrome will automatically launch the native host whenever the extension loads.
|
|
47
|
+
|
|
48
|
+
### Verify
|
|
49
|
+
|
|
50
|
+
Check the extension's service worker console (`chrome://extensions` → Inspect views → service worker):
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
[bg] background service worker loaded
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
No disconnect error means the native host is running and the TCP server is ready on port 9224.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### From TypeScript/JavaScript
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { connect } from '@ank1015/llm-extension';
|
|
64
|
+
|
|
65
|
+
// Auto-launch Chrome if not running, retry until native host is ready
|
|
66
|
+
const chrome = await connect({ launch: true });
|
|
67
|
+
|
|
68
|
+
// Query tabs
|
|
69
|
+
const tabs = await chrome.call('tabs.query', { active: true, currentWindow: true });
|
|
70
|
+
console.log(tabs);
|
|
71
|
+
|
|
72
|
+
// Execute JavaScript in a tab
|
|
73
|
+
const result = await chrome.call('scripting.executeScript', {
|
|
74
|
+
target: { tabId: tabs[0].id },
|
|
75
|
+
code: 'document.title',
|
|
76
|
+
});
|
|
77
|
+
console.log(result[0].result); // page title
|
|
78
|
+
|
|
79
|
+
// Use storage
|
|
80
|
+
await chrome.call('storage.local.set', { myKey: 'myValue' });
|
|
81
|
+
const data = await chrome.call('storage.local.get', 'myKey');
|
|
82
|
+
console.log(data.myKey); // 'myValue'
|
|
83
|
+
|
|
84
|
+
// Subscribe to events
|
|
85
|
+
const unsubscribe = chrome.subscribe('tabs.onUpdated', (args) => {
|
|
86
|
+
const [tabId, changeInfo, tab] = args;
|
|
87
|
+
console.log(`Tab ${tabId} updated:`, changeInfo);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Later...
|
|
91
|
+
unsubscribe();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### From Any Language
|
|
95
|
+
|
|
96
|
+
The protocol is language-agnostic. Connect to TCP port 9224 and exchange length-prefixed JSON:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
[4 bytes: uint32 LE message length][JSON payload]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Send a call:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{ "id": "uuid-here", "type": "call", "method": "tabs.query", "args": [{ "active": true }] }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Receive the result:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{ "id": "uuid-here", "type": "result", "data": [{ "id": 1, "url": "https://..." }] }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API
|
|
115
|
+
|
|
116
|
+
### `connect(opts?)`
|
|
117
|
+
|
|
118
|
+
Connect to the Chrome RPC server. Returns a `ChromeClient`.
|
|
119
|
+
|
|
120
|
+
| Option | Default | Description |
|
|
121
|
+
| --------------- | ------------- | --------------------------------------------------- |
|
|
122
|
+
| `port` | `9224` | TCP port to connect to |
|
|
123
|
+
| `host` | `'127.0.0.1'` | TCP host to connect to |
|
|
124
|
+
| `launch` | `false` | Launch Chrome automatically if connection fails |
|
|
125
|
+
| `launchTimeout` | `30000` | Max ms to wait for Chrome + native host to be ready |
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const chrome = await connect();
|
|
129
|
+
const chrome = await connect({ port: 9224, host: '127.0.0.1' });
|
|
130
|
+
const chrome = await connect({ launch: true }); // auto-open Chrome if not running
|
|
131
|
+
const chrome = await connect({ launch: true, launchTimeout: 15000 });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `ChromeClient.call(method, ...args)`
|
|
135
|
+
|
|
136
|
+
Call any Chrome API method. The method string maps to `chrome.<method>` in the extension.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// chrome.tabs.query({ active: true })
|
|
140
|
+
await chrome.call('tabs.query', { active: true });
|
|
141
|
+
|
|
142
|
+
// chrome.tabs.get(123)
|
|
143
|
+
await chrome.call('tabs.get', 123);
|
|
144
|
+
|
|
145
|
+
// chrome.storage.local.get('key')
|
|
146
|
+
await chrome.call('storage.local.get', 'key');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `ChromeClient.call('scripting.executeScript', opts)`
|
|
150
|
+
|
|
151
|
+
Special case: pass a `code` string instead of a function reference. The code runs in the page's main world via `eval`.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await chrome.call('scripting.executeScript', {
|
|
155
|
+
target: { tabId: 123 },
|
|
156
|
+
code: 'document.title',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await chrome.call('scripting.executeScript', {
|
|
160
|
+
target: { tabId: 123 },
|
|
161
|
+
code: '({ title: document.title, url: location.href })',
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `ChromeClient.subscribe(event, callback)`
|
|
166
|
+
|
|
167
|
+
Subscribe to a Chrome event. Returns an unsubscribe function.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const unsub = chrome.subscribe('tabs.onUpdated', (args) => {
|
|
171
|
+
console.log('Tab updated:', args);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Stop listening
|
|
175
|
+
unsub();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Configuration
|
|
179
|
+
|
|
180
|
+
| Variable | Default | Description |
|
|
181
|
+
| ----------------- | ------- | ----------------------------------- |
|
|
182
|
+
| `CHROME_RPC_PORT` | `9224` | TCP port the native host listens on |
|
|
183
|
+
|
|
184
|
+
Set in the wrapper script at `~/.local/share/llm-native-host/run-host.sh` or pass via `connect({ port })`.
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
pnpm dev # Watch mode (native + chrome)
|
|
190
|
+
pnpm test # Unit tests (25 tests)
|
|
191
|
+
pnpm test:e2e # E2E tests against live Chrome
|
|
192
|
+
pnpm typecheck # Type-check both tsconfigs
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Project Structure
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
src/
|
|
199
|
+
protocol/ # Shared message types and constants
|
|
200
|
+
sdk/ # ChromeClient, connect(), createChromeClient()
|
|
201
|
+
native/ # Node.js native host (host.ts, server.ts, stdio.ts)
|
|
202
|
+
chrome/ # Chrome extension (background.ts, manifest.json)
|
|
203
|
+
tests/
|
|
204
|
+
unit/ # Unit tests (stdio, client, server, factory)
|
|
205
|
+
e2e/ # E2E tests against live Chrome
|
|
206
|
+
manifests/ # Native host manifest and install script
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Two Build Targets
|
|
210
|
+
|
|
211
|
+
The package has two runtime contexts with separate TypeScript configs:
|
|
212
|
+
|
|
213
|
+
- **Node** (`tsconfig.json`): native host + SDK, compiled by `tsc` to `dist/`
|
|
214
|
+
- **Chrome** (`tsconfig.chrome.json`): extension service worker, bundled by `esbuild` to `dist/chrome/`
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// src/protocol/constants.ts
|
|
2
|
+
var NATIVE_HOST_NAME = "com.ank1015.llm";
|
|
3
|
+
var MAX_HOST_TO_CHROME_MESSAGE_SIZE_BYTES = 1024 * 1024;
|
|
4
|
+
var MAX_CHROME_TO_HOST_MESSAGE_SIZE_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
var MAX_TCP_MESSAGE_SIZE_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
// src/chrome/background.ts
|
|
8
|
+
var nativePort = null;
|
|
9
|
+
var subscriptions = /* @__PURE__ */ new Map();
|
|
10
|
+
function connectNative() {
|
|
11
|
+
const port = chrome.runtime.connectNative(NATIVE_HOST_NAME);
|
|
12
|
+
port.onMessage.addListener((message) => {
|
|
13
|
+
handleHostMessage(message);
|
|
14
|
+
});
|
|
15
|
+
port.onDisconnect.addListener(() => {
|
|
16
|
+
console.warn("[bg] native host disconnected", chrome.runtime.lastError?.message ?? "");
|
|
17
|
+
nativePort = null;
|
|
18
|
+
subscriptions.clear();
|
|
19
|
+
});
|
|
20
|
+
nativePort = port;
|
|
21
|
+
return port;
|
|
22
|
+
}
|
|
23
|
+
function sendToHost(message) {
|
|
24
|
+
if (!nativePort) {
|
|
25
|
+
connectNative();
|
|
26
|
+
}
|
|
27
|
+
nativePort.postMessage(message);
|
|
28
|
+
}
|
|
29
|
+
function handleHostMessage(message) {
|
|
30
|
+
switch (message.type) {
|
|
31
|
+
case "call":
|
|
32
|
+
handleCall(message);
|
|
33
|
+
break;
|
|
34
|
+
case "subscribe":
|
|
35
|
+
handleSubscribe(message);
|
|
36
|
+
break;
|
|
37
|
+
case "unsubscribe":
|
|
38
|
+
handleUnsubscribe(message);
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
console.warn("[bg] unknown message type:", message.type);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function handleCall(message) {
|
|
45
|
+
try {
|
|
46
|
+
let result;
|
|
47
|
+
if (message.method === "debugger.evaluate") {
|
|
48
|
+
result = await debuggerEvaluate(message.args);
|
|
49
|
+
} else if (message.method === "debugger.attach") {
|
|
50
|
+
result = await debuggerAttach(message.args);
|
|
51
|
+
} else if (message.method === "debugger.sendCommand") {
|
|
52
|
+
result = await debuggerSendCommand(message.args);
|
|
53
|
+
} else if (message.method === "debugger.detach") {
|
|
54
|
+
result = await debuggerDetach(message.args);
|
|
55
|
+
} else if (message.method === "debugger.getEvents") {
|
|
56
|
+
result = debuggerGetEvents(message.args);
|
|
57
|
+
} else if (message.method === "scripting.executeScript" && hasCodeArg(message.args)) {
|
|
58
|
+
result = await executeScriptWithCode(message.args);
|
|
59
|
+
} else {
|
|
60
|
+
const fn = resolveMethod(message.method);
|
|
61
|
+
result = await fn(...message.args);
|
|
62
|
+
}
|
|
63
|
+
sendToHost({ id: message.id, type: "result", data: result });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
sendToHost({
|
|
66
|
+
id: message.id,
|
|
67
|
+
type: "error",
|
|
68
|
+
error: error instanceof Error ? error.message : String(error)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function hasCodeArg(args2) {
|
|
73
|
+
return args2.length > 0 && typeof args2[0] === "object" && args2[0] !== null && "code" in args2[0];
|
|
74
|
+
}
|
|
75
|
+
async function executeScriptWithCode(args) {
|
|
76
|
+
const { code, target, world, ...rest } = args[0];
|
|
77
|
+
return chrome.scripting.executeScript({
|
|
78
|
+
...rest,
|
|
79
|
+
target,
|
|
80
|
+
world: world ?? "MAIN",
|
|
81
|
+
// func is serialized by Chrome and executed in the TAB context (not the service worker).
|
|
82
|
+
// eval is allowed in MAIN world under the page's CSP.
|
|
83
|
+
func: (codeStr) => eval(codeStr),
|
|
84
|
+
args: [code]
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function debuggerEvaluate(args2) {
|
|
88
|
+
const {
|
|
89
|
+
tabId,
|
|
90
|
+
code: code2,
|
|
91
|
+
returnByValue = true,
|
|
92
|
+
awaitPromise = false,
|
|
93
|
+
userGesture = false
|
|
94
|
+
} = args2[0];
|
|
95
|
+
if (typeof tabId !== "number") {
|
|
96
|
+
throw new Error("debugger.evaluate requires a numeric tabId");
|
|
97
|
+
}
|
|
98
|
+
if (typeof code2 !== "string" || !code2) {
|
|
99
|
+
throw new Error("debugger.evaluate requires a non-empty code string");
|
|
100
|
+
}
|
|
101
|
+
let attachedByThisMethod = false;
|
|
102
|
+
try {
|
|
103
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
104
|
+
attachedByThisMethod = true;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
if (!message.includes("Another debugger is already attached")) {
|
|
108
|
+
throw new Error(`Failed to attach debugger to tab ${tabId}: ${message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const response = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
|
|
113
|
+
expression: code2,
|
|
114
|
+
returnByValue,
|
|
115
|
+
awaitPromise,
|
|
116
|
+
userGesture
|
|
117
|
+
});
|
|
118
|
+
if (response.exceptionDetails) {
|
|
119
|
+
const detail = response.exceptionDetails.exception?.description ?? response.exceptionDetails.text ?? "Unknown evaluation error";
|
|
120
|
+
throw new Error(detail);
|
|
121
|
+
}
|
|
122
|
+
return { result: response.result?.value, type: response.result?.type };
|
|
123
|
+
} finally {
|
|
124
|
+
if (attachedByThisMethod) {
|
|
125
|
+
try {
|
|
126
|
+
await chrome.debugger.detach({ tabId });
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
var debuggerSessions = /* @__PURE__ */ new Map();
|
|
133
|
+
function handleDebuggerEvent(source, method, params) {
|
|
134
|
+
if (source.tabId === void 0) return;
|
|
135
|
+
const session = debuggerSessions.get(source.tabId);
|
|
136
|
+
if (session) {
|
|
137
|
+
session.events.push({ method, params: params ?? {} });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
chrome.debugger.onEvent.addListener(handleDebuggerEvent);
|
|
141
|
+
async function debuggerAttach(args2) {
|
|
142
|
+
const { tabId } = args2[0];
|
|
143
|
+
if (debuggerSessions.has(tabId)) {
|
|
144
|
+
return { alreadyAttached: true };
|
|
145
|
+
}
|
|
146
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
147
|
+
debuggerSessions.set(tabId, { events: [] });
|
|
148
|
+
return { attached: true };
|
|
149
|
+
}
|
|
150
|
+
async function debuggerSendCommand(args2) {
|
|
151
|
+
const { tabId, method, params } = args2[0];
|
|
152
|
+
if (!debuggerSessions.has(tabId)) {
|
|
153
|
+
throw new Error(`No debugger session for tab ${tabId} \u2014 call debugger.attach first`);
|
|
154
|
+
}
|
|
155
|
+
return chrome.debugger.sendCommand({ tabId }, method, params);
|
|
156
|
+
}
|
|
157
|
+
async function debuggerDetach(args2) {
|
|
158
|
+
const { tabId } = args2[0];
|
|
159
|
+
debuggerSessions.delete(tabId);
|
|
160
|
+
try {
|
|
161
|
+
await chrome.debugger.detach({ tabId });
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
return { detached: true };
|
|
165
|
+
}
|
|
166
|
+
function debuggerGetEvents(args2) {
|
|
167
|
+
const {
|
|
168
|
+
tabId,
|
|
169
|
+
filter,
|
|
170
|
+
clear = false
|
|
171
|
+
} = args2[0];
|
|
172
|
+
const session = debuggerSessions.get(tabId);
|
|
173
|
+
if (!session) {
|
|
174
|
+
throw new Error(`No debugger session for tab ${tabId}`);
|
|
175
|
+
}
|
|
176
|
+
let events = session.events;
|
|
177
|
+
if (filter) {
|
|
178
|
+
events = events.filter((e) => e.method.startsWith(filter));
|
|
179
|
+
}
|
|
180
|
+
const result = [...events];
|
|
181
|
+
if (clear) {
|
|
182
|
+
if (filter) {
|
|
183
|
+
session.events = session.events.filter((e) => !e.method.startsWith(filter));
|
|
184
|
+
} else {
|
|
185
|
+
session.events = [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
function resolveMethod(method) {
|
|
191
|
+
const parts = method.split(".");
|
|
192
|
+
let target2 = chrome;
|
|
193
|
+
let parent = chrome;
|
|
194
|
+
for (const part of parts) {
|
|
195
|
+
parent = target2;
|
|
196
|
+
target2 = target2[part];
|
|
197
|
+
if (target2 === void 0) {
|
|
198
|
+
throw new Error(`chrome.${method} is not available`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (typeof target2 !== "function") {
|
|
202
|
+
throw new Error(`chrome.${method} is not a function`);
|
|
203
|
+
}
|
|
204
|
+
return target2.bind(parent);
|
|
205
|
+
}
|
|
206
|
+
function handleSubscribe(message) {
|
|
207
|
+
try {
|
|
208
|
+
const parts = message.event.split(".");
|
|
209
|
+
let target2 = chrome;
|
|
210
|
+
for (const part of parts) {
|
|
211
|
+
target2 = target2[part];
|
|
212
|
+
if (target2 === void 0) {
|
|
213
|
+
throw new Error(`chrome.${message.event} is not available`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const eventTarget = target2;
|
|
217
|
+
if (typeof eventTarget.addListener !== "function") {
|
|
218
|
+
throw new Error(`chrome.${message.event} is not an event`);
|
|
219
|
+
}
|
|
220
|
+
const listener = (...args2) => {
|
|
221
|
+
sendToHost({ id: message.id, type: "event", data: args2 });
|
|
222
|
+
};
|
|
223
|
+
eventTarget.addListener(listener);
|
|
224
|
+
subscriptions.set(message.id, { target: eventTarget, listener });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
sendToHost({
|
|
227
|
+
id: message.id,
|
|
228
|
+
type: "error",
|
|
229
|
+
error: error instanceof Error ? error.message : String(error)
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function handleUnsubscribe(message) {
|
|
234
|
+
const sub = subscriptions.get(message.id);
|
|
235
|
+
if (sub) {
|
|
236
|
+
sub.target.removeListener(sub.listener);
|
|
237
|
+
subscriptions.delete(message.id);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
241
|
+
if (!nativePort) connectNative();
|
|
242
|
+
});
|
|
243
|
+
console.warn("[bg] background service worker loaded");
|
|
244
|
+
connectNative();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "LLM Extension",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Chrome extension with general-purpose native messaging RPC",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"nativeMessaging",
|
|
8
|
+
"scripting",
|
|
9
|
+
"activeTab",
|
|
10
|
+
"tabs",
|
|
11
|
+
"storage",
|
|
12
|
+
"cookies",
|
|
13
|
+
"downloads",
|
|
14
|
+
"debugger",
|
|
15
|
+
"accessibilityFeatures.read",
|
|
16
|
+
"accessibilityFeatures.modify"
|
|
17
|
+
],
|
|
18
|
+
"host_permissions": ["<all_urls>"],
|
|
19
|
+
"background": {
|
|
20
|
+
"service_worker": "background.js",
|
|
21
|
+
"type": "module"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { CallMessage, SubscribeMessage, UnsubscribeMessage, HostMessage, ResultMessage, ErrorMessage, EventMessage, ChromeMessage, } from './protocol/types.js';
|
|
2
|
+
export { NATIVE_HOST_NAME, MAX_HOST_TO_CHROME_MESSAGE_SIZE_BYTES, MAX_CHROME_TO_HOST_MESSAGE_SIZE_BYTES, MAX_TCP_MESSAGE_SIZE_BYTES, MAX_MESSAGE_SIZE_BYTES, LENGTH_PREFIX_BYTES, DEFAULT_PORT, } from './protocol/constants.js';
|
|
3
|
+
export { ChromeClient, createChromeClient, connect, Window } from './sdk/index.js';
|
|
4
|
+
export type { ChromeClientOptions, ConnectOptions, ObserveFilter, WindowActionOptions, WindowDownloadOptions, WindowEvaluateOptions, WindowGetPageOptions, WindowObserveOptions, WindowOpenOptions, WindowScrollBehavior, WindowScrollOptions, WindowSemanticFilter, WindowScreenshotOptions, WindowTab, WindowTypeOptions, } from './sdk/index.js';
|
|
5
|
+
export { ChromeServer } from './native/server.js';
|
|
6
|
+
export type { ChromeServerOptions } from './native/server.js';
|
|
7
|
+
export { readMessage, writeMessage } from './native/stdio.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,gBAAgB,EAChB,qCAAqC,EACrC,qCAAqC,EACrC,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,YAAY,GACb,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACnF,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,SAAS,EACT,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAG9D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { NATIVE_HOST_NAME, MAX_HOST_TO_CHROME_MESSAGE_SIZE_BYTES, MAX_CHROME_TO_HOST_MESSAGE_SIZE_BYTES, MAX_TCP_MESSAGE_SIZE_BYTES, MAX_MESSAGE_SIZE_BYTES, LENGTH_PREFIX_BYTES, DEFAULT_PORT, } from './protocol/constants.js';
|
|
2
|
+
// SDK
|
|
3
|
+
export { ChromeClient, createChromeClient, connect, Window } from './sdk/index.js';
|
|
4
|
+
// Server
|
|
5
|
+
export { ChromeServer } from './native/server.js';
|
|
6
|
+
// Native host stdio utilities
|
|
7
|
+
export { readMessage, writeMessage } from './native/stdio.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,OAAO,EACL,gBAAgB,EAChB,qCAAqC,EACrC,qCAAqC,EACrC,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,YAAY,GACb,MAAM,yBAAyB,CAAC;AAEjC,MAAM;AACN,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAmBnF,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,8BAA8B;AAC9B,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Chrome launches this script as the native messaging host.
|
|
3
|
+
# It resolves its own directory and invokes the Node.js host.
|
|
4
|
+
#
|
|
5
|
+
# NOTE: This wrapper is only used for local development.
|
|
6
|
+
# The install-host.sh script creates a separate wrapper in
|
|
7
|
+
# ~/.local/share/llm-native-host/ with an absolute node path.
|
|
8
|
+
|
|
9
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
11
|
+
exec node "$DIR/native/host.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/native/host.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_PORT, MAX_CHROME_TO_HOST_MESSAGE_SIZE_BYTES, MAX_HOST_TO_CHROME_MESSAGE_SIZE_BYTES, } from '../protocol/constants.js';
|
|
2
|
+
import { ChromeClient } from '../sdk/client.js';
|
|
3
|
+
import { ChromeServer } from './server.js';
|
|
4
|
+
function log(msg) {
|
|
5
|
+
process.stderr.write(`[host] ${msg}\n`);
|
|
6
|
+
}
|
|
7
|
+
log(`started (pid=${process.pid})`);
|
|
8
|
+
const client = new ChromeClient({
|
|
9
|
+
maxIncomingMessageSizeBytes: MAX_CHROME_TO_HOST_MESSAGE_SIZE_BYTES,
|
|
10
|
+
maxOutgoingMessageSizeBytes: MAX_HOST_TO_CHROME_MESSAGE_SIZE_BYTES,
|
|
11
|
+
});
|
|
12
|
+
client.run().then(() => {
|
|
13
|
+
log('Chrome disconnected, exiting');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}, (e) => {
|
|
16
|
+
log(`fatal: ${e instanceof Error ? e.message : String(e)}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
const port = process.env.CHROME_RPC_PORT ? parseInt(process.env.CHROME_RPC_PORT, 10) : DEFAULT_PORT;
|
|
20
|
+
new ChromeServer(client, { port });
|
|
21
|
+
//# sourceMappingURL=host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/native/host.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,qCAAqC,EACrC,qCAAqC,GACtC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,SAAS,GAAG,CAAC,GAAW;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED,GAAG,CAAC,gBAAgB,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;AAEpC,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;IAC9B,2BAA2B,EAAE,qCAAqC;IAClE,2BAA2B,EAAE,qCAAqC;CACnE,CAAC,CAAC;AACH,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CACf,GAAG,EAAE;IACH,GAAG,CAAC,8BAA8B,CAAC,CAAC;IACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,EACD,CAAC,CAAC,EAAE,EAAE;IACJ,GAAG,CAAC,UAAU,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CACF,CAAC;AAEF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;AAEpG,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ChromeClient } from '../sdk/client.js';
|
|
2
|
+
export interface ChromeServerOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
host?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* TCP server that proxies Chrome API calls from external agents
|
|
8
|
+
* through a ChromeClient connected to Chrome's native messaging.
|
|
9
|
+
*
|
|
10
|
+
* Each TCP connection gets its own read loop and subscription tracking.
|
|
11
|
+
* Multiple agents can connect simultaneously.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ChromeServer {
|
|
14
|
+
private server;
|
|
15
|
+
private chromeClient;
|
|
16
|
+
constructor(chromeClient: ChromeClient, opts?: ChromeServerOptions);
|
|
17
|
+
private handleConnection;
|
|
18
|
+
private handleCall;
|
|
19
|
+
private handleSubscribe;
|
|
20
|
+
private handleUnsubscribe;
|
|
21
|
+
private send;
|
|
22
|
+
close(): void;
|
|
23
|
+
get address(): {
|
|
24
|
+
port: number;
|
|
25
|
+
host: string;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/native/server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGrD,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAe;gBAEvB,YAAY,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,mBAAmB;YAmBpD,gBAAgB;YAuChB,UAAU;IAaxB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,IAAI;IAYZ,KAAK,IAAI,IAAI;IAIb,IAAI,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAMnD;CACF"}
|