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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, join, resolve } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
6
|
+
const mcpServerPath = join(rootDir, "mcp", "server.js")
|
|
7
|
+
const skillPath = join(rootDir, "skills", "yunti-browser-runtime")
|
|
8
|
+
const bridgePort = process.env.YUNTI_BROWSER_BRIDGE_PORT || "48887"
|
|
9
|
+
const bridgeTokenConfigured = Boolean(String(process.env.YUNTI_BROWSER_BRIDGE_TOKEN || "").trim())
|
|
10
|
+
const supportedAgents = ["codex", "claude-code", "cursor", "cline"]
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const result = { agent: "codex", format: "json" }
|
|
14
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
15
|
+
const arg = argv[i]
|
|
16
|
+
if (arg === "--agent") {
|
|
17
|
+
result.agent = argv[i + 1] || result.agent
|
|
18
|
+
i += 1
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
if (arg.startsWith("--agent=")) {
|
|
22
|
+
result.agent = arg.slice("--agent=".length)
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
if (arg === "--human") result.format = "human"
|
|
26
|
+
if (arg === "--json") result.format = "json"
|
|
27
|
+
}
|
|
28
|
+
result.agent = String(result.agent || "codex").trim().toLowerCase()
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mcpServerConfig() {
|
|
33
|
+
const env = {
|
|
34
|
+
YUNTI_BROWSER_BRIDGE_PORT: bridgePort,
|
|
35
|
+
}
|
|
36
|
+
if (bridgeTokenConfigured) {
|
|
37
|
+
env.YUNTI_BROWSER_BRIDGE_TOKEN = "<set the same local bridge token>"
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command: "node",
|
|
41
|
+
args: [mcpServerPath],
|
|
42
|
+
env,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function agentConfig(agent) {
|
|
47
|
+
const server = mcpServerConfig()
|
|
48
|
+
switch (agent) {
|
|
49
|
+
case "codex":
|
|
50
|
+
case "claude-code":
|
|
51
|
+
case "cursor":
|
|
52
|
+
case "cline":
|
|
53
|
+
return {
|
|
54
|
+
mcpServers: {
|
|
55
|
+
"yunti-browser-runtime": server,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return {
|
|
60
|
+
mcpServers: {
|
|
61
|
+
"yunti-browser-runtime": server,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildConfig(agent) {
|
|
68
|
+
return {
|
|
69
|
+
ok: supportedAgents.includes(agent),
|
|
70
|
+
agent,
|
|
71
|
+
supportedAgents,
|
|
72
|
+
projectRoot: rootDir,
|
|
73
|
+
mcpServerPath,
|
|
74
|
+
bridge: {
|
|
75
|
+
port: bridgePort,
|
|
76
|
+
tokenEnv: "YUNTI_BROWSER_BRIDGE_TOKEN",
|
|
77
|
+
tokenConfigured: bridgeTokenConfigured,
|
|
78
|
+
tokenInstruction: bridgeTokenConfigured
|
|
79
|
+
? "Token is configured in the current environment; copy the same secret into your agent and extension settings without committing it."
|
|
80
|
+
: "Set YUNTI_BROWSER_BRIDGE_TOKEN in your agent and save the same token in the extension popup when bridge auth is enabled.",
|
|
81
|
+
},
|
|
82
|
+
skill: {
|
|
83
|
+
sourcePath: skillPath,
|
|
84
|
+
installHint: "Copy the yunti-browser-runtime skill directory into your agent skills directory if the agent supports skills.",
|
|
85
|
+
},
|
|
86
|
+
config: agentConfig(agent),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function humanOutput(payload) {
|
|
91
|
+
return [
|
|
92
|
+
`Yunti Browser Runtime MCP config for ${payload.agent}`,
|
|
93
|
+
"",
|
|
94
|
+
JSON.stringify(payload.config, null, 2),
|
|
95
|
+
"",
|
|
96
|
+
`Project root: ${payload.projectRoot}`,
|
|
97
|
+
`MCP server: ${payload.mcpServerPath}`,
|
|
98
|
+
`Bridge port: ${payload.bridge.port}`,
|
|
99
|
+
`Token: ${payload.bridge.tokenInstruction}`,
|
|
100
|
+
`Skill: ${payload.skill.sourcePath}`,
|
|
101
|
+
payload.ok ? "" : `Unsupported agent "${payload.agent}". Supported: ${payload.supportedAgents.join(", ")}`,
|
|
102
|
+
]
|
|
103
|
+
.filter((line, index, lines) => line || lines[index - 1] !== "")
|
|
104
|
+
.join("\n")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const args = parseArgs(process.argv.slice(2))
|
|
108
|
+
const payload = buildConfig(args.agent)
|
|
109
|
+
|
|
110
|
+
if (args.format === "human") {
|
|
111
|
+
console.log(humanOutput(payload))
|
|
112
|
+
} else {
|
|
113
|
+
console.log(JSON.stringify(payload, null, 2))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!payload.ok) process.exitCode = 1
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
3
|
+
import { dirname, join, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { spawnSync } from "node:child_process"
|
|
6
|
+
|
|
7
|
+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
8
|
+
const residueTerms = ["/Users", "Codeg", "xyy", "ybm100"]
|
|
9
|
+
const residueRoots = ["README.md", "docs", "skills", "package.json"]
|
|
10
|
+
const requiredExtensionZipFiles = [
|
|
11
|
+
"background.js",
|
|
12
|
+
"cdp.js",
|
|
13
|
+
"content.css",
|
|
14
|
+
"content.js",
|
|
15
|
+
"network-monitor.js",
|
|
16
|
+
"manifest.json",
|
|
17
|
+
"popup.css",
|
|
18
|
+
"popup.html",
|
|
19
|
+
"popup.js",
|
|
20
|
+
"session-manager.js",
|
|
21
|
+
"settings.js",
|
|
22
|
+
"tool-handlers.js",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
function readJsonFile(path) {
|
|
26
|
+
return JSON.parse(readFileSync(join(rootDir, path), "utf8"))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listFiles(path) {
|
|
30
|
+
const absolutePath = join(rootDir, path)
|
|
31
|
+
const stats = statSync(absolutePath)
|
|
32
|
+
if (stats.isFile()) return [absolutePath]
|
|
33
|
+
if (!stats.isDirectory()) return []
|
|
34
|
+
return readdirSync(absolutePath, { withFileTypes: true }).flatMap((entry) => {
|
|
35
|
+
const childPath = join(path, entry.name)
|
|
36
|
+
if (entry.isDirectory()) return listFiles(childPath)
|
|
37
|
+
if (entry.isFile()) return [join(rootDir, childPath)]
|
|
38
|
+
return []
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkPublicDocResidue() {
|
|
43
|
+
const matches = []
|
|
44
|
+
for (const file of residueRoots.flatMap(listFiles)) {
|
|
45
|
+
const text = readFileSync(file, "utf8")
|
|
46
|
+
for (const term of residueTerms) {
|
|
47
|
+
if (text.includes(term)) {
|
|
48
|
+
matches.push({
|
|
49
|
+
file: file.slice(rootDir.length + 1),
|
|
50
|
+
term,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (matches.length) {
|
|
56
|
+
console.error("Public documentation residue check failed:")
|
|
57
|
+
for (const match of matches) console.error(`- ${match.file}: ${match.term}`)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
console.error("Public documentation residue check passed.")
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stripMarkdownCodeFences(text) {
|
|
65
|
+
return text.replace(/```[\s\S]*?```/g, "")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkMarkdownLinks() {
|
|
69
|
+
const files = residueRoots
|
|
70
|
+
.flatMap(listFiles)
|
|
71
|
+
.filter((file) => file.endsWith(".md"))
|
|
72
|
+
const broken = []
|
|
73
|
+
const linkPattern = /(?<!!)\[[^\]]+\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
|
|
74
|
+
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const text = stripMarkdownCodeFences(readFileSync(file, "utf8"))
|
|
77
|
+
for (const match of text.matchAll(linkPattern)) {
|
|
78
|
+
const rawTarget = String(match[1] || "").trim()
|
|
79
|
+
if (
|
|
80
|
+
!rawTarget ||
|
|
81
|
+
rawTarget.startsWith("#") ||
|
|
82
|
+
/^[a-z][a-z0-9+.-]*:/i.test(rawTarget) ||
|
|
83
|
+
rawTarget.startsWith("mailto:")
|
|
84
|
+
) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [targetPath, anchor] = rawTarget.split("#")
|
|
89
|
+
if (!targetPath) continue
|
|
90
|
+
const absoluteTarget = resolve(dirname(file), decodeURIComponent(targetPath))
|
|
91
|
+
if (!existsSync(absoluteTarget)) {
|
|
92
|
+
broken.push({
|
|
93
|
+
file: file.slice(rootDir.length + 1),
|
|
94
|
+
target: rawTarget,
|
|
95
|
+
reason: "target file missing",
|
|
96
|
+
})
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if (anchor && !statSync(absoluteTarget).isFile()) {
|
|
100
|
+
broken.push({
|
|
101
|
+
file: file.slice(rootDir.length + 1),
|
|
102
|
+
target: rawTarget,
|
|
103
|
+
reason: "anchor target is not a file",
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (broken.length) {
|
|
110
|
+
console.error("Public Markdown link check failed:")
|
|
111
|
+
for (const item of broken) {
|
|
112
|
+
console.error(`- ${item.file}: ${item.target} (${item.reason})`)
|
|
113
|
+
}
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.error(`Public Markdown link check passed (${files.length} files).`)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkVersionConsistency() {
|
|
122
|
+
const packageJson = readJsonFile("package.json")
|
|
123
|
+
const manifestJson = readJsonFile("extension/manifest.json")
|
|
124
|
+
const packageVersion = String(packageJson.version || "").trim()
|
|
125
|
+
const manifestVersion = String(manifestJson.version || "").trim()
|
|
126
|
+
|
|
127
|
+
if (!packageVersion || !manifestVersion || packageVersion !== manifestVersion) {
|
|
128
|
+
console.error("Version consistency check failed:")
|
|
129
|
+
console.error(`- package.json version: ${packageVersion || "(missing)"}`)
|
|
130
|
+
console.error(`- extension/manifest.json version: ${manifestVersion || "(missing)"}`)
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.error(`Version consistency check passed (${packageVersion}).`)
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function checkCliSmoke() {
|
|
139
|
+
const packageJson = readJsonFile("package.json")
|
|
140
|
+
const binPath = packageJson.bin?.["yunti-browser-runtime"]
|
|
141
|
+
if (binPath !== "bin/yunti-browser-runtime.js") {
|
|
142
|
+
console.error("CLI smoke check failed:")
|
|
143
|
+
console.error(`- package.json bin yunti-browser-runtime: ${binPath || "(missing)"}`)
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const help = spawnSync(process.execPath, ["bin/yunti-browser-runtime.js", "--help"], {
|
|
148
|
+
cwd: rootDir,
|
|
149
|
+
encoding: "utf8",
|
|
150
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
151
|
+
env: process.env,
|
|
152
|
+
})
|
|
153
|
+
if (
|
|
154
|
+
help.status !== 0 ||
|
|
155
|
+
!help.stdout.includes("Yunti Browser Runtime") ||
|
|
156
|
+
!help.stdout.includes("package-extension")
|
|
157
|
+
) {
|
|
158
|
+
console.error("CLI smoke check failed:")
|
|
159
|
+
console.error(`- --help exit code: ${help.status}`)
|
|
160
|
+
console.error(`- --help stdout: ${help.stdout.trim() || "(empty)"}`)
|
|
161
|
+
console.error(`- --help stderr: ${help.stderr.trim() || "(empty)"}`)
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const version = spawnSync(process.execPath, ["bin/yunti-browser-runtime.js", "--version"], {
|
|
166
|
+
cwd: rootDir,
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
169
|
+
env: process.env,
|
|
170
|
+
})
|
|
171
|
+
const expectedVersion = String(packageJson.version || "").trim()
|
|
172
|
+
const actualVersion = version.stdout.trim()
|
|
173
|
+
if (version.status !== 0 || actualVersion !== expectedVersion) {
|
|
174
|
+
console.error("CLI smoke check failed:")
|
|
175
|
+
console.error(`- --version exit code: ${version.status}`)
|
|
176
|
+
console.error(`- expected version: ${expectedVersion || "(missing)"}`)
|
|
177
|
+
console.error(`- actual version: ${actualVersion || "(empty)"}`)
|
|
178
|
+
console.error(`- --version stderr: ${version.stderr.trim() || "(empty)"}`)
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.error(`CLI smoke check passed (${actualVersion}).`)
|
|
183
|
+
return true
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function checkPrintConfigSmoke() {
|
|
187
|
+
const agents = ["codex", "claude-code", "cursor", "cline"]
|
|
188
|
+
for (const agent of agents) {
|
|
189
|
+
const result = spawnSync(
|
|
190
|
+
process.execPath,
|
|
191
|
+
["scripts/print-config.js", "--agent", agent, "--json"],
|
|
192
|
+
{
|
|
193
|
+
cwd: rootDir,
|
|
194
|
+
encoding: "utf8",
|
|
195
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
196
|
+
env: process.env,
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
if (result.status !== 0) {
|
|
200
|
+
console.error("print-config smoke check failed:")
|
|
201
|
+
console.error(`- agent: ${agent}`)
|
|
202
|
+
console.error(`- exit code: ${result.status}`)
|
|
203
|
+
console.error(`- stderr: ${result.stderr.trim() || "(empty)"}`)
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let payload
|
|
208
|
+
try {
|
|
209
|
+
payload = JSON.parse(result.stdout || "{}")
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error("print-config smoke check failed:")
|
|
212
|
+
console.error(`- agent: ${agent}`)
|
|
213
|
+
console.error(`- JSON parse error: ${error.message}`)
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const server =
|
|
218
|
+
payload?.config?.mcpServers?.["yunti-browser-runtime"]
|
|
219
|
+
const env = server?.env || {}
|
|
220
|
+
if (
|
|
221
|
+
payload?.ok !== true ||
|
|
222
|
+
payload?.agent !== agent ||
|
|
223
|
+
server?.command !== "node" ||
|
|
224
|
+
!Array.isArray(server?.args) ||
|
|
225
|
+
!server.args[0]?.endsWith("mcp/server.js") ||
|
|
226
|
+
env.YUNTI_BROWSER_BRIDGE_PORT !== "48887" ||
|
|
227
|
+
payload?.bridge?.tokenEnv !== "YUNTI_BROWSER_BRIDGE_TOKEN" ||
|
|
228
|
+
!String(payload?.skill?.sourcePath || "").endsWith("skills/yunti-browser-runtime")
|
|
229
|
+
) {
|
|
230
|
+
console.error("print-config smoke check failed:")
|
|
231
|
+
console.error(`- agent: ${agent}`)
|
|
232
|
+
console.error(`- unexpected payload: ${JSON.stringify(payload, null, 2)}`)
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const human = spawnSync(
|
|
238
|
+
process.execPath,
|
|
239
|
+
["scripts/print-config.js", "--agent", "codex", "--human"],
|
|
240
|
+
{
|
|
241
|
+
cwd: rootDir,
|
|
242
|
+
encoding: "utf8",
|
|
243
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
244
|
+
env: process.env,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
if (
|
|
248
|
+
human.status !== 0 ||
|
|
249
|
+
!human.stdout.includes("Yunti Browser Runtime MCP config for codex") ||
|
|
250
|
+
!human.stdout.includes("YUNTI_BROWSER_BRIDGE_TOKEN") ||
|
|
251
|
+
!human.stdout.includes("skills/yunti-browser-runtime")
|
|
252
|
+
) {
|
|
253
|
+
console.error("print-config smoke check failed:")
|
|
254
|
+
console.error(`- human exit code: ${human.status}`)
|
|
255
|
+
console.error(`- human stdout: ${human.stdout.trim() || "(empty)"}`)
|
|
256
|
+
console.error(`- human stderr: ${human.stderr.trim() || "(empty)"}`)
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.error(`print-config smoke check passed (${agents.length} agents).`)
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function checkDoctorSmoke() {
|
|
265
|
+
const result = spawnSync(process.execPath, ["scripts/doctor.js"], {
|
|
266
|
+
cwd: rootDir,
|
|
267
|
+
encoding: "utf8",
|
|
268
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
269
|
+
env: process.env,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
let payload
|
|
273
|
+
try {
|
|
274
|
+
payload = JSON.parse(result.stdout || "{}")
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error("doctor smoke check failed:")
|
|
277
|
+
console.error(`- JSON parse error: ${error.message}`)
|
|
278
|
+
console.error(`- stdout: ${result.stdout.trim() || "(empty)"}`)
|
|
279
|
+
console.error(`- stderr: ${result.stderr.trim() || "(empty)"}`)
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const checks = payload?.checks || {}
|
|
284
|
+
if (
|
|
285
|
+
!payload?.checkedAt ||
|
|
286
|
+
checks.node?.ok !== true ||
|
|
287
|
+
checks.node?.required !== ">=22" ||
|
|
288
|
+
checks.mcpServer?.ok !== true ||
|
|
289
|
+
!String(checks.mcpServer?.path || "").endsWith("mcp/server.js") ||
|
|
290
|
+
checks.skill?.ok !== true ||
|
|
291
|
+
!String(checks.skill?.path || "").endsWith("skills/yunti-browser-runtime/SKILL.md") ||
|
|
292
|
+
typeof checks.bridge?.reachable !== "boolean" ||
|
|
293
|
+
checks.bridge?.tokenHeader !== "x-yunti-browser-token" ||
|
|
294
|
+
!Array.isArray(payload?.nextSteps)
|
|
295
|
+
) {
|
|
296
|
+
console.error("doctor smoke check failed:")
|
|
297
|
+
console.error(`- unexpected payload: ${JSON.stringify(payload, null, 2)}`)
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.error(
|
|
302
|
+
`doctor smoke check passed (bridge ${checks.bridge.reachable ? "reachable" : "not reachable"}).`
|
|
303
|
+
)
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function run(command, args) {
|
|
308
|
+
console.error(`\n$ ${[command, ...args].join(" ")}`)
|
|
309
|
+
const result = spawnSync(command, args, {
|
|
310
|
+
cwd: rootDir,
|
|
311
|
+
stdio: "inherit",
|
|
312
|
+
env: process.env,
|
|
313
|
+
})
|
|
314
|
+
return result.status === 0
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function checkPackageContents() {
|
|
318
|
+
const requiredFiles = [
|
|
319
|
+
"README.md",
|
|
320
|
+
"LICENSE",
|
|
321
|
+
"package.json",
|
|
322
|
+
"bin/yunti-browser-runtime.js",
|
|
323
|
+
"mcp/server.js",
|
|
324
|
+
"mcp/http-server.js",
|
|
325
|
+
"mcp/bridge-hub.js",
|
|
326
|
+
"mcp/tools.js",
|
|
327
|
+
"extension/manifest.json",
|
|
328
|
+
"extension/background.js",
|
|
329
|
+
"extension/content.js",
|
|
330
|
+
"extension/cdp.js",
|
|
331
|
+
"extension/session-manager.js",
|
|
332
|
+
"extension/tool-handlers.js",
|
|
333
|
+
"skills/yunti-browser-runtime/SKILL.md",
|
|
334
|
+
"docs/INSTALL.md",
|
|
335
|
+
"docs/RELEASE.md",
|
|
336
|
+
"docs/PUBLISHING_BLOCKERS.md",
|
|
337
|
+
"scripts/check-package-metadata.js",
|
|
338
|
+
"scripts/check-published-package.js",
|
|
339
|
+
"scripts/release-check.js",
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
console.error("\n$ npm pack --json --dry-run")
|
|
343
|
+
const result = spawnSync("npm", ["pack", "--json", "--dry-run"], {
|
|
344
|
+
cwd: rootDir,
|
|
345
|
+
encoding: "utf8",
|
|
346
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
347
|
+
env: process.env,
|
|
348
|
+
})
|
|
349
|
+
if (result.status !== 0) return false
|
|
350
|
+
|
|
351
|
+
let payload
|
|
352
|
+
try {
|
|
353
|
+
payload = JSON.parse(result.stdout || "[]")
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(`npm pack JSON parse failed: ${error.message}`)
|
|
356
|
+
return false
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const pack = Array.isArray(payload) ? payload[0] : null
|
|
360
|
+
const packedFiles = new Set(
|
|
361
|
+
Array.isArray(pack?.files) ? pack.files.map((file) => file.path) : []
|
|
362
|
+
)
|
|
363
|
+
const missing = requiredFiles.filter((file) => !packedFiles.has(file))
|
|
364
|
+
if (missing.length) {
|
|
365
|
+
console.error("Required npm package contents check failed:")
|
|
366
|
+
for (const file of missing) console.error(`- missing ${file}`)
|
|
367
|
+
return false
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
console.error(
|
|
371
|
+
`Required npm package contents check passed (${packedFiles.size} files).`
|
|
372
|
+
)
|
|
373
|
+
return true
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function listZipEntries(zipPath) {
|
|
377
|
+
const data = readFileSync(zipPath)
|
|
378
|
+
let eocdOffset = -1
|
|
379
|
+
for (let i = data.length - 22; i >= 0; i -= 1) {
|
|
380
|
+
if (data.readUInt32LE(i) === 0x06054b50) {
|
|
381
|
+
eocdOffset = i
|
|
382
|
+
break
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (eocdOffset < 0) {
|
|
386
|
+
throw new Error(`ZIP end of central directory not found: ${zipPath}`)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const entryCount = data.readUInt16LE(eocdOffset + 10)
|
|
390
|
+
const centralDirectoryOffset = data.readUInt32LE(eocdOffset + 16)
|
|
391
|
+
const entries = []
|
|
392
|
+
let offset = centralDirectoryOffset
|
|
393
|
+
|
|
394
|
+
for (let i = 0; i < entryCount; i += 1) {
|
|
395
|
+
if (data.readUInt32LE(offset) !== 0x02014b50) {
|
|
396
|
+
throw new Error(`Invalid ZIP central directory entry at offset ${offset}`)
|
|
397
|
+
}
|
|
398
|
+
const nameLength = data.readUInt16LE(offset + 28)
|
|
399
|
+
const extraLength = data.readUInt16LE(offset + 30)
|
|
400
|
+
const commentLength = data.readUInt16LE(offset + 32)
|
|
401
|
+
const name = data
|
|
402
|
+
.subarray(offset + 46, offset + 46 + nameLength)
|
|
403
|
+
.toString("utf8")
|
|
404
|
+
if (name && !name.endsWith("/")) entries.push(name)
|
|
405
|
+
offset += 46 + nameLength + extraLength + commentLength
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return entries
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function checkExtensionZipContents() {
|
|
412
|
+
console.error("\n$ node scripts/package-extension.js")
|
|
413
|
+
const result = spawnSync("node", ["scripts/package-extension.js"], {
|
|
414
|
+
cwd: rootDir,
|
|
415
|
+
encoding: "utf8",
|
|
416
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
417
|
+
env: process.env,
|
|
418
|
+
})
|
|
419
|
+
if (result.status !== 0) return false
|
|
420
|
+
|
|
421
|
+
let payload
|
|
422
|
+
try {
|
|
423
|
+
payload = JSON.parse(result.stdout || "{}")
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error(`extension package JSON parse failed: ${error.message}`)
|
|
426
|
+
return false
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const zipPath = payload?.zipPath
|
|
430
|
+
if (!zipPath || !statSync(zipPath, { throwIfNoEntry: false })?.isFile()) {
|
|
431
|
+
console.error(`extension zip was not created: ${zipPath || "(missing zipPath)"}`)
|
|
432
|
+
return false
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let entries
|
|
436
|
+
try {
|
|
437
|
+
entries = listZipEntries(zipPath)
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error(error.message)
|
|
440
|
+
return false
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const expected = new Set(requiredExtensionZipFiles)
|
|
444
|
+
const actual = new Set(entries)
|
|
445
|
+
const missing = requiredExtensionZipFiles.filter((file) => !actual.has(file))
|
|
446
|
+
const unexpected = entries.filter((file) => !expected.has(file))
|
|
447
|
+
|
|
448
|
+
if (missing.length || unexpected.length) {
|
|
449
|
+
console.error("Extension zip contents check failed:")
|
|
450
|
+
for (const file of missing) console.error(`- missing ${file}`)
|
|
451
|
+
for (const file of unexpected) console.error(`- unexpected ${file}`)
|
|
452
|
+
return false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.error(
|
|
456
|
+
`Extension zip contents check passed (${entries.length} files).`
|
|
457
|
+
)
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let ok = checkPublicDocResidue()
|
|
462
|
+
ok = checkMarkdownLinks() && ok
|
|
463
|
+
ok = checkVersionConsistency() && ok
|
|
464
|
+
ok = checkCliSmoke() && ok
|
|
465
|
+
ok = checkPrintConfigSmoke() && ok
|
|
466
|
+
ok = checkDoctorSmoke() && ok
|
|
467
|
+
ok = run("npm", ["run", "check"]) && ok
|
|
468
|
+
ok = run("npm", ["test"]) && ok
|
|
469
|
+
ok = checkPackageContents() && ok
|
|
470
|
+
ok = checkExtensionZipContents() && ok
|
|
471
|
+
|
|
472
|
+
if (!ok) process.exitCode = 1
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yunti-browser-runtime
|
|
3
|
+
description: Use when an agent needs to operate or inspect a user's browser through Yunti Browser Runtime MCP tools, including viewing pages, switching tabs, clicking, typing, screenshots, network or console logs, CDP commands, browser target inventory, or recovering stale browserSessionId routes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Yunti Browser Runtime
|
|
7
|
+
|
|
8
|
+
## First Step
|
|
9
|
+
|
|
10
|
+
For any browser task, first call `yunti_get_tool_usage_hints` unless the user is only asking a conceptual question.
|
|
11
|
+
|
|
12
|
+
Then call `yunti_list_browser_targets` to understand the live browser state before choosing a page or tab.
|
|
13
|
+
|
|
14
|
+
## Routing Rules
|
|
15
|
+
|
|
16
|
+
- Treat `yunti_list_browser_targets` as the canonical live browser inventory.
|
|
17
|
+
- Keep the returned `browserSessionId` for follow-up page and CDP calls.
|
|
18
|
+
- If a stored `browserSessionId` fails or appears stale, call `yunti_list_browser_targets` again and retry with the latest route.
|
|
19
|
+
- Stale-session errors include a reason and recovery hint; do not keep retrying the expired id.
|
|
20
|
+
- New tabs may return a new `browserSessionId`; use that returned value for follow-up actions on the new tab.
|
|
21
|
+
- Do not use raw `tabId` or `targetId` as a replacement for `browserSessionId`.
|
|
22
|
+
- Local standalone mode defaults to `userId=local`; do not add `userId` unless the environment explicitly needs a different route user.
|
|
23
|
+
|
|
24
|
+
## Tool Choice
|
|
25
|
+
|
|
26
|
+
- Use `yunti_get_page_snapshot` for lightweight page text, title, URL, selected text, and page state.
|
|
27
|
+
- Use `yunti_take_snapshot` before uid-based clicks, fills, or hovers.
|
|
28
|
+
- Use `yunti_click`, `yunti_fill`, `yunti_hover`, `yunti_press_key`, and `yunti_type_text` for normal page actions.
|
|
29
|
+
- Use `yunti_take_screenshot` for visual verification.
|
|
30
|
+
- Use `yunti_list_browser_targets` for tab counts, tab selection, target IDs, and whole-browser awareness.
|
|
31
|
+
- Use `yunti_cdp_send_command` for Chrome DevTools Protocol commands.
|
|
32
|
+
- Use `yunti_list_network_requests`, `yunti_get_network_log`, and `yunti_get_network_request` for network diagnostics.
|
|
33
|
+
- Use `yunti_list_console_messages` and `yunti_get_console_message` for console diagnostics.
|
|
34
|
+
- Use `yunti_get_tool_usage_hints` before retrying a failed or uncertain tool call.
|
|
35
|
+
|
|
36
|
+
## CDP Rules
|
|
37
|
+
|
|
38
|
+
- Never run CDP method names in a terminal or shell.
|
|
39
|
+
- Always call CDP through `yunti_cdp_send_command`.
|
|
40
|
+
- `yunti_cdp_send_command` requires `method`; `params` is optional but must be an object when provided.
|
|
41
|
+
- Required CDP shape:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"browserSessionId": "yunti-...",
|
|
46
|
+
"method": "Runtime.evaluate",
|
|
47
|
+
"params": {
|
|
48
|
+
"expression": "document.title",
|
|
49
|
+
"returnByValue": true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- For tab activation or closing, pass top-level `tabId` or `targetId` with a valid `browserSessionId`.
|
|
55
|
+
- For multi-step JavaScript work that depends on page-local state, prefer one `Runtime.evaluate` expression that performs setup, action, wait, and readback in one call.
|
|
56
|
+
|
|
57
|
+
## Parameter Rules
|
|
58
|
+
|
|
59
|
+
- `yunti_click` and `yunti_hover` need `uid`, `selector`, or both `x` and `y`; call `yunti_take_snapshot` first when possible.
|
|
60
|
+
- `yunti_fill` requires `value` plus `uid` or `selector`; coordinate-only fill is not supported.
|
|
61
|
+
- `yunti_close_page` accepts `browserSessionId`, not raw `tabId` or `targetId`; use `yunti_cdp_send_command` with `Target.closeTarget` for raw browser targets.
|
|
62
|
+
- `yunti_forget_learning_memory` needs a memory `id`, or `all=true` and `confirmed=true` for deleting everything.
|
|
63
|
+
|
|
64
|
+
## Safety
|
|
65
|
+
|
|
66
|
+
- Read-only inspection is allowed by default.
|
|
67
|
+
- Before submitting forms, deleting data, uploading sensitive files, approving workflows, making purchases, or changing production data, ask the user for explicit confirmation.
|
|
68
|
+
- Do not expose raw cookies, passwords, authorization headers, or token-like values.
|
|
69
|
+
- If a tool output appears to include sensitive data, summarize only the safe parts.
|
|
70
|
+
|
|
71
|
+
## Recovery
|
|
72
|
+
|
|
73
|
+
- No connected tab: ask the user to open a page, load the extension, or refresh the page.
|
|
74
|
+
- Stale session: call `yunti_list_browser_targets` and use the latest `browserSessionId`.
|
|
75
|
+
- Wrong tab: use `yunti_list_browser_targets` to find the intended tab, then route CDP with that tab's `tabId` or `targetId`.
|
|
76
|
+
- Parameter uncertainty: call `yunti_get_tool_usage_hints` with the specific tool name.
|
|
77
|
+
- Missing memory id: call `yunti_get_learning_memory` first, then retry with a returned `id`.
|