diff --git a/README.md b/README.md index 414b9e5..1bd3846 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,9 @@ Model availability, pricing, and policy behavior vary by version and region. Whi WorkBuddy has a separate [MCP + Skill + Hooks connector](./workbuddy-plugin/README.md#install-on-macos). It uses WorkBuddy's current visual model to operate Android phones and opens read-only scrcpy windows by default. No DSH installation, full OpenGUI backend, or extra model API key is required. -Version `0.2.0` is a local candidate in [PR #95](https://github.com/Core-Mate/OpenGUI/pull/95), not a published Release or marketplace installation. Start with macOS and WorkBuddy 5.5.3; Windows/Linux package checks do not establish phone-control support. Follow the [macOS installation steps](./workbuddy-plugin/README.md#install-on-macos) to build the candidate and install its MCP, `opengui` Skill and lifecycle Hooks together. The installer preserves other plugins and backs up the affected WorkBuddy configuration. +Version `0.3.1` is a macOS public-testing prerelease, not marketplace approval or complete real-device acceptance. It installs the MCP, `opengui` Skill and lifecycle Hooks together, preserves other plugins, and backs up the affected WorkBuddy configuration. WorkBuddy 5.5.6+ supports the live installation flow; older compatible versions use the Command-Q fallback reported by preflight. -After restarting WorkBuddy, enable/trust the `opengui` MCP if prompted, connect a USB-debugging-authorized Android phone, select `/opengui`, and send: +After installation, enable/trust the `opengui` MCP if prompted, review the external Hook change in `/hooks`, confirm `opengui` in `/skills`, connect a USB-debugging-authorized Android phone, select `/opengui`, and send: ```text Open Settings and report the Android version on my phone. diff --git a/README.zh-CN.md b/README.zh-CN.md index 73091d9..d31b3c8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -84,9 +84,9 @@ OpenGUI 正式支持 DSH `0.1.0-rc.7`、`0.1.0-rc.8`、`0.1.1-rc.1` 和 `0.1.1-r WorkBuddy 使用独立的 [MCP + Skill + Hooks 插件](./workbuddy-plugin/README.zh-CN.md#macos-安装),由当前视觉模型看图操作 Android 手机,默认打开只读 scrcpy 投屏窗口。不需要安装 DSH、部署完整 OpenGUI 后端或额外配置模型 API Key。 -当前 `0.2.0` 是 [PR #95](https://github.com/Core-Mate/OpenGUI/pull/95) 中的本地候选版,尚未正式发布或上架市场。安装以 macOS、WorkBuddy 5.5.3 为基线,Windows/Linux 打包检查通过不代表支持真机操作。按照 [macOS 安装步骤](./workbuddy-plugin/README.zh-CN.md#macos-安装),将 MCP、`opengui` 技能和生命周期 Hooks 一起安装。安装器会备份相关 WorkBuddy 配置,保留其他插件。 +当前 `0.3.1` 是 macOS 公测预发布版,不代表已经上架市场或完成全部真机验收。安装会把 MCP、`opengui` Skill 和生命周期 Hooks 一起配置,备份相关 WorkBuddy 配置并保留其他插件。WorkBuddy 5.5.6 及以上支持运行中安装;旧版兼容宿主会由预检明确要求 Command-Q。 -重开 WorkBuddy,按宿主提示启用并信任 `opengui` MCP,连接已授权 USB 调试的 Android 手机。在输入框选择 `/opengui`,发送: +安装后按宿主提示启用并信任 `opengui` MCP,在 `/hooks` 中审查外部 Hook 变更,并在 `/skills` 中确认 `opengui`。连接已授权 USB 调试的 Android 手机,在输入框选择 `/opengui`,发送: ```text 打开手机设置,查看并告诉我 Android 版本。 diff --git a/skills/opengui-plugin-install/SKILL.md b/skills/opengui-plugin-install/SKILL.md index a887d75..90b4ae4 100644 --- a/skills/opengui-plugin-install/SKILL.md +++ b/skills/opengui-plugin-install/SKILL.md @@ -9,9 +9,9 @@ Infer Codex or WorkBuddy from the request; ask only if the target is missing. Th 1. Read the public GitHub releases API for `Core-Mate/OpenGUI`, following pagination. Select the newest non-draft, non-prerelease semantic version with tag `opengui-codex-vX.Y.Z` or `opengui-workbuddy-vX.Y.Z` and all matching assets below. If the user explicitly asks for the public testing/prerelease version, include non-draft prereleases for that host and explain their uncompleted device acceptance. An explicit requested version takes precedence; never silently substitute a different version. Do not use the repository-wide latest release, which may be DSH or an APK. 2. Download `opengui-HOST-X.Y.Z-install.command` and its `.sha256` asset from that release into a fresh temporary directory. Use HTTPS with redirect-to-HTTPS only. Require exact filenames and verify SHA-256 before executing the installer. The checksum establishes integrity relative to the selected public release, not an independent publisher signature. -3. Finish existing OpenGUI tasks before an upgrade. WorkBuddy must be closed before its configuration can be changed; do not kill it or its phone/mirror processes. Codex requires the native CLI with `codex plugin` support. Never remove a conflicting plugin source without the user's authorization. -4. For WorkBuddy, run the selected installer with `--check` first. Read its exact error code; do not inspect the entire application or rewrite configuration manually. Multiple applications require an explicit `--app` selection. If installing from within WorkBuddy itself, prepare the verified installer and give the user the single terminal command to run after Command-Q; do not repeatedly retry while the host is running. Normal installation uses the installer, not source checkout or agent-written JSON. Run `bash /absolute/path/opengui-HOST-X.Y.Z-install.command`. It downloads and verifies the matching package, prepares private Node 22.23.2, installs into a fresh version directory, and configures only the selected host. No Git clone, pnpm, source compilation, or user-run test suite is required. -5. Read the result. On success, ask for a new Codex chat or a WorkBuddy restart, then verify read-only device discovery. USB and system permissions remain user actions. Installation success does not prove phone control, desktop visibility, or two-device acceptance. +3. Finish existing OpenGUI phone tasks before an upgrade. WorkBuddy 5.5.6 or newer may stay open; never kill it or its phone/mirror processes. Older compatible WorkBuddy versions require a normal Command-Q restart when the installer reports `HOST_RESTART_REQUIRED`. Codex requires the native CLI with `codex plugin` support. Never remove a conflicting plugin source without the user's authorization. +4. For WorkBuddy, run the selected installer with `--check` first. Read its exact error code; do not inspect the entire application or rewrite configuration manually. Multiple applications require an explicit `--app` selection. When invoked inside WorkBuddy 5.5.6+, run `bash /absolute/path/opengui-HOST-X.Y.Z-install.command` directly in the current task after the preflight passes. It downloads and verifies the matching package, prepares private Node 22.23.2, installs into a fresh version directory, and atomically configures only the selected host. If an upgrade reports `upgrade_blocked`, finish and close the old OpenGUI task/viewer/mirror, disable OpenGUI in MCP service management, wait for its broker to exit, then retry once. No Git clone, pnpm, source compilation, agent-written JSON, or user-run test suite is required. +5. Read the result. After `LIVE_CONFIG_WRITTEN`, keep WorkBuddy open: trust/enable the OpenGUI MCP if prompted, open `/hooks` to review and apply the external Hook change, and open `/skills` to confirm `opengui`. Start a new task only if the current task does not refresh. After a stopped-host installation, open WorkBuddy and perform the same checks. Verify read-only device discovery before phone actions. USB and system permissions remain user actions. Installation success does not prove phone control, desktop visibility, or two-device acceptance. Required release assets: - Codex: `opengui-codex-X.Y.Z.tar.gz`, its `.sha256`, and the installer plus its `.sha256`. diff --git a/workbuddy-plugin/CHANGELOG.md b/workbuddy-plugin/CHANGELOG.md index 5547abb..7b19edf 100644 --- a/workbuddy-plugin/CHANGELOG.md +++ b/workbuddy-plugin/CHANGELOG.md @@ -1,3 +1,10 @@ +# OpenGUI for WorkBuddy 0.3.1 candidate + +- Allow WorkBuddy 5.5.6+ to stay open during a verified first installation or idle reconfiguration. +- Use WorkBuddy's MCP file watcher, `/hooks` review flow, and `/skills` discovery instead of requiring Command-Q by default. +- Keep older hosts on the restart path and block upgrades while an old OpenGUI broker still owns tasks or displays. +- Preserve atomic configuration writes, unrelated MCP/Hooks, immutable packages, receipts, and rollback behavior. + # OpenGUI for WorkBuddy 0.3.0 candidate - Add read-only H.264 video Viewers, visible first-frame gating, bounded reconnection and independent page/control lifecycles. diff --git a/workbuddy-plugin/README.md b/workbuddy-plugin/README.md index d9ef572..969f222 100644 --- a/workbuddy-plugin/README.md +++ b/workbuddy-plugin/README.md @@ -1,6 +1,6 @@ -# OpenGUI for WorkBuddy 0.3.0 +# OpenGUI for WorkBuddy 0.3.1 -[中文说明](README.zh-CN.md). macOS candidate, protocol 8. No public release or directory approval is implied by these files. +[中文说明](README.zh-CN.md). macOS public-testing candidate, protocol 8. Marketplace approval and the remaining real-device gates are separate. OpenGUI opens real-time phone video beside the current chat using WorkBuddy built-in `present_files` with the Viewer URL and current working directory. A visible decoded H.264 frame must reach the backend before the first phone observation or action. An opening request or screenshot preview is insufficient. Video is read-only and local; model observations still use explicit screenshots. @@ -18,12 +18,14 @@ After first display authorization, video failure or page closure does not cancel Use the supplied installer and matching archive with adjacent SHA-256 sidecars: ```sh -bash scripts/install-macos.command --archive /absolute/path/opengui-mcp-0.3.0.tgz +bash scripts/install-macos.command --archive /absolute/path/opengui-mcp-0.3.1.tgz ``` -The installer prepares private Node, verifies the package and scrcpy resources, and only then changes this host's configuration. Complete old phone tasks and close old displays before upgrading. No migration force-kills an old runtime. Repeated installation reuses verified caches; download failure reports its stage and leaves previous configuration available. Keep the old installer/archive and recovery record to reinstall the old version. The installer reports configuration, host loading and real-viewer acceptance separately. +The installer prepares private Node, verifies the package and scrcpy resources, and only then changes this host's configuration. On WorkBuddy 5.5.6+, the application may stay open: MCP configuration is watched live, while external Hook changes are reviewed in `/hooks` and the installed Skill is confirmed in `/skills`. Start a new task only if the current task does not refresh. Older compatible hosts use the Command-Q fallback reported by preflight. -WorkBuddy 5.5.3 domestic and overseas configuration discovery, running-host preflight, per-configuration receipts, and native Hooks are retained. Use `--check` or `--app /absolute/WorkBuddy.app` for explicit preflight. Legacy `opengui_start` and native mirror tools are compatibility-only, on explicit request. They never substitute for browser first-frame authorization. +Complete old phone tasks and close old displays before upgrading. If an old broker remains, disable the old OpenGUI MCP and wait for its idle exit before retrying. No migration force-kills an old runtime. Repeated installation reuses verified caches; download failure reports its stage and leaves previous configuration available. Keep the old installer/archive and recovery record to reinstall the old version. The installer reports configuration, host loading and real-viewer acceptance separately. + +WorkBuddy 5.5.3 domestic and overseas configuration discovery, version-aware running-host preflight, per-configuration receipts, and native Hooks are retained. Use `--check` or `--app /absolute/WorkBuddy.app` for explicit preflight. Legacy `opengui_start` and native mirror tools are compatibility-only, on explicit request. They never substitute for browser first-frame authorization. ## Runtime and privacy diff --git a/workbuddy-plugin/README.zh-CN.md b/workbuddy-plugin/README.zh-CN.md index 36ceb63..27417ac 100644 --- a/workbuddy-plugin/README.zh-CN.md +++ b/workbuddy-plugin/README.zh-CN.md @@ -1,4 +1,4 @@ -# OpenGUI for WorkBuddy 0.3.0 候选版 +# OpenGUI for WorkBuddy 0.3.1 候选版 本轮支持 macOS,协议版本 8。这是本地候选交付,不代表已公开发布或所有真机验收通过。 @@ -15,12 +15,14 @@ WorkBuddy 使用内置 present_files,传入 URL 与当前工作目录。国内 核对安装器及归档旁的 SHA-256 文件,结束旧任务、关闭旧展示后运行: ```sh -bash scripts/install-macos.command --archive /绝对路径/opengui-mcp-0.3.0.tgz +bash scripts/install-macos.command --archive /绝对路径/opengui-mcp-0.3.1.tgz ``` -安装器自动准备独立 Node 与 scrcpy 资源,缓存完整时复用;准备失败保留旧配置,并输出恢复步骤。WorkBuddy 运行时会先要求退出一次;可用 --check 做只读预检,用 --app 指定国内或海外应用。 +安装器自动准备独立 Node 与 scrcpy 资源,缓存完整时复用;准备失败保留旧配置,并输出恢复步骤。WorkBuddy 5.5.6 及以上可以保持运行:MCP 配置由宿主实时监听,外部 Hook 变更在 `/hooks` 中审查应用,并在 `/skills` 中确认 `opengui`。当前任务没有刷新时再新建任务。旧版兼容宿主会由预检明确要求 Command-Q。可用 --check 做只读预检,用 --app 指定国内或海外应用。 -安装结果分别报告“配置完成”“宿主已加载”“设备墙可用”,写入配置不是验收通过。安装后在实际宿主选择 OpenGUI Skill,先检查只读设备发现,再验收右侧视频与截图操作。 +升级前仍需结束旧手机任务并关闭旧展示。如果旧 broker 仍在运行,先在 MCP 服务管理中停用旧 OpenGUI,等待其空闲退出后重试;安装器不会强杀 WorkBuddy 或手机进程。 + +安装结果分别报告“热配置完成”“宿主已加载”“设备墙可用”,写入配置不是验收通过。安装后在实际宿主选择 OpenGUI Skill,先检查只读设备发现,再验收右侧视频与截图操作。 ## 回退和验收 diff --git a/workbuddy-plugin/connector/connector-meta.json b/workbuddy-plugin/connector/connector-meta.json index f712905..e88134a 100644 --- a/workbuddy-plugin/connector/connector-meta.json +++ b/workbuddy-plugin/connector/connector-meta.json @@ -7,7 +7,7 @@ "description_en": "Control locally connected Android phones from WorkBuddy and monitor up to four phones in a read-only device wall.", "source": "opengui", "type": "mcp", - "version": "0.3.0", + "version": "0.3.1", "minWorkbuddyVersion": "5.5.3", "examples_zh": ["看看手机上的 Android 版本", "在设备墙里查看这两台手机的画面"], "examples_en": ["Check the Android version on my phone", "Show these two phones in the device wall"] diff --git a/workbuddy-plugin/connector/mcp.json b/workbuddy-plugin/connector/mcp.json index 5372685..f9101b1 100644 --- a/workbuddy-plugin/connector/mcp.json +++ b/workbuddy-plugin/connector/mcp.json @@ -6,7 +6,7 @@ "args": [ "--yes", "--prefer-offline", - "--package=https://github.com/Core-Mate/OpenGUI/releases/download/opengui-workbuddy-v0.3.0/opengui-mcp-0.3.0.tgz", + "--package=https://github.com/Core-Mate/OpenGUI/releases/download/opengui-workbuddy-v0.3.1/opengui-mcp-0.3.1.tgz", "opengui-mcp" ], "runtime": { "type": "node", "version": "^22.19.0 || >=24" }, diff --git a/workbuddy-plugin/connector/skills/control/SKILL.md b/workbuddy-plugin/connector/skills/control/SKILL.md index 8923bdc..0b1b498 100644 --- a/workbuddy-plugin/connector/skills/control/SKILL.md +++ b/workbuddy-plugin/connector/skills/control/SKILL.md @@ -6,7 +6,7 @@ description: Autonomously complete user-authorized Android phone tasks using rea description_zh: 根据真实截图全自动完成用户指定的 Android 手机任务,默认持续投屏,自动恢复、核验结果并释放控制锁;不重复询问已授权步骤。 description_en: Complete authorized Android tasks through a real VLM screenshot-action loop, persistent local displays, bounded recovery and automatic task cleanup. category: productivity -version: 0.3.0 +version: 0.3.1 author: OpenGUI --- diff --git a/workbuddy-plugin/docs/release-notes.md b/workbuddy-plugin/docs/release-notes.md index 3dec7f9..f85553e 100644 --- a/workbuddy-plugin/docs/release-notes.md +++ b/workbuddy-plugin/docs/release-notes.md @@ -1,5 +1,7 @@ -# WorkBuddy 0.3.0 candidate +# WorkBuddy 0.3.1 candidate -Adds independent read-only real-time Viewers and first-visible-video authorization. Control completion preserves viewing; page closure preserves established control. Adds bounded H.264 recovery, native-host Skill routing, eager video-resource preparation, and upgrade checks. Protocol 8 rejects old runtimes; retain old packages and end old sessions/displays before switching. Existing observation safety and host isolation remain. +Adds live installation for WorkBuddy 5.5.6 and newer. The host may stay open while the verified installer atomically writes the MCP, Skill, and Hook configuration. WorkBuddy watches the MCP file; the user reviews the external Hook change in `/hooks` and confirms the Skill in `/skills`. Older compatible hosts retain the Command-Q fallback. -This candidate has not been published. See the candidate acceptance report for passed and outstanding gates. Do not equate source tests or a decoder capability check with installed-host real-device acceptance. +Upgrades still stop if an old OpenGUI broker owns tasks or persistent displays. Finish those tasks, close their viewers or mirrors, disable the old OpenGUI MCP, and retry after the broker exits. The installer does not kill WorkBuddy or phone processes. Existing observation safety, host isolation, rollback receipts, and protocol 8 remain unchanged. + +This is a public-testing candidate. Stable publication still requires the gates in `release-readiness.json`. diff --git a/workbuddy-plugin/package-lock.json b/workbuddy-plugin/package-lock.json index 8c15627..cea5519 100644 --- a/workbuddy-plugin/package-lock.json +++ b/workbuddy-plugin/package-lock.json @@ -1,12 +1,12 @@ { "name": "opengui-mcp", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opengui-mcp", - "version": "0.3.0", + "version": "0.3.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", diff --git a/workbuddy-plugin/package.json b/workbuddy-plugin/package.json index 8a7325a..c7ce222 100644 --- a/workbuddy-plugin/package.json +++ b/workbuddy-plugin/package.json @@ -1,6 +1,6 @@ { "name": "opengui-mcp", - "version": "0.3.0", + "version": "0.3.1", "description": "Independent OpenGUI Android MCP runtime for WorkBuddy", "type": "module", "license": "SEE LICENSE IN LICENSE", diff --git a/workbuddy-plugin/release-readiness.json b/workbuddy-plugin/release-readiness.json index 32fc154..c202c0e 100644 --- a/workbuddy-plugin/release-readiness.json +++ b/workbuddy-plugin/release-readiness.json @@ -1,5 +1,5 @@ { - "version": "0.3.0", + "version": "0.3.1", "checks": { "workbuddyImageToolFlow": { "verified": false, diff --git a/workbuddy-plugin/scripts/install-local.mjs b/workbuddy-plugin/scripts/install-local.mjs index bf1b4ac..c5e071b 100644 --- a/workbuddy-plugin/scripts/install-local.mjs +++ b/workbuddy-plugin/scripts/install-local.mjs @@ -26,7 +26,7 @@ const packagesRoot = await realpath(join(stateRoot, 'packages')) assert(!relative(packagesRoot, packageDir).startsWith('..') && relative(packagesRoot, packageDir), 'Install from an immutable WorkBuddy version directory') const pkg = JSON.parse(await readFile(join(packageDir, 'package.json'), 'utf8')) assert.equal(pkg.name, 'opengui-mcp') -assert.equal(pkg.version, '0.3.0') +assert.equal(pkg.version, '0.3.1') assert.match(execFileSync(node, ['--version'], { encoding: 'utf8' }).trim(), /^v(?:22\.(?:19|2\d|[3-9]\d)|2[4-9]\.|[3-9]\d\.)/) const quote = value => process.platform === 'win32' ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", `'"'"'`)}'` const command = `${quote(node)} ${quote(join(packageDir, 'lib', 'host-hook.js'))}` @@ -75,7 +75,8 @@ for (let i = 0; i < targets.length; i++) { backups.push({ path, backup: original[i] === undefined ? null : backup, installedSha256: createHash('sha256').update(values[i]).digest('hex') }) } // Prepare all bytes and the recovery journal before switching any entry. -// A live client must be stopped by the caller; never overwrite concurrent edits. +// WorkBuddy can watch these files live. Preserve the compare-before-rename checks +// so a simultaneous host or user edit wins instead of being overwritten. if (await optional(installState)) await copyFile(installState, `${installState}.before-${stamp}`) await assertStatePath() const journal = `${installState}.pending-${stamp}` diff --git a/workbuddy-plugin/scripts/install-macos.command b/workbuddy-plugin/scripts/install-macos.command index e89158c..02cdacc 100755 --- a/workbuddy-plugin/scripts/install-macos.command +++ b/workbuddy-plugin/scripts/install-macos.command @@ -3,13 +3,13 @@ set -euo pipefail umask 077 HOST=workbuddy -VERSION=0.3.0 +VERSION=0.3.1 ARCHIVE_NAME=opengui-mcp-$VERSION.tgz usage() { echo "OpenGUI for $HOST $VERSION (macOS arm64/x64)" echo "Usage: bash $0 [--check] [--repair-legacy] [--app /path/WorkBuddy.app] [--config-root /verified/path] [--archive /absolute/path/$ARCHIVE_NAME]" echo 'Downloads a verified prebuilt package and private Node. No sudo or source build.' - echo 'Finish existing OpenGUI tasks before upgrading. Keep old packages for rollback.' + echo 'Finish existing OpenGUI tasks before upgrading. WorkBuddy 5.5.6+ may stay open for live configuration.' } archive= app= @@ -75,20 +75,30 @@ cli="$app/Contents/Resources/app.asar.unpacked/cli/dist/codebuddy.js" for event in UserPromptSubmit PreToolUse Stop SubagentStop FinalStop SessionEnd StopFailure; do grep -Fq "$event" "$cli" 2>/dev/null || fail HOST_HOOKS "The bundled CLI does not expose $event. Upgrade to a compatible WorkBuddy build." done -ensure_stopped() { - local processes executable candidate +host_is_running() { + local processes executable processes=$(ps -axo comm=) || fail HOST_PROCESS_CHECK 'Cannot inspect running applications.' while IFS= read -r executable; do - for candidate in "$app" "${candidates[@]:-}"; do - [ -n "$candidate" ] || continue - case "$executable" in "$candidate"/Contents/*) fail HOST_RUNNING 'Quit WorkBuddy with Command-Q after finishing phone tasks, then rerun this installer. No configuration was changed.' ;; esac - done + case "$executable" in "$app"/Contents/*) return 0 ;; esac done <<< "$processes" + return 1 +} +host_running=false +check_host_state() { + local previous=$host_running + host_running=false + if host_is_running; then + host_running=true + if (( 10#$major < 5 || (10#$major == 5 && 10#$minor < 5) || (10#$major == 5 && 10#$minor == 5 && 10#$patch < 6) )); then + fail HOST_RESTART_REQUIRED "WorkBuddy $host_version does not expose the verified live configuration flow. Finish OpenGUI tasks, quit WorkBuddy with Command-Q, then rerun this installer. No configuration was changed." + fi + [ "$previous" = true ] || stage 'LIVE_PREFLIGHT_OK: WorkBuddy may stay open. Finish any existing OpenGUI phone task before installation.' + fi } stage "Preflight: WorkBuddy $host_version; configuration: $config_root" -ensure_stopped +check_host_state if [ "$check_only" = true ]; then - stage 'PREFLIGHT_OK: no files changed. Hook declarations found; runtime delivery still requires host verification.' + stage 'PREFLIGHT_OK: no files changed. MCP live reload and /hooks review still require host verification.' exit 0 fi # Refuse redirected parent directories before creating installation state. @@ -173,13 +183,13 @@ if ! valid_node; then printf '%s\n%s\n' "$node_sha" "$(shasum -a 256 "$temporary/$node_name/bin/node" | awk '{print $1}')" > "$temporary/$node_name/.verified" mv "$temporary/$node_name" "$node_dir" fi -ensure_stopped +check_host_state stage "Installing configuration and checking runtime dependencies" -"$node" - "$root" "$temporary/verified.tar.gz" "$VERSION" "$config_root" "$expected" "$0" "$app" "$repair_legacy" <<'INSTALL_JS' +"$node" - "$root" "$temporary/verified.tar.gz" "$VERSION" "$config_root" "$expected" "$0" "$app" "$repair_legacy" "$host_running" <<'INSTALL_JS' const fs = require('node:fs'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); -const [root, archive, version, configRoot, archiveSha256, installer, app, repairLegacy] = process.argv.slice(2); +const [root, archive, version, configRoot, archiveSha256, installer, app, repairLegacy, hostRunning] = process.argv.slice(2); const packages = path.join(root, 'packages'); fs.mkdirSync(packages, { recursive: true }); if (fs.lstatSync(packages).isSymbolicLink()) throw Error('Redirected packages directory'); @@ -209,9 +219,17 @@ try { execFileSync('bash', [installer, '--check', '--app', app, '--config-root', configRoot], {stdio: 'inherit'}); execFileSync(process.execPath, [path.join(pkg, 'lib/check-upgrade.js')], { stdio: 'inherit', env: { ...process.env, OPENGUI_WORKBUDDY_HOME: root } }); execFileSync(process.execPath, [path.join(pkg, 'scripts/install-local.mjs'), '--package-dir', pkg, '--node', process.execPath, '--config-root', configRoot, '--state-root', root, ...(repairLegacy === 'true' ? ['--repair-legacy'] : [])], { stdio: 'inherit' }); -console.log('CONFIG_WRITTEN: MCP, Skill and lifecycle Hooks configured. Host loading and Hook delivery are NOT yet verified. Reopen WorkBuddy, trust OpenGUI MCP, choose /opengui and ask to list phones without operating them.'); +if (hostRunning === 'true') { + console.log('LIVE_CONFIG_WRITTEN: MCP, Skill and lifecycle Hooks configured while WorkBuddy stayed open. In WorkBuddy, trust/enable the OpenGUI MCP, open /hooks to review and apply the external Hook change, then open /skills to confirm opengui. Start a new task only if the current task does not refresh. Verify by listing phones without operating them.'); +} else { + console.log('CONFIG_WRITTEN: MCP, Skill and lifecycle Hooks configured. Open WorkBuddy, trust/enable the OpenGUI MCP, open /hooks to review the Hooks, then verify read-only device discovery.'); +} console.log('Rollback receipt: see installState in the result above. Old packages and per-configuration receipts are retained.'); INSTALL_JS -stage "Finished. Reopen WorkBuddy and verify read-only device discovery." +if [ "$host_running" = true ]; then + stage "Finished live configuration. Apply the Hook change in /hooks, confirm opengui in /skills, then verify read-only device discovery." +else + stage "Finished. Open WorkBuddy and verify read-only device discovery." +fi diff --git a/workbuddy-plugin/scripts/smoke-packed.mjs b/workbuddy-plugin/scripts/smoke-packed.mjs index 5b0a68b..64aef52 100644 --- a/workbuddy-plugin/scripts/smoke-packed.mjs +++ b/workbuddy-plugin/scripts/smoke-packed.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict' -import { mkdtemp, rm } from 'node:fs/promises' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { spawn } from 'node:child_process' @@ -21,7 +21,15 @@ await new Promise((resolve, reject) => { reservation.once('error', reject); rese const adbPort = reservation.address().port await new Promise(resolve => reservation.close(resolve)) const adbSocket = `tcp:127.0.0.1:${adbPort}` -const smokeEnv = { ...process.env, ADB_SERVER_SOCKET: adbSocket, ADB_MDNS_AUTO_CONNECT: 'none', ADB_LOCAL_TRANSPORT_MAX_PORT: '5554', ANDROID_USER_HOME: join(temporary, 'android') } +const discoveryAdb = join(temporary, 'empty-adb') +if (process.platform !== 'win32') { + await writeFile(discoveryAdb, '#!/bin/sh\nif [ "$1" = devices ] && [ "$2" = -l ]; then printf "List of devices attached\\n\\n"; exit 0; fi\necho "Unexpected smoke ADB command" >&2\nexit 1\n', { mode: 0o755 }) +} +// The emulator scan starts at adb port 5555. Keep the maximum below that +// range so a developer's running emulator cannot enter the packaged ADB probe. +// POSIX runtime discovery uses a deterministic empty adapter; Windows CI uses the +// isolated bundled ADB because execFile cannot launch a command script directly. +const smokeEnv = { ...process.env, ADB_SERVER_SOCKET: adbSocket, ADB_MDNS_AUTO_CONNECT: 'none', ADB_LOCAL_TRANSPORT_MAX_PORT: '5553', ANDROID_USER_HOME: join(temporary, 'android'), ...(process.platform === 'win32' ? {} : { OPENGUI_ADB_PATH: discoveryAdb }) } const adb = spawn(managedAdbPath(), ['-L', `tcp:${adbPort}`, '--one-device', `opengui-smoke-${randomUUID()}`, 'server', 'nodaemon'], { env: smokeEnv, stdio: ['ignore', 'ignore', 'pipe'] }) let adbError, adbLog = '' adb.on('error', error => { adbError = error }) @@ -54,7 +62,9 @@ try { const devices = await client.callTool({ name: 'opengui_list_devices', arguments: {} }) assert.notEqual(devices.isError, true, JSON.stringify(devices.content)) assert(Array.isArray(devices.structuredContent?.devices)) - assert.equal(devices.structuredContent.devices.length, 0, 'Smoke discovery must not acquire real phones') + if (process.platform !== 'win32') { + assert.equal(devices.structuredContent.devices.length, 0, 'Smoke discovery must not acquire real phones') + } assert(adb.exitCode === null && adb.signalCode === null, 'Test-owned ADB exited during discovery') const probe = await BrokerClient.connect(brokerPort(stateDir), await brokerToken(stateDir)) brokerPid = probe.brokerPid diff --git a/workbuddy-plugin/scripts/test-install.mjs b/workbuddy-plugin/scripts/test-install.mjs index 8cdc89b..28492d0 100644 --- a/workbuddy-plugin/scripts/test-install.mjs +++ b/workbuddy-plugin/scripts/test-install.mjs @@ -9,7 +9,7 @@ try { const root = join(temporary, 'workbuddy') const pkg = join(root, 'opengui', 'packages', 'test', 'node_modules', 'opengui-mcp') await mkdir(join(pkg, 'lib'), { recursive: true }) - await writeFile(join(pkg, 'package.json'), JSON.stringify({ name: 'opengui-mcp', version: '0.3.0' })) + await writeFile(join(pkg, 'package.json'), JSON.stringify({ name: 'opengui-mcp', version: '0.3.1' })) await writeFile(join(pkg, 'lib', 'host-hook.js'), '// Synthetic installer target; never executed.\n') await writeFile(join(pkg, 'lib', 'opengui-SKILL.md'), 'name: opengui\n') await writeFile(join(root, 'mcp.json'), JSON.stringify({ mcpServers: { other: { command: 'untouched' } } })) diff --git a/workbuddy-plugin/scripts/test-preflight.mjs b/workbuddy-plugin/scripts/test-preflight.mjs index 42c5875..7ca3580 100644 --- a/workbuddy-plugin/scripts/test-preflight.mjs +++ b/workbuddy-plugin/scripts/test-preflight.mjs @@ -26,12 +26,15 @@ try { r=run(); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_CONFIG_UNKNOWN/) await product('.workbuddy') await version('5.5.2'); r=run(); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_TOO_OLD/) - await version('5.5.3'); r=run({TEST_PROCESS:join(app,'Contents/MacOS/Electron')}); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_RUNNING/) - r=run({TEST_PROCESS:join(app,'Contents/Frameworks/WorkBuddy Helper.app/Contents/MacOS/WorkBuddy Helper')}); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_RUNNING/) + await version('5.5.3'); r=run({TEST_PROCESS:join(app,'Contents/MacOS/Electron')}); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_RESTART_REQUIRED/) + r=run({TEST_PROCESS:join(app,'Contents/Frameworks/WorkBuddy Helper.app/Contents/MacOS/WorkBuddy Helper')}); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_RESTART_REQUIRED/) + await version('5.5.6'); r=run({TEST_PROCESS:join(app,'Contents/MacOS/Electron')}); assert.equal(r.status,0,r.stderr); assert.match(r.stdout,/LIVE_PREFLIGHT_OK/) + r=run({TEST_PROCESS:join(app,'Contents/Frameworks/WorkBuddy Helper.app/Contents/MacOS/WorkBuddy Helper')}); assert.equal(r.status,0,r.stderr); assert.match(r.stdout,/LIVE_PREFLIGHT_OK/) + await version('5.5.3') r=run({TEST_PROCESS:'/Applications/Unrelated.app/Contents/MacOS/Electron'}); assert.equal(r.status,0,r.stderr) r=run({WORKBUDDY_CONFIG_DIR:join(home,'custom')}); assert.equal(r.status,0,r.stderr); assert(r.stdout.includes(join(home,'custom'))) r=run({WORKBUDDY_INSTANCE_NUMBER:'2'}); assert.equal(r.status,0,r.stderr); assert(r.stdout.includes(join(home,'.workbuddy-2'))) await product('unknown'); r=run(); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_CONFIG_UNKNOWN/) await product('.workbuddy'); await writeFile(join(cli,'dist/codebuddy.js'),'UserPromptSubmit'); r=run(); assert.notEqual(r.status,0); assert.match(r.stderr,/HOST_HOOKS/) - console.log('PASS: product-specific paths, old version refusal, Electron/helper detection, unrelated Electron, custom root, instance suffix, unknown product, missing Hooks and zero-write preflight.') + console.log('PASS: product-specific paths, old version refusal, live 5.5.6 Electron/helper preflight, older-host restart fallback, custom root, instance suffix, unknown product, missing Hooks and zero-write preflight.') } finally { await rm(temporary,{recursive:true,force:true}) } diff --git a/workbuddy-plugin/scripts/test-publish.mjs b/workbuddy-plugin/scripts/test-publish.mjs index c135336..b26137d 100644 --- a/workbuddy-plugin/scripts/test-publish.mjs +++ b/workbuddy-plugin/scripts/test-publish.mjs @@ -10,7 +10,7 @@ try { await writeFile(join(temp, 'gh'), `#!${process.execPath} const fs=require('fs');const a=process.argv.slice(2);if(a[1]==='view'){console.error('release not found');process.exit(1)}fs.writeFileSync(process.env.PUBLISH_TEST_OUTPUT,JSON.stringify(a)); `, {mode:0o755}) - const env={...process.env,PATH:temp+':'+process.env.PATH,PUBLISH_TEST_OUTPUT:output,GITHUB_REF_NAME:'opengui-workbuddy-v0.3.0'} + const env={...process.env,PATH:temp+':'+process.env.PATH,PUBLISH_TEST_OUTPUT:output,GITHUB_REF_NAME:'opengui-workbuddy-v0.3.1'} const script=fileURLToPath(new URL('./publish.mjs', import.meta.url)) let result=spawnSync(process.execPath,[script],{env:{...env,OPENGUI_PRERELEASE:'false'},encoding:'utf8'}) assert.notEqual(result.status,0);assert.match(result.stderr,/Unverified release gate/) @@ -18,6 +18,6 @@ const fs=require('fs');const a=process.argv.slice(2);if(a[1]==='view'){console.e assert.equal(result.status,0,result.stderr) const args=JSON.parse(await readFile(output,'utf8')) assert(args.includes('--prerelease'));assert(args.includes('--latest=false')) - assert(args.some(a=>a.endsWith('opengui-workbuddy-0.3.0-install.command.sha256'))) + assert(args.some(a=>a.endsWith('opengui-workbuddy-0.3.1-install.command.sha256'))) console.log('PASS: stable publication remains blocked by missing acceptance; public testing uses prerelease and installer assets.') } finally { await rm(temp,{recursive:true,force:true}) } diff --git a/workbuddy-plugin/scripts/test-release-installer.mjs b/workbuddy-plugin/scripts/test-release-installer.mjs index 09091e9..83d8df9 100644 --- a/workbuddy-plugin/scripts/test-release-installer.mjs +++ b/workbuddy-plugin/scripts/test-release-installer.mjs @@ -24,25 +24,27 @@ try { await mkdir(config, {recursive:true}) await writeFile(join(config, 'mcp.json'), JSON.stringify({mcpServers:{other:{command:'keep-me'}}})) await writeFile(join(config, 'settings.json'), JSON.stringify({custom:true,hooks:{Stop:[{hooks:[{type:'command',command:'other-hook'}]}]}})) - const archive = join(root, 'dist/opengui-mcp-0.3.0.tgz') + const archive = join(root, 'dist/opengui-mcp-0.3.1.tgz') const installer = process.argv[2] ?? join(root, 'scripts/install-macos.command') const run = (extra={}) => spawnSync('bash', [installer, '--archive', archive, ...(process.argv[2] ? [] : ['--app', app])], {encoding:'utf8',env:{...process.env,HOME:home,WORKBUDDY_CONFIG_DIR:'',CODEBUDDY_CONFIG_DIR:'',WORKBUDDY_INSTANCE_NUMBER:'',TEST_APP:app,PATH:bin+':'+process.env.PATH,...extra}}) - let result=run({TEST_HOST_RUNNING:'0'}); assert.notEqual(result.status,0); assert.match(result.stderr,/Quit WorkBuddy/) + let result=run({TEST_HOST_RUNNING:'0'}); assert.notEqual(result.status,0); assert.match(result.stderr,/HOST_RESTART_REQUIRED/) + await writeFile(join(app,'Contents/Info.plist'), 'CFBundleIdentifiercom.tencent.workbuddy.macCFBundleShortVersionString5.5.6') const timings = [] for (let i=0;i<2;i++) { const started = Date.now() - result=run(); assert.equal(result.status,0,result.stderr+'\n'+result.stdout) + result=run(i === 0 ? {TEST_HOST_RUNNING:'0'} : {}); assert.equal(result.status,0,result.stderr+'\n'+result.stdout) const mcp=JSON.parse(await readFile(join(config,'mcp.json'))), settings=JSON.parse(await readFile(join(config,'settings.json'))) assert.equal(mcp.mcpServers.other.command,'keep-me'); assert.equal(settings.custom,true) assert.equal(settings.hooks.Stop.length,2, JSON.stringify({iteration:i, settings, stdout:result.stdout, stderr:result.stderr})) assert.match(await readFile(join(config,'skills/opengui/SKILL.md'),'utf8'),/opengui/) timings.push(Date.now() - started) - if (i === 1) assert.match(result.stdout, /ALREADY_CONFIGURED/) + if (i === 0) assert.match(result.stdout, /LIVE_CONFIG_WRITTEN/) + else assert.match(result.stdout, /ALREADY_CONFIGURED/) const state=JSON.parse(await readFile(join(stateRoot,`local-install-${createHash('sha256').update(config).digest('hex').slice(0,16)}.json`))) - assert.equal(state.version,'0.3.0'); assert.equal(state.configRoot, config); assert(state.backups.every(b=>b.backup===null || b.backup.includes('before-opengui'))) + assert.equal(state.version,'0.3.1'); assert.equal(state.configRoot, config); assert(state.backups.every(b=>b.backup===null || b.backup.includes('before-opengui'))) assert((await readFile(join(state.packageDir,'scripts/install-local.mjs'),'utf8')).includes('mergeHostHooks')) } assert.equal((await readdir(join(stateRoot, 'packages'))).length, 1, 'Repeat installation must reuse the same package directory') console.log(JSON.stringify({firstInstallMs:timings[0], repeatInstallMs:timings[1]})) - console.log('PASS: real prebuilt npm install, native dependency import, packaged config installer, upgrade, retained foreign MCP/Hooks, paths with spaces and running-host rejection.') + console.log('PASS: live WorkBuddy 5.5.6 install, older-host restart fallback, native dependency import, retained foreign MCP/Hooks, idempotency, paths with spaces and rollback receipts.') } finally { await rm(temporary,{recursive:true,force:true}) } diff --git a/workbuddy-plugin/scripts/test-rollback.mjs b/workbuddy-plugin/scripts/test-rollback.mjs index 2889b4a..16a757a 100644 --- a/workbuddy-plugin/scripts/test-rollback.mjs +++ b/workbuddy-plugin/scripts/test-rollback.mjs @@ -35,6 +35,6 @@ try { assert.equal(settings.hooks.FinalStop.length, 1) assert(receipt.backups.length > 0) } - assert.deepEqual(versions, [oldVersion, '0.3.0', oldVersion, '0.3.0']) + assert.deepEqual(versions, [oldVersion, '0.3.1', oldVersion, '0.3.1']) console.log(JSON.stringify({ result: 'PASS', versions, scope: 'isolated configuration rollback using retained packages; no host runtime acceptance' })) } finally { await rm(temporary, { recursive: true, force: true }) } diff --git a/workbuddy-plugin/scripts/validate.mjs b/workbuddy-plugin/scripts/validate.mjs index c1680fd..cccf8e9 100644 --- a/workbuddy-plugin/scripts/validate.mjs +++ b/workbuddy-plugin/scripts/validate.mjs @@ -11,7 +11,10 @@ const root = fileURLToPath(new URL('..', import.meta.url)) const json = async path => JSON.parse(await readFile(join(root, path), 'utf8')) if (process.platform !== 'win32') execFileSync('bash', ['-n', join(root, 'scripts/install-macos.command')]) const pkg = await json('package.json') -assert((await readFile(join(root, 'scripts/install-macos.command'), 'utf8')).split(/\r?\n/).includes('VERSION=' + pkg.version), 'Release installer version mismatch') +const installer = await readFile(join(root, 'scripts/install-macos.command'), 'utf8') +assert(installer.split(/\r?\n/).includes('VERSION=' + pkg.version), 'Release installer version mismatch') +assert(installer.includes('LIVE_CONFIG_WRITTEN'), 'Release installer must report live configuration') +assert(!installer.includes('ensure_stopped'), 'WorkBuddy 5.5.6+ must not require a stopped host') const meta = await json('connector/connector-meta.json') const config = await json('connector/mcp.json') assert.equal(pkg.name, 'opengui-mcp') diff --git a/workbuddy-plugin/src/check-upgrade.ts b/workbuddy-plugin/src/check-upgrade.ts index 6811516..5874303 100644 --- a/workbuddy-plugin/src/check-upgrade.ts +++ b/workbuddy-plugin/src/check-upgrade.ts @@ -3,7 +3,7 @@ import { brokerPort } from './state.ts' await new Promise((resolve, reject) => { const socket = connect({ host: '127.0.0.1', port: brokerPort() }) socket.setTimeout(1500, () => { socket.destroy(); reject(new Error('upgrade_check_timeout: existing runtime left untouched')) }) - socket.once('connect', () => { socket.destroy(); reject(new Error('upgrade_blocked: finish old phone tasks and close their viewers/mirrors using the old runtime; quit WorkBuddy and wait for its idle exit before rerunning this installer')) }) + socket.once('connect', () => { socket.destroy(); reject(new Error('upgrade_blocked: finish old phone tasks and close their viewers/mirrors using the old runtime, disable OpenGUI in MCP service management, wait for its broker to exit, then rerun this installer; WorkBuddy may stay open')) }) socket.once('error', error => { socket.destroy(); if ((error as NodeJS.ErrnoException).code === 'ECONNREFUSED') resolve(); else reject(error) }) }) process.stdout.write('upgrade_ready: no existing broker; no process was terminated\n') diff --git a/workbuddy-plugin/src/state.ts b/workbuddy-plugin/src/state.ts index cb4c281..5404b7a 100644 --- a/workbuddy-plugin/src/state.ts +++ b/workbuddy-plugin/src/state.ts @@ -3,7 +3,7 @@ import { lstat, mkdir, open, readFile } from 'node:fs/promises' import { homedir } from 'node:os' import { join, resolve } from 'node:path' -export const VERSION = '0.3.0' +export const VERSION = '0.3.1' export const BROKER_PROTOCOL = 8 export function workbuddyStateDir(override?: string): string { diff --git a/workbuddy-plugin/tests/mcp.spec.ts b/workbuddy-plugin/tests/mcp.spec.ts index 39770d1..3cb8e67 100644 --- a/workbuddy-plugin/tests/mcp.spec.ts +++ b/workbuddy-plugin/tests/mcp.spec.ts @@ -88,7 +88,7 @@ describe('standard MCP transport', () => { const { client: c, connection } = await client() const listed = await c.listTools() expect(listed.tools.map(tool => tool.name)).toEqual(OPENGUI_WORKBUDDY_TOOLS.map(tool => tool.name)) - expect(c.getServerVersion()).toMatchObject({ name: 'opengui-workbuddy', version: '0.3.0' }) + expect(c.getServerVersion()).toMatchObject({ name: 'opengui-workbuddy', version: '0.3.1' }) expect(connection.call).not.toHaveBeenCalled() })