import type { Command } from "commander"; import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import type { ChannelSetupInput } from "openclaw/plugin-sdk/setup"; import { resolveMatrixAccount, resolveMatrixAccountConfig } from "./matrix/accounts.js"; import { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } from "./matrix/actions/devices.js"; import { updateMatrixOwnProfile } from "./matrix/actions/profile.js"; import { acceptMatrixVerification, bootstrapMatrixVerification, cancelMatrixVerification, confirmMatrixVerificationSas, getMatrixVerificationSas, getMatrixRoomKeyBackupStatus, getMatrixVerificationStatus, listMatrixVerifications, mismatchMatrixVerificationSas, requestMatrixVerification, resetMatrixRoomKeyBackup, restoreMatrixRoomKeyBackup, runMatrixSelfVerification, startMatrixVerification, verifyMatrixRecoveryKey, } from "./matrix/actions/verification.js"; import { resolveMatrixRoomKeyBackupIssue } from "./matrix/backup-health.js"; import { resolveMatrixAuthContext } from "./matrix/client.js"; import { setMatrixSdkConsoleLogging, setMatrixSdkLogMode } from "./matrix/client/logging.js"; import { resolveMatrixConfigPath, updateMatrixAccountConfig } from "./matrix/config-update.js"; import { isOpenClawManagedMatrixDevice } from "./matrix/device-health.js"; import type { MatrixDirectRoomCandidate } from "./matrix/direct-management.js"; import { formatMatrixErrorMessage } from "./matrix/errors.js"; import { applyMatrixProfileUpdate, type MatrixProfileUpdateResult } from "./profile-update.js"; import { formatZonedTimestamp } from "./runtime-api.js"; import { getMatrixRuntime } from "./runtime.js"; import { matrixSetupAdapter } from "./setup-core.js"; import type { CoreConfig } from "./types.js"; let matrixCliExitScheduled = false; type MatrixActionClientModule = typeof import("./matrix/actions/client.js"); type MatrixDirectManagementModule = typeof import("./matrix/direct-management.js"); let matrixActionClientModulePromise: Promise | undefined; let matrixDirectManagementModulePromise: Promise | undefined; function loadMatrixActionClientModule(): Promise { matrixActionClientModulePromise ??= import("./matrix/actions/client.js"); return matrixActionClientModulePromise; } function loadMatrixDirectManagementModule(): Promise { matrixDirectManagementModulePromise ??= import("./matrix/direct-management.js"); return matrixDirectManagementModulePromise; } export function resetMatrixCliStateForTests(): void { matrixCliExitScheduled = false; } function scheduleMatrixCliExit(): void { if (matrixCliExitScheduled || process.env.VITEST) { return; } matrixCliExitScheduled = true; // matrix-js-sdk rust crypto can leave background async work alive after command completion. setTimeout(() => { process.stdout.write("", () => { process.stderr.write("", () => { process.exit(process.exitCode ?? 0); }); }); }, 0); } function markCliFailure(): void { process.exitCode = 1; } async function readMatrixCliRecoveryKeyFromStdin(): Promise { const chunks: Buffer[] = []; for await (const chunk of process.stdin) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))); } const recoveryKey = Buffer.concat(chunks).toString("utf8").trim(); if (!recoveryKey) { throw new Error("Matrix recovery key was requested from stdin, but stdin was empty."); } return recoveryKey; } async function resolveMatrixCliRecoveryKeyInput(options: { recoveryKey?: string; recoveryKeyStdin?: boolean; }): Promise { if (options.recoveryKey && options.recoveryKeyStdin === true) { throw new Error("Use either --recovery-key or --recovery-key-stdin, not both."); } if (options.recoveryKeyStdin === true) { return await readMatrixCliRecoveryKeyFromStdin(); } return options.recoveryKey; } async function requireMatrixCliRecoveryKeyInput(options: { recoveryKey?: string; recoveryKeyStdin?: boolean; }): Promise { const recoveryKey = await resolveMatrixCliRecoveryKeyInput(options); if (!recoveryKey) { throw new Error( "Matrix recovery key is required. Pass --recovery-key-stdin to read it from stdin.", ); } return recoveryKey; } function toErrorMessage(err: unknown): string { return formatMatrixErrorMessage(err); } function printJson(payload: unknown): void { process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); } function formatLocalTimestamp(value: string | null | undefined): string | null { if (!value) { return null; } const parsed = new Date(value); if (!Number.isFinite(parsed.getTime())) { return value; } return formatZonedTimestamp(parsed, { displaySeconds: true }) ?? value; } function printTimestamp(label: string, value: string | null | undefined): void { const formatted = formatLocalTimestamp(value); if (formatted) { console.log(`${label}: ${formatMatrixCliText(formatted)}`); } } function printAccountLabel(accountId?: string): void { console.log(`Account: ${formatMatrixCliText(normalizeAccountId(accountId))}`); } function resolveMatrixCliAccountId(accountId?: string): string { return resolveMatrixCliAccountContext(accountId).accountId; } function resolveMatrixCliAccountContext(accountId?: string): { accountId: string; cfg: CoreConfig; } { const cfg = getMatrixRuntime().config.current() as CoreConfig; return { accountId: resolveMatrixAuthContext({ cfg, accountId }).accountId, cfg, }; } function formatMatrixCliCommand(command: string, accountId?: string): string { return formatMatrixCliCommandParts(command.split(" "), accountId); } function formatMatrixCliRecoveryKeyStdinCommand(command: string, accountId?: string): string { const normalizedAccountId = normalizeAccountId(accountId); const envName = normalizedAccountId === "default" ? "MATRIX_RECOVERY_KEY" : `MATRIX_RECOVERY_KEY_${normalizedAccountId.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}`; return `printf '%s\\n' "$${envName}" | ${formatMatrixCliCommand(command, accountId)}`; } function formatMatrixCliCommandParts(parts: string[], accountId?: string): string { const normalizedAccountId = normalizeAccountId(accountId); const command = ["openclaw", "matrix", ...parts]; if (normalizedAccountId !== "default") { const optionTerminatorIndex = command.indexOf("--"); if (optionTerminatorIndex >= 0) { command.splice(optionTerminatorIndex, 0, "--account", normalizedAccountId); } else { command.push("--account", normalizedAccountId); } } return command.map(formatMatrixCliShellArg).join(" "); } function formatMatrixCliShellArg(value: string): string { if (/^[A-Za-z0-9_./:=@%+-]+$/.test(value)) { return value; } return `'${value.replaceAll("'", "'\\''")}'`; } function formatMatrixCliText(value: string | null | undefined, fallback = "unknown"): string { return sanitizeMatrixCliText(value ?? fallback); } function printMatrixOwnDevices( devices: Array<{ deviceId: string; displayName: string | null; lastSeenIp: string | null; lastSeenTs: number | null; current: boolean; }>, ): void { if (devices.length === 0) { console.log("Devices: none"); return; } for (const device of devices) { const labels = [device.current ? "current" : null, device.displayName] .filter((label): label is string => Boolean(label)) .map((label) => formatMatrixCliText(label)); console.log( `- ${formatMatrixCliText(device.deviceId)}${labels.length ? ` (${labels.join(", ")})` : ""}`, ); if (device.lastSeenTs) { printTimestamp(" Last seen", new Date(device.lastSeenTs).toISOString()); } if (device.lastSeenIp) { console.log(` Last IP: ${formatMatrixCliText(device.lastSeenIp)}`); } } } function configureCliLogMode(verbose: boolean): void { setMatrixSdkLogMode(verbose ? "default" : "quiet"); setMatrixSdkConsoleLogging(verbose); } function parseOptionalInt(value: string | undefined, fieldName: string): number | undefined { const trimmed = value?.trim(); if (!trimmed) { return undefined; } const parsed = Number.parseInt(trimmed, 10); if (!Number.isFinite(parsed)) { throw new Error(`${fieldName} must be an integer`); } return parsed; } type MatrixCliAccountAddResult = { accountId: string; configPath: string; useEnv: boolean; encryptionEnabled: boolean; deviceHealth: { currentDeviceId: string | null; staleOpenClawDeviceIds: string[]; error?: string; }; verificationBootstrap: { attempted: boolean; success: boolean; recoveryKeyCreatedAt: string | null; backupVersion: string | null; error?: string; }; profile: { attempted: boolean; displayNameUpdated: boolean; avatarUpdated: boolean; resolvedAvatarUrl: string | null; convertedAvatarFromHttp: boolean; error?: string; }; }; async function addMatrixAccount(params: { account?: string; name?: string; avatarUrl?: string; homeserver?: string; proxy?: string; userId?: string; accessToken?: string; password?: string; deviceName?: string; initialSyncLimit?: string; allowPrivateNetwork?: boolean; useEnv?: boolean; enableEncryption?: boolean; }): Promise { const runtime = getMatrixRuntime(); const cfg = runtime.config.current() as CoreConfig; if (!matrixSetupAdapter.applyAccountConfig) { throw new Error("Matrix account setup is unavailable."); } const input: ChannelSetupInput = { name: params.name, avatarUrl: params.avatarUrl, homeserver: params.homeserver, dangerouslyAllowPrivateNetwork: params.allowPrivateNetwork, proxy: params.proxy, userId: params.userId, accessToken: params.accessToken, password: params.password, deviceName: params.deviceName, initialSyncLimit: parseOptionalInt(params.initialSyncLimit, "--initial-sync-limit"), useEnv: params.useEnv === true, }; const accountId = matrixSetupAdapter.resolveAccountId?.({ cfg, accountId: params.account, input, }) ?? normalizeAccountId(params.account?.trim() || params.name?.trim()); const validationError = matrixSetupAdapter.validateInput?.({ cfg, accountId, input, }); if (validationError) { throw new Error(validationError); } let updated = matrixSetupAdapter.applyAccountConfig({ cfg, accountId, input, }) as CoreConfig; if (params.enableEncryption === true) { updated = updateMatrixAccountConfig(updated, accountId, { encryption: true }); } await runtime.config.replaceConfigFile({ nextConfig: updated as never, afterWrite: { mode: "auto" }, }); const accountConfig = resolveMatrixAccountConfig({ cfg: updated, accountId }); let verificationBootstrap: MatrixCliAccountAddResult["verificationBootstrap"] = { attempted: false, success: false, recoveryKeyCreatedAt: null, backupVersion: null, }; if (accountConfig.encryption === true) { const { maybeBootstrapNewEncryptedMatrixAccount } = await import("./setup-bootstrap.js"); verificationBootstrap = await maybeBootstrapNewEncryptedMatrixAccount({ previousCfg: cfg, cfg: updated, accountId, }); } const desiredDisplayName = input.name?.trim(); const desiredAvatarUrl = input.avatarUrl?.trim(); let profile: MatrixCliAccountAddResult["profile"] = { attempted: false, displayNameUpdated: false, avatarUpdated: false, resolvedAvatarUrl: null, convertedAvatarFromHttp: false, }; if (desiredDisplayName || desiredAvatarUrl) { try { const synced = await updateMatrixOwnProfile({ cfg: updated, accountId, displayName: desiredDisplayName, avatarUrl: desiredAvatarUrl, }); let resolvedAvatarUrl = synced.resolvedAvatarUrl; if (synced.convertedAvatarFromHttp && synced.resolvedAvatarUrl) { const latestCfg = runtime.config.current() as CoreConfig; const withAvatar = updateMatrixAccountConfig(latestCfg, accountId, { avatarUrl: synced.resolvedAvatarUrl, }); await runtime.config.replaceConfigFile({ nextConfig: withAvatar as never, afterWrite: { mode: "auto" }, }); resolvedAvatarUrl = synced.resolvedAvatarUrl; } profile = { attempted: true, displayNameUpdated: synced.displayNameUpdated, avatarUpdated: synced.avatarUpdated, resolvedAvatarUrl, convertedAvatarFromHttp: synced.convertedAvatarFromHttp, }; } catch (err) { profile = { attempted: true, displayNameUpdated: false, avatarUpdated: false, resolvedAvatarUrl: null, convertedAvatarFromHttp: false, error: toErrorMessage(err), }; } } let deviceHealth: MatrixCliAccountAddResult["deviceHealth"] = { currentDeviceId: null, staleOpenClawDeviceIds: [], }; try { const addedDevices = await listMatrixOwnDevices({ accountId, cfg: updated }); deviceHealth = { currentDeviceId: addedDevices.find((device) => device.current)?.deviceId ?? null, staleOpenClawDeviceIds: addedDevices .filter((device) => !device.current && isOpenClawManagedMatrixDevice(device.displayName)) .map((device) => device.deviceId), }; } catch (err) { deviceHealth = { currentDeviceId: null, staleOpenClawDeviceIds: [], error: toErrorMessage(err), }; } return { accountId, configPath: resolveMatrixConfigPath(updated, accountId), useEnv: input.useEnv === true, encryptionEnabled: accountConfig.encryption === true, deviceHealth, verificationBootstrap, profile, }; } function printDirectRoomCandidate(room: MatrixCliDirectRoomCandidate): void { const members = room.joinedMembers === null ? "unavailable" : room.joinedMembers.map((member) => formatMatrixCliText(member)).join(", ") || "none"; console.log( `- ${formatMatrixCliText(room.roomId)} [${room.source}] strict=${ room.strict ? "yes" : "no" } joined=${members}`, ); } function printDirectRoomInspection(result: MatrixCliDirectRoomInspection): void { printAccountLabel(result.accountId); console.log(`Peer: ${formatMatrixCliText(result.remoteUserId)}`); console.log(`Self: ${formatMatrixCliText(result.selfUserId)}`); console.log(`Active direct room: ${formatMatrixCliText(result.activeRoomId, "none")}`); console.log( `Mapped rooms: ${ result.mappedRoomIds.length ? result.mappedRoomIds.map((roomId) => formatMatrixCliText(roomId)).join(", ") : "none" }`, ); console.log( `Discovered strict rooms: ${ result.discoveredStrictRoomIds.length ? result.discoveredStrictRoomIds.map((roomId) => formatMatrixCliText(roomId)).join(", ") : "none" }`, ); if (result.mappedRooms.length > 0) { console.log("Mapped room details:"); for (const room of result.mappedRooms) { printDirectRoomCandidate(room); } } } async function inspectMatrixDirectRoom(params: { accountId: string; userId: string; }): Promise { const cfg = getMatrixRuntime().config.current() as CoreConfig; const [{ withResolvedActionClient }, { inspectMatrixDirectRooms }] = await Promise.all([ loadMatrixActionClientModule(), loadMatrixDirectManagementModule(), ]); return await withResolvedActionClient( { accountId: params.accountId, cfg }, async (client) => { const inspection = await inspectMatrixDirectRooms({ client, remoteUserId: params.userId, }); return { accountId: params.accountId, remoteUserId: inspection.remoteUserId, selfUserId: inspection.selfUserId, mappedRoomIds: inspection.mappedRoomIds, mappedRooms: inspection.mappedRooms.map(toCliDirectRoomCandidate), discoveredStrictRoomIds: inspection.discoveredStrictRoomIds, activeRoomId: inspection.activeRoomId, }; }, "persist", ); } async function repairMatrixDirectRoom(params: { accountId: string; userId: string; }): Promise { const cfg = getMatrixRuntime().config.current() as CoreConfig; const account = resolveMatrixAccount({ cfg, accountId: params.accountId }); const [{ withStartedActionClient }, { repairMatrixDirectRooms }] = await Promise.all([ loadMatrixActionClientModule(), loadMatrixDirectManagementModule(), ]); return await withStartedActionClient({ accountId: params.accountId, cfg }, async (client) => { const repaired = await repairMatrixDirectRooms({ client, remoteUserId: params.userId, encrypted: account.config.encryption === true, }); return { accountId: params.accountId, remoteUserId: repaired.remoteUserId, selfUserId: repaired.selfUserId, mappedRoomIds: repaired.mappedRoomIds, mappedRooms: repaired.mappedRooms.map(toCliDirectRoomCandidate), discoveredStrictRoomIds: repaired.discoveredStrictRoomIds, activeRoomId: repaired.activeRoomId, encrypted: account.config.encryption === true, createdRoomId: repaired.createdRoomId, changed: repaired.changed, directContentBefore: repaired.directContentBefore, directContentAfter: repaired.directContentAfter, }; }); } type MatrixCliProfileSetResult = MatrixProfileUpdateResult; async function setMatrixProfile(params: { account?: string; name?: string; avatarUrl?: string; }): Promise { return await applyMatrixProfileUpdate({ account: params.account, displayName: params.name, avatarUrl: params.avatarUrl, }); } type MatrixCliCommandConfig = { verbose: boolean; json: boolean; run: () => Promise; onText: (result: TResult, verbose: boolean) => void; onJson?: (result: TResult) => unknown; shouldFail?: (result: TResult) => boolean; errorPrefix: string; onJsonError?: (message: string) => unknown; onTextError?: (message: string) => void; }; async function runMatrixCliCommand( config: MatrixCliCommandConfig, ): Promise { configureCliLogMode(config.verbose); try { const result = await config.run(); if (config.json) { printJson(config.onJson ? config.onJson(result) : result); } else { config.onText(result, config.verbose); } if (config.shouldFail?.(result)) { markCliFailure(); } } catch (err) { const message = toErrorMessage(err); if (config.json) { printJson(config.onJsonError ? config.onJsonError(message) : { error: message }); } else { console.error(`${config.errorPrefix}: ${formatMatrixCliText(message)}`); config.onTextError?.(message); } markCliFailure(); } finally { scheduleMatrixCliExit(); } } type MatrixCliBackupStatus = { serverVersion: string | null; activeVersion: string | null; trusted: boolean | null; matchesDecryptionKey: boolean | null; decryptionKeyCached: boolean | null; keyLoadAttempted: boolean; keyLoadError: string | null; }; type MatrixCliVerificationStatus = { encryptionEnabled: boolean; verified: boolean; userId: string | null; deviceId: string | null; localVerified: boolean; crossSigningVerified: boolean; signedByOwner: boolean; backupVersion: string | null; backup?: MatrixCliBackupStatus; serverDeviceKnown?: boolean | null; recoveryKeyStored: boolean; recoveryKeyCreatedAt: string | null; recoveryKeyId: string | null; pendingVerifications: number; recoveryKeyAccepted?: boolean; backupUsable?: boolean; deviceOwnerVerified?: boolean; }; type MatrixCliVerificationCommandOptions = { account?: string; userId?: string; roomId?: string; verbose?: boolean; json?: boolean; }; type MatrixCliSelfVerificationCommandOptions = { account?: string; timeoutMs?: string; verbose?: boolean; }; type MatrixCliVerificationSummary = { id: string; transactionId?: string; roomId?: string; otherUserId: string; otherDeviceId?: string; isSelfVerification: boolean; initiatedByMe: boolean; phaseName: string; pending: boolean; methods: string[]; chosenMethod?: string | null; hasSas: boolean; sas?: MatrixCliVerificationSas; completed: boolean; error?: string; }; type MatrixCliVerificationSas = { decimal?: [number, number, number]; emoji?: Array<[string, string]>; }; type MatrixCliDirectRoomCandidate = { roomId: string; source: "account-data" | "joined"; strict: boolean; joinedMembers: string[] | null; }; type MatrixCliDirectRoomInspection = { accountId: string; remoteUserId: string; selfUserId: string | null; mappedRoomIds: string[]; mappedRooms: MatrixCliDirectRoomCandidate[]; discoveredStrictRoomIds: string[]; activeRoomId: string | null; }; type MatrixCliDirectRoomRepair = MatrixCliDirectRoomInspection & { encrypted: boolean; createdRoomId: string | null; changed: boolean; directContentBefore: Record; directContentAfter: Record; }; type MatrixCliVerificationBootstrap = Awaited>; type MatrixCliEncryptionSetupResult = { accountId: string; configPath: string; encryptionChanged: boolean; bootstrap: MatrixCliVerificationBootstrap; status: MatrixCliVerificationStatus; }; function isMatrixVerificationSetupComplete(status: MatrixCliVerificationStatus): boolean { return ( status.encryptionEnabled && status.verified && status.crossSigningVerified && status.signedByOwner && status.serverDeviceKnown === true && resolveMatrixRoomKeyBackupIssue(resolveBackupStatus(status)).code === "ok" ); } function buildNoopMatrixVerificationBootstrap( status: MatrixCliVerificationStatus, ): MatrixCliVerificationBootstrap { const verification = { ...status, backup: resolveBackupStatus(status), serverDeviceKnown: status.serverDeviceKnown ?? null, }; return { success: true, verification, crossSigning: { userId: status.userId, masterKeyPublished: status.crossSigningVerified, selfSigningKeyPublished: status.signedByOwner, userSigningKeyPublished: status.signedByOwner, published: status.crossSigningVerified && status.signedByOwner, }, pendingVerifications: status.pendingVerifications, cryptoBootstrap: null, }; } async function setupMatrixEncryption(params: { account?: string; recoveryKey?: string; forceResetCrossSigning?: boolean; }): Promise { const runtime = getMatrixRuntime(); const { accountId, cfg } = resolveMatrixCliAccountContext(params.account); const account = resolveMatrixAccount({ cfg, accountId }); if (!account.configured) { throw new Error( `Matrix account "${accountId}" is not configured; run ${formatMatrixCliCommand( "account add", accountId, )} first.`, ); } const currentAccountConfig = resolveMatrixAccountConfig({ cfg, accountId }); const encryptionChanged = currentAccountConfig.encryption !== true; const updated = encryptionChanged ? updateMatrixAccountConfig(cfg, accountId, { encryption: true }) : cfg; if (encryptionChanged) { await runtime.config.replaceConfigFile({ nextConfig: updated as never, afterWrite: { mode: "auto" }, }); } const canUseExistingBootstrap = !encryptionChanged && !params.recoveryKey && params.forceResetCrossSigning !== true; const existingStatus = canUseExistingBootstrap ? await getMatrixVerificationStatus({ accountId, cfg: updated, readiness: "none" }) : null; if (existingStatus && isMatrixVerificationSetupComplete(existingStatus)) { return { accountId, configPath: resolveMatrixConfigPath(updated, accountId), encryptionChanged, bootstrap: buildNoopMatrixVerificationBootstrap(existingStatus), status: existingStatus, }; } const bootstrap = await bootstrapMatrixVerification({ accountId, cfg: updated, recoveryKey: params.recoveryKey, forceResetCrossSigning: params.forceResetCrossSigning === true, }); const status = await getMatrixVerificationStatus({ accountId, cfg: updated }); return { accountId, configPath: resolveMatrixConfigPath(updated, accountId), encryptionChanged, bootstrap, status, }; } function toCliDirectRoomCandidate(room: MatrixDirectRoomCandidate): MatrixCliDirectRoomCandidate { return { roomId: room.roomId, source: room.source, strict: room.strict, joinedMembers: room.joinedMembers, }; } function resolveBackupStatus(status: { backupVersion: string | null; backup?: MatrixCliBackupStatus; }): MatrixCliBackupStatus { return { serverVersion: status.backup?.serverVersion ?? status.backupVersion ?? null, activeVersion: status.backup?.activeVersion ?? null, trusted: status.backup?.trusted ?? null, matchesDecryptionKey: status.backup?.matchesDecryptionKey ?? null, decryptionKeyCached: status.backup?.decryptionKeyCached ?? null, keyLoadAttempted: status.backup?.keyLoadAttempted ?? false, keyLoadError: status.backup?.keyLoadError ?? null, }; } function yesNoUnknown(value: boolean | null): string { if (value === true) { return "yes"; } if (value === false) { return "no"; } return "unknown"; } function printBackupStatus(backup: MatrixCliBackupStatus): void { console.log(`Backup server version: ${formatMatrixCliText(backup.serverVersion, "none")}`); console.log(`Backup active on this device: ${formatMatrixCliText(backup.activeVersion, "no")}`); console.log(`Backup trusted by this device: ${yesNoUnknown(backup.trusted)}`); console.log(`Backup matches local decryption key: ${yesNoUnknown(backup.matchesDecryptionKey)}`); console.log(`Backup key cached locally: ${yesNoUnknown(backup.decryptionKeyCached)}`); console.log(`Backup key load attempted: ${yesNoUnknown(backup.keyLoadAttempted)}`); if (backup.keyLoadError) { console.log(`Backup key load error: ${formatMatrixCliText(backup.keyLoadError)}`); } } function printVerificationIdentity(status: { userId: string | null; deviceId: string | null; }): void { console.log(`User: ${formatMatrixCliText(status.userId)}`); console.log(`Device: ${formatMatrixCliText(status.deviceId)}`); } function printVerificationBackupSummary(status: { backupVersion: string | null; backup?: MatrixCliBackupStatus; }): void { printBackupSummary(resolveBackupStatus(status)); } function printVerificationBackupStatus(status: { backupVersion: string | null; backup?: MatrixCliBackupStatus; }): void { printBackupStatus(resolveBackupStatus(status)); } function printVerificationTrustDiagnostics(status: { localVerified: boolean; crossSigningVerified: boolean; signedByOwner: boolean; }): void { console.log(`Locally trusted: ${status.localVerified ? "yes" : "no"}`); console.log(`Cross-signing verified: ${status.crossSigningVerified ? "yes" : "no"}`); console.log(`Signed by owner: ${status.signedByOwner ? "yes" : "no"}`); } function sanitizeMatrixCliText(value: string): string { let withoutAnsi = ""; for (let index = 0; index < value.length; index++) { const code = value.charCodeAt(index); if (code === 0x9b) { index++; while (index < value.length && !isAnsiFinalByte(value.charCodeAt(index))) { index++; } continue; } if (code === 0x9d) { index++; while (index < value.length) { const current = value.charCodeAt(index); if (current === 0x07 || current === 0x9c) { break; } if (current === 0x1b && value[index + 1] === "\\") { index++; break; } index++; } continue; } if (code === 0x90 || code === 0x9e || code === 0x9f) { index++; while (index < value.length) { const current = value.charCodeAt(index); if (current === 0x07 || current === 0x9c) { break; } if (current === 0x1b && value[index + 1] === "\\") { index++; break; } index++; } continue; } if (code !== 0x1b) { withoutAnsi += value[index]; continue; } const marker = value[index + 1]; if (marker === "[") { index += 2; while (index < value.length && !isAnsiFinalByte(value.charCodeAt(index))) { index++; } continue; } if (marker === "]") { index += 2; while (index < value.length) { const current = value.charCodeAt(index); if (current === 0x07) { break; } if (current === 0x1b && value[index + 1] === "\\") { index++; break; } index++; } continue; } index++; } let sanitized = ""; for (const character of withoutAnsi) { const code = character.charCodeAt(0); if (!isUnsafeMatrixCliTerminalCode(code)) { sanitized += character; } } return sanitized; } function isUnsafeMatrixCliTerminalCode(code: number): boolean { return ( code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f) || (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069) ); } function isAnsiFinalByte(code: number): boolean { return code >= 0x40 && code <= 0x7e; } function formatMatrixCliSasEmoji(emoji: NonNullable): string { return emoji .map( ([emojiValue, label]) => `${sanitizeMatrixCliText(emojiValue)} ${sanitizeMatrixCliText(label)}`, ) .join(" | "); } function printMatrixVerificationSummary(summary: MatrixCliVerificationSummary): void { console.log(`Verification id: ${sanitizeMatrixCliText(summary.id)}`); if (summary.transactionId) { console.log(`Transaction id: ${sanitizeMatrixCliText(summary.transactionId)}`); } if (summary.roomId) { console.log(`Room id: ${sanitizeMatrixCliText(summary.roomId)}`); } console.log(`Other user: ${sanitizeMatrixCliText(summary.otherUserId)}`); console.log(`Other device: ${sanitizeMatrixCliText(summary.otherDeviceId ?? "unknown")}`); console.log(`Self-verification: ${summary.isSelfVerification ? "yes" : "no"}`); console.log(`Initiated by OpenClaw: ${summary.initiatedByMe ? "yes" : "no"}`); console.log(`Phase: ${sanitizeMatrixCliText(summary.phaseName)}`); console.log(`Pending: ${summary.pending ? "yes" : "no"}`); console.log(`Completed: ${summary.completed ? "yes" : "no"}`); console.log( `Methods: ${ summary.methods.length ? summary.methods.map(sanitizeMatrixCliText).join(", ") : "none" }`, ); if (summary.chosenMethod) { console.log(`Chosen method: ${sanitizeMatrixCliText(summary.chosenMethod)}`); } if (summary.hasSas && summary.sas?.emoji?.length) { console.log(`SAS emoji: ${formatMatrixCliSasEmoji(summary.sas.emoji)}`); } else if (summary.hasSas && summary.sas?.decimal) { console.log(`SAS decimals: ${summary.sas.decimal.join(" ")}`); } if (summary.error) { console.log(`Verification error: ${sanitizeMatrixCliText(summary.error)}`); } } function printMatrixVerificationSummaries(summaries: MatrixCliVerificationSummary[]): void { if (summaries.length === 0) { console.log("Verifications: none"); return; } summaries.forEach((summary, index) => { if (index > 0) { console.log(""); } printMatrixVerificationSummary(summary); }); } function printMatrixVerificationSas(sas: MatrixCliVerificationSas): void { if (sas.emoji?.length) { console.log(`SAS emoji: ${formatMatrixCliSasEmoji(sas.emoji)}`); } else if (sas.decimal) { console.log(`SAS decimals: ${sas.decimal.join(" ")}`); } else { console.log("SAS: unavailable"); } } function matrixCliVerificationDmLookupOptions(options: MatrixCliVerificationCommandOptions): { verificationDmRoomId?: string; verificationDmUserId?: string; } { const lookup: { verificationDmRoomId?: string; verificationDmUserId?: string; } = {}; if (options.roomId !== undefined) { lookup.verificationDmRoomId = options.roomId; } if (options.userId !== undefined) { lookup.verificationDmUserId = options.userId; } return lookup; } function formatMatrixVerificationDmFollowupParts(params: { roomId?: string; userId?: string; }): string[] { if (!params.roomId || !params.userId) { return []; } return [ "--user-id", sanitizeMatrixCliText(params.userId), "--room-id", sanitizeMatrixCliText(params.roomId), ]; } function formatMatrixVerificationSummaryDmFollowupParts( summary: MatrixCliVerificationSummary, ): string[] { return formatMatrixVerificationDmFollowupParts({ roomId: summary.roomId, userId: summary.otherUserId, }); } function formatMatrixVerificationOptionsDmFollowupParts( options: MatrixCliVerificationCommandOptions, ): string[] { return formatMatrixVerificationDmFollowupParts({ roomId: options.roomId, userId: options.userId, }); } function formatMatrixVerificationPreferredDmFollowupParts( summary: MatrixCliVerificationSummary, options: MatrixCliVerificationCommandOptions, ): string[] { const summaryParts = formatMatrixVerificationSummaryDmFollowupParts(summary); return summaryParts.length ? summaryParts : formatMatrixVerificationOptionsDmFollowupParts(options); } function formatMatrixVerificationFollowupCommand(params: { action: string; requestId: string; accountId?: string; dmParts?: string[]; }): string { return formatMatrixCliCommandParts( ["verify", params.action, ...(params.dmParts ?? []), "--", params.requestId], params.accountId, ); } function printMatrixVerificationSasGuidance( requestId: string, accountId?: string, dmParts: string[] = [], ): void { printGuidance([ `Compare the emoji or decimals with the other Matrix client.`, `If they match, run ${formatMatrixVerificationFollowupCommand({ action: "confirm-sas", requestId, accountId, dmParts })}.`, `If they do not match, run ${formatMatrixVerificationFollowupCommand({ action: "mismatch-sas", requestId, accountId, dmParts })}.`, ]); } function formatMatrixVerificationCommandId(summary: MatrixCliVerificationSummary): string { return sanitizeMatrixCliText(summary.transactionId ?? summary.id); } async function promptMatrixVerificationSasMatch(): Promise { const { createInterface } = await import("node:readline/promises"); const prompt = createInterface({ input: process.stdin, output: process.stdout, }); try { const answer = await prompt.question("Do the emoji or decimals match? Type yes to confirm: "); return /^(?:y|yes)$/i.test(answer.trim()); } finally { prompt.close(); } } function printMatrixVerificationRequestGuidance( summary: MatrixCliVerificationSummary, accountId?: string, ): void { const requestId = formatMatrixVerificationCommandId(summary); const dmParts = formatMatrixVerificationSummaryDmFollowupParts(summary); printGuidance([ `Accept the verification request in another Matrix client for this account.`, `Then run ${formatMatrixVerificationFollowupCommand({ action: "start", requestId, accountId, dmParts })} to start SAS verification.`, `Run ${formatMatrixVerificationFollowupCommand({ action: "sas", requestId, accountId, dmParts })} to display the SAS emoji or decimals.`, `When the SAS matches, run ${formatMatrixVerificationFollowupCommand({ action: "confirm-sas", requestId, accountId, dmParts })}.`, ]); } async function runMatrixCliVerificationSummaryCommand(params: { options: MatrixCliVerificationCommandOptions; run: (accountId: string, cfg: CoreConfig) => Promise; afterText?: (summary: MatrixCliVerificationSummary, accountId: string) => void; errorPrefix: string; }): Promise { const { accountId, cfg } = resolveMatrixCliAccountContext(params.options.account); await runMatrixCliCommand({ verbose: params.options.verbose === true, json: params.options.json === true, run: async () => await params.run(accountId, cfg), onText: (summary) => { printAccountLabel(accountId); printMatrixVerificationSummary(summary); params.afterText?.(summary, accountId); }, errorPrefix: params.errorPrefix, }); } async function runMatrixCliSelfVerificationCommand( options: MatrixCliSelfVerificationCommandOptions, ): Promise { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: false, run: async () => await runMatrixSelfVerification({ accountId, cfg, timeoutMs: parseOptionalInt(options.timeoutMs, "--timeout-ms"), onRequested: (summary) => { printAccountLabel(accountId); printMatrixVerificationSummary(summary); console.log("Accept this verification request in another Matrix client."); }, onReady: (summary) => { console.log("Verification request accepted."); if (!summary.hasSas) { console.log("Starting SAS verification..."); } }, onSas: (summary) => { printMatrixVerificationSas(summary.sas ?? {}); console.log("Compare this SAS with the other Matrix client."); }, confirmSas: async () => await promptMatrixVerificationSasMatch(), }), onText: (summary, verbose) => { printMatrixVerificationSummary(summary); console.log(`Device verified by owner: ${summary.deviceOwnerVerified ? "yes" : "no"}`); printVerificationTrustDiagnostics(summary.ownerVerification); printVerificationBackupSummary(summary.ownerVerification); if (verbose) { printVerificationBackupStatus(summary.ownerVerification); } console.log("Self-verification complete."); }, onTextError: () => { printGuidance([ `Run ${formatMatrixCliCommand("verify self", accountId)} again and accept the request in another verified Matrix client for this account.`, `Then run ${formatMatrixCliCommand("verify status --verbose", accountId)} to confirm Cross-signing verified: yes and Signed by owner: yes.`, ]); }, errorPrefix: "Self-verification failed", }); } function printVerificationGuidance(status: MatrixCliVerificationStatus, accountId?: string): void { printGuidance(buildVerificationGuidance(status, accountId)); } function printBackupGuidance( backup: MatrixCliBackupStatus, accountId?: string, options: { recoveryKeyStored?: boolean } = {}, ): void { printGuidance(buildBackupGuidance(backup, accountId, options)); } function printBackupSummary(backup: MatrixCliBackupStatus): void { const issue = resolveMatrixRoomKeyBackupIssue(backup); console.log(`Backup: ${issue.summary}`); if (backup.serverVersion) { console.log(`Backup version: ${formatMatrixCliText(backup.serverVersion)}`); } } function buildVerificationGuidance( status: MatrixCliVerificationStatus, accountId?: string, ): string[] { const backup = resolveBackupStatus(status); const nextSteps = new Set(); if (!status.verified) { if (status.recoveryKeyAccepted === true && status.backupUsable === true) { nextSteps.add( `Recovery key can unlock the room-key backup, but full Matrix identity trust is still incomplete. Run ${formatMatrixCliCommand("verify self", accountId)}, accept the request in another verified Matrix client, and confirm the SAS only if it matches.`, ); nextSteps.add( `If you intend to replace the current cross-signing identity, run the shown printf pipeline with the Matrix recovery key env var for this account: ${formatMatrixCliRecoveryKeyStdinCommand("verify bootstrap --recovery-key-stdin --force-reset-cross-signing", accountId)}.`, ); } else { nextSteps.add( `Run the shown printf pipeline with the Matrix recovery key env var for this account: ${formatMatrixCliRecoveryKeyStdinCommand("verify device --recovery-key-stdin", accountId)}. If you do not have the recovery key but still have another verified Matrix client, run ${formatMatrixCliCommand("verify self", accountId)} instead.`, ); } } if (status.serverDeviceKnown === false) { nextSteps.add( `This Matrix device is no longer listed on the homeserver. Create a new OpenClaw Matrix device with ${formatMatrixCliCommand("account add --homeserver --user-id <@user:server> --password --device-name OpenClaw-Gateway", accountId)}. If you use token auth, create a fresh Matrix access token in your Matrix client or admin UI, then run ${formatMatrixCliCommand("account add --homeserver --access-token ", accountId)}.`, ); } for (const step of buildBackupGuidance(backup, accountId, { recoveryKeyStored: status.recoveryKeyStored, })) { nextSteps.add(step); } if (status.pendingVerifications > 0) { nextSteps.add( `Review pending verification requests with ${formatMatrixCliCommand("verify list", accountId)}. Complete each active request with ${formatMatrixCliCommand("verify sas ", accountId)} and ${formatMatrixCliCommand("verify confirm-sas ", accountId)}, or cancel stale requests with ${formatMatrixCliCommand("verify cancel ", accountId)}.`, ); } return Array.from(nextSteps); } function buildBackupGuidance( backup: MatrixCliBackupStatus, accountId?: string, options: { recoveryKeyStored?: boolean } = {}, ): string[] { const backupIssue = resolveMatrixRoomKeyBackupIssue(backup); const nextSteps = new Set(); if (backupIssue.code === "missing-server-backup") { nextSteps.add( `Run ${formatMatrixCliCommand("verify bootstrap", accountId)} to create a room key backup.`, ); } else if ( backupIssue.code === "key-load-failed" || backupIssue.code === "key-not-loaded" || backupIssue.code === "inactive" ) { if (options.recoveryKeyStored) { nextSteps.add( `Backup key is not loaded on this device. Run ${formatMatrixCliCommand("verify backup restore", accountId)} to load it and restore old room keys. If restore still cannot load the key, run the shown printf pipeline with the Matrix recovery key env var for this account: ${formatMatrixCliRecoveryKeyStdinCommand("verify backup restore --recovery-key-stdin", accountId)}.`, ); } else { nextSteps.add( `Run the shown printf pipeline with the Matrix recovery key env var for this account: ${formatMatrixCliRecoveryKeyStdinCommand("verify backup restore --recovery-key-stdin", accountId)} to load the server backup and store the key for future restores.`, ); } } else if (backupIssue.code === "key-mismatch") { nextSteps.add( `Backup key mismatch on this device. Run the shown printf pipeline with the active server backup recovery key env var for this account: ${formatMatrixCliRecoveryKeyStdinCommand("verify backup restore --recovery-key-stdin", accountId)}.`, ); nextSteps.add( `If you want a fresh backup baseline and accept losing unrecoverable history, run ${formatMatrixCliCommand("verify backup reset --yes", accountId)}. Add --rotate-recovery-key only when the old recovery key should stop unlocking the fresh backup.`, ); } else if (backupIssue.code === "untrusted-signature") { nextSteps.add( `Backup trust chain is not verified on this device. Run the shown printf pipeline with the correct recovery key env var for this account: ${formatMatrixCliRecoveryKeyStdinCommand("verify device --recovery-key-stdin", accountId)}.`, ); nextSteps.add( `If device identity trust remains incomplete after that, run ${formatMatrixCliCommand("verify self", accountId)} from another verified Matrix client.`, ); nextSteps.add( `If you want a fresh backup baseline and accept losing unrecoverable history, run ${formatMatrixCliCommand("verify backup reset --yes", accountId)}. Add --rotate-recovery-key only when the old recovery key should stop unlocking the fresh backup.`, ); } else if (backupIssue.code === "indeterminate") { nextSteps.add( `Run ${formatMatrixCliCommand("verify status --verbose", accountId)} to inspect backup trust diagnostics.`, ); } return Array.from(nextSteps); } function printGuidance(lines: string[]): void { if (lines.length === 0) { return; } console.log("Next steps:"); for (const line of lines) { console.log(`- ${line}`); } } function printVerificationStatus( status: MatrixCliVerificationStatus, verbose = false, accountId?: string, ): void { console.log(`Verified by owner: ${status.verified ? "yes" : "no"}`); if (status.serverDeviceKnown === false) { console.log("Device issue: current Matrix device is missing from the homeserver device list"); } const backup = resolveBackupStatus(status); const backupIssue = resolveMatrixRoomKeyBackupIssue(backup); printVerificationBackupSummary(status); if (backupIssue.message) { console.log(`Backup issue: ${backupIssue.message}`); } if (verbose) { console.log("Diagnostics:"); printVerificationIdentity(status); if (status.serverDeviceKnown !== undefined) { console.log(`Device present on server: ${yesNoUnknown(status.serverDeviceKnown ?? null)}`); } printVerificationTrustDiagnostics(status); printVerificationBackupStatus(status); console.log(`Recovery key stored: ${status.recoveryKeyStored ? "yes" : "no"}`); printTimestamp("Recovery key created at", status.recoveryKeyCreatedAt); console.log(`Pending verifications: ${status.pendingVerifications}`); } else { console.log(`Recovery key stored: ${status.recoveryKeyStored ? "yes" : "no"}`); } printVerificationGuidance(status, accountId); } function printMatrixEncryptionSetupResult( result: MatrixCliEncryptionSetupResult, verbose = false, ): void { printAccountLabel(result.accountId); console.log( `Encryption config: ${result.encryptionChanged ? "enabled" : "already enabled"} at ${formatMatrixCliText( result.configPath, )}`, ); console.log(`Bootstrap success: ${result.bootstrap.success ? "yes" : "no"}`); if (result.bootstrap.error) { console.log(`Bootstrap error: ${formatMatrixCliText(result.bootstrap.error)}`); } console.log(`Verified by owner: ${result.status.verified ? "yes" : "no"}`); printVerificationBackupSummary(result.status); if (verbose) { printVerificationIdentity(result.status); printVerificationTrustDiagnostics(result.status); printVerificationBackupStatus(result.status); console.log(`Recovery key stored: ${result.status.recoveryKeyStored ? "yes" : "no"}`); printTimestamp("Recovery key created at", result.status.recoveryKeyCreatedAt); console.log(`Pending verifications: ${result.status.pendingVerifications}`); } printVerificationGuidance(result.status, result.accountId); } export function registerMatrixCli(params: { program: Command }): void { const root = params.program .command("matrix") .description("Matrix channel utilities") .addHelpText("after", () => "\nDocs: https://docs.openclaw.ai/channels/matrix\n"); const account = root.command("account").description("Manage matrix channel accounts"); account .command("add") .description("Add or update a matrix account (wrapper around channel setup)") .option("--account ", "Account ID (default: normalized --name, else default)") .option("--name ", "Optional display name for this account") .option("--avatar-url ", "Optional Matrix avatar URL (mxc:// or http(s) URL)") .option("--homeserver ", "Matrix homeserver URL") .option("--proxy ", "Optional HTTP(S) proxy URL for Matrix requests") .option( "--allow-private-network", "Allow Matrix homeserver traffic to private/internal hosts for this account", ) .option("--user-id ", "Matrix user ID") .option("--access-token ", "Matrix access token") .option("--password ", "Matrix password") .option("--device-name ", "Matrix device display name") .option("--initial-sync-limit ", "Matrix initial sync limit") .option("--enable-e2ee", "Enable Matrix end-to-end encryption and bootstrap verification") .option("--encryption", "Alias for --enable-e2ee") .option( "--use-env", "Use MATRIX_* env vars (or MATRIX__* for non-default accounts)", ) .option("--verbose", "Show setup details") .option("--json", "Output as JSON") .action( async (options: { account?: string; name?: string; avatarUrl?: string; homeserver?: string; proxy?: string; allowPrivateNetwork?: boolean; userId?: string; accessToken?: string; password?: string; deviceName?: string; initialSyncLimit?: string; enableE2ee?: boolean; encryption?: boolean; useEnv?: boolean; verbose?: boolean; json?: boolean; }) => { await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await addMatrixAccount({ account: options.account, name: options.name, avatarUrl: options.avatarUrl, homeserver: options.homeserver, proxy: options.proxy, allowPrivateNetwork: options.allowPrivateNetwork === true, userId: options.userId, accessToken: options.accessToken, password: options.password, deviceName: options.deviceName, initialSyncLimit: options.initialSyncLimit, enableEncryption: options.enableE2ee === true || options.encryption === true, useEnv: options.useEnv === true, }), onText: (result) => { console.log(`Saved matrix account: ${formatMatrixCliText(result.accountId)}`); console.log(`Config path: ${formatMatrixCliText(result.configPath)}`); console.log( `Credentials source: ${result.useEnv ? "MATRIX_* / MATRIX__* env vars" : "inline config"}`, ); console.log(`Encryption: ${result.encryptionEnabled ? "enabled" : "disabled"}`); if (result.verificationBootstrap.attempted) { if (result.verificationBootstrap.success) { console.log("Matrix verification bootstrap: complete"); printTimestamp( "Recovery key created at", result.verificationBootstrap.recoveryKeyCreatedAt, ); if (result.verificationBootstrap.backupVersion) { console.log( `Backup version: ${formatMatrixCliText(result.verificationBootstrap.backupVersion)}`, ); } } else { console.error( `Matrix verification bootstrap warning: ${formatMatrixCliText(result.verificationBootstrap.error)}`, ); } } if (result.deviceHealth.error) { console.error( `Matrix device health warning: ${formatMatrixCliText(result.deviceHealth.error)}`, ); } else if (result.deviceHealth.staleOpenClawDeviceIds.length > 0) { const staleDeviceIds = result.deviceHealth.staleOpenClawDeviceIds .map((deviceId) => formatMatrixCliText(deviceId)) .join(", "); console.log( `Matrix device hygiene warning: stale OpenClaw devices detected (${staleDeviceIds}). Run ${formatMatrixCliCommand("devices prune-stale", result.accountId)}.`, ); } if (result.profile.attempted) { if (result.profile.error) { console.error(`Profile sync warning: ${formatMatrixCliText(result.profile.error)}`); } else { console.log( `Profile sync: name ${result.profile.displayNameUpdated ? "updated" : "unchanged"}, avatar ${result.profile.avatarUpdated ? "updated" : "unchanged"}`, ); if (result.profile.convertedAvatarFromHttp && result.profile.resolvedAvatarUrl) { console.log( `Avatar converted and saved as: ${formatMatrixCliText(result.profile.resolvedAvatarUrl)}`, ); } } } const bindHint = `openclaw agents bind --agent --bind matrix:${result.accountId}`; console.log(`Bind this account to an agent: ${bindHint}`); }, errorPrefix: "Account setup failed", }); }, ); const profile = root.command("profile").description("Manage Matrix bot profile"); profile .command("set") .description("Update Matrix profile display name and/or avatar") .option("--account ", "Account ID (for multi-account setups)") .option("--name ", "Profile display name") .option("--avatar-url ", "Profile avatar URL (mxc:// or http(s) URL)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { account?: string; name?: string; avatarUrl?: string; verbose?: boolean; json?: boolean; }) => { await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await setMatrixProfile({ account: options.account, name: options.name, avatarUrl: options.avatarUrl, }), onText: (result) => { printAccountLabel(result.accountId); console.log(`Config path: ${result.configPath}`); console.log( `Profile update: name ${result.profile.displayNameUpdated ? "updated" : "unchanged"}, avatar ${result.profile.avatarUpdated ? "updated" : "unchanged"}`, ); if (result.profile.convertedAvatarFromHttp && result.avatarUrl) { console.log( `Avatar converted and saved as: ${formatMatrixCliText(result.avatarUrl)}`, ); } }, errorPrefix: "Profile update failed", }); }, ); const direct = root.command("direct").description("Inspect and repair Matrix direct-room state"); direct .command("inspect") .description("Inspect direct-room mappings for a Matrix user") .requiredOption("--user-id ", "Peer Matrix user ID") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { userId: string; account?: string; verbose?: boolean; json?: boolean }) => { const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await inspectMatrixDirectRoom({ accountId, userId: options.userId, }), onText: (result) => { printDirectRoomInspection(result); }, errorPrefix: "Direct room inspection failed", }); }, ); direct .command("repair") .description("Repair Matrix direct-room mappings for a Matrix user") .requiredOption("--user-id ", "Peer Matrix user ID") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { userId: string; account?: string; verbose?: boolean; json?: boolean }) => { const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await repairMatrixDirectRoom({ accountId, userId: options.userId, }), onText: (result, verbose) => { printDirectRoomInspection(result); console.log(`Encrypted room creation: ${result.encrypted ? "enabled" : "disabled"}`); console.log(`Created room: ${formatMatrixCliText(result.createdRoomId, "none")}`); console.log(`m.direct updated: ${result.changed ? "yes" : "no"}`); if (verbose) { console.log( `m.direct before: ${formatMatrixCliText(JSON.stringify(result.directContentBefore[result.remoteUserId] ?? []))}`, ); console.log( `m.direct after: ${formatMatrixCliText(JSON.stringify(result.directContentAfter[result.remoteUserId] ?? []))}`, ); } }, errorPrefix: "Direct room repair failed", }); }, ); const encryption = root.command("encryption").description("Set up Matrix end-to-end encryption"); encryption .command("setup") .description("Enable Matrix E2EE, bootstrap verification, and print next steps") .option("--account ", "Account ID (for multi-account setups)") .option("--recovery-key ", "Recovery key to apply before bootstrap") .option("--force-reset-cross-signing", "Force reset cross-signing identity before bootstrap") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { account?: string; recoveryKey?: string; forceResetCrossSigning?: boolean; verbose?: boolean; json?: boolean; }) => { await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await setupMatrixEncryption({ account: options.account, recoveryKey: options.recoveryKey, forceResetCrossSigning: options.forceResetCrossSigning === true, }), onText: (result, verbose) => { printMatrixEncryptionSetupResult(result, verbose); }, onJson: (result) => ({ success: result.bootstrap.success, ...result }), shouldFail: (result) => !result.bootstrap.success, errorPrefix: "Encryption setup failed", onJsonError: (message) => ({ success: false, error: message }), }); }, ); const verify = root.command("verify").description("Device verification for Matrix E2EE"); verify .command("list") .description("List pending Matrix verification requests") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await listMatrixVerifications({ accountId, cfg }), onText: (summaries) => { printAccountLabel(accountId); printMatrixVerificationSummaries(summaries); }, errorPrefix: "Verification listing failed", }); }); verify .command("self") .description("Interactively self-verify this Matrix device") .option("--account ", "Account ID (for multi-account setups)") .option("--timeout-ms ", "How long to wait for the other Matrix client") .option("--verbose", "Show detailed diagnostics") .action(async (options: MatrixCliSelfVerificationCommandOptions) => { await runMatrixCliSelfVerificationCommand(options); }); verify .command("request") .description("Request Matrix device verification from another Matrix client") .option("--account ", "Account ID (for multi-account setups)") .option("--own-user", "Request self-verification for this Matrix account") .option("--user-id ", "Matrix user ID to verify") .option("--device-id ", "Matrix device ID to verify") .option("--room-id ", "Matrix direct-message room ID for verification") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { account?: string; ownUser?: boolean; userId?: string; deviceId?: string; roomId?: string; verbose?: boolean; json?: boolean; }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => { if ( options.ownUser === true && (options.userId || options.deviceId || options.roomId) ) { throw new Error( "--own-user cannot be combined with --user-id, --device-id, or --room-id", ); } return await requestMatrixVerification({ accountId, cfg, ownUser: options.ownUser === true ? true : undefined, userId: options.userId, deviceId: options.deviceId, roomId: options.roomId, }); }, onText: (summary) => { printAccountLabel(accountId); printMatrixVerificationSummary(summary); printMatrixVerificationRequestGuidance(summary, accountId); }, errorPrefix: "Verification request failed", }); }, ); verify .command("accept ") .description("Accept an inbound Matrix verification request") .option("--account ", "Account ID (for multi-account setups)") .option("--user-id ", "Matrix user ID for DM verification follow-up") .option("--room-id ", "Matrix direct-message room ID for verification follow-up") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (id: string, options: MatrixCliVerificationCommandOptions) => { await runMatrixCliVerificationSummaryCommand({ options, run: async (accountId, cfg) => await acceptMatrixVerification(id, { accountId, cfg, ...matrixCliVerificationDmLookupOptions(options), }), afterText: (summary, accountId) => { const requestId = formatMatrixVerificationCommandId(summary); const dmParts = formatMatrixVerificationPreferredDmFollowupParts(summary, options); printGuidance([ `Run ${formatMatrixVerificationFollowupCommand({ action: "start", requestId, accountId, dmParts })} to start SAS verification.`, ]); }, errorPrefix: "Verification accept failed", }); }); verify .command("start ") .description("Start SAS verification for a Matrix verification request") .option("--account ", "Account ID (for multi-account setups)") .option("--user-id ", "Matrix user ID for DM verification follow-up") .option("--room-id ", "Matrix direct-message room ID for verification follow-up") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (id: string, options: MatrixCliVerificationCommandOptions) => { await runMatrixCliVerificationSummaryCommand({ options, run: async (accountId, cfg) => await startMatrixVerification(id, { accountId, cfg, method: "sas", ...matrixCliVerificationDmLookupOptions(options), }), afterText: (summary, accountId) => printMatrixVerificationSasGuidance( formatMatrixVerificationCommandId(summary), accountId, formatMatrixVerificationPreferredDmFollowupParts(summary, options), ), errorPrefix: "Verification start failed", }); }); verify .command("sas ") .description("Show SAS emoji or decimals for a Matrix verification request") .option("--account ", "Account ID (for multi-account setups)") .option("--user-id ", "Matrix user ID for DM verification follow-up") .option("--room-id ", "Matrix direct-message room ID for verification follow-up") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (id: string, options: MatrixCliVerificationCommandOptions) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await getMatrixVerificationSas(id, { accountId, cfg, ...matrixCliVerificationDmLookupOptions(options), }), onText: (sas) => { const requestId = formatMatrixCliText(id); printAccountLabel(accountId); console.log(`Verification id: ${requestId}`); printMatrixVerificationSas(sas); printMatrixVerificationSasGuidance( requestId, accountId, formatMatrixVerificationOptionsDmFollowupParts(options), ); }, errorPrefix: "Verification SAS lookup failed", }); }); verify .command("confirm-sas ") .description("Confirm matching SAS emoji or decimals for a Matrix verification request") .option("--account ", "Account ID (for multi-account setups)") .option("--user-id ", "Matrix user ID for DM verification follow-up") .option("--room-id ", "Matrix direct-message room ID for verification follow-up") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (id: string, options: MatrixCliVerificationCommandOptions) => { await runMatrixCliVerificationSummaryCommand({ options, run: async (accountId, cfg) => await confirmMatrixVerificationSas(id, { accountId, cfg, ...matrixCliVerificationDmLookupOptions(options), }), errorPrefix: "Verification SAS confirm failed", }); }); verify .command("mismatch-sas ") .description("Reject a Matrix SAS verification when the emoji or decimals do not match") .option("--account ", "Account ID (for multi-account setups)") .option("--user-id ", "Matrix user ID for DM verification follow-up") .option("--room-id ", "Matrix direct-message room ID for verification follow-up") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (id: string, options: MatrixCliVerificationCommandOptions) => { await runMatrixCliVerificationSummaryCommand({ options, run: async (accountId, cfg) => await mismatchMatrixVerificationSas(id, { accountId, cfg, ...matrixCliVerificationDmLookupOptions(options), }), errorPrefix: "Verification SAS mismatch failed", }); }); verify .command("cancel ") .description("Cancel a Matrix verification request") .option("--account ", "Account ID (for multi-account setups)") .option("--user-id ", "Matrix user ID for DM verification follow-up") .option("--room-id ", "Matrix direct-message room ID for verification follow-up") .option("--reason ", "Cancellation reason") .option("--code ", "Matrix cancellation code") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async ( id: string, options: MatrixCliVerificationCommandOptions & { reason?: string; code?: string; }, ) => { await runMatrixCliVerificationSummaryCommand({ options, run: async (accountId, cfg) => await cancelMatrixVerification(id, { accountId, cfg, reason: options.reason, code: options.code, ...matrixCliVerificationDmLookupOptions(options), }), errorPrefix: "Verification cancel failed", }); }, ); verify .command("status") .description("Check Matrix device verification status") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--include-recovery-key", "Include stored recovery key in output") .option( "--allow-degraded-local-state", "Return best-effort diagnostics without preparing the Matrix account", ) .option("--json", "Output as JSON") .action( async (options: { allowDegradedLocalState?: boolean; account?: string; verbose?: boolean; includeRecoveryKey?: boolean; json?: boolean; }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await getMatrixVerificationStatus({ accountId, cfg, includeRecoveryKey: options.includeRecoveryKey === true, ...(options.allowDegradedLocalState === true ? { readiness: "none" as const } : {}), }), onText: (status, verbose) => { printAccountLabel(accountId); printVerificationStatus(status, verbose, accountId); }, shouldFail: (status) => status.serverDeviceKnown === false, errorPrefix: "Error", }); }, ); const backup = verify.command("backup").description("Matrix room-key backup health and restore"); backup .command("status") .description("Show Matrix room-key backup status for this device") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await getMatrixRoomKeyBackupStatus({ accountId, cfg }), onText: (status, verbose) => { printAccountLabel(accountId); printBackupSummary(status); if (verbose) { printBackupStatus(status); } printBackupGuidance(status, accountId); }, errorPrefix: "Backup status failed", }); }); backup .command("reset") .description( "Delete the current server backup and create a fresh room-key backup baseline, repairing secret storage if needed for a durable reset", ) .option("--account ", "Account ID (for multi-account setups)") .option("--yes", "Confirm destructive backup reset", false) .option("--rotate-recovery-key", "Create a new Matrix recovery key for the fresh backup") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { account?: string; yes?: boolean; rotateRecoveryKey?: boolean; verbose?: boolean; json?: boolean; }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => { if (options.yes !== true) { throw new Error( `Refusing to reset Matrix room-key backup without --yes. If you accept losing unrecoverable history, re-run ${formatMatrixCliCommand("verify backup reset --yes", accountId)}.`, ); } return await resetMatrixRoomKeyBackup({ accountId, cfg, rotateRecoveryKey: options.rotateRecoveryKey === true, }); }, onText: (result, verbose) => { printAccountLabel(accountId); console.log(`Reset success: ${result.success ? "yes" : "no"}`); if (result.error) { console.log(`Error: ${formatMatrixCliText(result.error)}`); } console.log( `Previous backup version: ${formatMatrixCliText(result.previousVersion, "none")}`, ); console.log( `Deleted backup version: ${formatMatrixCliText(result.deletedVersion, "none")}`, ); console.log( `Current backup version: ${formatMatrixCliText(result.createdVersion, "none")}`, ); printBackupSummary(result.backup); if (verbose) { printTimestamp("Reset at", result.resetAt); printBackupStatus(result.backup); } printBackupGuidance(result.backup, accountId); }, shouldFail: (result) => !result.success, errorPrefix: "Backup reset failed", onJsonError: (message) => ({ success: false, error: message }), }); }, ); backup .command("restore") .description("Restore encrypted room keys from server backup") .option("--account ", "Account ID (for multi-account setups)") .option( "--recovery-key ", "Optional recovery key to load before restoring (prefer --recovery-key-stdin)", ) .option("--recovery-key-stdin", "Read the Matrix recovery key from stdin") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { account?: string; recoveryKey?: string; recoveryKeyStdin?: boolean; verbose?: boolean; json?: boolean; }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await restoreMatrixRoomKeyBackup({ accountId, cfg, recoveryKey: await resolveMatrixCliRecoveryKeyInput(options), }), onText: (result, verbose) => { printAccountLabel(accountId); console.log(`Restore success: ${result.success ? "yes" : "no"}`); if (result.error) { console.log(`Error: ${formatMatrixCliText(result.error)}`); } console.log(`Backup version: ${formatMatrixCliText(result.backupVersion, "none")}`); console.log(`Imported keys: ${result.imported}/${result.total}`); printBackupSummary(result.backup); if (verbose) { console.log( `Loaded key from secret storage: ${result.loadedFromSecretStorage ? "yes" : "no"}`, ); printTimestamp("Restored at", result.restoredAt); printBackupStatus(result.backup); } printBackupGuidance(result.backup, accountId, { recoveryKeyStored: result.loadedFromSecretStorage, }); }, shouldFail: (result) => !result.success, errorPrefix: "Backup restore failed", onJsonError: (message) => ({ success: false, error: message }), }); }, ); verify .command("bootstrap") .description("Bootstrap Matrix cross-signing and device verification state") .option("--account ", "Account ID (for multi-account setups)") .option( "--recovery-key ", "Recovery key to apply before bootstrap (prefer --recovery-key-stdin)", ) .option("--recovery-key-stdin", "Read the Matrix recovery key from stdin") .option("--force-reset-cross-signing", "Force reset cross-signing identity before bootstrap") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { account?: string; recoveryKey?: string; recoveryKeyStdin?: boolean; forceResetCrossSigning?: boolean; verbose?: boolean; json?: boolean; }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await bootstrapMatrixVerification({ accountId, cfg, recoveryKey: await resolveMatrixCliRecoveryKeyInput(options), forceResetCrossSigning: options.forceResetCrossSigning === true, }), onText: (result, verbose) => { printAccountLabel(accountId); console.log(`Bootstrap success: ${result.success ? "yes" : "no"}`); if (result.error) { console.log(`Error: ${formatMatrixCliText(result.error)}`); } console.log(`Verified by owner: ${result.verification.verified ? "yes" : "no"}`); printVerificationIdentity(result.verification); if (verbose) { printVerificationTrustDiagnostics(result.verification); console.log( `Cross-signing published: ${result.crossSigning.published ? "yes" : "no"} (master=${result.crossSigning.masterKeyPublished ? "yes" : "no"}, self=${result.crossSigning.selfSigningKeyPublished ? "yes" : "no"}, user=${result.crossSigning.userSigningKeyPublished ? "yes" : "no"})`, ); printVerificationBackupStatus(result.verification); printTimestamp("Recovery key created at", result.verification.recoveryKeyCreatedAt); console.log(`Pending verifications: ${result.pendingVerifications}`); } else { console.log( `Cross-signing published: ${result.crossSigning.published ? "yes" : "no"}`, ); printVerificationBackupSummary(result.verification); } printVerificationGuidance( { ...result.verification, pendingVerifications: result.pendingVerifications, }, accountId, ); }, shouldFail: (result) => !result.success, errorPrefix: "Verification bootstrap failed", onJsonError: (message) => ({ success: false, error: message }), }); }, ); verify .command("device [key]") .description("Verify device using a Matrix recovery key") .option("--account ", "Account ID (for multi-account setups)") .option("--recovery-key-stdin", "Read the Matrix recovery key from stdin") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async ( key: string | undefined, options: { account?: string; recoveryKeyStdin?: boolean; verbose?: boolean; json?: boolean; }, ) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await verifyMatrixRecoveryKey( await requireMatrixCliRecoveryKeyInput({ recoveryKey: key, recoveryKeyStdin: options.recoveryKeyStdin, }), { accountId, cfg }, ), onText: (result, verbose) => { printAccountLabel(accountId); if (!result.success) { console.error(`Verification failed: ${formatMatrixCliText(result.error)}`); printVerificationIdentity(result); console.log(`Recovery key accepted: ${result.recoveryKeyAccepted ? "yes" : "no"}`); console.log(`Backup usable: ${result.backupUsable ? "yes" : "no"}`); console.log(`Device verified by owner: ${result.deviceOwnerVerified ? "yes" : "no"}`); printVerificationBackupSummary(result); if (verbose) { printVerificationTrustDiagnostics(result); printVerificationBackupStatus(result); printTimestamp("Recovery key created at", result.recoveryKeyCreatedAt); } printVerificationGuidance( { ...result, pendingVerifications: 0, }, accountId, ); return; } console.log("Device verification completed successfully."); printVerificationIdentity(result); console.log(`Recovery key accepted: ${result.recoveryKeyAccepted ? "yes" : "no"}`); console.log(`Backup usable: ${result.backupUsable ? "yes" : "no"}`); console.log(`Device verified by owner: ${result.deviceOwnerVerified ? "yes" : "no"}`); printVerificationBackupSummary(result); if (verbose) { printVerificationTrustDiagnostics(result); printVerificationBackupStatus(result); printTimestamp("Recovery key created at", result.recoveryKeyCreatedAt); printTimestamp("Verified at", result.verifiedAt); } printVerificationGuidance( { ...result, pendingVerifications: 0, }, accountId, ); }, shouldFail: (result) => !result.success, errorPrefix: "Verification failed", onJsonError: (message) => ({ success: false, error: message }), }); }, ); const devices = root.command("devices").description("Inspect and clean up Matrix devices"); devices .command("list") .description("List server-side Matrix devices for this account") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await listMatrixOwnDevices({ accountId, cfg }), onText: (result) => { printAccountLabel(accountId); printMatrixOwnDevices(result); }, errorPrefix: "Device listing failed", }); }); devices .command("prune-stale") .description("Delete stale OpenClaw-managed devices for this account") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await pruneMatrixStaleGatewayDevices({ accountId, cfg }), onText: (result, verbose) => { printAccountLabel(accountId); console.log( `Deleted stale OpenClaw devices: ${ result.deletedDeviceIds.length ? result.deletedDeviceIds .map((deviceId) => formatMatrixCliText(deviceId)) .join(", ") : "none" }`, ); console.log(`Current device: ${formatMatrixCliText(result.currentDeviceId)}`); console.log(`Remaining devices: ${result.remainingDevices.length}`); if (verbose) { console.log("Devices before cleanup:"); printMatrixOwnDevices(result.before); console.log("Devices after cleanup:"); printMatrixOwnDevices(result.remainingDevices); } }, errorPrefix: "Device cleanup failed", }); }); }