dsh-tabbit 0.2.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/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # tabbit-browser for DeepSeek Harness
2
+
3
+ **English** | [简体中文](README.zh-CN.md)
4
+
5
+ ![Tabbit Browser for DeepSeek Harness](tabbit-for-dsh.png?v=2)
6
+
7
+ A plugin for DeepSeek Harness (DSH) that gives the agent control over your Tabbit Browser: real pages, real login state, and real interactions, driven through `tabbit-cli` — the task-isolated Playwright CLI owned by the browser itself. Use it for web automation, information extraction, QA, and benchmarks.
8
+
9
+ ## What you get
10
+
11
+ | Component | Description |
12
+ | --------- | ----------- |
13
+ | `tabbit-browser` skill | The working guide for browser automation: persistent task spaces, locators and waits, screenshots, receipts and recovery. Discovered and loaded automatically with the plugin — no separate skill install. The model loads it via `skill({ name: "tabbit-browser" })` or `/tabbit-browser`. |
14
+ | `tabbit_browser_install` tool | Environment preflight: detects installed stable Tabbit editions, requires version `1.9.0` or newer, and verifies the `tabbit-cli` resident runtime. When Tabbit is missing or outdated, it starts a DSH background job that downloads the region-appropriate installer. |
15
+ | `tabbit_plugin_update` tool | Plugin update check: compares the installed plugin version with the published changelog at most once a day, silently skips offline failures, and records a version the user declined. When a newer release exists, the skill loads with an update notice showing what the new version added. |
16
+
17
+ ## Installation
18
+
19
+ ### 1. Check or install DeepSeek Harness
20
+
21
+ Check whether DSH is already installed:
22
+
23
+ ```sh
24
+ dsh --version
25
+ ```
26
+
27
+ If the command prints a version number, continue to the next step. If it is not found, install it for your operating system.
28
+
29
+ #### macOS
30
+
31
+ Install Node.js 20 or newer, then install DSH:
32
+
33
+ ```sh
34
+ brew install node
35
+ npm install -g @deepseek-ai/dsh
36
+ ```
37
+
38
+ #### Windows
39
+
40
+ Install Node.js LTS in PowerShell:
41
+
42
+ ```powershell
43
+ winget install OpenJS.NodeJS.LTS
44
+ ```
45
+
46
+ Reopen PowerShell after the installation, then install DSH:
47
+
48
+ ```powershell
49
+ npm install -g @deepseek-ai/dsh
50
+ ```
51
+
52
+ Run `dsh --version` again to confirm DSH works.
53
+
54
+ ### 2. Install the tabbit-browser plugin
55
+
56
+ ```sh
57
+ dsh plugin --profile web add github:Tabbit-Browser/dsh-plugin
58
+ ```
59
+
60
+ ### 3. Start DSH
61
+
62
+ ```sh
63
+ dsh web
64
+ ```
65
+
66
+ ## How it works
67
+
68
+ After installation, the bundle automatically registers its skill provider. The model loads the skill via `skill({ name: "tabbit-browser" })` or `/tabbit-browser`. Before the first browser operation in a task, the skill calls `tabbit_browser_install`:
69
+
70
+ - **`ready`** — a stable Tabbit edition at `1.9.0` or newer is installed and the runtime is running; the agent continues with the `tabbit-cli` workflow.
71
+ - **`restart-required`** — the installed version is sufficient, but the `tabbit-cli` runtime is not running; the user is asked to restart Tabbit Browser once.
72
+ - **`background`** — no stable edition is installed, or none reaches `1.9.0`; the tool starts a DSH background job that reads the operating system's configured region (macOS reads the system locale, Windows calls the system region API) and downloads the matching stable installer: the domestic build from `tabbit.com` for mainland China, or the international build from `tabbit.ai` for every other or unknown region. It selects the right Windows x64, macOS Apple Silicon, or macOS Intel package, saves it to the user's `Downloads` folder, reports download progress, and notifies the absolute installer path on completion.
73
+
74
+ The environment check also:
75
+
76
+ - Treats the runtime as available when multiple Tabbit instances are running; the agent sets `TABBIT_PLAYWRIGHT_INSTANCE` from the CLI's hint instead of reporting the instance ambiguity as an unavailable runtime.
77
+ - Diagnoses the DSH sandbox mode required to invoke the CLI on the current platform: Windows reports `cliSandboxMode: danger-full-access`, other platforms report `default`.
78
+ - Caches a successful environment check per agent session and re-checks only after a Runtime/launcher failure or an installation change, via `refresh: true`.
79
+
80
+ ## Requirements
81
+
82
+ - A stable Tabbit Browser at version `1.9.0` or newer: either the international `Tabbit` or the domestic `Tabbit Browser` — installing either one is enough. If it is missing or outdated, the plugin downloads the installer for you.
83
+ - The current DSH profile provides `ctx.skills`, `ctx.tools`, and `ctx.jobs` together with the corresponding model tools.
84
+ - `dsh-tool-jobs` provides background job control and completion notifications for the current agent.
85
+ - The current DSH profile provides a Bash/Shell tool running on the same host machine as Tabbit Browser.
86
+ - The shell's execution environment can reach the Browser-owned Runtime Service.
87
+ - On Windows, DSH's `read-only` and `workspace-write` restricted tokens cannot write to the Runtime named pipe. The skill first runs the normal `tabbit-cli tasks` connection probe and requests no permission at all when it succeeds. Only when the Browser, launcher, and Runtime processes are all detected but the connection returns `BROWSER_RUNTIME_UNAVAILABLE` does it ask the user to switch the current DSH session to Full Permission — and it then stops the task immediately, without retrying or continuing browser operations.
88
+
89
+ ## Notes and limitations
90
+
91
+ - Mainland China uses the domestic `tabbit.com` download source; all other regions use the international `tabbit.ai` source.
92
+ - The background download reports progress and notifies the absolute installer path when it finishes. It never opens the `.dmg`/`.exe` automatically.
93
+ - Development builds are not detected.
94
+ - The plugin does not provide native browser tools such as `tabbit_browser_evaluate`.
95
+ - If DSH's Bash runs in a sandbox such as E2B or a remote container that cannot access the local GUI browser, this skill cannot make Tabbit automation work there.
96
+
97
+ ## Development
98
+
99
+ ```sh
100
+ npm test
101
+ npm pack --dry-run
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,107 @@
1
+ # tabbit-browser for DeepSeek Harness
2
+
3
+ [English](README.md) | **简体中文**
4
+
5
+ ![Tabbit Browser for DeepSeek Harness](tabbit-for-dsh.png?v=2)
6
+
7
+ 这是一个为 DeepSeek Harness(DSH)打造的插件。安装后,DSH 中的 Agent 获得控制 Tabbit 浏览器的能力:通过 `tabbit-cli`——Tabbit 浏览器自带的、任务隔离的 Playwright CLI——操作真实网页、复用真实登录态,完成网页自动化、信息提取、QA 与基准测试等任务。
8
+
9
+ ## 插件内容
10
+
11
+ | 组件 | 说明 |
12
+ | ---- | ---- |
13
+ | `tabbit-browser` skill | 浏览器自动化工作指南:持久化任务空间、locator 与等待、截图、回执与恢复。随插件安装自动发现和加载,无需单独安装。模型通过 `skill({ name: "tabbit-browser" })` 或 `/tabbit-browser` 加载。 |
14
+ | `tabbit_browser_install` 工具 | 环境预检:检测已安装的正式版 Tabbit、要求版本 ≥ `1.9.0`、检查 `tabbit-cli` 常驻运行时;未安装或版本过低时,创建 DSH 后台任务按地区下载对应安装包。 |
15
+ | `tabbit_plugin_update` 工具 | 插件更新检查:每天至多一次对比本地插件版本与仓库发布的 CHANGELOG,离线失败时静默跳过,并可记录用户已拒绝的版本。存在新版本时,skill 会附带更新提示加载,展示新版本的新增功能。 |
16
+
17
+ ## 安装
18
+
19
+ ### 1. 检查并安装 DeepSeek Harness
20
+
21
+ 先检查本地是否已经安装 DSH:
22
+
23
+ ```sh
24
+ dsh --version
25
+ ```
26
+
27
+ 如果命令能够正常输出版本号,直接进入下一步。如果提示找不到命令,请根据操作系统安装。
28
+
29
+ #### macOS
30
+
31
+ 安装 Node.js 20 或更高版本,然后安装 DSH:
32
+
33
+ ```sh
34
+ brew install node
35
+ npm install -g @deepseek-ai/dsh
36
+ ```
37
+
38
+ #### Windows
39
+
40
+ 在 PowerShell 中安装 Node.js LTS:
41
+
42
+ ```powershell
43
+ winget install OpenJS.NodeJS.LTS
44
+ ```
45
+
46
+ 安装完成后重新打开 PowerShell,再安装 DSH:
47
+
48
+ ```powershell
49
+ npm install -g @deepseek-ai/dsh
50
+ ```
51
+
52
+ 安装后再次运行 `dsh --version`,确认 DSH 可以正常使用。
53
+
54
+ ### 2. 安装 tabbit-browser 插件
55
+
56
+ ```sh
57
+ dsh plugin --profile web add github:Tabbit-Browser/dsh-plugin
58
+ ```
59
+
60
+ ### 3. 启动 DSH
61
+
62
+ ```sh
63
+ dsh web
64
+ ```
65
+
66
+ ## 工作原理
67
+
68
+ 安装插件后,bundle 会自动加载 Skill Provider,模型可通过 `skill({ name: "tabbit-browser" })` 或 `/tabbit-browser` 加载说明。在任务中的第一次浏览器操作之前,skill 会先调用 `tabbit_browser_install` 做环境预检:
69
+
70
+ - **`ready`** — 已安装 `1.9.0` 或更高版本的正式版 Tabbit 且运行时正在运行,Agent 继续通过 `tabbit-cli` 操作浏览器。
71
+ - **`restart-required`** — 已安装的版本达标,但 `tabbit-cli` 常驻运行时未运行,提示用户重启一次 Tabbit 浏览器。
72
+ - **`background`** — 未安装任何正式版,或版本低于 `1.9.0`;工具会创建 DSH 后台任务,读取系统地区(macOS 读取系统地区,Windows 调用系统地区 API),中国大陆下载国内正式版安装包,其他地区或无法识别地区时下载国际正式版安装包;自动选择对应的 Windows x64、macOS Apple Silicon 或 macOS Intel 包,保存到用户的 `Downloads` 目录,下载过程会输出进度,完成后 DSH 会通知安装包的绝对路径。
73
+
74
+ 环境检查还会:
75
+
76
+ - 多个 Tabbit 实例同时运行时,仍判定 Runtime 可用;模型根据 CLI 提示设置 `TABBIT_PLAYWRIGHT_INSTANCE`,不会把实例选择歧义误报为 Runtime 未运行。
77
+ - 诊断当前平台调用 CLI 所需的 DSH sandbox mode;Windows 返回 `cliSandboxMode: danger-full-access`,其他平台返回 `default`。
78
+ - 按 Agent session 缓存成功的环境检查;仅在 Runtime/launcher 失败或安装变化后通过 `refresh: true` 主动失效并重新检查。
79
+
80
+ ## 前提
81
+
82
+ - 需要 `1.9.0` 或更高版本的正式版 Tabbit 浏览器。国际版 `Tabbit` 和国内版 `Tabbit Browser` 均支持,安装任意一个即可;如果未安装或版本过低,插件会自动下载对应安装包。
83
+ - 当前 DSH profile 已提供 `ctx.skills`、`ctx.tools`、`ctx.jobs` 以及对应模型工具。
84
+ - `dsh-tool-jobs` 已为当前 Agent 提供后台任务控制和完成通知。
85
+ - 当前 DSH profile 已提供运行在 Tabbit Browser 所在宿主机的 Bash/Shell 工具。
86
+ - Shell 的执行环境可以访问 Browser-owned Runtime Service。
87
+ - Windows 上 DSH 的 `read-only` 与 `workspace-write` 限制令牌无法写入 Runtime 命名管道。Skill 先正常执行 `tabbit-cli tasks` 连接探测;成功时完全不询问权限。仅当 Browser、launcher 和 Runtime 进程均已检测到但连接返回 `BROWSER_RUNTIME_UNAVAILABLE` 时,才要求用户把当前 DSH 会话切换到 Full Permission,并立即停止当前任务,不重试或继续浏览器操作。
88
+
89
+ ## 行为说明与限制
90
+
91
+ - 中国大陆使用 `tabbit.com` 国内版下载源,其他地区使用 `tabbit.ai` 国际版下载源。
92
+ - 后台下载会输出进度,完成后通知安装包的绝对路径,但不会自动打开 `.dmg` 或 `.exe`。
93
+ - 不会检测开发版。
94
+ - 不提供 `tabbit_browser_evaluate` 等原生浏览器工具。
95
+
96
+ 如果 DSH 的 Bash 运行在 E2B、远程容器或无法访问本机 GUI Browser 的沙箱中,本 Skill 不会使 Tabbit 自动化变得可用。
97
+
98
+ ## 开发验证
99
+
100
+ ```sh
101
+ npm test
102
+ npm pack --dry-run
103
+ ```
104
+
105
+ ## 许可证
106
+
107
+ MIT
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: skill-tabbit-browser
3
+ name: tabbit-browser
package/index.js ADDED
@@ -0,0 +1,352 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { createDownloadJob, detectTabbit } from './installer.js'
4
+ import { checkPluginUpdate, dismissUpdate } from './update-check.js'
5
+
6
+ const PROVIDER_NAME = 'tabbit-browser-bundled-skill'
7
+ const SKILL_NAME = 'tabbit-browser'
8
+ const SKILL_DESCRIPTION = 'Control the user\'s Tabbit Browser through its Browser-owned, task-isolated Playwright CLI and runtime helpers. Use for Tabbit browser automation, website interaction, extraction, QA, and benchmarks, including stable-browser version checks, runtime-process checks, and background installer download when Tabbit is absent or outdated; never silently fall back to another browser automation backend.'
9
+ const SKILL_URL = new URL('./skills/tabbit-browser/SKILL.md', import.meta.url)
10
+ const RESOURCE_BASE = {
11
+ kind: 'directory',
12
+ path: fileURLToPath(new URL('./skills/tabbit-browser/', import.meta.url)),
13
+ }
14
+ const INVOCATION = { modelInvocable: true, userInvocable: true }
15
+
16
+ // DSH reserves rank 600 for skills shipped inside a package.
17
+ const BUNDLED_SKILL_RANK = 600
18
+
19
+ const candidate = {
20
+ name: SKILL_NAME,
21
+ description: SKILL_DESCRIPTION,
22
+ invocation: INVOCATION,
23
+ provider: PROVIDER_NAME,
24
+ source: 'bundled',
25
+ resourceBase: RESOURCE_BASE,
26
+ rank: BUNDLED_SKILL_RANK,
27
+ locator: SKILL_URL,
28
+ }
29
+
30
+ export function formatUpdateNotice({ currentVersion, latestVersion, changelog }) {
31
+ return [
32
+ `> **Plugin update available**: tabbit-browser ${latestVersion} (installed ${currentVersion}).`,
33
+ changelog ? `> New in ${latestVersion}: ${changelog}` : null,
34
+ '> Show the offered version and these changes to the user, then ask whether to update now.',
35
+ '> If they agree, tell them to rerun the install command below over the current install and restart the DSH session afterwards:',
36
+ '> ```bash',
37
+ '> dsh plugin --profile web add github:Tabbit-Browser/dsh-plugin',
38
+ '> ```',
39
+ `> If they decline, call \`tabbit_plugin_update\` with \`dismiss: "${latestVersion}"\`, then continue the task.`,
40
+ ]
41
+ .filter(Boolean)
42
+ .join('\n')
43
+ }
44
+
45
+ async function prependUpdateNotice(content, checkUpdate) {
46
+ let update
47
+ try {
48
+ update = await checkUpdate()
49
+ } catch {
50
+ return content
51
+ }
52
+ if (update?.status !== 'update-available') return content
53
+ return `${formatUpdateNotice(update)}\n\n${content}`
54
+ }
55
+
56
+ export function createSkillProvider({ checkUpdate = checkPluginUpdate } = {}) {
57
+ return {
58
+ name: PROVIDER_NAME,
59
+ list: () => Promise.resolve([candidate]),
60
+ async get(selected) {
61
+ if (selected.name !== SKILL_NAME) return undefined
62
+ const source = await readFile(SKILL_URL, 'utf8')
63
+ return {
64
+ name: SKILL_NAME,
65
+ description: SKILL_DESCRIPTION,
66
+ invocation: INVOCATION,
67
+ provider: PROVIDER_NAME,
68
+ source: 'bundled',
69
+ resourceBase: RESOURCE_BASE,
70
+ path: fileURLToPath(SKILL_URL),
71
+ content: await prependUpdateNotice(stripFrontmatter(source), checkUpdate),
72
+ }
73
+ },
74
+ }
75
+ }
76
+
77
+ function stripFrontmatter(source) {
78
+ if (!source.startsWith('---\n')) return source
79
+ const end = source.indexOf('\n---\n', 4)
80
+ return end === -1 ? source : source.slice(end + 5)
81
+ }
82
+
83
+ export const name = 'tabbit-browser'
84
+ export const inject = ['skills', 'tools', 'jobs']
85
+
86
+ export function describeCliSandbox(platform = process.platform) {
87
+ if (platform === 'win32') {
88
+ return {
89
+ cliSandboxMode: 'default',
90
+ cliSandboxReason: 'Invoke tabbit-cli normally. Only if its Runtime connection probe returns BROWSER_RUNTIME_UNAVAILABLE while Tabbit Browser and the Runtime process are detected, ask the user to change the current DSH session to Full Permission and stop the task.',
91
+ }
92
+ }
93
+ return {
94
+ cliSandboxMode: 'default',
95
+ cliSandboxReason: 'The default DSH sandbox mode can invoke tabbit-cli on this platform.',
96
+ }
97
+ }
98
+
99
+ function withCliSandboxGuidance(message, platform) {
100
+ const diagnosis = describeCliSandbox(platform)
101
+ return {
102
+ ...diagnosis,
103
+ message,
104
+ }
105
+ }
106
+
107
+ export function apply(ctx, options = {}) {
108
+ ctx.skills.registerProvider(() => createSkillProvider(options))
109
+ registerInstallerTool(ctx)
110
+ registerUpdateTool(ctx, options)
111
+ }
112
+
113
+ function messageForUpdate(update) {
114
+ if (update.status === 'update-available') {
115
+ const changes = update.changelog || 'see the release notes'
116
+ return `tabbit-browser ${update.latestVersion} is available (installed ${update.currentVersion}). New in this version: ${changes}. Ask the user whether to update now.`
117
+ }
118
+ if (update.status === 'current') {
119
+ return `The tabbit-browser plugin is up to date (${update.currentVersion}).`
120
+ }
121
+ return 'Could not determine the latest tabbit-browser plugin version. The check stays silent for a day before retrying.'
122
+ }
123
+
124
+ export function registerUpdateTool(ctx, {
125
+ checkUpdate = checkPluginUpdate,
126
+ dismiss = dismissUpdate,
127
+ } = {}) {
128
+ ctx.tools.register({
129
+ name: 'tabbit_plugin_update',
130
+ description: 'Record that the user declined an offered tabbit-browser plugin version, or force a recheck of the published changelog. The skill already loads an update notice automatically when a newer version exists; call this tool only after the user declines an offered version, or after a plugin update or connectivity change.',
131
+ parameters: {
132
+ type: 'object',
133
+ properties: {
134
+ dismiss: {
135
+ type: 'string',
136
+ description: 'The offered version the user declined. The skill stops announcing this version; a newer release is announced again.',
137
+ },
138
+ refresh: {
139
+ type: 'boolean',
140
+ description: 'Skip the daily cache and the failure backoff and check the latest release again.',
141
+ },
142
+ },
143
+ additionalProperties: false,
144
+ },
145
+ output: {
146
+ schema: {
147
+ type: 'object',
148
+ properties: {
149
+ status: {
150
+ type: 'string',
151
+ enum: ['current', 'update-available', 'unknown', 'dismissed'],
152
+ },
153
+ message: { type: 'string' },
154
+ currentVersion: { type: 'string' },
155
+ latestVersion: { type: 'string' },
156
+ changelog: { type: 'string' },
157
+ dismissedVersion: { type: 'string' },
158
+ },
159
+ required: ['status', 'message'],
160
+ additionalProperties: false,
161
+ },
162
+ render: (_args, value) => [{ type: 'text', text: value.message }],
163
+ },
164
+ isConcurrencySafe: () => true,
165
+ async execute(args = {}) {
166
+ if (args.dismiss) {
167
+ await dismiss(args.dismiss)
168
+ return {
169
+ status: 'dismissed',
170
+ message: `Recorded that the user declined tabbit-browser ${args.dismiss}. The skill will stop announcing this version; a newer release will be announced again.`,
171
+ dismissedVersion: args.dismiss,
172
+ }
173
+ }
174
+ const update = await checkUpdate({ force: args.refresh === true })
175
+ return {
176
+ status: update.status,
177
+ message: messageForUpdate(update),
178
+ currentVersion: update.currentVersion,
179
+ ...(update.latestVersion ? { latestVersion: update.latestVersion } : {}),
180
+ ...(update.changelog ? { changelog: update.changelog } : {}),
181
+ }
182
+ },
183
+ })
184
+ }
185
+
186
+ export function registerInstallerTool(ctx, {
187
+ detect = detectTabbit,
188
+ hostPlatform = process.platform,
189
+ } = {}) {
190
+ const activeJobs = new WeakMap()
191
+ const readyByOwner = new WeakMap()
192
+
193
+ ctx.tools.register({
194
+ name: 'tabbit_browser_install',
195
+ description: 'Check stable Tabbit editions, require version 1.9.0 or newer, verify the tabbit-cli launcher and Runtime process, and diagnose how to perform the session-scoped CLI connection probe. A successful detection result is cached for the calling agent session; set refresh only after a Runtime/launcher failure or installation change. Download the region-appropriate installer in the background when Tabbit is missing or outdated; otherwise report when the browser must be restarted once.',
196
+ parameters: {
197
+ type: 'object',
198
+ properties: {
199
+ refresh: {
200
+ type: 'boolean',
201
+ description: 'Discard this agent session\'s cached ready result and run every environment check again. Use only after a Runtime/launcher failure or installation change.',
202
+ },
203
+ },
204
+ additionalProperties: false,
205
+ },
206
+ output: {
207
+ schema: {
208
+ type: 'object',
209
+ properties: {
210
+ status: {
211
+ type: 'string',
212
+ enum: ['ready', 'restart-required', 'background'],
213
+ },
214
+ message: { type: 'string' },
215
+ cached: { type: 'boolean' },
216
+ jobId: { type: 'string' },
217
+ cliReady: { type: 'boolean' },
218
+ cliSandboxMode: {
219
+ type: 'string',
220
+ enum: ['default', 'danger-full-access'],
221
+ },
222
+ cliSandboxReason: { type: 'string' },
223
+ minimumVersion: { type: 'string' },
224
+ playwrightProcessRunning: { type: 'boolean' },
225
+ playwrightInstanceCount: { type: 'integer' },
226
+ playwrightRuntimeAmbiguous: { type: 'boolean' },
227
+ installations: {
228
+ type: 'array',
229
+ items: {
230
+ type: 'object',
231
+ properties: {
232
+ name: { type: 'string' },
233
+ edition: { type: 'string', enum: ['international', 'domestic'] },
234
+ channel: { type: 'string', const: 'stable' },
235
+ path: { type: 'string' },
236
+ executable: { type: 'string' },
237
+ version: { type: 'string' },
238
+ bundleId: { type: 'string' },
239
+ registryKey: { type: 'string' },
240
+ },
241
+ required: ['name', 'edition', 'channel'],
242
+ additionalProperties: false,
243
+ },
244
+ },
245
+ },
246
+ required: ['status', 'message', 'cached', 'cliSandboxMode', 'cliSandboxReason'],
247
+ additionalProperties: false,
248
+ },
249
+ render: (_args, value) => [{ type: 'text', text: value.message }],
250
+ },
251
+ isConcurrencySafe: () => true,
252
+ async execute(args = {}, exec) {
253
+ const owner = exec.agent
254
+ if (owner && args.refresh === true) readyByOwner.delete(owner)
255
+ if (owner && args.refresh !== true) {
256
+ const cached = readyByOwner.get(owner)
257
+ if (cached) {
258
+ return {
259
+ ...cached,
260
+ cached: true,
261
+ message: 'Browser installation and Runtime-process detection reused this session\'s result. Run the normal tabbit-cli tasks connection probe to finish the environment check.',
262
+ }
263
+ }
264
+ }
265
+ const existingJobId = owner ? activeJobs.get(owner) : undefined
266
+ if (existingJobId) {
267
+ try {
268
+ const snapshot = ctx.jobs.get(existingJobId, owner)
269
+ if (snapshot.status === 'running' || snapshot.status === 'stopping') {
270
+ return {
271
+ status: 'background',
272
+ ...withCliSandboxGuidance(`Environment check failed: a Tabbit installer download is already running as ${existingJobId}.`, hostPlatform),
273
+ cached: false,
274
+ jobId: String(existingJobId),
275
+ }
276
+ }
277
+ } catch {
278
+ activeJobs.delete(owner)
279
+ }
280
+ }
281
+
282
+ const detected = await detect()
283
+ if (detected.recommendation === 'ready') {
284
+ const versions = detected.supportedInstallations
285
+ .map(item => `${item.name} ${item.version}`)
286
+ .join(', ')
287
+ const instanceNote = detected.playwrightRuntimeAmbiguous
288
+ ? ` Multiple Tabbit instances are running (${detected.playwrightInstanceCount}); set TABBIT_PLAYWRIGHT_INSTANCE before invoking tabbit-cli.`
289
+ : ''
290
+ const result = {
291
+ status: 'ready',
292
+ ...withCliSandboxGuidance(`Browser installation and Runtime-process detection passed${versions ? ` (${versions})` : ''}.${instanceNote} Run the normal tabbit-cli tasks connection probe to finish the environment check.`, detected.platform),
293
+ cached: false,
294
+ cliReady: detected.cliReady,
295
+ minimumVersion: detected.minimumVersion,
296
+ playwrightProcessRunning: detected.playwrightProcessRunning,
297
+ playwrightInstanceCount: detected.playwrightInstanceCount,
298
+ playwrightRuntimeAmbiguous: detected.playwrightRuntimeAmbiguous,
299
+ installations: detected.installations,
300
+ }
301
+ if (owner) readyByOwner.set(owner, result)
302
+ return result
303
+ }
304
+ if (detected.recommendation === 'restart-required') {
305
+ const versions = detected.supportedInstallations
306
+ .map(item => `${item.name} ${item.version}`)
307
+ .join(', ')
308
+ return {
309
+ status: 'restart-required',
310
+ ...withCliSandboxGuidance(`Environment check failed: ${versions} meets the minimum version ${detected.minimumVersion}, but the tabbit-cli Runtime is not running. Please restart Tabbit Browser once before using browser automation.`, detected.platform),
311
+ cached: false,
312
+ cliReady: detected.cliReady,
313
+ minimumVersion: detected.minimumVersion,
314
+ playwrightProcessRunning: false,
315
+ playwrightInstanceCount: detected.playwrightInstanceCount,
316
+ playwrightRuntimeAmbiguous: detected.playwrightRuntimeAmbiguous,
317
+ installations: detected.installations,
318
+ }
319
+ }
320
+
321
+ const downloadReason = detected.installations.length === 0
322
+ ? 'No stable Tabbit edition is installed.'
323
+ : `Installed stable Tabbit version(s) do not meet the minimum ${detected.minimumVersion}: ${detected.installations.map(item => `${item.name} ${item.version ?? 'unknown'}`).join(', ')}.`
324
+
325
+ let jobId
326
+ jobId = ctx.jobs.start({
327
+ kind: 'tabbit-installer',
328
+ label: 'Download the region-appropriate Tabbit installer',
329
+ outputLimitBytes: 16 * 1024,
330
+ owner,
331
+ run: () => createDownloadJob({
332
+ onSettled: () => {
333
+ if (owner && activeJobs.get(owner) === jobId) activeJobs.delete(owner)
334
+ },
335
+ }),
336
+ })
337
+ if (owner) activeJobs.set(owner, jobId)
338
+ return {
339
+ status: 'background',
340
+ ...withCliSandboxGuidance(`Environment check failed: ${downloadReason} Started the region-appropriate Tabbit installer download as ${jobId}. DSH will report progress and notify you when the installer is ready.`, detected.platform),
341
+ cached: false,
342
+ jobId: String(jobId),
343
+ cliReady: detected.cliReady,
344
+ minimumVersion: detected.minimumVersion,
345
+ playwrightProcessRunning: false,
346
+ playwrightInstanceCount: detected.playwrightInstanceCount,
347
+ playwrightRuntimeAmbiguous: detected.playwrightRuntimeAmbiguous,
348
+ installations: detected.installations,
349
+ }
350
+ },
351
+ })
352
+ }