fix: harden doctor install checks

This commit is contained in:
Peter Steinberger
2026-01-13 07:25:25 +00:00
parent aa394d0e14
commit 4fd1a6dec3
6 changed files with 97 additions and 7 deletions

View File

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

View File

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

View File

@@ -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 npms 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 youre debugging), set:

View File

@@ -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");
}
}

View File

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

View File

@@ -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);