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
@@ -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`.