yunti-browser-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +256 -0
- package/bin/yunti-browser-runtime.js +86 -0
- package/docs/EXECUTION_PLAN.md +1278 -0
- package/docs/INSTALL.md +205 -0
- package/docs/PROJECT_INTENT.md +44 -0
- package/docs/PROJECT_STATUS.md +263 -0
- package/docs/PUBLISHING_BLOCKERS.md +110 -0
- package/docs/RELEASE.md +148 -0
- package/docs/ROADMAP.md +42 -0
- package/docs/SECURITY.md +56 -0
- package/docs/TOOL_GUIDE.md +69 -0
- package/extension/background.js +55 -0
- package/extension/cdp.js +582 -0
- package/extension/content.css +9 -0
- package/extension/content.js +946 -0
- package/extension/manifest.json +35 -0
- package/extension/network-monitor.js +140 -0
- package/extension/popup.css +66 -0
- package/extension/popup.html +30 -0
- package/extension/popup.js +55 -0
- package/extension/session-manager.js +332 -0
- package/extension/settings.js +94 -0
- package/extension/tool-handlers.js +1158 -0
- package/lib/runtime-paths.js +39 -0
- package/mcp/bridge-hub.js +604 -0
- package/mcp/http-server.js +326 -0
- package/mcp/json-rpc.js +35 -0
- package/mcp/memory.js +126 -0
- package/mcp/redaction.js +94 -0
- package/mcp/server.js +269 -0
- package/mcp/tools.js +1092 -0
- package/package.json +60 -0
- package/scripts/check-package-metadata.js +131 -0
- package/scripts/check-published-package.js +113 -0
- package/scripts/doctor.js +163 -0
- package/scripts/package-extension.js +137 -0
- package/scripts/print-config.js +116 -0
- package/scripts/release-check.js +472 -0
- package/skills/yunti-browser-runtime/SKILL.md +77 -0
package/mcp/server.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import { DEFAULT_TOOL_TIMEOUT_MS, normalizeRouteUserId, requireRouteUserId } from "./bridge-hub.js"
|
|
5
|
+
import {
|
|
6
|
+
BRIDGE_TOKEN_HEADER,
|
|
7
|
+
DEFAULT_HOST,
|
|
8
|
+
startBridgeServer,
|
|
9
|
+
} from "./http-server.js"
|
|
10
|
+
import { jsonRpcErr, jsonRpcOk, toolError, toolOk } from "./json-rpc.js"
|
|
11
|
+
import { forgetLearningMemory, getLearningMemory, rememberLearning } from "./memory.js"
|
|
12
|
+
import {
|
|
13
|
+
BRIDGE_LOCAL_TOOLS,
|
|
14
|
+
BROWSER_SCOPED_TOOLS,
|
|
15
|
+
MEMORY_LOCAL_TOOLS,
|
|
16
|
+
META_LOCAL_TOOLS,
|
|
17
|
+
TOOLS,
|
|
18
|
+
toolUsageHints,
|
|
19
|
+
} from "./tools.js"
|
|
20
|
+
|
|
21
|
+
export { TOOLS } from "./tools.js"
|
|
22
|
+
export { BridgeHub, DEFAULT_SESSION_TTL_MS } from "./bridge-hub.js"
|
|
23
|
+
export {
|
|
24
|
+
BRIDGE_TOKEN_HEADER,
|
|
25
|
+
DEFAULT_HOST,
|
|
26
|
+
DEFAULT_PORT,
|
|
27
|
+
startBridgeServer,
|
|
28
|
+
} from "./http-server.js"
|
|
29
|
+
|
|
30
|
+
const RELAY_URL = normalizeBaseUrl(process.env.YUNTI_BROWSER_RELAY_URL || "")
|
|
31
|
+
const RELAY_TOKEN = process.env.YUNTI_BROWSER_RELAY_TOKEN || ""
|
|
32
|
+
const SERVER_NAME = process.env.YUNTI_BROWSER_SERVER_NAME || "Yunti Browser Runtime"
|
|
33
|
+
const _ROUTE_USER_ID = process.env.YUNTI_BROWSER_USER_ID || "local"
|
|
34
|
+
const _ROUTE_USER_NAME = process.env.YUNTI_BROWSER_USER_NAME || "local"
|
|
35
|
+
|
|
36
|
+
function normalizeBaseUrl(value) {
|
|
37
|
+
return String(value || "")
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/\/+$/, "")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeBridgeToken(value) {
|
|
43
|
+
return String(value || "").trim()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function withRouteUser(args = {}) {
|
|
47
|
+
const next = args && typeof args === "object" && !Array.isArray(args) ? { ...args } : {}
|
|
48
|
+
if (!next.userId) {
|
|
49
|
+
const routeUserId = normalizeRouteUserId(_ROUTE_USER_ID)
|
|
50
|
+
if (routeUserId) next.userId = routeUserId
|
|
51
|
+
}
|
|
52
|
+
if (!next.userName && _ROUTE_USER_NAME) next.userName = _ROUTE_USER_NAME
|
|
53
|
+
return next
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function preflightBrowserToolScope(tool, args = {}) {
|
|
57
|
+
if (!BROWSER_SCOPED_TOOLS.has(tool)) return
|
|
58
|
+
requireRouteUserId(args, tool)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bridgeRequestHeaders(bridgeToken = "") {
|
|
62
|
+
const headers = { "content-type": "application/json" }
|
|
63
|
+
const token = normalizeBridgeToken(bridgeToken)
|
|
64
|
+
if (token) headers[BRIDGE_TOKEN_HEADER] = token
|
|
65
|
+
return headers
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function proxyToolRequest(port, tool, args, timeoutMs, bridgeToken = "") {
|
|
69
|
+
const routedArgs = withRouteUser(args)
|
|
70
|
+
const response = await fetch(`http://${DEFAULT_HOST}:${port}/mcp/request`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: bridgeRequestHeaders(bridgeToken),
|
|
73
|
+
body: JSON.stringify({ tool, arguments: routedArgs, timeoutMs }),
|
|
74
|
+
})
|
|
75
|
+
const data = await response.json().catch(() => null)
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(data?.error || `bridge HTTP ${response.status}`)
|
|
78
|
+
}
|
|
79
|
+
return data?.result ?? null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function proxyBridgeLocalToolRequest(port, tool, args, bridgeToken = "") {
|
|
83
|
+
const routedArgs = withRouteUser(args)
|
|
84
|
+
const response = await fetch(`http://${DEFAULT_HOST}:${port}/mcp/local-tool`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: bridgeRequestHeaders(bridgeToken),
|
|
87
|
+
body: JSON.stringify({ tool, arguments: routedArgs }),
|
|
88
|
+
})
|
|
89
|
+
const data = await response.json().catch(() => null)
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(data?.error || `bridge HTTP ${response.status}`)
|
|
92
|
+
}
|
|
93
|
+
return data?.result ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function relayToolRequest(tool, args, timeoutMs) {
|
|
97
|
+
const routedArgs = withRouteUser(args)
|
|
98
|
+
const response = await fetch(`${RELAY_URL}/mcp/request`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: relayHeaders(),
|
|
101
|
+
body: JSON.stringify({ tool, arguments: routedArgs, timeoutMs }),
|
|
102
|
+
})
|
|
103
|
+
const data = await response.json().catch(() => null)
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(data?.error || `relay HTTP ${response.status}`)
|
|
106
|
+
}
|
|
107
|
+
return data?.result ?? null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function relayBridgeLocalToolRequest(tool, args) {
|
|
111
|
+
const routedArgs = withRouteUser(args)
|
|
112
|
+
const response = await fetch(`${RELAY_URL}/mcp/local-tool`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: relayHeaders(),
|
|
115
|
+
body: JSON.stringify({ tool, arguments: routedArgs }),
|
|
116
|
+
})
|
|
117
|
+
const data = await response.json().catch(() => null)
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw new Error(data?.error || `relay HTTP ${response.status}`)
|
|
120
|
+
}
|
|
121
|
+
return data?.result ?? null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function relayHeaders() {
|
|
125
|
+
const headers = { "content-type": "application/json" }
|
|
126
|
+
if (RELAY_TOKEN) headers.authorization = `Bearer ${RELAY_TOKEN}`
|
|
127
|
+
return headers
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function callTool(bridge, tool, args) {
|
|
131
|
+
args = withRouteUser(args)
|
|
132
|
+
preflightBrowserToolScope(tool, args)
|
|
133
|
+
if (META_LOCAL_TOOLS.has(tool)) {
|
|
134
|
+
return toolUsageHints(args)
|
|
135
|
+
}
|
|
136
|
+
if (MEMORY_LOCAL_TOOLS.has(tool)) {
|
|
137
|
+
if (bridge?.mode === "owner") return bridge.hub.callBridgeLocalTool(tool, args)
|
|
138
|
+
if (bridge?.mode === "proxy") return proxyBridgeLocalToolRequest(bridge.port, tool, args, bridge.bridgeToken)
|
|
139
|
+
return callMemoryLocalTool(tool, args)
|
|
140
|
+
}
|
|
141
|
+
if (RELAY_URL) {
|
|
142
|
+
if (BRIDGE_LOCAL_TOOLS.has(tool)) return relayBridgeLocalToolRequest(tool, args)
|
|
143
|
+
return relayToolRequest(tool, args, DEFAULT_TOOL_TIMEOUT_MS)
|
|
144
|
+
}
|
|
145
|
+
if (BRIDGE_LOCAL_TOOLS.has(tool)) {
|
|
146
|
+
if (bridge.mode === "owner") return bridge.hub.callBridgeLocalTool(tool, args)
|
|
147
|
+
return proxyBridgeLocalToolRequest(bridge.port, tool, args, bridge.bridgeToken)
|
|
148
|
+
}
|
|
149
|
+
if (bridge.mode === "owner") {
|
|
150
|
+
return bridge.hub.callTool(tool, args)
|
|
151
|
+
}
|
|
152
|
+
return proxyToolRequest(bridge.port, tool, args, DEFAULT_TOOL_TIMEOUT_MS, bridge.bridgeToken)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function callMemoryLocalTool(tool, args = {}) {
|
|
156
|
+
switch (tool) {
|
|
157
|
+
case "yunti_get_tool_usage_hints":
|
|
158
|
+
return toolUsageHints(args)
|
|
159
|
+
case "yunti_remember_learning":
|
|
160
|
+
return rememberLearning(args)
|
|
161
|
+
case "yunti_get_learning_memory":
|
|
162
|
+
return getLearningMemory(args)
|
|
163
|
+
case "yunti_forget_learning_memory":
|
|
164
|
+
return forgetLearningMemory(args)
|
|
165
|
+
default:
|
|
166
|
+
throw new Error(`unknown memory-local tool: ${tool}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function handleJsonRpc(req, bridge) {
|
|
171
|
+
if (!req || typeof req !== "object") return jsonRpcErr(null, -32600, "invalid request")
|
|
172
|
+
if (req.id === undefined || req.id === null) return null
|
|
173
|
+
|
|
174
|
+
if (req.method === "initialize") {
|
|
175
|
+
return jsonRpcOk(req.id, {
|
|
176
|
+
protocolVersion: "2024-11-05",
|
|
177
|
+
serverInfo: { name: SERVER_NAME, version: "1.0.0" },
|
|
178
|
+
capabilities: { tools: {} },
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (req.method === "tools/list") {
|
|
183
|
+
return jsonRpcOk(req.id, { tools: TOOLS })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (req.method === "tools/call") {
|
|
187
|
+
const name = req.params?.name
|
|
188
|
+
const args = req.params?.arguments ?? {}
|
|
189
|
+
if (!TOOLS.some((tool) => tool.name === name)) {
|
|
190
|
+
return jsonRpcErr(req.id, -32602, `unknown tool: ${name}`)
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const result = await callTool(bridge, name, args)
|
|
194
|
+
return jsonRpcOk(req.id, toolOk(result))
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return jsonRpcOk(
|
|
197
|
+
req.id,
|
|
198
|
+
toolError(
|
|
199
|
+
error instanceof Error ? error.message : String(error),
|
|
200
|
+
"Open an internal platform page with the Yunti Browser Runtime extension loaded, then try again."
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return jsonRpcErr(req.id, -32601, `method not found: ${req.method}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function runStdio() {
|
|
210
|
+
const bridge = RELAY_URL ? { mode: "relay", host: "", port: 0, hub: null } : await startBridgeServer()
|
|
211
|
+
if (RELAY_URL) {
|
|
212
|
+
console.error(`[yunti-browser-runtime] relay ${RELAY_URL}`)
|
|
213
|
+
} else {
|
|
214
|
+
console.error(
|
|
215
|
+
`[yunti-browser-runtime] bridge ${bridge.mode} on http://${bridge.host}:${bridge.port}`
|
|
216
|
+
)
|
|
217
|
+
if (bridge.mode === "owner") {
|
|
218
|
+
console.error(
|
|
219
|
+
`[yunti-browser-runtime] bridge token header ${BRIDGE_TOKEN_HEADER}: ${bridge.bridgeToken}`
|
|
220
|
+
)
|
|
221
|
+
} else if (!bridge.bridgeToken) {
|
|
222
|
+
console.error(
|
|
223
|
+
`[yunti-browser-runtime] proxy mode requires YUNTI_BROWSER_BRIDGE_TOKEN to match the running bridge`
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
229
|
+
for await (const rawLine of rl) {
|
|
230
|
+
const line = rawLine.trim()
|
|
231
|
+
if (!line) continue
|
|
232
|
+
let req
|
|
233
|
+
try {
|
|
234
|
+
req = JSON.parse(line)
|
|
235
|
+
} catch (error) {
|
|
236
|
+
process.stdout.write(
|
|
237
|
+
`${JSON.stringify(jsonRpcErr(null, -32700, `parse error: ${error.message}`))}\n`
|
|
238
|
+
)
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
const response = await handleJsonRpc(req, bridge)
|
|
242
|
+
if (response) process.stdout.write(`${JSON.stringify(response)}\n`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function runBridgeOnly() {
|
|
247
|
+
const bridge = await startBridgeServer()
|
|
248
|
+
console.error(
|
|
249
|
+
`[yunti-browser-runtime] bridge ${bridge.mode} on http://${bridge.host}:${bridge.port}`
|
|
250
|
+
)
|
|
251
|
+
if (bridge.mode === "owner") {
|
|
252
|
+
console.error(
|
|
253
|
+
`[yunti-browser-runtime] bridge token header ${BRIDGE_TOKEN_HEADER}: ${bridge.bridgeToken}`
|
|
254
|
+
)
|
|
255
|
+
} else if (!bridge.bridgeToken) {
|
|
256
|
+
console.error(
|
|
257
|
+
`[yunti-browser-runtime] proxy mode requires YUNTI_BROWSER_BRIDGE_TOKEN to match the running bridge`
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
await new Promise(() => {})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
264
|
+
const runner = process.env.YUNTI_BROWSER_BRIDGE_ONLY === "1" ? runBridgeOnly : runStdio
|
|
265
|
+
runner().catch((error) => {
|
|
266
|
+
console.error("[yunti-browser-runtime] fatal:", error)
|
|
267
|
+
process.exit(1)
|
|
268
|
+
})
|
|
269
|
+
}
|