@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
1,242 lines (1,230 loc) • 232 kB
JavaScript
#!/usr/bin/env node
import "./pi-embedded-helpers-CC00lEFI.js";
import { $t as waitForever, Ct as waitForGatewayReachable, E as runMessageAction, F as CHANNEL_MESSAGE_ACTION_NAMES, Jt as runMemoryStatus, K as CHANNEL_TARGETS_DESCRIPTION, N as formatTargetDisplay, Yt as monitorWebChannel, Zt as installUnhandledRejectionHandler, bt as resolveControlUiLinks, dt as formatControlUiSshHint, gt as openUrl, jr as applyTemplate, lr as lookupContextTokens, lt as detectBrowserOpenSupport, mt as moveToTrash, nt as resolveCommitHash, ot as DEFAULT_WORKSPACE, pt as handleReset, q as CHANNEL_TARGET_DESCRIPTION, qt as registerMemoryCli, st as applyWizardMetadata, t as getReplyFromConfig, ut as ensureWorkspaceAndSessions, yt as randomToken } from "./reply-CoztdrN_.js";
import { g as resolveStateDir, i as isNixMode, m as resolveOAuthDir, o as resolveConfigPath, r as STATE_DIR, u as resolveGatewayPort } from "./paths-BDd7_JUB.js";
import { D as DEFAULT_AGENT_ID, E as DEFAULT_ACCOUNT_ID, N as normalizeAgentId, c as resolveDefaultAgentId, f as DEFAULT_AGENT_WORKSPACE_DIR, h as DEFAULT_IDENTITY_FILENAME, r as resolveAgentDir, s as resolveAgentWorkspaceDir, t as listAgentIds, w as resolveDefaultAgentWorkspaceDir, x as ensureAgentWorkspace } from "./agent-scope-CrgUOY3f.js";
import { D as info, E as danger, I as colorize, L as isRich, M as setVerbose, R as theme, S as normalizeChatChannelId, c as defaultRuntime, h as DEFAULT_CHAT_CHANNEL, l as restoreTerminalState, r as enableConsoleCapture, s as visibleWidth, v as getChatChannelMeta } from "./subsystem-46MXi6Ip.js";
import { _ as shortenHomePath, b as toWhatsappJid, d as normalizeE164, f as resolveConfigDir, g as shortenHomeInString, h as resolveUserPath, n as assertWebChannel, p as resolveHomeDir } from "./utils-Dg0Xbl6w.js";
import { n as runExec, t as runCommandWithTimeout } from "./exec-CTo4hK94.js";
import { G as resolveAuthProfileOrder, P as resolveEnvApiKey, Z as resolveApiKeyForProfile, bt as DEFAULT_MODEL, ct as upsertAuthProfile, l as resolveConfiguredModelRef, lt as ensureAuthProfileStore, o as normalizeProviderId, pt as resolveAuthStorePath, xt as DEFAULT_PROVIDER, yt as DEFAULT_CONTEXT_TOKENS } from "./model-selection-Cp1maz7B.js";
import "./github-copilot-token-VZsS4013.js";
import { n as replaceCliName, r as resolveCliName, t as formatCliCommand } from "./command-format-BQK1OIvH.js";
import { t as parseBooleanValue } from "./boolean-CE7i9tBR.js";
import { r as normalizeEnv, t as isTruthyEnvValue } from "./env-DOcCob95.js";
import { M as VERSION, a as readConfigFileSnapshot, h as parseDurationMs, n as createConfigIO, r as loadConfig, s as writeConfigFile } from "./config-qgIz1lbh.js";
import "./manifest-registry-BFpLJJDB.js";
import { n as listChannelPlugins, r as normalizeChannelId, t as getChannelPlugin } from "./plugins-D1CxUobm.js";
import { D as resolveSessionKey, E as deriveSessionKey, d as loadSessionStore, m as saveSessionStore } from "./sandbox-BXUfp_qv.js";
import "./image-ClOB6QDJ.js";
import "./pi-model-discovery-DjGamP_B.js";
import { E as formatUncaughtError, c as describePortOwner, l as ensurePortAvailable, s as PortInUseError, u as handlePortError } from "./chrome-D2LUApAY.js";
import "./skills-C4b1FA1e.js";
import "./routes-Ds-tIZFJ.js";
import { n as movePathToTrash } from "./server-context-D2cv-pIA.js";
import { l as normalizeMessageChannel, m as GATEWAY_CLIENT_NAMES, p as GATEWAY_CLIENT_MODES } from "./message-channel-CQGWXVL4.js";
import "./logging-BdnOSVPD.js";
import "./accounts-BlHoTziG.js";
import { a as resolveSessionTranscriptsDirForAgent, i as resolveSessionTranscriptsDir, o as resolveStorePath } from "./paths-50eo6DV6.js";
import "./redact-BKh-zp-c.js";
import "./tool-display-rIUh61kT.js";
import "./deliver-Bsvrattg.js";
import "./dispatcher-BNB5aCZ6.js";
import "./manager-BArueSTR.js";
import "./sqlite-Dz6S6ijV.js";
import "./channel-summary-C8GoEKgH.js";
import "./client-zqMhLTAX.js";
import { n as callGateway, r as randomIdempotencyKey } from "./call-CPBhMXxo.js";
import "./login-qr-C2H_iQJU.js";
import "./pairing-store-BnMngoWQ.js";
import { t as formatDocsLink } from "./links-C9fyAH-V.js";
import { r as runCommandWithRuntime } from "./cli-utils-CukoNm8O.js";
import { n as withProgress } from "./progress-uNDQDtGB.js";
import "./pi-tools.policy-DleRi9eC.js";
import { n as stylePromptMessage, r as stylePromptTitle, t as stylePromptHint } from "./prompt-style-gfROyHgB.js";
import "./pairing-labels-DK2aLSd2.js";
import "./control-service-BW5sW2U1.js";
import "./restart-sentinel-CdcBcziq.js";
import "./channel-selection-BCn8_qun.js";
import { t as createDefaultDeps } from "./deps-BrcOX0ht.js";
import { l as ensureBinary, u as promptYesNo } from "./tailscale-Cvk3mQDZ.js";
import { t as isMainModule } from "./is-main-DrJg4t1R.js";
import { t as ensureOpenClawCliOnPath } from "./path-env-CuGC6r1F.js";
import { t as assertSupportedRuntime } from "./runtime-guard-BG6JybtL.js";
import { a as buildAgentSummaries, c as loadAgentIdentity, f as parseIdentityMarkdown, i as applyAgentConfig, l as pruneAgentConfig, n as statusCommand, o as findAgentEntryIndex, s as listAgentEntries, t as runOnboardingWizard, u as identityHasValues } from "./onboarding-DcN1avQK.js";
import { t as resolveChannelDefaultAccountId } from "./helpers-Cw9kFCkw.js";
import { n as logConfigUpdated, t as formatConfigPath } from "./logging-CLCWl7Iu.js";
import "./note-BFpD-42H.js";
import { t as WizardCancelledError } from "./prompts-CIDznuZR.js";
import { t as createClackPrompter } from "./clack-prompter-OmDa1Hm1.js";
import { _ as applyGoogleGeminiModelDefault, g as upsertSharedEnvVar, h as applyAuthChoice, m as warnIfModelConfigLooksOff, v as promptAuthChoiceGrouped } from "./onboard-skills-fL84FI8F.js";
import { $ as setVeniceApiKey, C as applyMoonshotConfig, D as applyOpenrouterConfig, F as applyXiaomiConfig, G as setCloudflareAiGatewayConfig, J as setMinimaxApiKey, K as setGeminiApiKey, L as applyZaiConfig, N as applyVercelAiGatewayConfig, Q as setSyntheticApiKey, W as setAnthropicApiKey, X as setOpencodeZenApiKey, Y as setMoonshotApiKey, Z as setOpenrouterApiKey, at as validateAnthropicSetupToken, et as setVercelAiGatewayApiKey, f as applyOpencodeZenConfig, g as applyMinimaxConfig, it as buildTokenProfileId, j as applyVeniceConfig, k as applySyntheticConfig, m as applyMinimaxApiConfig, nt as setZaiApiKey, q as setKimiCodingApiKey, tt as setXiaomiApiKey, v as applyAuthProfileConfig, w as applyMoonshotConfigCn, x as applyKimiCodeConfig, y as applyCloudflareAiGatewayConfig } from "./github-copilot-auth-BoRchp_Q.js";
import { n as setupChannels } from "./onboard-channels-CJCy7TTQ.js";
import "./plugin-auto-enable-JQ63k0Ce.js";
import "./archive-BrH5WBhI.js";
import "./installs-W4Pc1LJz.js";
import { i as healthCommand } from "./health-format-B4_A88V3.js";
import "./update-runner-s8Rs8ex3.js";
import "./auth-DK3l201_.js";
import "./audit-CWGOX7Eh.js";
import { t as renderTable } from "./table-DuNe7Qes.js";
import "./skills-status-CSCMqNZP.js";
import { t as resolveGatewayService } from "./service-CAxAjHNB.js";
import { r as isSystemdUserServiceAvailable } from "./systemd-CNYEkRek.js";
import "./service-audit-DBSAGhm8.js";
import "./node-service-CjtBRyjp.js";
import "./channels-status-issues-C9vMMPG0.js";
import "./status.update-2ZdxeIeT.js";
import { a as getFlagValue, c as hasFlag, i as getCommandPath, l as hasHelpOrVersion, o as getPositiveIntFlagValue, r as registerSubCliCommands, s as getVerboseFlag } from "./register.subclis-Bo-xmQ-W.js";
import { n as callGatewayFromCli, t as addGatewayClientOptions } from "./gateway-rpc-SvVB4Fmv.js";
import { t as formatHelpExamples } from "./help-format-CGcnDM3D.js";
import { a as createOutboundSendDeps, n as resolveSessionKeyForRequest, t as agentCommand } from "./agent-DuQt3QDO.js";
import { i as hasExplicitOptions, n as resolveCliChannelOptions, r as ensurePluginRegistryLoaded } from "./channel-options-BCSvD6JM.js";
import { n as parsePositiveIntOrUndefined, t as collectOption } from "./helpers-HZ-6iA1e.js";
import { a as CONFIGURE_WIZARD_SECTIONS, n as configureCommandWithSections, t as configureCommand } from "./configure-C1_0rFdQ.js";
import { a as gatewayInstallErrorHint, i as buildGatewayInstallPlan, r as isGatewayDaemonRuntime, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-vNkYv9tq.js";
import { n as ensureSystemdUserLingerNonInteractive } from "./systemd-linger-Cbkoh9qw.js";
import "./widearea-dns-BIhjVRG1.js";
import "./auth-health-Ba9GTScq.js";
import { n as loadAndMaybeMigrateDoctorConfig, t as doctorCommand } from "./doctor-CT9JPoCt.js";
import "./hooks-status-XV9oQICf.js";
import "./tui-cV-R-JXi.js";
import process$1 from "node:process";
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs";
import JSON5 from "json5";
import fs$1 from "node:fs/promises";
import { cancel, confirm, isCancel, multiselect, select } from "@clack/prompts";
import dotenv from "dotenv";
import { Command } from "commander";
//#region src/infra/dotenv.ts
function loadDotEnv(opts) {
const quiet = opts?.quiet ?? true;
dotenv.config({ quiet });
const globalEnvPath = path.join(resolveConfigDir(process.env), ".env");
if (!fs.existsSync(globalEnvPath)) return;
dotenv.config({
quiet,
path: globalEnvPath,
override: false
});
}
//#endregion
//#region src/commands/agents.bindings.ts
function bindingMatchKey(match) {
const accountId = match.accountId?.trim() || DEFAULT_ACCOUNT_ID;
return [
match.channel,
accountId,
match.peer?.kind ?? "",
match.peer?.id ?? "",
match.guildId ?? "",
match.teamId ?? ""
].join("|");
}
function describeBinding(binding) {
const match = binding.match;
const parts = [match.channel];
if (match.accountId) parts.push(`accountId=${match.accountId}`);
if (match.peer) parts.push(`peer=${match.peer.kind}:${match.peer.id}`);
if (match.guildId) parts.push(`guild=${match.guildId}`);
if (match.teamId) parts.push(`team=${match.teamId}`);
return parts.join(" ");
}
function applyAgentBindings(cfg, bindings) {
const existing = cfg.bindings ?? [];
const existingMatchMap = /* @__PURE__ */ new Map();
for (const binding of existing) {
const key = bindingMatchKey(binding.match);
if (!existingMatchMap.has(key)) existingMatchMap.set(key, normalizeAgentId(binding.agentId));
}
const added = [];
const skipped = [];
const conflicts = [];
for (const binding of bindings) {
const agentId = normalizeAgentId(binding.agentId);
const key = bindingMatchKey(binding.match);
const existingAgentId = existingMatchMap.get(key);
if (existingAgentId) {
if (existingAgentId === agentId) skipped.push(binding);
else conflicts.push({
binding,
existingAgentId
});
continue;
}
existingMatchMap.set(key, agentId);
added.push({
...binding,
agentId
});
}
if (added.length === 0) return {
config: cfg,
added,
skipped,
conflicts
};
return {
config: {
...cfg,
bindings: [...existing, ...added]
},
added,
skipped,
conflicts
};
}
function resolveDefaultAccountId$1(cfg, provider) {
const plugin = getChannelPlugin(provider);
if (!plugin) return DEFAULT_ACCOUNT_ID;
return resolveChannelDefaultAccountId({
plugin,
cfg
});
}
function buildChannelBindings(params) {
const bindings = [];
const agentId = normalizeAgentId(params.agentId);
for (const channel of params.selection) {
const match = { channel };
const accountId = params.accountIds?.[channel]?.trim();
if (accountId) match.accountId = accountId;
else if (getChannelPlugin(channel)?.meta.forceAccountBinding) match.accountId = resolveDefaultAccountId$1(params.config, channel);
bindings.push({
agentId,
match
});
}
return bindings;
}
function parseBindingSpecs(params) {
const bindings = [];
const errors = [];
const specs = params.specs ?? [];
const agentId = normalizeAgentId(params.agentId);
for (const raw of specs) {
const trimmed = raw?.trim();
if (!trimmed) continue;
const [channelRaw, accountRaw] = trimmed.split(":", 2);
const channel = normalizeChannelId(channelRaw);
if (!channel) {
errors.push(`Unknown channel "${channelRaw}".`);
continue;
}
let accountId = accountRaw?.trim();
if (accountRaw !== void 0 && !accountId) {
errors.push(`Invalid binding "${trimmed}" (empty account id).`);
continue;
}
if (!accountId) {
if (getChannelPlugin(channel)?.meta.forceAccountBinding) accountId = resolveDefaultAccountId$1(params.config, channel);
}
const match = { channel };
if (accountId) match.accountId = accountId;
bindings.push({
agentId,
match
});
}
return {
bindings,
errors
};
}
//#endregion
//#region src/commands/agents.command-shared.ts
function createQuietRuntime(runtime) {
return {
...runtime,
log: () => {}
};
}
async function requireValidConfig(runtime) {
const snapshot = await readConfigFileSnapshot();
if (snapshot.exists && !snapshot.valid) {
const issues = snapshot.issues.length > 0 ? snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n") : "Unknown validation issue.";
runtime.error(`Config invalid:\n${issues}`);
runtime.error(`Fix the config or run ${formatCliCommand("openclaw doctor")}.`);
runtime.exit(1);
return null;
}
return snapshot.config;
}
//#endregion
//#region src/commands/agents.commands.add.ts
async function fileExists(pathname) {
try {
await fs$1.stat(pathname);
return true;
} catch {
return false;
}
}
async function agentsAddCommand(opts, runtime = defaultRuntime, params) {
const cfg = await requireValidConfig(runtime);
if (!cfg) return;
const workspaceFlag = opts.workspace?.trim();
const nameInput = opts.name?.trim();
const hasFlags = params?.hasFlags === true;
const nonInteractive = Boolean(opts.nonInteractive || hasFlags);
if (nonInteractive && !workspaceFlag) {
runtime.error("Non-interactive mode requires --workspace. Re-run without flags to use the wizard.");
runtime.exit(1);
return;
}
if (nonInteractive) {
if (!nameInput) {
runtime.error("Agent name is required in non-interactive mode.");
runtime.exit(1);
return;
}
if (!workspaceFlag) {
runtime.error("Non-interactive mode requires --workspace. Re-run without flags to use the wizard.");
runtime.exit(1);
return;
}
const agentId = normalizeAgentId(nameInput);
if (agentId === DEFAULT_AGENT_ID) {
runtime.error(`"${DEFAULT_AGENT_ID}" is reserved. Choose another name.`);
runtime.exit(1);
return;
}
if (agentId !== nameInput) runtime.log(`Normalized agent id to "${agentId}".`);
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) >= 0) {
runtime.error(`Agent "${agentId}" already exists.`);
runtime.exit(1);
return;
}
const workspaceDir = resolveUserPath(workspaceFlag);
const agentDir = opts.agentDir?.trim() ? resolveUserPath(opts.agentDir.trim()) : resolveAgentDir(cfg, agentId);
const model = opts.model?.trim();
const nextConfig = applyAgentConfig(cfg, {
agentId,
name: nameInput,
workspace: workspaceDir,
agentDir,
...model ? { model } : {}
});
const bindingParse = parseBindingSpecs({
agentId,
specs: opts.bind,
config: nextConfig
});
if (bindingParse.errors.length > 0) {
runtime.error(bindingParse.errors.join("\n"));
runtime.exit(1);
return;
}
const bindingResult = bindingParse.bindings.length > 0 ? applyAgentBindings(nextConfig, bindingParse.bindings) : {
config: nextConfig,
added: [],
skipped: [],
conflicts: []
};
await writeConfigFile(bindingResult.config);
if (!opts.json) logConfigUpdated(runtime);
await ensureWorkspaceAndSessions(workspaceDir, opts.json ? createQuietRuntime(runtime) : runtime, {
skipBootstrap: Boolean(bindingResult.config.agents?.defaults?.skipBootstrap),
agentId
});
const payload = {
agentId,
name: nameInput,
workspace: workspaceDir,
agentDir,
model,
bindings: {
added: bindingResult.added.map(describeBinding),
skipped: bindingResult.skipped.map(describeBinding),
conflicts: bindingResult.conflicts.map((conflict) => `${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`)
}
};
if (opts.json) runtime.log(JSON.stringify(payload, null, 2));
else {
runtime.log(`Agent: ${agentId}`);
runtime.log(`Workspace: ${shortenHomePath(workspaceDir)}`);
runtime.log(`Agent dir: ${shortenHomePath(agentDir)}`);
if (model) runtime.log(`Model: ${model}`);
if (bindingResult.conflicts.length > 0) runtime.error(["Skipped bindings already claimed by another agent:", ...bindingResult.conflicts.map((conflict) => `- ${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`)].join("\n"));
}
return;
}
const prompter = createClackPrompter();
try {
await prompter.intro("Add OpenClaw agent");
const name = nameInput ?? await prompter.text({
message: "Agent name",
validate: (value) => {
if (!value?.trim()) return "Required";
if (normalizeAgentId(value) === DEFAULT_AGENT_ID) return `"${DEFAULT_AGENT_ID}" is reserved. Choose another name.`;
}
});
const agentName = String(name).trim();
const agentId = normalizeAgentId(agentName);
if (agentName !== agentId) await prompter.note(`Normalized id to "${agentId}".`, "Agent id");
if (listAgentEntries(cfg).find((agent) => normalizeAgentId(agent.id) === agentId)) {
if (!await prompter.confirm({
message: `Agent "${agentId}" already exists. Update it?`,
initialValue: false
})) {
await prompter.outro("No changes made.");
return;
}
}
const workspaceDefault = resolveAgentWorkspaceDir(cfg, agentId);
const workspaceInput = await prompter.text({
message: "Workspace directory",
initialValue: workspaceDefault,
validate: (value) => value?.trim() ? void 0 : "Required"
});
const workspaceDir = resolveUserPath(String(workspaceInput).trim() || workspaceDefault);
const agentDir = resolveAgentDir(cfg, agentId);
let nextConfig = applyAgentConfig(cfg, {
agentId,
name: agentName,
workspace: workspaceDir,
agentDir
});
const defaultAgentId = resolveDefaultAgentId(cfg);
if (defaultAgentId !== agentId) {
const sourceAuthPath = resolveAuthStorePath(resolveAgentDir(cfg, defaultAgentId));
const destAuthPath = resolveAuthStorePath(agentDir);
if (!(path.resolve(sourceAuthPath).toLowerCase() === path.resolve(destAuthPath).toLowerCase()) && await fileExists(sourceAuthPath) && !await fileExists(destAuthPath)) {
if (await prompter.confirm({
message: `Copy auth profiles from "${defaultAgentId}"?`,
initialValue: false
})) {
await fs$1.mkdir(path.dirname(destAuthPath), { recursive: true });
await fs$1.copyFile(sourceAuthPath, destAuthPath);
await prompter.note(`Copied auth profiles from "${defaultAgentId}".`, "Auth profiles");
}
}
}
if (await prompter.confirm({
message: "Configure model/auth for this agent now?",
initialValue: false
})) {
const authResult = await applyAuthChoice({
authChoice: await promptAuthChoiceGrouped({
prompter,
store: ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }),
includeSkip: true
}),
config: nextConfig,
prompter,
runtime,
agentDir,
setDefaultModel: false,
agentId
});
nextConfig = authResult.config;
if (authResult.agentModelOverride) nextConfig = applyAgentConfig(nextConfig, {
agentId,
model: authResult.agentModelOverride
});
}
await warnIfModelConfigLooksOff(nextConfig, prompter, {
agentId,
agentDir
});
let selection = [];
const channelAccountIds = {};
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
allowSignalInstall: true,
onSelection: (value) => {
selection = value;
},
promptAccountIds: true,
onAccountId: (channel, accountId) => {
channelAccountIds[channel] = accountId;
}
});
if (selection.length > 0) if (await prompter.confirm({
message: "Route selected channels to this agent now? (bindings)",
initialValue: false
})) {
const desiredBindings = buildChannelBindings({
agentId,
selection,
config: nextConfig,
accountIds: channelAccountIds
});
const result = applyAgentBindings(nextConfig, desiredBindings);
nextConfig = result.config;
if (result.conflicts.length > 0) await prompter.note(["Skipped bindings already claimed by another agent:", ...result.conflicts.map((conflict) => `- ${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`)].join("\n"), "Routing bindings");
} else await prompter.note(["Routing unchanged. Add bindings when you're ready.", "Docs: https://docs.openclaw.ai/concepts/multi-agent"].join("\n"), "Routing");
await writeConfigFile(nextConfig);
logConfigUpdated(runtime);
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
agentId
});
const payload = {
agentId,
name: agentName,
workspace: workspaceDir,
agentDir
};
if (opts.json) runtime.log(JSON.stringify(payload, null, 2));
await prompter.outro(`Agent "${agentId}" ready.`);
} catch (err) {
if (err instanceof WizardCancelledError) {
runtime.exit(0);
return;
}
throw err;
}
}
//#endregion
//#region src/commands/agents.commands.delete.ts
async function agentsDeleteCommand(opts, runtime = defaultRuntime) {
const cfg = await requireValidConfig(runtime);
if (!cfg) return;
const input = opts.id?.trim();
if (!input) {
runtime.error("Agent id is required.");
runtime.exit(1);
return;
}
const agentId = normalizeAgentId(input);
if (agentId !== input) runtime.log(`Normalized agent id to "${agentId}".`);
if (agentId === DEFAULT_AGENT_ID) {
runtime.error(`"${DEFAULT_AGENT_ID}" cannot be deleted.`);
runtime.exit(1);
return;
}
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {
runtime.error(`Agent "${agentId}" not found.`);
runtime.exit(1);
return;
}
if (!opts.force) {
if (!process.stdin.isTTY) {
runtime.error("Non-interactive session. Re-run with --force.");
runtime.exit(1);
return;
}
if (!await createClackPrompter().confirm({
message: `Delete agent "${agentId}" and prune workspace/state?`,
initialValue: false
})) {
runtime.log("Cancelled.");
return;
}
}
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const agentDir = resolveAgentDir(cfg, agentId);
const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId);
const result = pruneAgentConfig(cfg, agentId);
await writeConfigFile(result.config);
if (!opts.json) logConfigUpdated(runtime);
const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
await moveToTrash(workspaceDir, quietRuntime);
await moveToTrash(agentDir, quietRuntime);
await moveToTrash(sessionsDir, quietRuntime);
if (opts.json) runtime.log(JSON.stringify({
agentId,
workspace: workspaceDir,
agentDir,
sessionsDir,
removedBindings: result.removedBindings,
removedAllow: result.removedAllow
}, null, 2));
else runtime.log(`Deleted agent: ${agentId}`);
}
//#endregion
//#region src/commands/agents.commands.identity.ts
const normalizeWorkspacePath = (input) => path.resolve(resolveUserPath(input));
const coerceTrimmed = (value) => {
const trimmed = value?.trim();
return trimmed ? trimmed : void 0;
};
async function loadIdentityFromFile(filePath) {
try {
const parsed = parseIdentityMarkdown(await fs$1.readFile(filePath, "utf-8"));
if (!identityHasValues(parsed)) return null;
return parsed;
} catch {
return null;
}
}
function resolveAgentIdByWorkspace(cfg, workspaceDir) {
const list = listAgentEntries(cfg);
const ids = list.length > 0 ? list.map((entry) => normalizeAgentId(entry.id)) : [resolveDefaultAgentId(cfg)];
const normalizedTarget = normalizeWorkspacePath(workspaceDir);
return ids.filter((id) => normalizeWorkspacePath(resolveAgentWorkspaceDir(cfg, id)) === normalizedTarget);
}
async function agentsSetIdentityCommand(opts, runtime = defaultRuntime) {
const cfg = await requireValidConfig(runtime);
if (!cfg) return;
const agentRaw = coerceTrimmed(opts.agent);
const nameRaw = coerceTrimmed(opts.name);
const emojiRaw = coerceTrimmed(opts.emoji);
const themeRaw = coerceTrimmed(opts.theme);
const avatarRaw = coerceTrimmed(opts.avatar);
const hasExplicitIdentity = Boolean(nameRaw || emojiRaw || themeRaw || avatarRaw);
const identityFileRaw = coerceTrimmed(opts.identityFile);
const workspaceRaw = coerceTrimmed(opts.workspace);
const wantsIdentityFile = Boolean(opts.fromIdentity || identityFileRaw || !hasExplicitIdentity);
let identityFilePath;
let workspaceDir;
if (identityFileRaw) {
identityFilePath = normalizeWorkspacePath(identityFileRaw);
workspaceDir = path.dirname(identityFilePath);
} else if (workspaceRaw) workspaceDir = normalizeWorkspacePath(workspaceRaw);
else if (wantsIdentityFile || !agentRaw) workspaceDir = path.resolve(process.cwd());
let agentId = agentRaw ? normalizeAgentId(agentRaw) : void 0;
if (!agentId) {
if (!workspaceDir) {
runtime.error("Select an agent with --agent or provide a workspace via --workspace.");
runtime.exit(1);
return;
}
const matches = resolveAgentIdByWorkspace(cfg, workspaceDir);
if (matches.length === 0) {
runtime.error(`No agent workspace matches ${shortenHomePath(workspaceDir)}. Pass --agent to target a specific agent.`);
runtime.exit(1);
return;
}
if (matches.length > 1) {
runtime.error(`Multiple agents match ${shortenHomePath(workspaceDir)}: ${matches.join(", ")}. Pass --agent to choose one.`);
runtime.exit(1);
return;
}
agentId = matches[0];
}
let identityFromFile = null;
if (wantsIdentityFile) {
if (identityFilePath) identityFromFile = await loadIdentityFromFile(identityFilePath);
else if (workspaceDir) identityFromFile = loadAgentIdentity(workspaceDir);
if (!identityFromFile) {
const targetPath = identityFilePath ?? (workspaceDir ? path.join(workspaceDir, DEFAULT_IDENTITY_FILENAME) : "IDENTITY.md");
runtime.error(`No identity data found in ${shortenHomePath(targetPath)}.`);
runtime.exit(1);
return;
}
}
const fileTheme = identityFromFile?.theme ?? identityFromFile?.creature ?? identityFromFile?.vibe ?? void 0;
const incomingIdentity = {
...nameRaw || identityFromFile?.name ? { name: nameRaw ?? identityFromFile?.name } : {},
...emojiRaw || identityFromFile?.emoji ? { emoji: emojiRaw ?? identityFromFile?.emoji } : {},
...themeRaw || fileTheme ? { theme: themeRaw ?? fileTheme } : {},
...avatarRaw || identityFromFile?.avatar ? { avatar: avatarRaw ?? identityFromFile?.avatar } : {}
};
if (!incomingIdentity.name && !incomingIdentity.emoji && !incomingIdentity.theme && !incomingIdentity.avatar) {
runtime.error("No identity fields provided. Use --name/--emoji/--theme/--avatar or --from-identity.");
runtime.exit(1);
return;
}
const list = listAgentEntries(cfg);
const index = findAgentEntryIndex(list, agentId);
const base = index >= 0 ? list[index] : { id: agentId };
const nextIdentity = {
...base.identity,
...incomingIdentity
};
const nextEntry = {
...base,
identity: nextIdentity
};
const nextList = [...list];
if (index >= 0) nextList[index] = nextEntry;
else {
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
if (nextList.length === 0 && agentId !== defaultId) nextList.push({ id: defaultId });
nextList.push(nextEntry);
}
await writeConfigFile({
...cfg,
agents: {
...cfg.agents,
list: nextList
}
});
if (opts.json) {
runtime.log(JSON.stringify({
agentId,
identity: nextIdentity,
workspace: workspaceDir ?? null,
identityFile: identityFilePath ?? null
}, null, 2));
return;
}
logConfigUpdated(runtime);
runtime.log(`Agent: ${agentId}`);
if (nextIdentity.name) runtime.log(`Name: ${nextIdentity.name}`);
if (nextIdentity.theme) runtime.log(`Theme: ${nextIdentity.theme}`);
if (nextIdentity.emoji) runtime.log(`Emoji: ${nextIdentity.emoji}`);
if (nextIdentity.avatar) runtime.log(`Avatar: ${nextIdentity.avatar}`);
if (workspaceDir) runtime.log(`Workspace: ${shortenHomePath(workspaceDir)}`);
}
//#endregion
//#region src/commands/agents.providers.ts
function providerAccountKey(provider, accountId) {
return `${provider}:${accountId ?? DEFAULT_ACCOUNT_ID}`;
}
function formatChannelAccountLabel(params) {
return `${getChannelPlugin(params.provider)?.meta.label ?? params.provider} ${params.name?.trim() ? `${params.accountId} (${params.name.trim()})` : params.accountId}`;
}
function formatProviderState(entry) {
const parts = [entry.state];
if (entry.enabled === false && entry.state !== "disabled") parts.push("disabled");
return parts.join(", ");
}
async function buildProviderStatusIndex(cfg) {
const map = /* @__PURE__ */ new Map();
for (const plugin of listChannelPlugins()) {
const accountIds = plugin.config.listAccountIds(cfg);
for (const accountId of accountIds) {
const account = plugin.config.resolveAccount(cfg, accountId);
const snapshot = plugin.config.describeAccount?.(account, cfg);
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : typeof snapshot?.enabled === "boolean" ? snapshot.enabled : account.enabled;
const configured = plugin.config.isConfigured ? await plugin.config.isConfigured(account, cfg) : snapshot?.configured;
const resolvedEnabled = typeof enabled === "boolean" ? enabled : true;
const resolvedConfigured = typeof configured === "boolean" ? configured : true;
const state = plugin.status?.resolveAccountState?.({
account,
cfg,
configured: resolvedConfigured,
enabled: resolvedEnabled
}) ?? (typeof snapshot?.linked === "boolean" ? snapshot.linked ? "linked" : "not linked" : resolvedConfigured ? "configured" : "not configured");
const name = snapshot?.name ?? account.name;
map.set(providerAccountKey(plugin.id, accountId), {
provider: plugin.id,
accountId,
name,
state,
enabled,
configured
});
}
}
return map;
}
function resolveDefaultAccountId(cfg, provider) {
const plugin = getChannelPlugin(provider);
if (!plugin) return DEFAULT_ACCOUNT_ID;
return resolveChannelDefaultAccountId({
plugin,
cfg
});
}
function shouldShowProviderEntry(entry, cfg) {
const plugin = getChannelPlugin(entry.provider);
if (!plugin) return Boolean(entry.configured);
if (plugin.meta.showConfigured === false) {
const providerConfig = cfg[plugin.id];
return Boolean(entry.configured) || Boolean(providerConfig);
}
return Boolean(entry.configured);
}
function formatProviderEntry(entry) {
return `${formatChannelAccountLabel({
provider: entry.provider,
accountId: entry.accountId,
name: entry.name
})}: ${formatProviderState(entry)}`;
}
function summarizeBindings(cfg, bindings) {
if (bindings.length === 0) return [];
const seen = /* @__PURE__ */ new Map();
for (const binding of bindings) {
const channel = normalizeChannelId(binding.match.channel);
if (!channel) continue;
const accountId = binding.match.accountId ?? resolveDefaultAccountId(cfg, channel);
const key = providerAccountKey(channel, accountId);
if (!seen.has(key)) {
const label = formatChannelAccountLabel({
provider: channel,
accountId
});
seen.set(key, label);
}
}
return [...seen.values()];
}
function listProvidersForAgent(params) {
const allProviderEntries = [...params.providerStatus.values()];
const providerLines = [];
if (params.bindings.length > 0) {
const seen = /* @__PURE__ */ new Set();
for (const binding of params.bindings) {
const channel = normalizeChannelId(binding.match.channel);
if (!channel) continue;
const accountId = binding.match.accountId ?? resolveDefaultAccountId(params.cfg, channel);
const key = providerAccountKey(channel, accountId);
if (seen.has(key)) continue;
seen.add(key);
const status = params.providerStatus.get(key);
if (status) providerLines.push(formatProviderEntry(status));
else providerLines.push(`${formatChannelAccountLabel({
provider: channel,
accountId
})}: unknown`);
}
return providerLines;
}
if (params.summaryIsDefault) {
for (const entry of allProviderEntries) if (shouldShowProviderEntry(entry, params.cfg)) providerLines.push(formatProviderEntry(entry));
}
return providerLines;
}
//#endregion
//#region src/commands/agents.commands.list.ts
function formatSummary(summary) {
const defaultTag = summary.isDefault ? " (default)" : "";
const header = summary.name && summary.name !== summary.id ? `${summary.id}${defaultTag} (${summary.name})` : `${summary.id}${defaultTag}`;
const identityParts = [];
if (summary.identityEmoji) identityParts.push(summary.identityEmoji);
if (summary.identityName) identityParts.push(summary.identityName);
const identityLine = identityParts.length > 0 ? identityParts.join(" ") : null;
const identitySource = summary.identitySource === "identity" ? "IDENTITY.md" : summary.identitySource === "config" ? "config" : null;
const lines = [`- ${header}`];
if (identityLine) lines.push(` Identity: ${identityLine}${identitySource ? ` (${identitySource})` : ""}`);
lines.push(` Workspace: ${shortenHomePath(summary.workspace)}`);
lines.push(` Agent dir: ${shortenHomePath(summary.agentDir)}`);
if (summary.model) lines.push(` Model: ${summary.model}`);
lines.push(` Routing rules: ${summary.bindings}`);
if (summary.routes?.length) lines.push(` Routing: ${summary.routes.join(", ")}`);
if (summary.providers?.length) {
lines.push(" Providers:");
for (const provider of summary.providers) lines.push(` - ${provider}`);
}
if (summary.bindingDetails?.length) {
lines.push(" Routing rules:");
for (const binding of summary.bindingDetails) lines.push(` - ${binding}`);
}
return lines.join("\n");
}
async function agentsListCommand(opts, runtime = defaultRuntime) {
const cfg = await requireValidConfig(runtime);
if (!cfg) return;
const summaries = buildAgentSummaries(cfg);
const bindingMap = /* @__PURE__ */ new Map();
for (const binding of cfg.bindings ?? []) {
const agentId = normalizeAgentId(binding.agentId);
const list = bindingMap.get(agentId) ?? [];
list.push(binding);
bindingMap.set(agentId, list);
}
if (opts.bindings) for (const summary of summaries) {
const bindings = bindingMap.get(summary.id) ?? [];
if (bindings.length > 0) summary.bindingDetails = bindings.map((binding) => describeBinding(binding));
}
const providerStatus = await buildProviderStatusIndex(cfg);
for (const summary of summaries) {
const bindings = bindingMap.get(summary.id) ?? [];
const routes = summarizeBindings(cfg, bindings);
if (routes.length > 0) summary.routes = routes;
else if (summary.isDefault) summary.routes = ["default (no explicit rules)"];
const providerLines = listProvidersForAgent({
summaryIsDefault: summary.isDefault,
cfg,
bindings,
providerStatus
});
if (providerLines.length > 0) summary.providers = providerLines;
}
if (opts.json) {
runtime.log(JSON.stringify(summaries, null, 2));
return;
}
const lines = ["Agents:", ...summaries.map(formatSummary)];
lines.push("Routing rules map channel/account/peer to an agent. Use --bindings for full rules.");
lines.push(`Channel status reflects local config/creds. For live health: ${formatCliCommand("openclaw channels status --probe")}.`);
runtime.log(lines.join("\n"));
}
//#endregion
//#region src/commands/sessions.ts
const KIND_PAD = 6;
const KEY_PAD = 26;
const AGE_PAD = 9;
const MODEL_PAD = 14;
const TOKENS_PAD = 20;
const formatKTokens = (value) => `${(value / 1e3).toFixed(value >= 1e4 ? 0 : 1)}k`;
const truncateKey = (key) => {
if (key.length <= KEY_PAD) return key;
const head = Math.max(4, KEY_PAD - 10);
return `${key.slice(0, head)}...${key.slice(-6)}`;
};
const colorByPct = (label, pct, rich) => {
if (!rich || pct === null) return label;
if (pct >= 95) return theme.error(label);
if (pct >= 80) return theme.warn(label);
if (pct >= 60) return theme.success(label);
return theme.muted(label);
};
const formatTokensCell = (total, contextTokens, rich) => {
if (!total) return "-".padEnd(TOKENS_PAD);
const totalLabel = formatKTokens(total);
const ctxLabel = contextTokens ? formatKTokens(contextTokens) : "?";
const pct = contextTokens ? Math.min(999, Math.round(total / contextTokens * 100)) : null;
return colorByPct(`${totalLabel}/${ctxLabel} (${pct ?? "?"}%)`.padEnd(TOKENS_PAD), pct, rich);
};
const formatKindCell = (kind, rich) => {
const label = kind.padEnd(KIND_PAD);
if (!rich) return label;
if (kind === "group") return theme.accentBright(label);
if (kind === "global") return theme.warn(label);
if (kind === "direct") return theme.accent(label);
return theme.muted(label);
};
const formatAgeCell = (updatedAt, rich) => {
const padded = (updatedAt ? formatAge(Date.now() - updatedAt) : "unknown").padEnd(AGE_PAD);
return rich ? theme.muted(padded) : padded;
};
const formatModelCell = (model, rich) => {
const label = (model ?? "unknown").padEnd(MODEL_PAD);
return rich ? theme.info(label) : label;
};
const formatFlagsCell = (row, rich) => {
const label = [
row.thinkingLevel ? `think:${row.thinkingLevel}` : null,
row.verboseLevel ? `verbose:${row.verboseLevel}` : null,
row.reasoningLevel ? `reasoning:${row.reasoningLevel}` : null,
row.elevatedLevel ? `elev:${row.elevatedLevel}` : null,
row.responseUsage ? `usage:${row.responseUsage}` : null,
row.groupActivation ? `activation:${row.groupActivation}` : null,
row.systemSent ? "system" : null,
row.abortedLastRun ? "aborted" : null,
row.sessionId ? `id:${row.sessionId}` : null
].filter(Boolean).join(" ");
return label.length === 0 ? "" : rich ? theme.muted(label) : label;
};
const formatAge = (ms) => {
if (!ms || ms < 0) return "unknown";
const minutes = Math.round(ms / 6e4);
if (minutes < 1) return "just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.round(minutes / 60);
if (hours < 48) return `${hours}h ago`;
return `${Math.round(hours / 24)}d ago`;
};
function classifyKey(key, entry) {
if (key === "global") return "global";
if (key === "unknown") return "unknown";
if (entry?.chatType === "group" || entry?.chatType === "channel") return "group";
if (key.includes(":group:") || key.includes(":channel:")) return "group";
return "direct";
}
function toRows(store) {
return Object.entries(store).map(([key, entry]) => {
const updatedAt = entry?.updatedAt ?? null;
return {
key,
kind: classifyKey(key, entry),
updatedAt,
ageMs: updatedAt ? Date.now() - updatedAt : null,
sessionId: entry?.sessionId,
systemSent: entry?.systemSent,
abortedLastRun: entry?.abortedLastRun,
thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel,
reasoningLevel: entry?.reasoningLevel,
elevatedLevel: entry?.elevatedLevel,
responseUsage: entry?.responseUsage,
groupActivation: entry?.groupActivation,
inputTokens: entry?.inputTokens,
outputTokens: entry?.outputTokens,
totalTokens: entry?.totalTokens,
model: entry?.model,
contextTokens: entry?.contextTokens
};
}).toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
}
async function sessionsCommand(opts, runtime) {
const cfg = loadConfig();
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL
});
const configContextTokens = cfg.agents?.defaults?.contextTokens ?? lookupContextTokens(resolved.model) ?? DEFAULT_CONTEXT_TOKENS;
const configModel = resolved.model ?? DEFAULT_MODEL;
const storePath = resolveStorePath(opts.store ?? cfg.session?.store);
const store = loadSessionStore(storePath);
let activeMinutes;
if (opts.active !== void 0) {
const parsed = Number.parseInt(String(opts.active), 10);
if (Number.isNaN(parsed) || parsed <= 0) {
runtime.error("--active must be a positive integer (minutes)");
runtime.exit(1);
return;
}
activeMinutes = parsed;
}
const rows = toRows(store).filter((row) => {
if (activeMinutes === void 0) return true;
if (!row.updatedAt) return false;
return Date.now() - row.updatedAt <= activeMinutes * 6e4;
});
if (opts.json) {
runtime.log(JSON.stringify({
path: storePath,
count: rows.length,
activeMinutes: activeMinutes ?? null,
sessions: rows.map((r) => ({
...r,
contextTokens: r.contextTokens ?? lookupContextTokens(r.model) ?? configContextTokens ?? null,
model: r.model ?? configModel ?? null
}))
}, null, 2));
return;
}
runtime.log(info(`Session store: ${storePath}`));
runtime.log(info(`Sessions listed: ${rows.length}`));
if (activeMinutes) runtime.log(info(`Filtered to last ${activeMinutes} minute(s)`));
if (rows.length === 0) {
runtime.log("No sessions found.");
return;
}
const rich = isRich();
const header = [
"Kind".padEnd(KIND_PAD),
"Key".padEnd(KEY_PAD),
"Age".padEnd(AGE_PAD),
"Model".padEnd(MODEL_PAD),
"Tokens (ctx %)".padEnd(TOKENS_PAD),
"Flags"
].join(" ");
runtime.log(rich ? theme.heading(header) : header);
for (const row of rows) {
const model = row.model ?? configModel;
const contextTokens = row.contextTokens ?? lookupContextTokens(model) ?? configContextTokens;
const input = row.inputTokens ?? 0;
const output = row.outputTokens ?? 0;
const total = row.totalTokens ?? input + output;
const keyLabel = truncateKey(row.key).padEnd(KEY_PAD);
const keyCell = rich ? theme.accent(keyLabel) : keyLabel;
const line = [
formatKindCell(row.kind, rich),
keyCell,
formatAgeCell(row.updatedAt, rich),
formatModelCell(model, rich),
formatTokensCell(total, contextTokens ?? null, rich),
formatFlagsCell(row, rich)
].join(" ");
runtime.log(line.trimEnd());
}
}
//#endregion
//#region src/cli/browser-cli-shared.ts
function normalizeQuery(query) {
if (!query) return;
const out = {};
for (const [key, value] of Object.entries(query)) {
if (value === void 0) continue;
out[key] = String(value);
}
return Object.keys(out).length ? out : void 0;
}
async function callBrowserRequest(opts, params, extra) {
const resolvedTimeoutMs = typeof extra?.timeoutMs === "number" && Number.isFinite(extra.timeoutMs) ? Math.max(1, Math.floor(extra.timeoutMs)) : typeof opts.timeout === "string" ? Number.parseInt(opts.timeout, 10) : void 0;
const resolvedTimeout = typeof resolvedTimeoutMs === "number" && Number.isFinite(resolvedTimeoutMs) ? resolvedTimeoutMs : void 0;
const timeout = typeof resolvedTimeout === "number" ? String(resolvedTimeout) : opts.timeout;
const payload = await callGatewayFromCli("browser.request", {
...opts,
timeout
}, {
method: params.method,
path: params.path,
query: normalizeQuery(params.query),
body: params.body,
timeoutMs: resolvedTimeout
}, { progress: extra?.progress });
if (payload === void 0) throw new Error("Unexpected browser.request response");
return payload;
}
//#endregion
//#region src/cli/browser-cli-actions-input/shared.ts
function resolveBrowserActionContext(cmd, parentOpts) {
const parent = parentOpts(cmd);
return {
parent,
profile: parent?.browserProfile
};
}
async function callBrowserAct(params) {
return await callBrowserRequest(params.parent, {
method: "POST",
path: "/act",
query: params.profile ? { profile: params.profile } : void 0,
body: params.body
}, { timeoutMs: params.timeoutMs ?? 2e4 });
}
function requireRef(ref) {
const refValue = typeof ref === "string" ? ref.trim() : "";
if (!refValue) {
defaultRuntime.error(danger("ref is required"));
defaultRuntime.exit(1);
return null;
}
return refValue;
}
async function readFile(path) {
return await (await import("node:fs/promises")).readFile(path, "utf8");
}
async function readFields(opts) {
const payload = opts.fieldsFile ? await readFile(opts.fieldsFile) : opts.fields ?? "";
if (!payload.trim()) throw new Error("fields are required");
const parsed = JSON.parse(payload);
if (!Array.isArray(parsed)) throw new Error("fields must be an array");
return parsed.map((entry, index) => {
if (!entry || typeof entry !== "object") throw new Error(`fields[${index}] must be an object`);
const rec = entry;
const ref = typeof rec.ref === "string" ? rec.ref.trim() : "";
const type = typeof rec.type === "string" ? rec.type.trim() : "";
if (!ref || !type) throw new Error(`fields[${index}] must include ref and type`);
if (typeof rec.value === "string" || typeof rec.value === "number" || typeof rec.value === "boolean") return {
ref,
type,
value: rec.value
};
if (rec.value === void 0 || rec.value === null) return {
ref,
type
};
throw new Error(`fields[${index}].value must be string, number, boolean, or null`);
});
}
//#endregion
//#region src/cli/browser-cli-actions-input/register.element.ts
function registerBrowserElementCommands(browser, parentOpts) {
browser.command("click").description("Click an element by ref from snapshot").argument("<ref>", "Ref id from snapshot").option("--target-id <id>", "CDP target id (or unique prefix)").option("--double", "Double click", false).option("--button <left|right|middle>", "Mouse button to use").option("--modifiers <list>", "Comma-separated modifiers (Shift,Alt,Meta)").action(async (ref, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) return;
const modifiers = opts.modifiers ? String(opts.modifiers).split(",").map((v) => v.trim()).filter(Boolean) : void 0;
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "click",
ref: refValue,
targetId: opts.targetId?.trim() || void 0,
doubleClick: Boolean(opts.double),
button: opts.button?.trim() || void 0,
modifiers
}
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
const suffix = result.url ? ` on ${result.url}` : "";
defaultRuntime.log(`clicked ref ${refValue}${suffix}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
});
browser.command("type").description("Type into an element by ref from snapshot").argument("<ref>", "Ref id from snapshot").argument("<text>", "Text to type").option("--submit", "Press Enter after typing", false).option("--slowly", "Type slowly (human-like)", false).option("--target-id <id>", "CDP target id (or unique prefix)").action(async (ref, text, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) return;
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "type",
ref: refValue,
text,
submit: Boolean(opts.submit),
slowly: Boolean(opts.slowly),
targetId: opts.targetId?.trim() || void 0
}
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log(`typed into ref ${refValue}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
});
browser.command("press").description("Press a key").argument("<key>", "Key to press (e.g. Enter)").option("--target-id <id>", "CDP target id (or unique prefix)").action(async (key, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "press",
key,
targetId: opts.targetId?.trim() || void 0
}
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log(`pressed ${key}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
});
browser.command("hover").description("Hover an element by ai ref").argument("<ref>", "Ref id from snapshot").option("--target-id <id>", "CDP target id (or unique prefix)").action(async (ref, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "hover",
ref,
targetId: opts.targetId?.trim() || void 0
}
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log(`hovered ref ${ref}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
});
browser.command("scrollintoview").description("Scroll an element into view by ref from snapshot").argument("<ref>", "Ref id from snapshot").option("--target-id <id>", "CDP target id (or unique prefix)").option("--timeout-ms <ms>", "How long to wait for scroll (default: 20000)", (v) => Number(v)).action(async (ref, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) return;
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "scrollIntoView",
ref: refValue,
targetId: opts.targetId?.trim() || void 0,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : void 0
},
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : void 0
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log(`scrolled into view: ${refValue}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
});
browser.command("drag").description("Drag from one ref to another").argument("<startRef>", "Start ref id").argument("<endRef>", "End ref id").option("--target-id <id>", "CDP target id (or unique prefix)").action(async (startRef, endRef, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "drag",
startRef,
endRef,
targetId: opts.t