import type { Command } from "commander"; import { formatZonedTimestamp, normalizeAccountId, type ChannelSetupInput, } from "openclaw/plugin-sdk/matrix"; import { matrixPlugin } from "./channel.js"; import { resolveMatrixAccount, resolveMatrixAccountConfig } from "./matrix/accounts.js"; import { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } from "./matrix/actions/devices.js"; import { updateMatrixOwnProfile } from "./matrix/actions/profile.js"; import { bootstrapMatrixVerification, getMatrixRoomKeyBackupStatus, getMatrixVerificationStatus, restoreMatrixRoomKeyBackup, verifyMatrixRecoveryKey, } from "./matrix/actions/verification.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 { applyMatrixProfileUpdate, type MatrixProfileUpdateResult } from "./profile-update.js"; import { getMatrixRuntime } from "./runtime.js"; import type { CoreConfig } from "./types.js"; let 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.exit(process.exitCode ?? 0); }, 0); } function markCliFailure(): void { process.exitCode = 1; } function toErrorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } function printJson(payload: unknown): void { console.log(JSON.stringify(payload, null, 2)); } 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}: ${formatted}`); } } function printAccountLabel(accountId?: string): void { console.log(`Account: ${normalizeAccountId(accountId)}`); } function resolveMatrixCliAccountId(accountId?: string): string { const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig; return resolveMatrixAuthContext({ cfg, accountId }).accountId; } function formatMatrixCliCommand(command: string, accountId?: string): string { const normalizedAccountId = normalizeAccountId(accountId); const suffix = normalizedAccountId === "default" ? "" : ` --account ${normalizedAccountId}`; return `openclaw matrix ${command}${suffix}`; } 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(Boolean); console.log(`- ${device.deviceId}${labels.length ? ` (${labels.join(", ")})` : ""}`); if (device.lastSeenTs) { printTimestamp(" Last seen", new Date(device.lastSeenTs).toISOString()); } if (device.lastSeenIp) { console.log(` Last IP: ${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; deviceHealth: { currentDeviceId: string | null; staleOpenClawDeviceIds: 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; userId?: string; accessToken?: string; password?: string; deviceName?: string; initialSyncLimit?: string; useEnv?: boolean; }): Promise { const runtime = getMatrixRuntime(); const cfg = runtime.config.loadConfig() as CoreConfig; const setup = matrixPlugin.setup; if (!setup?.applyAccountConfig) { throw new Error("Matrix account setup is unavailable."); } const input: ChannelSetupInput & { avatarUrl?: string } = { name: params.name, avatarUrl: params.avatarUrl, homeserver: params.homeserver, 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 = setup.resolveAccountId?.({ cfg, accountId: params.account, input, }) ?? normalizeAccountId(params.account?.trim() || params.name?.trim()); const existingAccount = resolveMatrixAccount({ cfg, accountId }); const validationError = setup.validateInput?.({ cfg, accountId, input, }); if (validationError) { throw new Error(validationError); } const updated = setup.applyAccountConfig({ cfg, accountId, input, }) as CoreConfig; await runtime.config.writeConfigFile(updated as never); const accountConfig = resolveMatrixAccountConfig({ cfg: updated, accountId }); let verificationBootstrap: MatrixCliAccountAddResult["verificationBootstrap"] = { attempted: false, success: false, recoveryKeyCreatedAt: null, backupVersion: null, }; if (existingAccount.configured !== true && accountConfig.encryption === true) { try { const bootstrap = await bootstrapMatrixVerification({ accountId }); verificationBootstrap = { attempted: true, success: bootstrap.success === true, recoveryKeyCreatedAt: bootstrap.verification.recoveryKeyCreatedAt, backupVersion: bootstrap.verification.backupVersion, ...(bootstrap.success ? {} : { error: bootstrap.error ?? "Matrix verification bootstrap failed" }), }; } catch (err) { verificationBootstrap = { attempted: true, success: false, recoveryKeyCreatedAt: null, backupVersion: null, error: toErrorMessage(err), }; } } 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({ accountId, displayName: desiredDisplayName, avatarUrl: desiredAvatarUrl, }); let resolvedAvatarUrl = synced.resolvedAvatarUrl; if (synced.convertedAvatarFromHttp && synced.resolvedAvatarUrl) { const latestCfg = runtime.config.loadConfig() as CoreConfig; const withAvatar = updateMatrixAccountConfig(latestCfg, accountId, { avatarUrl: synced.resolvedAvatarUrl, }); await runtime.config.writeConfigFile(withAvatar as never); 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), }; } } const addedDevices = await listMatrixOwnDevices({ accountId }); const currentDeviceId = addedDevices.find((device) => device.current)?.deviceId ?? null; const staleOpenClawDeviceIds = addedDevices .filter((device) => !device.current && isOpenClawManagedMatrixDevice(device.displayName)) .map((device) => device.deviceId); return { accountId, configPath: resolveMatrixConfigPath(updated, accountId), useEnv: input.useEnv === true, deviceHealth: { currentDeviceId, staleOpenClawDeviceIds, }, verificationBootstrap, profile, }; } 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; }; 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}: ${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; recoveryKeyStored: boolean; recoveryKeyCreatedAt: string | null; pendingVerifications: number; }; 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, }; } type MatrixCliBackupIssueCode = | "missing-server-backup" | "key-load-failed" | "key-not-loaded" | "key-mismatch" | "untrusted-signature" | "inactive" | "indeterminate" | "ok"; type MatrixCliBackupIssue = { code: MatrixCliBackupIssueCode; summary: string; message: string | 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: ${backup.serverVersion ?? "none"}`); console.log(`Backup active on this device: ${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: ${backup.keyLoadError}`); } } function printVerificationIdentity(status: { userId: string | null; deviceId: string | null; }): void { console.log(`User: ${status.userId ?? "unknown"}`); console.log(`Device: ${status.deviceId ?? "unknown"}`); } 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 printVerificationGuidance(status: MatrixCliVerificationStatus, accountId?: string): void { printGuidance(buildVerificationGuidance(status, accountId)); } function resolveBackupIssue(backup: MatrixCliBackupStatus): MatrixCliBackupIssue { if (!backup.serverVersion) { return { code: "missing-server-backup", summary: "missing on server", message: "no room-key backup exists on the homeserver", }; } if (backup.decryptionKeyCached === false) { if (backup.keyLoadError) { return { code: "key-load-failed", summary: "present but backup key unavailable on this device", message: `backup decryption key could not be loaded from secret storage (${backup.keyLoadError})`, }; } if (backup.keyLoadAttempted) { return { code: "key-not-loaded", summary: "present but backup key unavailable on this device", message: "backup decryption key is not loaded on this device (secret storage did not return a key)", }; } return { code: "key-not-loaded", summary: "present but backup key unavailable on this device", message: "backup decryption key is not loaded on this device", }; } if (backup.matchesDecryptionKey === false) { return { code: "key-mismatch", summary: "present but backup key mismatch on this device", message: "backup key mismatch (this device does not have the matching backup decryption key)", }; } if (backup.trusted === false) { return { code: "untrusted-signature", summary: "present but not trusted on this device", message: "backup signature chain is not trusted by this device", }; } if (!backup.activeVersion) { return { code: "inactive", summary: "present on server but inactive on this device", message: "backup exists but is not active on this device", }; } if ( backup.trusted === null || backup.matchesDecryptionKey === null || backup.decryptionKeyCached === null ) { return { code: "indeterminate", summary: "present but trust state unknown", message: "backup trust state could not be fully determined", }; } return { code: "ok", summary: "active and trusted on this device", message: null, }; } function printBackupSummary(backup: MatrixCliBackupStatus): void { const issue = resolveBackupIssue(backup); console.log(`Backup: ${issue.summary}`); if (backup.serverVersion) { console.log(`Backup version: ${backup.serverVersion}`); } } function buildVerificationGuidance( status: MatrixCliVerificationStatus, accountId?: string, ): string[] { const backup = resolveBackupStatus(status); const backupIssue = resolveBackupIssue(backup); const nextSteps = new Set(); if (!status.verified) { nextSteps.add( `Run '${formatMatrixCliCommand("verify device ", accountId)}' to verify this device.`, ); } 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 (status.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.`, ); } else { nextSteps.add( `Store a recovery key with '${formatMatrixCliCommand("verify device ", accountId)}', then run '${formatMatrixCliCommand("verify backup restore", accountId)}'.`, ); } } else if (backupIssue.code === "key-mismatch") { nextSteps.add( `Backup key mismatch on this device. Re-run '${formatMatrixCliCommand("verify device ", accountId)}' with the matching recovery key.`, ); } else if (backupIssue.code === "untrusted-signature") { nextSteps.add( `Backup trust chain is not verified on this device. Re-run '${formatMatrixCliCommand("verify device ", accountId)}'.`, ); } else if (backupIssue.code === "indeterminate") { nextSteps.add( `Run '${formatMatrixCliCommand("verify status --verbose", accountId)}' to inspect backup trust diagnostics.`, ); } if (status.pendingVerifications > 0) { nextSteps.add(`Complete ${status.pendingVerifications} pending verification request(s).`); } 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"}`); const backup = resolveBackupStatus(status); const backupIssue = resolveBackupIssue(backup); printVerificationBackupSummary(status); if (backupIssue.message) { console.log(`Backup issue: ${backupIssue.message}`); } if (verbose) { console.log("Diagnostics:"); printVerificationIdentity(status); 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); } 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("--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( "--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; userId?: string; accessToken?: string; password?: string; deviceName?: string; initialSyncLimit?: string; 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, userId: options.userId, accessToken: options.accessToken, password: options.password, deviceName: options.deviceName, initialSyncLimit: options.initialSyncLimit, useEnv: options.useEnv === true, }), onText: (result) => { console.log(`Saved matrix account: ${result.accountId}`); console.log(`Config path: ${result.configPath}`); console.log( `Credentials source: ${result.useEnv ? "MATRIX_* / MATRIX__* env vars" : "inline config"}`, ); 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: ${result.verificationBootstrap.backupVersion}`); } } else { console.error( `Matrix verification bootstrap warning: ${result.verificationBootstrap.error}`, ); } } if (result.deviceHealth.staleOpenClawDeviceIds.length > 0) { console.log( `Matrix device hygiene warning: stale OpenClaw devices detected (${result.deviceHealth.staleOpenClawDeviceIds.join(", ")}). Run 'openclaw matrix devices prune-stale --account ${result.accountId}'.`, ); } if (result.profile.attempted) { if (result.profile.error) { console.error(`Profile sync warning: ${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: ${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: ${result.avatarUrl}`); } }, errorPrefix: "Profile update failed", }); }, ); const verify = root.command("verify").description("Device verification for Matrix E2EE"); 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("--json", "Output as JSON") .action( async (options: { account?: string; verbose?: boolean; includeRecoveryKey?: boolean; json?: boolean; }) => { const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await getMatrixVerificationStatus({ accountId, includeRecoveryKey: options.includeRecoveryKey === true, }), onText: (status, verbose) => { printAccountLabel(accountId); printVerificationStatus(status, verbose, accountId); }, 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 = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await getMatrixRoomKeyBackupStatus({ accountId }), onText: (status, verbose) => { printAccountLabel(accountId); printBackupSummary(status); if (verbose) { printBackupStatus(status); } }, errorPrefix: "Backup status failed", }); }); 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") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (options: { account?: string; recoveryKey?: string; verbose?: boolean; json?: boolean; }) => { const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await restoreMatrixRoomKeyBackup({ accountId, recoveryKey: options.recoveryKey, }), onText: (result, verbose) => { printAccountLabel(accountId); console.log(`Restore success: ${result.success ? "yes" : "no"}`); if (result.error) { console.log(`Error: ${result.error}`); } console.log(`Backup version: ${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); } }, 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") .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; }) => { const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await bootstrapMatrixVerification({ accountId, recoveryKey: options.recoveryKey, forceResetCrossSigning: options.forceResetCrossSigning === true, }), onText: (result, verbose) => { printAccountLabel(accountId); console.log(`Bootstrap success: ${result.success ? "yes" : "no"}`); if (result.error) { console.log(`Error: ${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 ") .description("Verify device using a Matrix recovery key") .option("--account ", "Account ID (for multi-account setups)") .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action( async (key: string, options: { account?: string; verbose?: boolean; json?: boolean }) => { const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await verifyMatrixRecoveryKey(key, { accountId }), onText: (result, verbose) => { printAccountLabel(accountId); if (!result.success) { console.error(`Verification failed: ${result.error ?? "unknown error"}`); return; } console.log("Device verification completed successfully."); printVerificationIdentity(result); 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 = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await listMatrixOwnDevices({ accountId }), 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 = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await pruneMatrixStaleGatewayDevices({ accountId }), onText: (result, verbose) => { printAccountLabel(accountId); console.log( `Deleted stale OpenClaw devices: ${result.deletedDeviceIds.length ? result.deletedDeviceIds.join(", ") : "none"}`, ); console.log(`Current device: ${result.currentDeviceId ?? "unknown"}`); 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", }); }); }