UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

349 lines (348 loc) • 15.4 kB
import { F as resolveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js"; import { a as writeRuntimeJson, t as ExitError } from "./runtime-CF2WjnNZ.js"; import { t as sleep } from "./sleep-D7nua6TP.js"; import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js"; import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js"; import { t as createDeferredCore } from "./deferred-D0La5CRk.js"; import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js"; import { n as resolveGatewayLocalPortOverride } from "./gateway-port-option-0NYr1eQR.js"; import "./version-CwNT1gaY.js"; import { n as readGatewayDispatchConfigWithShellEnvFallback } from "./gateway-dispatch-config-CWzUgXmC.js"; import { r as projectGatewayUrlForDiagnostics } from "./connection-details-dzrfNKN8.js"; import { r as resolveGatewayClientBootstrap } from "./client-bootstrap-BE0hdooM.js"; import { t as startGatewayClientWhenEventLoopReady } from "./client-start-readiness-B1nULpha.js"; import { t as GatewayClient } from "./client-I-RoP1Al.js"; import { t as GatewayClientRequestError } from "./request-error-BSFClQsA.js"; import { n as normalizeEdgeAuthHeadersConfig, r as resolveEdgeAuthHeaders, t as gatewayEdgeAuthValueForTarget } from "./edge-auth-DAIxzz6z.js"; import { r as isTerminalInteractive } from "./terminal-interactivity-DXUXAq5U.js"; import { n as parseTimeoutMsWithFallback } from "./parse-timeout-BhPKqfrV.js"; import { n as WizardCancelledError } from "./prompts-DLsO8MlU.js"; import { n as openUrl } from "./browser-open-m1tKJTvZ.js"; import { t as createClackPrompter } from "./clack-prompter-BSpWH_ak.js"; import { t as resolveGatewayAuthOptions } from "./gateway-secret-options-BZ9vZmJx.js"; //#region src/commands/models/accounts-gateway.ts async function withModelsAccountsGateway(options, access, runtime, run) { const localPortOverride = resolveGatewayLocalPortOverride(options); const timeoutMs = resolveTimerTimeoutMs(parseTimeoutMsWithFallback(options.timeout, 3e4, { invalidType: "error" }), 3e4); const config = await readGatewayDispatchConfigWithShellEnvFallback(); const { gatewayToken: token, gatewayPassword: password } = resolveGatewayAuthOptions(options); const bootstrap = await resolveGatewayClientBootstrap({ config, gatewayUrl: options.url, localPortOverride, explicitAuth: { token, password } }); const gatewayUrl = projectGatewayUrlForDiagnostics(bootstrap.url); runtime.error(`Scope: Personal\nGateway: ${sanitizeTerminalText(gatewayUrl)}`); const edgeAuthHeaders = await resolveEdgeAuthHeaders({ config, value: normalizeEdgeAuthHeadersConfig(gatewayEdgeAuthValueForTarget({ config, targetUrl: bootstrap.url })), targetUrl: bootstrap.url, env: process.env }); const lifetime = new AbortController(); const ready = createDeferredCore(); const fail = (error) => { lifetime.abort(error); ready.reject(error); client.stop(); }; const client = new GatewayClient({ url: bootstrap.url, deviceAuthScope: bootstrap.deviceAuthScope, token: bootstrap.auth.token, password: bootstrap.auth.password, edgeAuthHeaders, tlsFingerprint: bootstrap.tlsFingerprint, preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs, requestTimeoutMs: timeoutMs, clientName: GATEWAY_CLIENT_NAMES.CLI, clientDisplayName: "openclaw models accounts", mode: GATEWAY_CLIENT_MODES.CLI, role: "operator", scopes: access === "write" ? ["operator.read", "operator.write"] : ["operator.read"], minProtocol: 4, maxProtocol: 4, notifyOnStartupRetry: true, onHelloOk: () => ready.resolve(), onConnectError: fail, onClose: () => { fail(/* @__PURE__ */ new Error("Gateway connection closed. Re-run the personal-account command.")); } }); const cancel = () => { const error = new ExitError(130, "Personal account operation cancelled."); lifetime.abort(error); ready.reject(error); }; process.once("SIGINT", cancel); process.once("SIGTERM", cancel); const timer = setTimeout(() => fail(/* @__PURE__ */ new Error("Gateway connection timed out.")), timeoutMs); try { await Promise.all([ready.promise, startGatewayClientWhenEventLoopReady(client, { timeoutMs, signal: lifetime.signal, clientOptions: { preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs } }).then((result) => { if (!result.ready) throw lifetime.signal.reason ?? /* @__PURE__ */ new Error("Gateway connection timed out."); return result; })]); clearTimeout(timer); lifetime.signal.throwIfAborted(); let profile; try { ({profile} = await client.request("users.self", {}, { signal: lifetime.signal })); } catch (error) { lifetime.signal.throwIfAborted(); if (error instanceof GatewayClientRequestError && error.gatewayCode === "FORBIDDEN") throw new Error([ "Personal model accounts require a signed-in person with access to this Gateway.", "Use --url with its Tailscale Serve or trusted-proxy WSS address. Omit shared Gateway token/password credentials when using Tailscale identity.", "For proxy client sign-in, see https://docs.openclaw.ai/gateway/remote#gateway-behind-an-identity-aware-proxy. Browser sign-in and device pairing alone do not identify this CLI; ask an administrator if access is denied.", "For shared or agent-local credentials instead, use `openclaw models auth login`." ].join("\n"), { cause: error }); throw error; } lifetime.signal.throwIfAborted(); const person = profile.displayName?.trim() || profile.emails[0] || profile.githubIdentity?.login || profile.id; runtime.error(`Person: ${sanitizeTerminalText(person)}`); return await run({ client, signal: lifetime.signal, profile }); } finally { clearTimeout(timer); process.off("SIGINT", cancel); process.off("SIGTERM", cancel); lifetime.abort(); try { await client.stopAndWait({ timeoutMs: 1e3 }); } catch { client.stop(); } } } //#endregion //#region src/commands/models/accounts.ts const SESSION_DEFAULT_NOTE = "This default applies to new sessions. Existing sessions keep their selected account."; async function selectAccountChoice(message, choices, requested, prompter) { if (choices.length === 0) throw new Error(`No ${message.toLowerCase()} is available for personal accounts on this Gateway.`); const id = requested?.trim() ?? (choices.length === 1 ? choices[0]?.id : await prompter.select({ message, options: choices.map((choice) => ({ value: choice.id, label: sanitizeTerminalText(choice.label), hint: choice.hint && sanitizeTerminalText(choice.hint) })), searchable: true })); const selected = choices.find((choice) => choice.id === id); if (!selected) throw new Error(`Unknown ${message.toLowerCase()} ${sanitizeTerminalText(id ?? "")}. Available: ${choices.map((choice) => sanitizeTerminalText(choice.id)).join(", ")}.`); return selected; } async function answerAccountStep(step, signal, runtime) { const prompter = createClackPrompter(process.stderr, signal); signal.throwIfAborted(); if (step.externalUrl) { runtime.error(`Open this URL to continue:\n${sanitizeTerminalText(step.externalUrl)}`); await openUrl(step.externalUrl); signal.throwIfAborted(); } const message = sanitizeTerminalText(step.message ?? step.title ?? "Continue"); const options = step.options?.map((option) => ({ ...option, label: sanitizeTerminalText(option.label), hint: option.hint && sanitizeTerminalText(option.hint) })) ?? []; switch (step.type) { case "note": if (step.format === "plain") await prompter.plain?.(message); else await prompter.note(message, step.title && sanitizeTerminalText(step.title)); return; case "text": { const value = await prompter.text({ message, sensitive: step.sensitive, placeholder: step.placeholder && sanitizeTerminalText(step.placeholder), initialValue: typeof step.initialValue === "string" ? sanitizeTerminalText(step.initialValue) : void 0 }); if (step.sensitive) registerSecretValueForRedaction(value); return value; } case "select": return await prompter.select({ message, options, initialValue: step.initialValue, searchable: true }); case "multiselect": return await prompter.multiselect({ message, options, initialValues: Array.isArray(step.initialValue) ? step.initialValue : void 0, searchable: true }); case "confirm": case "action": return await prompter.confirm({ message, initialValue: typeof step.initialValue === "boolean" ? step.initialValue : void 0 }); case "progress": return; } } async function connectAccount(client, start, signal, runtime) { signal.throwIfAborted(); const started = await client.request("users.authConnect.start", start); const params = { profileId: start.profileId, connectId: started.connectId }; let active; let displayedProgress; const retirePrompt = async () => { active?.controller.abort(); await active?.answer; active = void 0; }; try { signal.throwIfAborted(); let result = await client.request("users.authConnect.status", params, { signal }); while (result.status === "pending") { const step = result.step; if (active?.id !== step?.id) await retirePrompt(); if (step?.type === "progress" || step?.type === "action" && step.executor !== "client") { if (displayedProgress !== step.id) { runtime.error(sanitizeTerminalText(step.message ?? step.title ?? "Working…")); displayedProgress = step.id; } } else if (step && !active) { const controller = new AbortController(); active = { id: step.id, controller, answer: answerAccountStep(step, AbortSignal.any([signal, controller.signal]), runtime).then((value) => ({ value }), (error) => ({ error })) }; } const tick = new AbortController(); let answer; try { answer = await Promise.race([sleep(1e3, AbortSignal.any([signal, tick.signal])).then(() => void 0), ...active ? [active.answer] : []]); } finally { tick.abort(); } signal.throwIfAborted(); if (answer && active) { const stepId = active.id; await retirePrompt(); if ("error" in answer) throw answer.error; result = await client.request("users.authConnect.answer", { ...params, stepId, value: answer.value }, { signal }); if (result.status === "pending" && result.error) runtime.error(sanitizeTerminalText(result.error)); } else result = await client.request("users.authConnect.status", params, { signal }); } return result; } catch (error) { await retirePrompt(); let cancelled; try { cancelled = await client.request("users.authConnect.cancel", params, { timeoutMs: 3e3 }); if (cancelled.status === "pending") throw new Error("The sign-in operation is still pending.", { cause: error }); } catch (cancelError) { throw new Error("Could not confirm sign-in cancellation. The connection is closing; run `openclaw models accounts list` to check whether an account was saved.", { cause: cancelError }); } if (cancelled.status === "connected" || error instanceof WizardCancelledError || signal.aborted) return cancelled; throw new Error("Sign-in did not complete. Re-run `openclaw models accounts login`.", { cause: error }); } finally { await retirePrompt(); } } async function modelsAccountsListCommand(options, runtime) { await withModelsAccountsGateway(options, "read", runtime, async ({ client, signal }) => { const result = await client.request("users.listModelAccounts", options.cursor ? { cursor: options.cursor } : {}, { signal }); if (options.json) { writeRuntimeJson(runtime, result); return; } runtime.log("Personal model accounts:"); if (result.accounts.length === 0) runtime.log("No saved accounts on this page. Use `openclaw models accounts login`."); for (const account of result.accounts) runtime.log(`${account.selected ? "*" : "-"} ${sanitizeTerminalText(account.authProfileId)} ${sanitizeTerminalText(account.provider)}/${account.authType} ${sanitizeTerminalText(account.label)}${account.selected ? " (new-session default)" : ""}`); runtime.log(SESSION_DEFAULT_NOTE); if (result.nextCursor) runtime.log(`Next page: openclaw models accounts list --cursor ${sanitizeTerminalText(result.nextCursor)}`); }); } async function modelsAccountsLoginCommand(options, runtime) { if (!isTerminalInteractive(process.stderr)) throw new Error("Personal account login requires an interactive terminal for protected input. Run this command in a terminal, or use Connected accounts in Profile. Never paste credentials or authorization codes into chat or command arguments."); try { await withModelsAccountsGateway(options, "write", runtime, async ({ client, signal, profile }) => { const profileId = profile.id; const catalog = await client.request("users.authConnect.catalog", { profileId }, { signal }); signal.throwIfAborted(); const prompter = createClackPrompter(process.stderr, signal); const provider = await selectAccountChoice("Provider", catalog.providers, options.provider, prompter); const method = await selectAccountChoice("Sign-in method", provider.methods, options.method, prompter); signal.throwIfAborted(); const result = await connectAccount(client, { profileId, provider: provider.id, method: method.id }, signal, runtime); if (options.json) writeRuntimeJson(runtime, { profileId, provider: provider.id, ...result }); if (result.status === "connected") { if (!options.json) runtime.log(`Signed in: ${sanitizeTerminalText(result.authProfileId)}. ${SESSION_DEFAULT_NOTE}`); return; } if (result.status === "cancelled") { runtime.error("Personal account sign-in cancelled."); throw new ExitError(130); } if (options.json) throw new ExitError(1); const reason = result.status === "failed" ? ` (${result.reason})` : ""; throw new Error(`Personal account sign-in ${result.status}${reason}. Re-run the login command.`); }); } catch (error) { if (error instanceof WizardCancelledError) throw new ExitError(130, "Personal account sign-in cancelled."); throw error; } } async function modelsAccountsUseCommand(options, runtime) { await withModelsAccountsGateway(options, "write", runtime, async ({ client, signal, profile }) => { const profileId = profile.id; const result = await client.request("users.selectModelAccount", { profileId, authProfileId: options.authProfileId }, { signal }); if (options.json) writeRuntimeJson(runtime, { profileId, ...result }); else runtime.log(`Selected ${sanitizeTerminalText(options.authProfileId)}. ${SESSION_DEFAULT_NOTE}`); }); } async function modelsAccountsClearDefaultCommand(options, runtime) { await withModelsAccountsGateway(options, "write", runtime, async ({ client, signal, profile }) => { const profileId = profile.id; const result = await client.request("users.unlinkAuthProfile", { profileId, provider: options.provider }, { signal }); if (options.json) writeRuntimeJson(runtime, { profileId, ...result }); else runtime.log(`Cleared the ${sanitizeTerminalText(options.provider)} new-session default. Saved credentials and existing session accounts are unchanged.`); }); } //#endregion export { modelsAccountsClearDefaultCommand, modelsAccountsListCommand, modelsAccountsLoginCommand, modelsAccountsUseCommand };