UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

385 lines (384 loc) 18.2 kB
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js"; import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import { m as shortenHomePath } from "./utils-P__uGsPB.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { t as formatCliCommand } from "./command-format-C7YfyMTd.js"; import { D as tryResolveLegacyCompatibilityAgentId, i as listAgentEntries, m as resolveAgentWorkspaceDir, u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js"; import "./session-key-BnWWjqNc.js"; import "./agent-scope-DbtJyKUL.js"; import { r as isTerminalInteractive } from "./terminal-interactivity-DXUXAq5U.js"; import { t as ExpectedCliError } from "./failure-output-Dc5ce9_F.js"; import { c as resolveSharedMainAuthAgentDir } from "./path-resolve-oRkRBkQd.js"; import { h as resolveAuthProfileDatabasePath, o as inspectPersistedAuthProfileStoreRaw } from "./sqlite-MN_7y26V.js"; import { a as loadPersistedAuthProfileStore } from "./persisted-B_qhhBlh.js"; import { n as AuthProfileStoreUnreadableError } from "./legacy-source-diagnostic-BNaogmw1.js"; import { T as buildPortableAuthProfileStoreForAgentCopy, m as loadAuthProfileStoreWithoutExternalProfiles, r as ensureAuthProfileStore } from "./store-F1B2duCT.js"; import "./auth-profiles-BdUEhE7u.js"; import { t as withPluginLifecycleLease } from "./plugin-lifecycle-lease-BtAFH872.js"; import { n as WizardCancelledError } from "./prompts-DLsO8MlU.js"; import { i as ensureOnboardingAgentWorkspace, s as resolveOnboardingAgentTarget } from "./onboard-agent-target-CNKxVAZK.js"; import { t as persistProviderAuthProfileBatch } from "./provider-auth-persistence-BgUvISv3.js"; import { t as createClackPrompter } from "./clack-prompter-BSpWH_ak.js"; import { r as logConfigUpdated } from "./logging-Idqd9ylL.js"; import { t as describeBinding } from "./agents.binding-format-BRYI5aWJ.js"; import { n as buildChannelBindings, t as applyAgentBindings } from "./agents.bindings-xbGQEjU6.js"; import { t as applyAgentConfig } from "./agents.config-CFXmxe4B.js"; import { n as createAgent, r as validateAgentIdInput, t as checkAgentCreationGate } from "./agent-create-DMopBnuA.js"; import { s as transformConfigWithPendingPluginInstalls, t as commitConfigWithPendingPluginInstalls } from "./install-record-commit-C4pioTsf.js"; import { n as requireValidConfigFileSnapshot } from "./config-validation-C1Wo5r0Q.js"; import { i as setupChannels, n as createChannelSetupTransaction } from "./onboard-channels-B4-ZMnMR.js"; import { n as promptAuthChoiceGrouped } from "./auth-choice-prompt-DhFOt_H7.js"; import { a as prepareAuthChoice, r as warnIfModelConfigLooksOff } from "./auth-choice-DY18kI3l.js"; import path from "node:path"; //#region src/commands/agents.commands.add.ts function failAgentsAdd(message) { throw new ExpectedCliError({ message, humanOutput: message, machineOutput: message }); } function emptyBindingResult(config) { return { config, added: [], updated: [], skipped: [], conflicts: [] }; } function loadReadablePersistedAuthProfileStore(agentDir) { const store = loadPersistedAuthProfileStore(agentDir); if (!store && inspectPersistedAuthProfileStoreRaw(agentDir).status !== "missing") throw new AuthProfileStoreUnreadableError(resolveAuthProfileDatabasePath(agentDir)); return store; } function hasOAuthProfiles(store, profileIds) { return profileIds.some((profileId) => store.profiles[profileId]?.type === "oauth"); } function formatSkippedOAuthProfilesMessage(sourceAgentId, sourceIsInheritedMain) { return sourceIsInheritedMain ? `OAuth profiles stay shared from "${sourceAgentId}" unless this agent signs in separately.` : `OAuth profiles were not copied from "${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 hasAutomationFlags = params?.hasAutomationFlags === true; const nonInteractive = opts.nonInteractive === true || hasAutomationFlags; const wizardOutput = opts.json ? process.stderr : process.stdout; if (!nonInteractive && !isTerminalInteractive(wizardOutput)) failAgentsAdd(`Agent creation needs an interactive TTY. Use \`${formatCliCommand("openclaw agents add <id> --non-interactive --workspace <dir>")}\` for automation.`); 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(); if (nonInteractive) { if (!workspaceFlag) failAgentsAdd(`Non-interactive agent creation requires --workspace. Re-run ${formatCliCommand("openclaw agents add <id> --workspace <path>")} or omit flags to use the wizard.`); if (!nameInput) failAgentsAdd(`Agent name is required in non-interactive mode. Run ${formatCliCommand("openclaw agents add <id> --workspace <path>")}.`); const validation = validateAgentIdInput(nameInput); if (!validation.ok) failAgentsAdd(validation.reason === "reserved-id" ? `"${validation.agentId}" is reserved. Choose another name, or run ${formatCliCommand("openclaw agents list")} to inspect configured agents.` : validation.message); const agentId = validation.agentId; if (agentId !== nameInput) runtime.log(`Normalized agent id to "${agentId}".`); const created = await withPluginLifecycleLease({}, async () => { return await createAgent({ name: nameInput, workspace: workspaceFlag, ...opts.agentDir ? { agentDir: opts.agentDir } : {}, ...opts.model ? { model: opts.model } : {}, ...opts.bind?.length ? { bindingSpecs: opts.bind } : {}, transformConfig: transformConfigWithPendingPluginInstalls }); }); if (created.status === "error") failAgentsAdd(created.reason === "reserved-id" ? `"${created.agentId}" is reserved. Choose another name, or run ${formatCliCommand("openclaw agents list")} to inspect configured agents.` : created.reason === "already-exists" ? `Agent "${created.agentId}" already exists.` : created.message); const bindingResult = created.bindingResult ?? emptyBindingResult(cfg); if (!opts.json) logConfigUpdated(runtime); const payload = { agentId: created.agentId, name: created.name, workspace: created.workspace, agentDir: created.agentDir, model: created.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(created.workspace)}`); runtime.log(`Agent dir: ${shortenHomePath(created.agentDir)}`); if (created.model) runtime.log(`Model: ${created.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(wizardOutput); const wizardRuntime = opts.json ? { ...runtime, log: (...args) => runtime.error(...args) } : runtime; try { await prompter.intro("Add OpenClaw agent"); const name = nameInput ?? await prompter.text({ message: "Agent name", validate: (value) => { if (!value?.trim()) return "Required"; const validation = validateAgentIdInput(value); if (!validation.ok) return validation.reason === "reserved-id" ? `"${validation.agentId}" is reserved. Choose another name.` : validation.message; } }); const agentName = normalizeOptionalString(name) ?? ""; const validation = validateAgentIdInput(agentName); if (!validation.ok) { if (validation.reason === "reserved-id") { await prompter.outro(`"${validation.agentId}" is reserved. Choose another name.`); return; } await prompter.outro(validation.message); return; } const agentId = validation.agentId; if (agentName !== agentId) await prompter.note(`Normalized id to "${agentId}".`, "Agent id"); const existingAgent = listAgentEntries(cfg).find((agent) => normalizeAgentId(agent.id) === agentId); if (existingAgent) { if (!await prompter.confirm({ message: `Agent "${agentId}" already exists. Update it?`, initialValue: false })) { await prompter.outro("No changes made."); return; } } else { const gateError = await checkAgentCreationGate(agentId); if (gateError) { await prompter.outro(gateError.message); 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(normalizeOptionalString(workspaceInput) || workspaceDefault); const agentDir = resolveAgentDir(cfg, agentId); let nextConfig = applyAgentConfig(cfg, { agentId, name: agentName, workspace: workspaceDir, agentDir }); const stagedAuthProfiles = []; let stagedAuthOrder; let reportPortableAuthCopy; const copySourceAgentId = tryResolveLegacyCompatibilityAgentId(cfg) ?? await prompter.select({ message: "Copy auth profiles from another agent?", initialValue: "__skip__", options: [{ value: "__skip__", label: "Skip copying auth profiles" }, ...listAgentEntries(cfg).map((agent) => normalizeAgentId(agent.id)).filter((id) => id !== agentId).map((id) => ({ value: id, label: id }))] }); if (copySourceAgentId !== "__skip__" && copySourceAgentId !== agentId) { const sourceAgentDir = resolveAgentDir(cfg, copySourceAgentId); const sourceAuthPath = resolveAuthProfileDatabasePath(sourceAgentDir); const destAuthPath = resolveAuthProfileDatabasePath(agentDir); const sharedMainAgentPath = resolveAuthProfileDatabasePath(resolveSharedMainAuthAgentDir()); const sameAuthPath = normalizeLowercaseStringOrEmpty(path.resolve(sourceAuthPath)) === normalizeLowercaseStringOrEmpty(path.resolve(destAuthPath)); const sourceIsInheritedMain = normalizeLowercaseStringOrEmpty(path.resolve(sourceAuthPath)) === normalizeLowercaseStringOrEmpty(path.resolve(sharedMainAgentPath)); if (!sameAuthPath) { const sourceStore = sourceIsInheritedMain ? loadAuthProfileStoreWithoutExternalProfiles(sourceAgentDir) : loadReadablePersistedAuthProfileStore(sourceAgentDir); const destStore = loadReadablePersistedAuthProfileStore(agentDir); const portable = sourceStore ? buildPortableAuthProfileStoreForAgentCopy(sourceStore) : void 0; const skippedOAuthProfiles = sourceStore && portable ? hasOAuthProfiles(sourceStore, portable.skippedProfileIds) : false; if (sourceStore && portable && portable.copiedProfileIds.length > 0 && Object.keys(destStore?.profiles ?? {}).length === 0) { if (await prompter.confirm({ message: `Copy portable auth profiles from "${copySourceAgentId}"?`, initialValue: false })) { const copiedProfileIds = portable.copiedProfileIds; const copiedOAuthProfileIds = copiedProfileIds.filter((profileId) => sourceStore.profiles[profileId]?.type === "oauth"); const sourceAgentId = copySourceAgentId; const sourceInheritedMain = sourceIsInheritedMain; const destinationAgentDir = agentDir; for (const [profileId, credential] of Object.entries(portable.store.profiles)) stagedAuthProfiles.push({ profileId, credential, replaceExisting: false }); stagedAuthOrder = portable.store.order; reportPortableAuthCopy = async () => { const persisted = loadPersistedAuthProfileStore(destinationAgentDir); const persistedIds = new Set(Object.keys(persisted?.profiles ?? {})); const copiedCount = copiedProfileIds.filter((profileId) => persistedIds.has(profileId)).length; const skippedOAuth = skippedOAuthProfiles || copiedOAuthProfileIds.some((profileId) => !persistedIds.has(profileId)); const copied = copiedCount ? `Copied ${copiedCount} portable auth profile${copiedCount === 1 ? "" : "s"} from "${sourceAgentId}".` : ""; const skipped = skippedOAuth ? ` ${formatSkippedOAuthProfilesMessage(sourceAgentId, sourceInheritedMain)}` : ""; await prompter.note(`${copied}${skipped}`.trim(), "Auth profiles"); }; } } else if (skippedOAuthProfiles) { const sourceAgentId = copySourceAgentId; const sourceInheritedMain = sourceIsInheritedMain; reportPortableAuthCopy = async () => { await prompter.note(formatSkippedOAuthProfilesMessage(sourceAgentId, sourceInheritedMain), "Auth profiles"); }; } } } if (await prompter.confirm({ message: "Configure model/auth for this agent now?", initialValue: false })) { const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false, readOnly: true, syncExternalCli: false }); while (true) { const authChoice = await promptAuthChoiceGrouped({ prompter, store: authStore, includeSkip: true, config: nextConfig }); const authResult = await prepareAuthChoice({ authChoice, config: nextConfig, prompter, runtime: wizardRuntime, agentDir, setDefaultModel: false, agentId }); nextConfig = authResult.config; if (authResult.retrySelection) continue; stagedAuthProfiles.push(...authResult.authProfiles); if (authResult.agentModelOverride) nextConfig = applyAgentConfig(nextConfig, { agentId, model: authResult.agentModelOverride }); break; } } await warnIfModelConfigLooksOff(nextConfig, prompter, { agentId, agentDir, pendingAuthProfiles: stagedAuthProfiles.map(({ profileId, credential }) => ({ profileId, credential })), validateCatalog: false }); const channelSetup = createChannelSetupTransaction({ runtime: wizardRuntime }); let selection = []; const channelAccountIds = {}; nextConfig = await setupChannels(nextConfig, wizardRuntime, prompter, { workspaceDir, deferStatusUntilSelection: true, allowIMessageInstall: true, allowSignalInstall: true, onSelection: (value) => { selection = value; }, promptAccountIds: true, onAccountId: (channel, accountId) => { channelAccountIds[channel] = accountId; }, onPostWriteHook: channelSetup.onPostWriteHook }); 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"); } const stagedEntry = existingAgent ? void 0 : listAgentEntries(nextConfig).find((candidate) => normalizeAgentId(candidate.id) === agentId); const stagedAuthBatch = stagedAuthProfiles.length > 0 ? { profiles: stagedAuthProfiles, ...stagedAuthOrder ? { order: stagedAuthOrder } : {}, agentDir, config: nextConfig } : void 0; let payload; if (existingAgent) { const target = resolveOnboardingAgentTarget(nextConfig, agentId); await ensureOnboardingAgentWorkspace(target, wizardRuntime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles }); const authPersistence = stagedAuthBatch ? await persistProviderAuthProfileBatch(stagedAuthBatch) : void 0; try { nextConfig = await channelSetup.commit(nextConfig, async (configToCommit) => { return (await commitConfigWithPendingPluginInstalls({ nextConfig: configToCommit, ...baseHash !== void 0 ? { baseHash } : {} })).config; }); } catch (error) { authPersistence?.rollback(); throw error; } payload = { agentId: target.agentId, name: agentName, workspace: target.workspaceDir, agentDir: target.agentDir }; } else { if (!stagedEntry) throw new Error(`staged agent "${agentId}" is missing from config`); const created = await withPluginLifecycleLease({}, async () => { return await createAgent({ entry: { ...stagedEntry, id: agentId }, expectedConfigHash: baseHash ?? null, stagedConfig: nextConfig, transformConfig: transformConfigWithPendingPluginInstalls, ...stagedAuthBatch ? { prepareConfigCommit: async () => (await persistProviderAuthProfileBatch(stagedAuthBatch)).rollback } : {} }); }); if (created.status === "error") { await prompter.outro(created.message); return; } nextConfig = created.config; payload = { agentId: created.agentId, name: created.name, workspace: created.workspace, agentDir: created.agentDir }; await channelSetup.runPostWriteHooks(nextConfig); } await reportPortableAuthCopy?.(); if (!opts.json) logConfigUpdated(runtime); 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 };