UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,042 lines (1,031 loc) 41.6 kB
import "./paths-B4BZAPZh.js"; import { B as theme, P as setVerbose, S as shortenHomePath, y as resolveUserPath } from "./utils-CP9YLh6M.js"; import "./thinking-EAliFiVK.js"; import { Ht as createDefaultDeps, Mt as resolveSessionKeyForRequest, jt as agentCommand } from "./reply-BylFHBd4.js"; import { l as normalizeAgentId, n as DEFAULT_AGENT_ID, t as DEFAULT_ACCOUNT_ID } from "./session-key-CZ6OwgSB.js"; import { r as DEFAULT_CHAT_CHANNEL } from "./registry-B-j4DRfe.js"; import { f as defaultRuntime } from "./subsystem-BCQGGxdd.js"; import "./exec-DYqRzFbo.js"; import { _ as DEFAULT_IDENTITY_FILENAME, c as resolveAgentWorkspaceDir, i as resolveAgentDir, l as resolveDefaultAgentId, n as listAgentIds, t as listAgentEntries } from "./agent-scope-BnZW9Gh2.js"; import { Mt as resolveAuthStorePath, kt as ensureAuthProfileStore } from "./model-selection-CqaTAlhy.js"; import "./github-copilot-token-D2zp6kMZ.js"; import { t as formatCliCommand } from "./command-format-DEKzLnLg.js"; import "./boolean-BsqeuxE6.js"; import "./env-VriqyjXT.js"; import { h as GATEWAY_CLIENT_NAMES, l as normalizeMessageChannel, m as GATEWAY_CLIENT_MODES } from "./message-channel-Bena1Tzd.js"; import "./send-CsyONLnQ.js"; import { i as loadConfig, l as writeConfigFile } from "./config-PQiujvsf.js"; import "./manifest-registry-4k4vkhPS.js"; import "./runner-U04aiHHC.js"; import "./image-DxEpBZim.js"; import "./models-config-B459BnCS.js"; import "./pi-model-discovery-4uUnLc3n.js"; import "./pi-embedded-helpers-BdscQhyS.js"; import "./sandbox-UAzvS0V6.js"; import "./image-ops-CI1VknD1.js"; import "./common-a25M2Kvi.js"; import "./chrome-Dd5zBIFu.js"; import "./tailscale-C3JsBEiY.js"; import "./auth-DZSWPd8D.js"; import "./server-context-DAWsUNUs.js"; import "./frontmatter-6HZ0_y2V.js"; import "./skills-CLmzWt48.js"; import "./routes-DIQa_0pt.js"; import "./redact-B7Mjvk0c.js"; import "./errors-kfGqPQ4b.js"; import "./fs-safe-_OpKPuZA.js"; import "./paths-D1WZUYry.js"; import "./ssrf-6f5m2MMA.js"; import "./store-DSGYY-H9.js"; import "./ports-DwDxX6cS.js"; import "./trash-DLzIf__E.js"; import "./sessions-Z1BZU3xh.js"; import "./dock-7F-MiPtF.js"; import "./normalize-Ci8UyR3Z.js"; import "./accounts-Bj1dJ-fd.js"; import "./accounts-DOl1Wkxo.js"; import "./accounts-t6R5_PzC.js"; import "./bindings-Dja1z-h8.js"; import "./logging-xYH6GmRT.js"; import "./send-uu2Jc-Js.js"; import { n as listChannelPlugins, r as normalizeChannelId, t as getChannelPlugin } from "./plugins-CKbXkuXd.js"; import { s as resolveSessionTranscriptsDirForAgent } from "./paths-C2NfoGZE.js"; import "./tool-display-Bx4M6uNT.js"; import "./fetch-guard-C1sYsWgl.js"; import "./api-key-rotation-CpGefdGL.js"; import "./local-roots-DiDvXIIo.js"; import "./sqlite-JcMMx8Z5.js"; import "./model-catalog-CIixTnGt.js"; import "./tokens-D4lZk7-h.js"; import "./with-timeout-DijiQjw6.js"; import "./deliver-DgvS3uCz.js"; import "./diagnostic-UUwku4RV.js"; import "./diagnostic-session-state-ByqoIyGn.js"; import "./send-DbwyyQAv.js"; import "./model-VbWjwqaW.js"; import "./reply-prefix-DAK7-zK4.js"; import "./memory-cli-DxmtLhh_.js"; import "./manager-C0teWocQ.js"; import "./retry-rUEdE6zT.js"; import "./chunk-CMylpCPi.js"; import "./markdown-tables-B9VGUdDc.js"; import "./ir-24qCLTKH.js"; import "./render-CXDO_kgw.js"; import "./commands-registry-bb_U-3FM.js"; import "./client-BdSkEtCd.js"; import { a as randomIdempotencyKey, n as callGateway } from "./call-BCz_8mqq.js"; import "./pairing-token-B-_eiWlR.js"; import "./channel-activity-DynIQUB_.js"; import "./fetch-Bu5zBCce.js"; import "./tables-C70h_80U.js"; import "./send-CAfFUUkm.js"; import "./pairing-store-T5DpOfoL.js"; import "./proxy-BiOWrea2.js"; import { t as formatDocsLink } from "./links-BVCMOGeE.js"; import { n as runCommandWithRuntime } from "./cli-utils-CX1oQ81G.js"; import { t as formatHelpExamples } from "./help-format-dbk2xW0E.js"; import { n as withProgress } from "./progress-CaVMHLaE.js"; import "./resolve-route-2TU_aaLs.js"; import "./replies-NhrZJ3yf.js"; import "./skill-commands-N8Sho46-.js"; import "./workspace-dirs-CE3CxabZ.js"; import "./pi-tools.policy-DTy1DnPK.js"; import "./outbound-attachment-CntoLTI_.js"; import "./delivery-queue-CxDDEWQg.js"; import "./session-cost-usage-BlUDaylg.js"; import "./send-4LoRaltQ.js"; import { a as ensureWorkspaceAndSessions, l as moveToTrash } from "./onboard-helpers-DbjfBSMX.js"; import "./prompt-style-VqCNjBi8.js"; import "./pairing-labels-DaESq9ML.js"; import "./exec-approvals-q0C8VDMT.js"; import "./nodes-screen-5fMfTT2n.js"; import "./control-service-BoO2TGOx.js"; import "./stagger-B6VQyn1F.js"; import "./channel-selection-DuDSoVSC.js"; import { t as collectOption } from "./helpers-B5MBaaJr.js"; import "./catalog-CgtgPHMj.js"; import { t as hasExplicitOptions } from "./command-options-ByE5SNIE.js"; import "./note-C-OTWf0F.js"; import { t as WizardCancelledError } from "./prompts-xO_GDgJd.js"; import { t as createClackPrompter } from "./clack-prompter-C8qX15zH.js"; import { t as resolveChannelDefaultAccountId } from "./helpers-tGq4L3A8.js"; import "./plugin-auto-enable-DJOFx9vI.js"; import "./install-safe-path-DSS8Pu5f.js"; import "./npm-registry-spec-pAPjxG47.js"; import "./skill-scanner-D56xvwQF.js"; import "./installs-BNOz2SvJ.js"; import { r as setupChannels } from "./onboard-channels--VW5mf3G.js"; import { t as requireValidConfigSnapshot } from "./config-validation-C3sQczw2.js"; import { a as pruneAgentConfig, c as parseIdentityMarkdown, i as loadAgentIdentity, n as buildAgentSummaries, o as identityHasValues, r as findAgentEntryIndex, t as applyAgentConfig } from "./agents.config--k-cL9FH.js"; import "./shared-B-zggieW.js"; import "./auth-token-CliHE70g.js"; import { n as logConfigUpdated } from "./logging-Cl6-FWdR.js"; import "./provider-auth-helpers-Bnx6HWum.js"; import { n as promptAuthChoiceGrouped } from "./auth-choice-prompt-CpiJWkMe.js"; import { i as applyAuthChoice, r as warnIfModelConfigLooksOff } from "./auth-choice-DfFGpXtC.js"; import "./openai-model-default-8jds2v9m.js"; import "./vllm-setup-G6R-Hqd8.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/commands/agent-via-gateway.ts const NO_GATEWAY_TIMEOUT_MS = 2147e6; function parseTimeoutSeconds(opts) { const raw = opts.timeout !== void 0 ? Number.parseInt(String(opts.timeout), 10) : opts.cfg.agents?.defaults?.timeoutSeconds ?? 600; if (Number.isNaN(raw) || raw < 0) throw new Error("--timeout must be a non-negative integer (seconds; 0 means no timeout)"); return raw; } function formatPayloadForLog(payload) { const lines = []; if (payload.text) lines.push(payload.text.trimEnd()); const mediaUrl = typeof payload.mediaUrl === "string" && payload.mediaUrl.trim() ? payload.mediaUrl.trim() : void 0; const media = payload.mediaUrls ?? (mediaUrl ? [mediaUrl] : []); for (const url of media) lines.push(`MEDIA:${url}`); return lines.join("\n").trimEnd(); } async function agentViaGatewayCommand(opts, runtime) { const body = (opts.message ?? "").trim(); if (!body) throw new Error("Message (--message) is required"); if (!opts.to && !opts.sessionId && !opts.agent) throw new Error("Pass --to <E.164>, --session-id, or --agent to choose a session"); const cfg = loadConfig(); const agentIdRaw = opts.agent?.trim(); const agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : void 0; if (agentId) { if (!listAgentIds(cfg).includes(agentId)) throw new Error(`Unknown agent id "${agentIdRaw}". Use "${formatCliCommand("openclaw agents list")}" to see configured agents.`); } const timeoutSeconds = parseTimeoutSeconds({ cfg, timeout: opts.timeout }); const gatewayTimeoutMs = timeoutSeconds === 0 ? NO_GATEWAY_TIMEOUT_MS : Math.max(1e4, (timeoutSeconds + 30) * 1e3); const sessionKey = resolveSessionKeyForRequest({ cfg, agentId, to: opts.to, sessionId: opts.sessionId }).sessionKey; const channel = normalizeMessageChannel(opts.channel) ?? DEFAULT_CHAT_CHANNEL; const idempotencyKey = opts.runId?.trim() || randomIdempotencyKey(); const response = await withProgress({ label: "Waiting for agent reply…", indeterminate: true, enabled: opts.json !== true }, async () => await callGateway({ method: "agent", params: { message: body, agentId, to: opts.to, replyTo: opts.replyTo, sessionId: opts.sessionId, sessionKey, thinking: opts.thinking, deliver: Boolean(opts.deliver), channel, replyChannel: opts.replyChannel, replyAccountId: opts.replyAccount, timeout: timeoutSeconds, lane: opts.lane, extraSystemPrompt: opts.extraSystemPrompt, idempotencyKey }, expectFinal: true, timeoutMs: gatewayTimeoutMs, clientName: GATEWAY_CLIENT_NAMES.CLI, mode: GATEWAY_CLIENT_MODES.CLI })); if (opts.json) { runtime.log(JSON.stringify(response, null, 2)); return response; } const payloads = (response?.result)?.payloads ?? []; if (payloads.length === 0) { runtime.log(response?.summary ? String(response.summary) : "No reply from agent."); return response; } for (const payload of payloads) { const out = formatPayloadForLog(payload); if (out) runtime.log(out); } return response; } async function agentCliCommand(opts, runtime, deps) { const localOpts = { ...opts, agentId: opts.agent, replyAccountId: opts.replyAccount }; if (opts.local === true) return await agentCommand(localOpts, runtime, deps); try { return await agentViaGatewayCommand(opts, runtime); } catch (err) { runtime.error?.(`Gateway agent failed; falling back to embedded: ${String(err)}`); return await agentCommand(localOpts, runtime, deps); } } //#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) { return await requireValidConfigSnapshot(runtime); } //#endregion //#region src/commands/agents.commands.add.ts async function fileExists(pathname) { try { await fs.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.mkdir(path.dirname(destAuthPath), { recursive: true }); await fs.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(1); 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.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/cli/program/register.agent.ts function registerAgentCommands(program, args) { program.command("agent").description("Run an agent turn via the Gateway (use --local for embedded)").requiredOption("-m, --message <text>", "Message body for the agent").option("-t, --to <number>", "Recipient number in E.164 used to derive the session key").option("--session-id <id>", "Use an explicit session id").option("--agent <id>", "Agent id (overrides routing bindings)").option("--thinking <level>", "Thinking level: off | minimal | low | medium | high").option("--verbose <on|off>", "Persist agent verbose level for the session").option("--channel <channel>", `Delivery channel: ${args.agentChannelOptions} (default: ${DEFAULT_CHAT_CHANNEL})`).option("--reply-to <target>", "Delivery target override (separate from session routing)").option("--reply-channel <channel>", "Delivery channel override (separate from routing)").option("--reply-account <id>", "Delivery account id override").option("--local", "Run the embedded agent locally (requires model provider API keys in your shell)", false).option("--deliver", "Send the agent's reply back to the selected channel", false).option("--json", "Output result as JSON", false).option("--timeout <seconds>", "Override agent command timeout (seconds, default 600 or config value)").addHelpText("after", () => ` ${theme.heading("Examples:")} ${formatHelpExamples([ ["openclaw agent --to +15555550123 --message \"status update\"", "Start a new session."], ["openclaw agent --agent ops --message \"Summarize logs\"", "Use a specific agent."], ["openclaw agent --session-id 1234 --message \"Summarize inbox\" --thinking medium", "Target a session with explicit thinking level."], ["openclaw agent --to +15555550123 --message \"Trace logs\" --verbose on --json", "Enable verbose logging and JSON output."], ["openclaw agent --to +15555550123 --message \"Summon reply\" --deliver", "Deliver reply."], ["openclaw agent --agent ops --message \"Generate report\" --deliver --reply-channel slack --reply-to \"#reports\"", "Send reply to a different channel/target."] ])} ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/agent")}`).action(async (opts) => { setVerbose((typeof opts.verbose === "string" ? opts.verbose.toLowerCase() : "") === "on"); const deps = createDefaultDeps(); await runCommandWithRuntime(defaultRuntime, async () => { await agentCliCommand(opts, defaultRuntime, deps); }); }); const agents = program.command("agents").description("Manage isolated agents (workspaces + auth + routing)").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/agents", "docs.openclaw.ai/cli/agents")}\n`); agents.command("list").description("List configured agents").option("--json", "Output JSON instead of text", false).option("--bindings", "Include routing bindings", false).action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { await agentsListCommand({ json: Boolean(opts.json), bindings: Boolean(opts.bindings) }, defaultRuntime); }); }); agents.command("add [name]").description("Add a new isolated agent").option("--workspace <dir>", "Workspace directory for the new agent").option("--model <id>", "Model id for this agent").option("--agent-dir <dir>", "Agent state directory for this agent").option("--bind <channel[:accountId]>", "Route channel binding (repeatable)", collectOption, []).option("--non-interactive", "Disable prompts; requires --workspace", false).option("--json", "Output JSON summary", false).action(async (name, opts, command) => { await runCommandWithRuntime(defaultRuntime, async () => { const hasFlags = hasExplicitOptions(command, [ "workspace", "model", "agentDir", "bind", "nonInteractive" ]); await agentsAddCommand({ name: typeof name === "string" ? name : void 0, workspace: opts.workspace, model: opts.model, agentDir: opts.agentDir, bind: Array.isArray(opts.bind) ? opts.bind : void 0, nonInteractive: Boolean(opts.nonInteractive), json: Boolean(opts.json) }, defaultRuntime, { hasFlags }); }); }); agents.command("set-identity").description("Update an agent identity (name/theme/emoji/avatar)").option("--agent <id>", "Agent id to update").option("--workspace <dir>", "Workspace directory used to locate the agent + IDENTITY.md").option("--identity-file <path>", "Explicit IDENTITY.md path to read").option("--from-identity", "Read values from IDENTITY.md", false).option("--name <name>", "Identity name").option("--theme <theme>", "Identity theme").option("--emoji <emoji>", "Identity emoji").option("--avatar <value>", "Identity avatar (workspace path, http(s) URL, or data URI)").option("--json", "Output JSON summary", false).addHelpText("after", () => ` ${theme.heading("Examples:")} ${formatHelpExamples([ ["openclaw agents set-identity --agent main --name \"OpenClaw\" --emoji \"🦞\"", "Set name + emoji."], ["openclaw agents set-identity --agent main --avatar avatars/openclaw.png", "Set avatar path."], ["openclaw agents set-identity --workspace ~/.openclaw/workspace --from-identity", "Load from IDENTITY.md."], ["openclaw agents set-identity --identity-file ~/.openclaw/workspace/IDENTITY.md --agent main", "Use a specific IDENTITY.md."] ])} `).action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { await agentsSetIdentityCommand({ agent: opts.agent, workspace: opts.workspace, identityFile: opts.identityFile, fromIdentity: Boolean(opts.fromIdentity), name: opts.name, theme: opts.theme, emoji: opts.emoji, avatar: opts.avatar, json: Boolean(opts.json) }, defaultRuntime); }); }); agents.command("delete <id>").description("Delete an agent and prune workspace/state").option("--force", "Skip confirmation", false).option("--json", "Output JSON summary", false).action(async (id, opts) => { await runCommandWithRuntime(defaultRuntime, async () => { await agentsDeleteCommand({ id: String(id), force: Boolean(opts.force), json: Boolean(opts.json) }, defaultRuntime); }); }); agents.action(async () => { await runCommandWithRuntime(defaultRuntime, async () => { await agentsListCommand({}, defaultRuntime); }); }); } //#endregion export { registerAgentCommands };