diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dfe96e6d..80405d8f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,11 @@ - Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer. - Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal. +### Installer +- Install: run `clawdbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. + ### Fixes +- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds. - Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. - Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors. - MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 152a72ab8..0c3977b9a 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -76,6 +76,7 @@ cat ~/.clawdbot/clawdbot.json - Security warnings for open DM policies. - Gateway auth warnings when no `gateway.auth.token` is set (local mode; offers token generation). - systemd linger check on Linux. +- Source install checks (pnpm workspace mismatch, missing UI assets, missing tsx binary). - Writes updated config + wizard metadata. ## Detailed behavior and rationale diff --git a/docs/install/installer.md b/docs/install/installer.md index 65c709bab..1733c9087 100644 --- a/docs/install/installer.md +++ b/docs/install/installer.md @@ -30,6 +30,7 @@ What it does (high level): - `git`: clone/build a source checkout and install a wrapper script - On Linux: avoid global npm permission errors by switching npm’s prefix to `~/.npm-global` when needed. - If upgrading an existing install: runs `clawdbot doctor --non-interactive` (best effort). +- For git installs: runs `clawdbot doctor --non-interactive` after install/update (best effort). - Mitigates `sharp` native install gotchas by defaulting `SHARP_IGNORE_GLOBAL_LIBVIPS=1` (avoids building against system libvips). If you *want* `sharp` to link against a globally-installed libvips (or you’re debugging), set: diff --git a/src/commands/doctor-install.ts b/src/commands/doctor-install.ts new file mode 100644 index 000000000..9128575ef --- /dev/null +++ b/src/commands/doctor-install.ts @@ -0,0 +1,39 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { note } from "../terminal/note.js"; + +export function noteSourceInstallIssues(root: string | null) { + if (!root) return; + + const workspaceMarker = path.join(root, "pnpm-workspace.yaml"); + if (!fs.existsSync(workspaceMarker)) return; + + const warnings: string[] = []; + const nodeModules = path.join(root, "node_modules"); + const pnpmStore = path.join(nodeModules, ".pnpm"); + const tsxBin = path.join(nodeModules, ".bin", "tsx"); + const srcEntry = path.join(root, "src", "entry.ts"); + + if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) { + warnings.push( + "- node_modules was not installed by pnpm (missing node_modules/.pnpm). Run: pnpm install", + ); + } + + if (fs.existsSync(path.join(root, "package-lock.json"))) { + warnings.push( + "- package-lock.json present in a pnpm workspace. If you ran npm install, remove it and reinstall with pnpm.", + ); + } + + if (fs.existsSync(srcEntry) && !fs.existsSync(tsxBin)) { + warnings.push( + "- tsx binary is missing for source runs. Run: pnpm install", + ); + } + + if (warnings.length > 0) { + note(warnings.join("\n"), "Install"); + } +} diff --git a/src/commands/doctor-ui.ts b/src/commands/doctor-ui.ts index b83f7e281..2eaf06277 100644 --- a/src/commands/doctor-ui.ts +++ b/src/commands/doctor-ui.ts @@ -23,10 +23,52 @@ export async function maybeRepairUiProtocolFreshness( try { const [schemaStats, uiStats] = await Promise.all([ - fs.stat(schemaPath), - fs.stat(uiIndexPath), + fs.stat(schemaPath).catch(() => null), + fs.stat(uiIndexPath).catch(() => null), ]); + if (schemaStats && !uiStats) { + note( + [ + "- Control UI assets are missing.", + "- Run: pnpm ui:build", + ].join("\n"), + "UI", + ); + + const shouldRepair = await prompter.confirmRepair({ + message: "Build Control UI assets now?", + initialValue: true, + }); + + if (shouldRepair) { + note("Building Control UI assets... (this may take a moment)", "UI"); + const uiScriptPath = path.join(root, "scripts/ui.js"); + const buildResult = await runCommandWithTimeout( + [process.execPath, uiScriptPath, "build"], + { + cwd: root, + timeoutMs: 120_000, + env: { ...process.env, FORCE_COLOR: "1" }, + }, + ); + if (buildResult.code === 0) { + note("UI build complete.", "UI"); + } else { + const details = [ + `UI build failed (exit ${buildResult.code ?? "unknown"}).`, + buildResult.stderr.trim() ? buildResult.stderr.trim() : null, + ] + .filter(Boolean) + .join("\n"); + note(details, "UI"); + } + } + return; + } + + if (!schemaStats || !uiStats) return; + if (schemaStats.mtime > uiStats.mtime) { const uiMtimeIso = uiStats.mtime.toISOString(); // Find changes since the UI build diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 085dbd50b..10ec12808 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -62,6 +62,7 @@ import { maybeRepairGatewayServiceConfig, maybeScanExtraGatewayServices, } from "./doctor-gateway-services.js"; +import { noteSourceInstallIssues } from "./doctor-install.js"; import { maybeMigrateLegacyConfigFile, normalizeLegacyConfigValues, @@ -187,6 +188,12 @@ export async function doctorCommand( printWizardHeader(runtime); intro("Clawdbot doctor"); + const root = await resolveClawdbotPackageRoot({ + moduleUrl: import.meta.url, + argv1: process.argv[1], + cwd: process.cwd(), + }); + const updateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS === "1"; const canOfferUpdate = !updateInProgress && @@ -195,11 +202,6 @@ export async function doctorCommand( options.repair !== true && Boolean(process.stdin.isTTY); if (canOfferUpdate) { - const root = await resolveClawdbotPackageRoot({ - moduleUrl: import.meta.url, - argv1: process.argv[1], - cwd: process.cwd(), - }); if (root) { const git = await detectClawdbotGitCheckout(root); if (git === "git") { @@ -250,6 +252,7 @@ export async function doctorCommand( } await maybeRepairUiProtocolFreshness(runtime, prompter); + noteSourceInstallIssues(root); await maybeMigrateLegacyConfigFile(runtime);