UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,242 lines (1,230 loc) 232 kB
#!/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