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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -0
  3. package/bin/yunti-browser-runtime.js +86 -0
  4. package/docs/EXECUTION_PLAN.md +1278 -0
  5. package/docs/INSTALL.md +205 -0
  6. package/docs/PROJECT_INTENT.md +44 -0
  7. package/docs/PROJECT_STATUS.md +263 -0
  8. package/docs/PUBLISHING_BLOCKERS.md +110 -0
  9. package/docs/RELEASE.md +148 -0
  10. package/docs/ROADMAP.md +42 -0
  11. package/docs/SECURITY.md +56 -0
  12. package/docs/TOOL_GUIDE.md +69 -0
  13. package/extension/background.js +55 -0
  14. package/extension/cdp.js +582 -0
  15. package/extension/content.css +9 -0
  16. package/extension/content.js +946 -0
  17. package/extension/manifest.json +35 -0
  18. package/extension/network-monitor.js +140 -0
  19. package/extension/popup.css +66 -0
  20. package/extension/popup.html +30 -0
  21. package/extension/popup.js +55 -0
  22. package/extension/session-manager.js +332 -0
  23. package/extension/settings.js +94 -0
  24. package/extension/tool-handlers.js +1158 -0
  25. package/lib/runtime-paths.js +39 -0
  26. package/mcp/bridge-hub.js +604 -0
  27. package/mcp/http-server.js +326 -0
  28. package/mcp/json-rpc.js +35 -0
  29. package/mcp/memory.js +126 -0
  30. package/mcp/redaction.js +94 -0
  31. package/mcp/server.js +269 -0
  32. package/mcp/tools.js +1092 -0
  33. package/package.json +60 -0
  34. package/scripts/check-package-metadata.js +131 -0
  35. package/scripts/check-published-package.js +113 -0
  36. package/scripts/doctor.js +163 -0
  37. package/scripts/package-extension.js +137 -0
  38. package/scripts/print-config.js +116 -0
  39. package/scripts/release-check.js +472 -0
  40. 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
+ }