openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
179 lines (178 loc) • 8.44 kB
JavaScript
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { m as resolveAgentWorkspaceDir, o as listAgentIds } from "./agent-scope-config-DcbEhP0R.js";
import "./agent-scope-DbtJyKUL.js";
import { t as note } from "./note-DqHk3fA1.js";
import { t as buildWorkspaceSkillStatus } from "./status-BDd5nSbi.js";
import { n as disableUnavailableSkillsInConfig, t as collectUnavailableAgentSkills } from "./doctor-skills-core-DZQVCoNO.js";
import { existsSync } from "node:fs";
import { posix, win32 } from "node:path";
//#region src/skills/lifecycle/gh-config-discovery.ts
function pathFor(platform) {
return platform === "win32" ? win32 : posix;
}
const HOSTS_FILE = "hosts.yml";
function resolveEffectiveGhConfigDir(input) {
const env = input.env;
if (env.GH_CONFIG_DIR && env.GH_CONFIG_DIR.trim()) return env.GH_CONFIG_DIR.trim();
const xdg = env.XDG_CONFIG_HOME?.trim();
if (xdg) return pathFor(input.platform).join(xdg, "gh");
if (input.platform === "win32") {
const appData = env.APPDATA?.trim();
if (appData) return pathFor(input.platform).join(appData, "GitHub CLI");
const profile = env.USERPROFILE?.trim();
if (profile) return pathFor(input.platform).join(profile, "AppData", "Roaming", "GitHub CLI");
}
const home = env.HOME?.trim();
if (!home) return;
return pathFor(input.platform).join(home, ".config", "gh");
}
function defaultCandidateOperatorHomes(input) {
const env = input.env;
const homes = /* @__PURE__ */ new Set();
if (input.platform !== "win32") homes.add("/root");
if (env.SUDO_USER?.trim()) {
const sudoUser = env.SUDO_USER.trim();
homes.add(pathFor(input.platform).join("/home", sudoUser));
if (input.platform === "darwin") homes.add(pathFor(input.platform).join("/Users", sudoUser));
}
if (env.USER?.trim()) {
const user = env.USER.trim();
if (user !== "root") {
if (input.platform === "darwin") homes.add(pathFor(input.platform).join("/Users", user));
else if (input.platform !== "win32") homes.add(pathFor(input.platform).join("/home", user));
}
}
const processHome = env.HOME?.trim();
if (processHome) homes.delete(processHome);
return [...homes];
}
function ghConfigDirForHome(home, platform) {
return pathFor(platform).join(home, ".config", "gh");
}
function detectGhConfigDirMismatch(input) {
const env = input.env;
if (env.GH_CONFIG_DIR && env.GH_CONFIG_DIR.trim()) return {
kind: "explicit-gh-config-dir-set",
ghConfigDir: env.GH_CONFIG_DIR.trim()
};
const effective = resolveEffectiveGhConfigDir(input);
if (!effective) return { kind: "no-process-home" };
const effectiveHosts = pathFor(input.platform).join(effective, HOSTS_FILE);
if (input.fileExists(effectiveHosts)) return {
kind: "auth-discoverable",
effectiveConfigDir: effective
};
const candidates = input.candidateOperatorHomes ?? defaultCandidateOperatorHomes(input);
for (const home of candidates) {
const candidateDir = ghConfigDirForHome(home, input.platform);
if (candidateDir === effective) continue;
const candidateHosts = pathFor(input.platform).join(candidateDir, HOSTS_FILE);
if (input.fileExists(candidateHosts)) return {
kind: "mismatch",
effectiveConfigDir: effective,
alternateConfigDir: candidateDir,
alternateHostsFile: candidateHosts,
alternateHomeHint: home,
suggestedEnvValue: candidateDir
};
}
return {
kind: "no-known-auth",
effectiveConfigDir: effective
};
}
function formatGhConfigDirMismatchHint(mismatch) {
const lines = [
"GitHub CLI auth was found at a different HOME than the one this OpenClaw process uses.",
` Process gh config dir: ${mismatch.effectiveConfigDir}`,
` Authenticated config: ${mismatch.alternateConfigDir} (contains ${HOSTS_FILE})`
];
if (mismatch.alternateHomeHint) lines.push(` Authenticated HOME: ${mismatch.alternateHomeHint}`);
lines.push(` Fix: set GH_CONFIG_DIR=${mismatch.suggestedEnvValue} on the OpenClaw service environment, then restart the gateway.`);
return lines;
}
//#endregion
//#region src/commands/doctor-skills.ts
/** Doctor checks and repair prompts for unavailable configured skills. */
function defaultGhConfigDiscoveryInput() {
return {
platform: process.platform,
env: process.env,
fileExists: (absolutePath) => existsSync(absolutePath)
};
}
/** Builds a GitHub CLI config-dir hint for eligible GitHub skill setups. */
function describeGhConfigDirHint(skills) {
return describeGhConfigDirHintFromDiscovery(skills, defaultGhConfigDiscoveryInput());
}
/** Builds a GitHub CLI config-dir hint from injected discovery inputs for tests. */
function describeGhConfigDirHintFromDiscovery(skills, discoveryInput) {
const githubSkill = skills.find((skill) => skill.name === "github");
if (!githubSkill) return [];
if (!githubSkill.eligible || githubSkill.blockedByAgentFilter || githubSkill.disabled || githubSkill.blockedByAllowlist) return [];
const result = detectGhConfigDirMismatch(discoveryInput);
if (result.kind !== "mismatch") return [];
return formatGhConfigDirMismatchHint(result);
}
/** Formats doctor note lines for skills that are allowed but unavailable. */
function formatUnavailableSkillDoctorLines(skills, includeDisableHint = true) {
const count = skills.length;
const lines = [`${count} allowed skill${count === 1 ? " is" : "s are"} not usable in this environment (missing binaries, env vars, or config).`, `- ${skills.map((skill) => skill.name).toSorted((a, b) => a.localeCompare(b)).join(", ")}`];
if (includeDisableHint) lines.push(`Disable unused skills: ${formatCliCommand("openclaw doctor --fix")}`);
lines.push(`Inspect details: ${formatCliCommand("openclaw skills check --agent <id>")} or ${formatCliCommand("openclaw skills info <name> --agent <id>")}`);
return lines;
}
function collectFleetUnavailableSkills(reports) {
const healthyKeys = new Set(reports.flatMap(({ skills }) => skills.filter((skill) => skill.eligible && !skill.blockedByAgentFilter).map((skill) => skill.skillKey)));
const candidates = /* @__PURE__ */ new Map();
for (const skill of reports.flatMap(({ unavailable }) => unavailable)) if (!healthyKeys.has(skill.skillKey)) candidates.set(skill.skillKey, skill);
return [...candidates.values()];
}
/** Checks every agent's skill readiness and disables only fleet-wide unavailable skills. */
async function maybeRepairSkillReadiness(params) {
const agentIds = listAgentIds(params.cfg);
const reports = agentIds.map((agentId) => ({
agentId,
workspaceDir: resolveAgentWorkspaceDir(params.cfg, agentId)
})).map(({ agentId, workspaceDir }) => {
const buildReport = () => {
const report = buildWorkspaceSkillStatus(workspaceDir, {
config: params.cfg,
agentId
});
return {
agentId,
report,
unavailable: collectUnavailableAgentSkills(report)
};
};
return params.runWithPluginMetadataSnapshot ? params.runWithPluginMetadataSnapshot({
config: params.cfg,
workspaceDir
}, buildReport) : buildReport();
});
const fleetUnavailable = collectFleetUnavailableSkills(reports.map(({ report, unavailable: unavailableForAgent }) => ({
skills: report.skills,
unavailable: unavailableForAgent
})));
const globallyUnavailableKeys = new Set(fleetUnavailable.map((skill) => skill.skillKey));
for (const { agentId, report, unavailable: unavailableForAgent } of reports) {
const prefix = agentIds.length > 1 ? `Agent "${agentId}":\n` : "";
const githubHint = describeGhConfigDirHint(report.skills);
if (githubHint.length > 0) note(`${prefix}${githubHint.join("\n")}`, "GitHub CLI");
if (unavailableForAgent.length > 0) {
const includesGlobalCandidate = unavailableForAgent.some((skill) => globallyUnavailableKeys.has(skill.skillKey));
note(`${prefix}${formatUnavailableSkillDoctorLines(unavailableForAgent, includesGlobalCandidate).join("\n")}`, "Skills");
}
}
if (fleetUnavailable.length === 0) return params.cfg;
if (!await params.prompter.confirmRuntimeRepair({
message: agentIds.length === 1 ? `Disable ${fleetUnavailable.length} unavailable skill${fleetUnavailable.length === 1 ? "" : "s"} in config?` : `Disable ${fleetUnavailable.length} skill${fleetUnavailable.length === 1 ? "" : "s"} unavailable to every configured agent?`,
initialValue: false
})) return params.cfg;
const next = disableUnavailableSkillsInConfig(params.cfg, fleetUnavailable);
note(fleetUnavailable.map((skill) => `- Disabled ${skill.name}`).join("\n"), "Doctor changes");
return next;
}
//#endregion
export { maybeRepairSkillReadiness };