Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,13 @@
"transcribeAudio": { "type": "boolean", "default": true },
"agent": { "type": "string", "minLength": 1 }
},
"required": ["enabled", "serverUrl", "auth"]
"required": ["enabled", "serverUrl"]
}
}
}
},
"uiHints": {
"accounts.*.serverUrl": { "label": "Server URL", "placeholder": "https://chat.example.com" },
"accounts.*.auth.userId": { "label": "User ID" },
"accounts.*.auth.accessToken": { "label": "Access Token", "sensitive": true },
"accounts.*.auth.username": { "label": "Username" },
"accounts.*.auth.password": { "label": "Password", "sensitive": true },
"accounts.*.transport.pollIntervalMs": { "label": "Polling Interval (ms)", "placeholder": "3000" }
}
}
Expand Down Expand Up @@ -132,7 +128,7 @@
"transcribeAudio": { "type": "boolean", "default": true },
"agent": { "type": "string", "minLength": 1 }
},
"required": ["enabled", "serverUrl", "auth"]
"required": ["enabled", "serverUrl"]
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"setup": "node --import tsx src/cli/setup.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
Expand All @@ -28,6 +29,7 @@
"devDependencies": {
"@types/node": "^25.9.1",
"ts-node": "^10.9.2",
"tsx": "^4.22.4",
"typescript": "^6.0.3"
},
"dependencies": {
Expand Down
65 changes: 65 additions & 0 deletions src/cli/config-updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
import { resolve } from "node:path";
import { homedir } from "node:os";
import type { AuthCredentials } from "../types/types.js";

const OC_CONFIG_PATH = resolve(homedir(), ".openclaw", "openclaw.json");

type OcConfig = Record<string, unknown>;

function readConfig(): OcConfig {
if (!existsSync(OC_CONFIG_PATH)) return {};
return JSON.parse(readFileSync(OC_CONFIG_PATH, "utf-8"));
}

function writeConfig(cfg: OcConfig): void {
const tmp = OC_CONFIG_PATH + ".tmp";
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
renameSync(tmp, OC_CONFIG_PATH);
}

export function updateConfig(opts: {
pluginPath: string;
pluginId: string;
accountId: string;
serverUrl: string;
transport?: { mode: string; pollIntervalMs?: number };
mentionNames?: string[];
auth: AuthCredentials;
}) {
const cfg = readConfig() as Record<string, any>;

if (!cfg.plugins) cfg.plugins = {};
if (!cfg.plugins.load) cfg.plugins.load = {};
if (!cfg.plugins.load.paths) cfg.plugins.load.paths = [];
if (!cfg.plugins.load.paths.includes(opts.pluginPath)) {
cfg.plugins.load.paths.push(opts.pluginPath);
}
if (!cfg.plugins.allow) cfg.plugins.allow = [];
if (!cfg.plugins.allow.includes(opts.pluginId)) {
cfg.plugins.allow.push(opts.pluginId);
}

if (!cfg.channels) cfg.channels = {};
if (!cfg.channels.rocketchat) cfg.channels.rocketchat = {};
if (!cfg.channels.rocketchat.accounts) cfg.channels.rocketchat.accounts = {};

cfg.channels.rocketchat.accounts[opts.accountId] = {
enabled: true,
serverUrl: opts.serverUrl,
auth: { mode: "token", userId: opts.auth.userId, accessToken: opts.auth.accessToken },
transport: opts.transport ?? { mode: "polling", pollIntervalMs: 5000 },
mentionNames: opts.mentionNames ?? [],
};

writeConfig(cfg);
}

export function removeAccount(accountId: string): boolean {
const cfg = readConfig() as Record<string, any>;
const accounts = cfg.channels?.rocketchat?.accounts;
if (!accounts || !accounts[accountId]) return false;
delete accounts[accountId];
writeConfig(cfg);
return true;
}
60 changes: 60 additions & 0 deletions src/cli/credential-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
existsSync, mkdirSync, readFileSync, readdirSync,
writeFileSync, unlinkSync, chmodSync, renameSync
} from "node:fs";
import { join, resolve } from "node:path";
import { homedir } from "node:os";
import type { AccountCredentials } from "../types/types.js";

function getBaseDir(): string {
const home = process.env.OPENCLAW_HOME?.trim() ?? homedir();
return resolve(home, ".openclaw", "credentials", "rocketchat");
}

function filePath(accountId: string): string {
return join(getBaseDir(), `${accountId}.json`);
}

function ensureDir(): void {
const dir = getBaseDir();
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}

export function exists(accountId: string): boolean {
return existsSync(filePath(accountId));
}

export async function read(accountId: string): Promise<AccountCredentials | null> {
const path = filePath(accountId);
if (!existsSync(path)) return null;
try {
const raw = readFileSync(path, "utf-8");
return JSON.parse(raw) as AccountCredentials;
} catch {
return null;
}
}

export async function write(accountId: string, data: AccountCredentials): Promise<void> {
ensureDir();
const path = filePath(accountId);
const tmp = path + ".tmp";
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
chmodSync(tmp, 0o600);
renameSync(tmp, path);
}

export async function remove(accountId: string): Promise<boolean> {
const path = filePath(accountId);
if (!existsSync(path)) return false;
unlinkSync(path);
return true;
}

export async function list(): Promise<string[]> {
const dir = getBaseDir();
if (!existsSync(dir)) return [];
return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
}
152 changes: 152 additions & 0 deletions src/cli/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { createInterface } from "node:readline";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

import { loginAs, createBotUser, getUserByUsername, createDirectMessage, sendMessage } from "../client.js";
import * as store from "./credential-store.js";
import { updateConfig } from "./config-updater.js";
import type { RCLoginResult } from "../types/types.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_PATH = resolve(__dirname, "..", "..");

interface PrevConfig {
rcUrl?: string;
bot?: { username?: string; name?: string; email?: string } | undefined;
}

let prev: PrevConfig = {};

function prompt(question: string, fallback?: string): Promise<string> {
return new Promise((resolve) => {
const rl = createInterface({ input: process.stdin, output: process.stdout });
const suffix = fallback ? ` [${fallback}]` : "";
rl.question(` ${question}${suffix}: `, (answer) => {
rl.close();
resolve(answer.trim() || fallback || "");
});
});
}

function info(msg: string) { console.log(` ${msg}`); }
function ok(msg: string) { console.log(` \u2705 ${msg}`); }
function fail(msg: string) { console.log(` \u274c ${msg}`); }

function heading(n: number, title: string) {
console.log(`\n\u2500\u2500 Step ${n}: ${title}`);
}

async function main() {
console.log(`\n OpenClaw Rocket.Chat Setup\n`);

const prevConfig = await store.read("main");
if (prevConfig) {
if (prevConfig.bot) {
prev.bot = { username: prevConfig.bot.username };
}
info("Existing credentials found for account 'main'.");
const answer = await prompt("Re-run setup? (y/N)", "N");
if (answer.toLowerCase() !== "y") {
info("Aborted.");
process.exit(0);
}
}

heading(1, "Rocket.Chat Connection");
const rcUrl = await prompt("Rocket.Chat URL", "http://localhost:3000");
const adminUser = await prompt("Admin username");
const adminPass = await prompt("Admin password");

info("Logging in...");
let adminAuth: RCLoginResult;
try {
adminAuth = await loginAs(rcUrl, adminUser, adminPass);
ok(`Logged in as ${adminUser}`);
} catch (e: any) {
fail(`Login failed: ${e.message}`);
process.exit(1);
}

heading(2, "Bot User");
const botUsername = await prompt("Bot username", "rocketbot");
if (!botUsername) { fail("Bot username is required"); process.exit(1); }
const botName = await prompt("Bot display name", botUsername);
const botEmail = await prompt("Bot email", `${botUsername.toLowerCase()}@openclaw.local`);
const botPassword = await prompt("Bot password");

if (!botPassword) { fail("Password is required"); process.exit(1); }

info("Checking if bot already exists...");
let botUser: { _id: string; username: string; name: string };
const existing = await getUserByUsername(rcUrl, adminAuth, botUsername);

if (existing) {
ok(`Bot "${botUsername}" already exists (${existing._id}) -- reusing`);
botUser = existing;
} else {
info("Creating bot...");
try {
botUser = await createBotUser(rcUrl, adminAuth, {
username: botUsername, name: botName, password: botPassword, email: botEmail,
});
ok(`Created bot: ${botUser.username} (${botUser._id})`);
} catch (e: any) {
fail(`Failed: ${e.message}`);
process.exit(1);
}
}

info("Getting bot auth token...");
let botAuth: RCLoginResult;
try {
botAuth = await loginAs(rcUrl, botUsername, botPassword);
ok("Bot token obtained");
} catch (e: any) {
fail(`Bot login failed: ${e.message}`);
process.exit(1);
}

heading(3, "Welcome Message");
let dmRoomId = "";
try {
info("Creating DM channel...");
dmRoomId = await createDirectMessage(rcUrl, adminAuth, botUsername);
await sendMessage(rcUrl, botAuth, dmRoomId, "OpenClaw is connected! Send me a message to get started.");
ok(`Welcome message sent to @${botUsername}`);
} catch (e: any) {
info(`Welcome message skipped: ${e.message}`);
}

heading(4, "Save & Configure");
await store.write("main", {
accountId: "main",
auth: { mode: "token", userId: botAuth.userId, accessToken: botAuth.authToken },
bot: { username: botUsername, userId: botUser._id },
createdAt: new Date().toISOString(),
});
ok("Saved credentials to ~/.openclaw/credentials/rocketchat/main.json");

try {
updateConfig({
pluginPath: PLUGIN_PATH,
pluginId: "rocketchat",
accountId: "main",
serverUrl: rcUrl,
transport: { mode: "polling", pollIntervalMs: 5000 },
mentionNames: [botUsername],
auth: { mode: "token", userId: botAuth.userId, accessToken: botAuth.authToken },
});
ok("Updated ~/.openclaw/openclaw.json (plugin paths + channel config)");
} catch (e: any) {
info(`Skipped openclaw.json update: ${e.message}`);
}

console.log(`\n\u2500\u2500 Done! \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
console.log(`
Next steps:
1. Restart OpenClaw: openclaw restart
2. Message @${botUsername} in Rocket.Chat
`);
}

main().catch((e) => { console.error("\nSetup failed:", e.message ?? e); process.exit(1); });
Loading