Matrix-js: add account add CLI wrapper

This commit is contained in:
Gustavo Madeira Santana
2026-02-25 18:05:27 -05:00
parent e108632e21
commit 6ec6ccb854
2 changed files with 319 additions and 1 deletions

View File

@@ -5,6 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const bootstrapMatrixVerificationMock = vi.fn();
const getMatrixRoomKeyBackupStatusMock = vi.fn();
const getMatrixVerificationStatusMock = vi.fn();
const matrixSetupApplyAccountConfigMock = vi.fn();
const matrixSetupValidateInputMock = vi.fn();
const matrixRuntimeLoadConfigMock = vi.fn();
const matrixRuntimeWriteConfigFileMock = vi.fn();
const restoreMatrixRoomKeyBackupMock = vi.fn();
const setMatrixSdkLogModeMock = vi.fn();
const verifyMatrixRecoveryKeyMock = vi.fn();
@@ -21,6 +25,24 @@ vi.mock("./matrix/client/logging.js", () => ({
setMatrixSdkLogMode: (...args: unknown[]) => setMatrixSdkLogModeMock(...args),
}));
vi.mock("./channel.js", () => ({
matrixPlugin: {
setup: {
applyAccountConfig: (...args: unknown[]) => matrixSetupApplyAccountConfigMock(...args),
validateInput: (...args: unknown[]) => matrixSetupValidateInputMock(...args),
},
},
}));
vi.mock("./runtime.js", () => ({
getMatrixRuntime: () => ({
config: {
loadConfig: (...args: unknown[]) => matrixRuntimeLoadConfigMock(...args),
writeConfigFile: (...args: unknown[]) => matrixRuntimeWriteConfigFileMock(...args),
},
}),
}));
let registerMatrixJsCli: typeof import("./cli.js").registerMatrixJsCli;
function buildProgram(): Command {
@@ -41,6 +63,10 @@ describe("matrix-js CLI verification commands", () => {
({ registerMatrixJsCli } = await import("./cli.js"));
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
matrixSetupValidateInputMock.mockReturnValue(null);
matrixSetupApplyAccountConfigMock.mockImplementation(({ cfg }: { cfg: unknown }) => cfg);
matrixRuntimeLoadConfigMock.mockReturnValue({});
matrixRuntimeWriteConfigFileMock.mockResolvedValue(undefined);
});
afterEach(() => {
@@ -103,6 +129,89 @@ describe("matrix-js CLI verification commands", () => {
expect(process.exitCode).toBe(1);
});
it("adds a matrix-js account and prints a binding hint", async () => {
matrixRuntimeLoadConfigMock.mockReturnValue({ channels: {} });
matrixSetupApplyAccountConfigMock.mockImplementation(
({ cfg, accountId }: { cfg: Record<string, unknown>; accountId: string }) => ({
...cfg,
channels: {
...(cfg.channels as Record<string, unknown> | undefined),
"matrix-js": {
accounts: {
[accountId]: {
homeserver: "https://matrix.example.org",
},
},
},
},
}),
);
const program = buildProgram();
await program.parseAsync(
[
"matrix-js",
"account",
"add",
"--account",
"Ops",
"--homeserver",
"https://matrix.example.org",
"--user-id",
"@ops:example.org",
"--password",
"secret",
"--register",
"on",
],
{ from: "user" },
);
expect(matrixSetupValidateInputMock).toHaveBeenCalledWith(
expect.objectContaining({
accountId: "ops",
input: expect.objectContaining({
homeserver: "https://matrix.example.org",
userId: "@ops:example.org",
password: "secret",
}),
}),
);
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
"matrix-js": {
accounts: {
ops: expect.objectContaining({
homeserver: "https://matrix.example.org",
register: true,
}),
},
},
},
}),
);
expect(console.log).toHaveBeenCalledWith("Saved matrix-js account: ops");
expect(console.log).toHaveBeenCalledWith("Register mode: on");
expect(console.log).toHaveBeenCalledWith(
"Bind this account to an agent: openclaw agents bind --agent <id> --bind matrix-js:ops",
);
});
it("returns JSON errors for invalid account setup input", async () => {
matrixSetupValidateInputMock.mockReturnValue("Matrix requires --homeserver");
const program = buildProgram();
await program.parseAsync(["matrix-js", "account", "add", "--json"], {
from: "user",
});
expect(process.exitCode).toBe(1);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('"error": "Matrix requires --homeserver"'),
);
});
it("keeps zero exit code for successful bootstrap in JSON mode", async () => {
process.exitCode = 0;
bootstrapMatrixVerificationMock.mockResolvedValue({

View File

@@ -1,5 +1,11 @@
import type { Command } from "commander";
import { formatZonedTimestamp } from "openclaw/plugin-sdk";
import {
DEFAULT_ACCOUNT_ID,
formatZonedTimestamp,
normalizeAccountId,
type ChannelSetupInput,
} from "openclaw/plugin-sdk";
import { matrixPlugin } from "./channel.js";
import {
bootstrapMatrixVerification,
getMatrixRoomKeyBackupStatus,
@@ -8,6 +14,8 @@ import {
verifyMatrixRecoveryKey,
} from "./matrix/actions/verification.js";
import { setMatrixSdkLogMode } from "./matrix/client/logging.js";
import { getMatrixRuntime } from "./runtime.js";
import type { CoreConfig } from "./types.js";
let matrixJsCliExitScheduled = false;
@@ -56,6 +64,139 @@ function configureCliLogMode(verbose: boolean): void {
setMatrixSdkLogMode(verbose ? "default" : "quiet");
}
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;
}
function parseToggle(value: string | undefined, fieldName: string): boolean | undefined {
const trimmed = value?.trim().toLowerCase();
if (!trimmed) {
return undefined;
}
if (["on", "true", "1", "yes"].includes(trimmed)) {
return true;
}
if (["off", "false", "0", "no"].includes(trimmed)) {
return false;
}
throw new Error(`${fieldName} must be on|off`);
}
function applyRegisterFlag(
cfg: CoreConfig,
accountId: string,
register: boolean | undefined,
): CoreConfig {
if (typeof register !== "boolean") {
return cfg;
}
const matrix = cfg.channels?.["matrix-js"] ?? {};
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...cfg,
channels: {
...(cfg.channels ?? {}),
"matrix-js": {
...matrix,
register,
},
},
};
}
const account = matrix.accounts?.[accountId] ?? {};
return {
...cfg,
channels: {
...(cfg.channels ?? {}),
"matrix-js": {
...matrix,
accounts: {
...matrix.accounts,
[accountId]: {
...account,
register,
},
},
},
},
};
}
type MatrixCliAccountAddResult = {
accountId: string;
configPath: string;
registerMode: boolean | undefined;
useEnv: boolean;
};
async function addMatrixJsAccount(params: {
account?: string;
name?: string;
homeserver?: string;
userId?: string;
accessToken?: string;
password?: string;
deviceName?: string;
initialSyncLimit?: string;
useEnv?: boolean;
register?: string;
}): Promise<MatrixCliAccountAddResult> {
const runtime = getMatrixRuntime();
const cfg = runtime.config.loadConfig() as CoreConfig;
const accountId = normalizeAccountId(params.account);
const registerMode = parseToggle(params.register, "--register");
const setup = matrixPlugin.setup;
if (!setup?.applyAccountConfig) {
throw new Error("Matrix-js account setup is unavailable.");
}
const input: ChannelSetupInput = {
name: params.name,
homeserver: params.homeserver,
userId: params.userId,
accessToken: params.accessToken,
password: params.password,
deviceName: params.deviceName,
initialSyncLimit: parseOptionalInt(params.initialSyncLimit, "--initial-sync-limit"),
useEnv: params.useEnv === true,
};
const validationError = setup.validateInput?.({
cfg,
accountId,
input,
});
if (validationError) {
throw new Error(validationError);
}
const updated = setup.applyAccountConfig({
cfg,
accountId,
input,
}) as CoreConfig;
const next = applyRegisterFlag(updated, accountId, registerMode);
await runtime.config.writeConfigFile(next as never);
return {
accountId,
configPath:
accountId === DEFAULT_ACCOUNT_ID
? "channels.matrix-js"
: `channels.matrix-js.accounts.${accountId}`,
registerMode,
useEnv: input.useEnv === true,
};
}
type MatrixCliCommandConfig<TResult> = {
verbose: boolean;
json: boolean;
@@ -351,6 +492,74 @@ export function registerMatrixJsCli(params: { program: Command }): void {
.description("Matrix-js channel utilities")
.addHelpText("after", () => "\nDocs: https://docs.openclaw.ai/channels/matrix-js\n");
const account = root.command("account").description("Manage matrix-js channel accounts");
account
.command("add")
.description("Add or update a matrix-js account (wrapper around channel setup)")
.option("--account <id>", "Account ID (default: default)")
.option("--name <name>", "Optional display name for this account")
.option("--homeserver <url>", "Matrix homeserver URL")
.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("--use-env", "Use MATRIX_* env vars (default account only)")
.option("--register <on|off>", "Enable/disable register mode for password auth")
.option("--verbose", "Show setup details")
.option("--json", "Output as JSON")
.action(
async (options: {
account?: string;
name?: string;
homeserver?: string;
userId?: string;
accessToken?: string;
password?: string;
deviceName?: string;
initialSyncLimit?: string;
useEnv?: boolean;
register?: string;
verbose?: boolean;
json?: boolean;
}) => {
await runMatrixCliCommand({
verbose: options.verbose === true,
json: options.json === true,
run: async () =>
await addMatrixJsAccount({
account: options.account,
name: options.name,
homeserver: options.homeserver,
userId: options.userId,
accessToken: options.accessToken,
password: options.password,
deviceName: options.deviceName,
initialSyncLimit: options.initialSyncLimit,
useEnv: options.useEnv === true,
register: options.register,
}),
onText: (result) => {
console.log(`Saved matrix-js account: ${result.accountId}`);
console.log(`Config path: ${result.configPath}`);
if (typeof result.registerMode === "boolean") {
console.log(`Register mode: ${result.registerMode ? "on" : "off"}`);
}
console.log(
`Credentials source: ${result.useEnv ? "MATRIX_* env vars" : "inline config"}`,
);
const bindHint =
result.accountId === DEFAULT_ACCOUNT_ID
? "openclaw agents bind --agent <id> --bind matrix-js"
: `openclaw agents bind --agent <id> --bind matrix-js:${result.accountId}`;
console.log(`Bind this account to an agent: ${bindHint}`);
},
errorPrefix: "Account setup failed",
});
},
);
const verify = root.command("verify").description("Device verification for Matrix E2EE");
verify