mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 23:04:26 +00:00
Matrix-js: add account add CLI wrapper
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user