UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

308 lines (307 loc) 13.5 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { g as shortenHomePath, p as resolveUserPath } from "./utils-CCC-BEJH.js"; import { n as defaultRuntime, r as writeRuntimeJson } from "./runtime-B4lgFmsS.js"; import "./agent-scope-MrLta7Pq.js"; import { t as DEFAULT_AGENT_ID, u as normalizeAgentId } from "./session-key-B_NoIfpX.js"; import { a as resolveAgentDir, c as resolveDefaultAgentId, o as resolveAgentWorkspaceDir, t as listAgentEntries } from "./agent-scope-config-CgCYpZfK.js"; import { l as resolveAuthStorePath } from "./runtime-snapshots-CGKcj2Tz.js"; import { n as ensureAuthProfileStore, p as saveAuthProfileStore, v as loadPersistedAuthProfileStore } from "./store-C8spD0DG.js"; import { t as buildPortableAuthProfileSecretsStoreForAgentCopy } from "./auth-profiles-84rzaGag.js"; import { t as WizardCancelledError } from "./prompts-E0FayNjJ.js"; import { t as createClackPrompter } from "./clack-prompter-fc9v8HJv.js"; import { r as logConfigUpdated } from "./logging-Cr7I5jF_.js"; import { o as transformConfigWithPendingPluginInstalls, t as commitConfigWithPendingPluginInstalls } from "./plugins-install-record-commit--Zbbdxlm.js"; import { r as ensureWorkspaceAndSessions } from "./onboard-helpers-DXyUiHQ-.js"; import { t as describeBinding } from "./agents.binding-format-C3S9Mq5U.js"; import { n as buildChannelBindings, r as parseBindingSpecs, t as applyAgentBindings } from "./agents.bindings-Ih6TOgX-.js"; import { r as requireValidConfigFileSnapshot, t as createQuietRuntime } from "./agents.command-shared-DGOy_cbP.js"; import { r as findAgentEntryIndex, t as applyAgentConfig } from "./agents.config-DfQl3_hl.js"; import { t as promptAuthChoiceGrouped } from "./auth-choice-prompt-BelGczGT.js"; import { n as applyAuthChoice, t as warnIfModelConfigLooksOff } from "./auth-choice-Bb5NCx3i.js"; import { i as setupChannels } from "./onboard-channels-CR34I3LC.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/commands/agents.commands.add.ts var AgentsAddMutationError = class extends Error { constructor(message) { super(message); this.name = "AgentsAddMutationError"; } }; function emptyBindingResult(config) { return { config, added: [], updated: [], skipped: [], conflicts: [] }; } function formatSkippedOAuthProfilesMessage(params) { return params.sourceIsInheritedMain ? `OAuth profiles stay shared from "${params.sourceAgentId}" unless this agent signs in separately.` : `OAuth profiles were not copied from "${params.sourceAgentId}"; sign in separately for this agent.`; } /** Create or update an agent through the non-interactive path or guided wizard. */ async function agentsAddCommand(opts, runtime = defaultRuntime, params) { const configSnapshot = await requireValidConfigFileSnapshot(runtime); if (!configSnapshot) return; const cfg = configSnapshot.sourceConfig ?? configSnapshot.config; const baseHash = configSnapshot.hash; const workspaceFlag = opts.workspace?.trim(); const nameInput = opts.name?.trim(); const hasFlags = params?.hasFlags === true; const nonInteractive = opts.nonInteractive === true || hasFlags; if (nonInteractive && !workspaceFlag) { runtime.error(`Non-interactive agent creation requires --workspace. Re-run ${formatCliCommand("openclaw agents add <id> --workspace <path>")} or omit flags to use the wizard.`); runtime.exit(1); return; } if (nonInteractive) { if (!nameInput) { runtime.error(`Agent name is required in non-interactive mode. Run ${formatCliCommand("openclaw agents add <id> --workspace <path>")}.`); runtime.exit(1); return; } if (!workspaceFlag) { runtime.error(`Non-interactive agent creation requires --workspace. Re-run ${formatCliCommand("openclaw agents add <id> --workspace <path>")} or omit flags to use the wizard.`); runtime.exit(1); return; } const agentId = normalizeAgentId(nameInput); if (agentId === "main") { runtime.error(`"${DEFAULT_AGENT_ID}" is reserved. Choose another name, or run ${formatCliCommand("openclaw agents list")} to inspect the default agent.`); 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. Run ${formatCliCommand("openclaw agents list")} to inspect configured agents.`); runtime.exit(1); return; } const workspaceDir = resolveUserPath(workspaceFlag); const explicitAgentDir = opts.agentDir?.trim() ? resolveUserPath(opts.agentDir.trim()) : void 0; const model = opts.model?.trim(); let committed; try { committed = await transformConfigWithPendingPluginInstalls({ transform: (latestConfig) => { if (findAgentEntryIndex(listAgentEntries(latestConfig), agentId) >= 0) throw new AgentsAddMutationError(`Agent "${agentId}" already exists.`); const agentDir = explicitAgentDir ?? resolveAgentDir(latestConfig, agentId); const nextConfig = applyAgentConfig(latestConfig, { agentId, name: nameInput, workspace: workspaceDir, agentDir, ...model ? { model } : {} }); const bindingParse = parseBindingSpecs({ agentId, specs: opts.bind, config: nextConfig }); if (bindingParse.errors.length > 0) throw new AgentsAddMutationError(bindingParse.errors.join("\n")); const bindingResult = bindingParse.bindings.length > 0 ? applyAgentBindings(nextConfig, bindingParse.bindings) : emptyBindingResult(nextConfig); return { nextConfig: bindingResult.config, result: { agentDir, bindingResult } }; } }); } catch (err) { if (err instanceof AgentsAddMutationError) { runtime.error(err.message); runtime.exit(1); return; } throw err; } const mutationResult = committed.result; if (!mutationResult) throw new Error("Agent config mutation did not return a result."); const { agentDir, bindingResult } = mutationResult; if (!opts.json) logConfigUpdated(runtime); await ensureWorkspaceAndSessions(workspaceDir, opts.json ? createQuietRuntime(runtime) : runtime, { skipBootstrap: Boolean(committed.nextConfig.agents?.defaults?.skipBootstrap), skipOptionalBootstrapFiles: committed.nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, agentId }); const payload = { agentId, name: nameInput, workspace: workspaceDir, agentDir, model, bindings: { added: bindingResult.added.map(describeBinding), updated: bindingResult.updated.map(describeBinding), skipped: bindingResult.skipped.map(describeBinding), conflicts: bindingResult.conflicts.map((conflict) => `${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`) } }; if (opts.json) writeRuntimeJson(runtime, payload); 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 agentName = normalizeOptionalString(nameInput ?? await prompter.text({ message: "Agent name", validate: (value) => { if (!value?.trim()) return "Required"; if (normalizeAgentId(value) === "main") return `"main" is reserved. Choose another name.`; } })) ?? ""; 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 workspaceDir = resolveUserPath(normalizeOptionalString(await prompter.text({ message: "Workspace directory", initialValue: workspaceDefault, validate: (value) => value?.trim() ? void 0 : "Required" })) || workspaceDefault); const agentDir = resolveAgentDir(cfg, agentId); let nextConfig = applyAgentConfig(cfg, { agentId, name: agentName, workspace: workspaceDir, agentDir }); const defaultAgentId = resolveDefaultAgentId(cfg); if (defaultAgentId !== agentId) { const sourceAgentDir = resolveAgentDir(cfg, defaultAgentId); const sourceAuthPath = resolveAuthStorePath(sourceAgentDir); const destAuthPath = resolveAuthStorePath(agentDir); const mainAuthPath = resolveAuthStorePath(void 0); const sameAuthPath = normalizeLowercaseStringOrEmpty(path.resolve(sourceAuthPath)) === normalizeLowercaseStringOrEmpty(path.resolve(destAuthPath)); const sourceIsInheritedMain = normalizeLowercaseStringOrEmpty(path.resolve(sourceAuthPath)) === normalizeLowercaseStringOrEmpty(path.resolve(mainAuthPath)); if (!sameAuthPath) { const sourceStore = loadPersistedAuthProfileStore(sourceAgentDir); const destStore = loadPersistedAuthProfileStore(agentDir); const portable = sourceStore ? buildPortableAuthProfileSecretsStoreForAgentCopy(sourceStore) : void 0; if (portable && portable.copiedProfileIds.length > 0 && Object.keys(destStore?.profiles ?? {}).length === 0) { if (await prompter.confirm({ message: `Copy portable auth profiles from "${defaultAgentId}"?`, initialValue: false })) { await fs.mkdir(agentDir, { recursive: true }); saveAuthProfileStore(portable.store, agentDir, { filterExternalAuthProfiles: false, syncExternalCli: false }); const skippedText = portable.skippedProfileIds.length > 0 ? ` ${formatSkippedOAuthProfilesMessage({ sourceAgentId: defaultAgentId, sourceIsInheritedMain })}` : ""; await prompter.note(`Copied ${portable.copiedProfileIds.length} portable auth profile${portable.copiedProfileIds.length === 1 ? "" : "s"} from "${defaultAgentId}".${skippedText}`, "Auth profiles"); } } else if ((portable?.skippedProfileIds.length ?? 0) > 0) await prompter.note(formatSkippedOAuthProfilesMessage({ sourceAgentId: defaultAgentId, sourceIsInheritedMain }), "Auth profiles"); } } if (await prompter.confirm({ message: "Configure model/auth for this agent now?", initialValue: false })) { const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); while (true) { const authResult = await applyAuthChoice({ authChoice: await promptAuthChoiceGrouped({ prompter, store: authStore, includeSkip: true, config: nextConfig }), config: nextConfig, prompter, runtime, agentDir, setDefaultModel: false, agentId }); nextConfig = authResult.config; if (authResult.retrySelection) continue; if (authResult.agentModelOverride) nextConfig = applyAgentConfig(nextConfig, { agentId, model: authResult.agentModelOverride }); break; } } await warnIfModelConfigLooksOff(nextConfig, prompter, { agentId, agentDir, validateCatalog: false }); 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"); nextConfig = (await commitConfigWithPendingPluginInstalls({ nextConfig, ...baseHash !== void 0 ? { baseHash } : {} })).config; logConfigUpdated(runtime); await ensureWorkspaceAndSessions(workspaceDir, runtime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, agentId }); const payload = { agentId, name: agentName, workspace: workspaceDir, agentDir }; if (opts.json) writeRuntimeJson(runtime, payload); await prompter.outro(`Agent "${agentId}" ready.`); } catch (err) { if (err instanceof WizardCancelledError) { runtime.exit(1); return; } throw err; } } //#endregion export { agentsAddCommand };