mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 06:20:42 +00:00
2299 lines
83 KiB
TypeScript
2299 lines
83 KiB
TypeScript
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<MatrixActionClientModule> | undefined;
|
|
let matrixDirectManagementModulePromise: Promise<MatrixDirectManagementModule> | undefined;
|
|
|
|
function loadMatrixActionClientModule(): Promise<MatrixActionClientModule> {
|
|
matrixActionClientModulePromise ??= import("./matrix/actions/client.js");
|
|
return matrixActionClientModulePromise;
|
|
}
|
|
|
|
function loadMatrixDirectManagementModule(): Promise<MatrixDirectManagementModule> {
|
|
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<string> {
|
|
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<string | undefined> {
|
|
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<string> {
|
|
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<MatrixCliAccountAddResult> {
|
|
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<MatrixCliDirectRoomInspection> {
|
|
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<MatrixCliDirectRoomRepair> {
|
|
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<MatrixCliProfileSetResult> {
|
|
return await applyMatrixProfileUpdate({
|
|
account: params.account,
|
|
displayName: params.name,
|
|
avatarUrl: params.avatarUrl,
|
|
});
|
|
}
|
|
|
|
type MatrixCliCommandConfig<TResult> = {
|
|
verbose: boolean;
|
|
json: boolean;
|
|
run: () => Promise<TResult>;
|
|
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<TResult>(
|
|
config: MatrixCliCommandConfig<TResult>,
|
|
): Promise<void> {
|
|
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<string, string[]>;
|
|
directContentAfter: Record<string, string[]>;
|
|
};
|
|
|
|
type MatrixCliVerificationBootstrap = Awaited<ReturnType<typeof bootstrapMatrixVerification>>;
|
|
|
|
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<MatrixCliEncryptionSetupResult> {
|
|
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<MatrixCliVerificationSas["emoji"]>): 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<boolean> {
|
|
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<MatrixCliVerificationSummary>;
|
|
afterText?: (summary: MatrixCliVerificationSummary, accountId: string) => void;
|
|
errorPrefix: string;
|
|
}): Promise<void> {
|
|
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<void> {
|
|
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<string>();
|
|
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 <url> --user-id <@user:server> --password <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 <url> --access-token <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 <id>", accountId)} and ${formatMatrixCliCommand("verify confirm-sas <id>", accountId)}, or cancel stale requests with ${formatMatrixCliCommand("verify cancel <id>", accountId)}.`,
|
|
);
|
|
}
|
|
return Array.from(nextSteps);
|
|
}
|
|
|
|
function buildBackupGuidance(
|
|
backup: MatrixCliBackupStatus,
|
|
accountId?: string,
|
|
options: { recoveryKeyStored?: boolean } = {},
|
|
): string[] {
|
|
const backupIssue = resolveMatrixRoomKeyBackupIssue(backup);
|
|
const nextSteps = new Set<string>();
|
|
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 <id>", "Account ID (default: normalized --name, else default)")
|
|
.option("--name <name>", "Optional display name for this account")
|
|
.option("--avatar-url <url>", "Optional Matrix avatar URL (mxc:// or http(s) URL)")
|
|
.option("--homeserver <url>", "Matrix homeserver URL")
|
|
.option("--proxy <url>", "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 <id>", "Matrix user ID")
|
|
.option("--access-token <token>", "Matrix access token")
|
|
.option("--password <password>", "Matrix password")
|
|
.option("--device-name <name>", "Matrix device display name")
|
|
.option("--initial-sync-limit <n>", "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_<ACCOUNT_ID>_* 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_<ACCOUNT_ID>_* 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 <id> --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 <id>", "Account ID (for multi-account setups)")
|
|
.option("--name <name>", "Profile display name")
|
|
.option("--avatar-url <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 <id>", "Peer Matrix user ID")
|
|
.option("--account <id>", "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 <id>", "Peer Matrix user ID")
|
|
.option("--account <id>", "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 <id>", "Account ID (for multi-account setups)")
|
|
.option("--recovery-key <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 <id>", "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 <id>", "Account ID (for multi-account setups)")
|
|
.option("--timeout-ms <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 <id>", "Account ID (for multi-account setups)")
|
|
.option("--own-user", "Request self-verification for this Matrix account")
|
|
.option("--user-id <id>", "Matrix user ID to verify")
|
|
.option("--device-id <id>", "Matrix device ID to verify")
|
|
.option("--room-id <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 <id>")
|
|
.description("Accept an inbound Matrix verification request")
|
|
.option("--account <id>", "Account ID (for multi-account setups)")
|
|
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
|
.option("--room-id <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 <id>")
|
|
.description("Start SAS verification for a Matrix verification request")
|
|
.option("--account <id>", "Account ID (for multi-account setups)")
|
|
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
|
.option("--room-id <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 <id>")
|
|
.description("Show SAS emoji or decimals for a Matrix verification request")
|
|
.option("--account <id>", "Account ID (for multi-account setups)")
|
|
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
|
.option("--room-id <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 <id>")
|
|
.description("Confirm matching SAS emoji or decimals for a Matrix verification request")
|
|
.option("--account <id>", "Account ID (for multi-account setups)")
|
|
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
|
.option("--room-id <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 <id>")
|
|
.description("Reject a Matrix SAS verification when the emoji or decimals do not match")
|
|
.option("--account <id>", "Account ID (for multi-account setups)")
|
|
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
|
.option("--room-id <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 <id>")
|
|
.description("Cancel a Matrix verification request")
|
|
.option("--account <id>", "Account ID (for multi-account setups)")
|
|
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
|
.option("--room-id <id>", "Matrix direct-message room ID for verification follow-up")
|
|
.option("--reason <text>", "Cancellation reason")
|
|
.option("--code <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 <id>", "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 <id>", "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 <id>", "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 <id>", "Account ID (for multi-account setups)")
|
|
.option(
|
|
"--recovery-key <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 <id>", "Account ID (for multi-account setups)")
|
|
.option(
|
|
"--recovery-key <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 <id>", "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 <id>", "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 <id>", "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",
|
|
});
|
|
});
|
|
}
|