diff --git a/docs/channels/matrix-js.md b/docs/channels/matrix-js.md index 9d9949d084d..62b36aed02f 100644 --- a/docs/channels/matrix-js.md +++ b/docs/channels/matrix-js.md @@ -141,19 +141,62 @@ Check verification status: openclaw matrix-js verify status ``` +Verbose status (full diagnostics): + +```bash +openclaw matrix-js verify status --verbose +``` + Bootstrap cross-signing and verification state: ```bash openclaw matrix-js verify bootstrap ``` +Verbose bootstrap diagnostics: + +```bash +openclaw matrix-js verify bootstrap --verbose +``` + Verify this device with a recovery key: ```bash openclaw matrix-js verify device "" ``` -Use `openclaw matrix-js verify status --json` when scripting verification checks. +Verbose device verification details: + +```bash +openclaw matrix-js verify device "" --verbose +``` + +Check room-key backup health: + +```bash +openclaw matrix-js verify backup status +``` + +Verbose backup health diagnostics: + +```bash +openclaw matrix-js verify backup status --verbose +``` + +Restore room keys from server backup: + +```bash +openclaw matrix-js verify backup restore +``` + +Verbose restore diagnostics: + +```bash +openclaw matrix-js verify backup restore --verbose +``` + +All `verify` commands are concise by default and show detailed diagnostics only with `--verbose`. +Use `--json` for full machine-readable output when scripting. ## Automatic verification routing diff --git a/extensions/matrix-js/src/cli.test.ts b/extensions/matrix-js/src/cli.test.ts index 751c9db6f7a..da5014e1749 100644 --- a/extensions/matrix-js/src/cli.test.ts +++ b/extensions/matrix-js/src/cli.test.ts @@ -114,7 +114,7 @@ describe("matrix-js CLI verification commands", () => { expect(process.exitCode).toBe(0); }); - it("prints local timezone timestamps for verify status output", async () => { + it("prints local timezone timestamps for verify status output in verbose mode", async () => { const recoveryCreatedAt = "2026-02-25T20:10:11.000Z"; getMatrixVerificationStatusMock.mockResolvedValue({ encryptionEnabled: true, @@ -135,14 +135,14 @@ describe("matrix-js CLI verification commands", () => { }); const program = buildProgram(); - await program.parseAsync(["matrix-js", "verify", "status"], { from: "user" }); + await program.parseAsync(["matrix-js", "verify", "status", "--verbose"], { from: "user" }); expect(console.log).toHaveBeenCalledWith( `Recovery key created at: ${formatExpectedLocalTimestamp(recoveryCreatedAt)}`, ); }); - it("prints local timezone timestamps for verify bootstrap and device output", async () => { + it("prints local timezone timestamps for verify bootstrap and device output in verbose mode", async () => { const recoveryCreatedAt = "2026-02-25T20:10:11.000Z"; const verifiedAt = "2026-02-25T20:14:00.000Z"; bootstrapMatrixVerificationMock.mockResolvedValue({ @@ -200,8 +200,12 @@ describe("matrix-js CLI verification commands", () => { }); const program = buildProgram(); - await program.parseAsync(["matrix-js", "verify", "bootstrap"], { from: "user" }); - await program.parseAsync(["matrix-js", "verify", "device", "valid-key"], { from: "user" }); + await program.parseAsync(["matrix-js", "verify", "bootstrap", "--verbose"], { + from: "user", + }); + await program.parseAsync(["matrix-js", "verify", "device", "valid-key", "--verbose"], { + from: "user", + }); expect(console.log).toHaveBeenCalledWith( `Recovery key created at: ${formatExpectedLocalTimestamp(recoveryCreatedAt)}`, @@ -211,7 +215,37 @@ describe("matrix-js CLI verification commands", () => { ); }); - it("prints backup health lines for verify backup status", async () => { + it("keeps default output concise when verbose is not provided", async () => { + const recoveryCreatedAt = "2026-02-25T20:10:11.000Z"; + getMatrixVerificationStatusMock.mockResolvedValue({ + encryptionEnabled: true, + verified: true, + userId: "@bot:example.org", + deviceId: "DEVICE123", + backupVersion: "1", + backup: { + serverVersion: "1", + activeVersion: "1", + trusted: true, + matchesDecryptionKey: true, + decryptionKeyCached: true, + }, + recoveryKeyStored: true, + recoveryKeyCreatedAt: recoveryCreatedAt, + pendingVerifications: 0, + }); + const program = buildProgram(); + + await program.parseAsync(["matrix-js", "verify", "status"], { from: "user" }); + + expect(console.log).not.toHaveBeenCalledWith( + `Recovery key created at: ${formatExpectedLocalTimestamp(recoveryCreatedAt)}`, + ); + expect(console.log).not.toHaveBeenCalledWith("Pending verifications: 0"); + expect(console.log).toHaveBeenCalledWith("Backup: active and trusted on this device"); + }); + + it("prints backup health lines for verify backup status in verbose mode", async () => { getMatrixRoomKeyBackupStatusMock.mockResolvedValue({ serverVersion: "2", activeVersion: null, @@ -221,7 +255,7 @@ describe("matrix-js CLI verification commands", () => { }); const program = buildProgram(); - await program.parseAsync(["matrix-js", "verify", "backup", "status"], { + await program.parseAsync(["matrix-js", "verify", "backup", "status", "--verbose"], { from: "user", }); diff --git a/extensions/matrix-js/src/cli.ts b/extensions/matrix-js/src/cli.ts index dfe9368743a..bf48f465500 100644 --- a/extensions/matrix-js/src/cli.ts +++ b/extensions/matrix-js/src/cli.ts @@ -94,6 +94,26 @@ function printBackupStatus(backup: MatrixCliBackupStatus): void { console.log(`Backup key cached locally: ${yesNoUnknown(backup.decryptionKeyCached)}`); } +function summarizeBackupHealth(backup: MatrixCliBackupStatus): string { + if (!backup.serverVersion) { + return "missing on server"; + } + if (backup.trusted === false || backup.matchesDecryptionKey === false) { + return "present but not trusted on this device"; + } + if (!backup.activeVersion) { + return "present on server but inactive on this device"; + } + return "active and trusted on this device"; +} + +function printBackupSummary(backup: MatrixCliBackupStatus): void { + console.log(`Backup: ${summarizeBackupHealth(backup)}`); + if (backup.serverVersion) { + console.log(`Backup version: ${backup.serverVersion}`); + } +} + function buildVerificationGuidance(status: MatrixCliVerificationStatus): string[] { const backup = resolveBackupStatus(status); const nextSteps = new Set(); @@ -133,7 +153,8 @@ function printGuidance(lines: string[]): void { } } -function printVerificationStatus(status: MatrixCliVerificationStatus): void { +function printVerificationStatus(status: MatrixCliVerificationStatus, verbose = false): void { + const backup = resolveBackupStatus(status); if (status.verified) { console.log("Verified: yes"); console.log(`User: ${status.userId ?? "unknown"}`); @@ -143,10 +164,15 @@ function printVerificationStatus(status: MatrixCliVerificationStatus): void { console.log(`User: ${status.userId ?? "unknown"}`); console.log(`Device: ${status.deviceId ?? "unknown"}`); } - printBackupStatus(resolveBackupStatus(status)); + printBackupSummary(backup); + if (verbose) { + printBackupStatus(backup); + } console.log(`Recovery key stored: ${status.recoveryKeyStored ? "yes" : "no"}`); - printTimestamp("Recovery key created at", status.recoveryKeyCreatedAt); - console.log(`Pending verifications: ${status.pendingVerifications}`); + if (verbose) { + printTimestamp("Recovery key created at", status.recoveryKeyCreatedAt); + console.log(`Pending verifications: ${status.pendingVerifications}`); + } printGuidance(buildVerificationGuidance(status)); } @@ -162,31 +188,39 @@ export function registerMatrixJsCli(params: { program: Command }): void { .command("status") .description("Check Matrix-js 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; includeRecoveryKey?: boolean; json?: boolean }) => { - try { - const status = await getMatrixVerificationStatus({ - accountId: options.account, - includeRecoveryKey: options.includeRecoveryKey === true, - }); - if (options.json) { - console.log(JSON.stringify(status, null, 2)); - return; + .action( + async (options: { + account?: string; + verbose?: boolean; + includeRecoveryKey?: boolean; + json?: boolean; + }) => { + try { + const status = await getMatrixVerificationStatus({ + accountId: options.account, + includeRecoveryKey: options.includeRecoveryKey === true, + }); + if (options.json) { + console.log(JSON.stringify(status, null, 2)); + return; + } + printVerificationStatus(status, options.verbose === true); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (options.json) { + console.log(JSON.stringify({ error: message }, null, 2)); + } else { + console.error(`Error: ${message}`); + } + markCliFailure(); + } finally { + scheduleMatrixJsCliExit(); } - printVerificationStatus(status); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - if (options.json) { - console.log(JSON.stringify({ error: message }, null, 2)); - } else { - console.error(`Error: ${message}`); - } - markCliFailure(); - } finally { - scheduleMatrixJsCliExit(); - } - }); + }, + ); const backup = verify.command("backup").description("Matrix room-key backup health and restore"); @@ -194,15 +228,19 @@ export function registerMatrixJsCli(params: { program: Command }): void { .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; json?: boolean }) => { + .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { try { const status = await getMatrixRoomKeyBackupStatus({ accountId: options.account }); if (options.json) { console.log(JSON.stringify(status, null, 2)); return; } - printBackupStatus(status); + printBackupSummary(status); + if (options.verbose === true) { + printBackupStatus(status); + } } catch (err) { const message = err instanceof Error ? err.message : String(err); if (options.json) { @@ -221,46 +259,57 @@ export function registerMatrixJsCli(params: { program: Command }): void { .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; json?: boolean }) => { - try { - const result = await restoreMatrixRoomKeyBackup({ - accountId: options.account, - recoveryKey: options.recoveryKey, - }); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); + .action( + async (options: { + account?: string; + recoveryKey?: string; + verbose?: boolean; + json?: boolean; + }) => { + try { + const result = await restoreMatrixRoomKeyBackup({ + accountId: options.account, + recoveryKey: options.recoveryKey, + }); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + if (!result.success) { + markCliFailure(); + } + return; + } + 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 (options.verbose === true) { + console.log( + `Loaded key from secret storage: ${result.loadedFromSecretStorage ? "yes" : "no"}`, + ); + printTimestamp("Restored at", result.restoredAt); + printBackupStatus(result.backup); + } if (!result.success) { markCliFailure(); } - return; - } - 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}`); - console.log( - `Loaded key from secret storage: ${result.loadedFromSecretStorage ? "yes" : "no"}`, - ); - printTimestamp("Restored at", result.restoredAt); - printBackupStatus(result.backup); - if (!result.success) { + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (options.json) { + console.log(JSON.stringify({ success: false, error: message }, null, 2)); + } else { + console.error(`Backup restore failed: ${message}`); + } markCliFailure(); + } finally { + scheduleMatrixJsCliExit(); } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - if (options.json) { - console.log(JSON.stringify({ success: false, error: message }, null, 2)); - } else { - console.error(`Backup restore failed: ${message}`); - } - markCliFailure(); - } finally { - scheduleMatrixJsCliExit(); - } - }); + }, + ); verify .command("bootstrap") @@ -268,12 +317,14 @@ export function registerMatrixJsCli(params: { program: Command }): void { .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; }) => { try { @@ -296,12 +347,17 @@ export function registerMatrixJsCli(params: { program: Command }): void { console.log(`Verified: ${result.verification.verified ? "yes" : "no"}`); console.log(`User: ${result.verification.userId ?? "unknown"}`); console.log(`Device: ${result.verification.deviceId ?? "unknown"}`); - 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"})`, - ); - printBackupStatus(resolveBackupStatus(result.verification)); - printTimestamp("Recovery key created at", result.verification.recoveryKeyCreatedAt); - console.log(`Pending verifications: ${result.pendingVerifications}`); + if (options.verbose === true) { + 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"})`, + ); + printBackupStatus(resolveBackupStatus(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"}`); + printBackupSummary(resolveBackupStatus(result.verification)); + } printGuidance( buildVerificationGuidance({ ...result.verification, @@ -329,42 +385,48 @@ export function registerMatrixJsCli(params: { program: Command }): void { .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; json?: boolean }) => { - try { - const result = await verifyMatrixRecoveryKey(key, { accountId: options.account }); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - if (!result.success) { + .action( + async (key: string, options: { account?: string; verbose?: boolean; json?: boolean }) => { + try { + const result = await verifyMatrixRecoveryKey(key, { accountId: options.account }); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + if (!result.success) { + markCliFailure(); + } + } else if (result.success) { + console.log("Device verification completed successfully."); + console.log(`User: ${result.userId ?? "unknown"}`); + console.log(`Device: ${result.deviceId ?? "unknown"}`); + printBackupSummary(resolveBackupStatus(result)); + if (options.verbose === true) { + printBackupStatus(resolveBackupStatus(result)); + printTimestamp("Recovery key created at", result.recoveryKeyCreatedAt); + printTimestamp("Verified at", result.verifiedAt); + } + printGuidance( + buildVerificationGuidance({ + ...result, + pendingVerifications: 0, + }), + ); + } else { + console.error(`Verification failed: ${result.error ?? "unknown error"}`); markCliFailure(); } - } else if (result.success) { - console.log("Device verification completed successfully."); - console.log(`User: ${result.userId ?? "unknown"}`); - console.log(`Device: ${result.deviceId ?? "unknown"}`); - printBackupStatus(resolveBackupStatus(result)); - printTimestamp("Recovery key created at", result.recoveryKeyCreatedAt); - printTimestamp("Verified at", result.verifiedAt); - printGuidance( - buildVerificationGuidance({ - ...result, - pendingVerifications: 0, - }), - ); - } else { - console.error(`Verification failed: ${result.error ?? "unknown error"}`); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (options.json) { + console.log(JSON.stringify({ success: false, error: message }, null, 2)); + } else { + console.error(`Verification failed: ${message}`); + } markCliFailure(); + } finally { + scheduleMatrixJsCliExit(); } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - if (options.json) { - console.log(JSON.stringify({ success: false, error: message }, null, 2)); - } else { - console.error(`Verification failed: ${message}`); - } - markCliFailure(); - } finally { - scheduleMatrixJsCliExit(); - } - }); + }, + ); }