From d6f12df0bde3d4af545a6d358caaaf26e69377c5 Mon Sep 17 00:00:00 2001 From: dodaa08 Date: Tue, 8 Sep 2026 18:08:54 +0530 Subject: [PATCH 1/7] Added support for attachment path --- docs/ARCHITECTURE.md | 27 +++++++++++++++++++++++++++ src/service/inbound.ts | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3800860..e9b2207 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -123,6 +123,33 @@ Plugin DDP Client | **Post** | DDP: send typing stop signal; REST: post message | | **Attachments** | Download → upload via REST → attach reference | +### Inbound media context + +`buildMediaContext()` (`src/service/inbound.ts`) downloads inbound Rocket.Chat file +attachments to temp paths (or keeps URLs) and exposes them to OpenClaw core media +understanding. It emits **both** the legacy `Media*` fields and the newer +`Attachment*` compatibility names, so all the following context keys are available: + +| Family | Keys | +| --------------- | --------------------------------------------------------------------- | +| **Path** | `MediaPath`/`MediaPaths`, `AttachmentPath`/`AttachmentPaths` | +| **URL** | `MediaUrl`/`MediaUrls`, `AttachmentUrl`/`AttachmentUrls` | +| **Type/MIME** | `MediaType`/`MediaTypes`, `AttachmentContentType`/`AttachmentContentTypes` | +| **Directory** | `AttachmentDir`/`AttachmentDirs` (path dirname) | +| **Index** | `AttachmentIndex`/`AttachmentIndexes` | + +Media understanding in core reads the `MediaPath`/`MediaUrls`/`MediaType` family +via `normalizeAttachments()`; the `Attachment*` names are the current CLI-template +tokens the docs reference. + +> **Why the audio CLI config uses `{{MediaPath}}`, not `{{AttachmentPath}}`** +> For a `whisper-cli` audio CLI entry, core's `resolveCliMediaPath()` transcodes +> non-WAV audio (e.g. Rocket.Chat `.ogg` voice notes) to a 16 kHz mono WAV and sets +> that converted path as `templCtx.MediaPath`. `{{AttachmentPath}}` resolves to the +> **original** (unconverted) file from the inbound context and would bypass that +> transcode. Keep `{{MediaPath}}` in `tools.media.audio.models[].args` so +> whisper-cli always receives the transcoded WAV. + ## Commands Commands are parsed by `CommandParser.parse()` and route three ways: diff --git a/src/service/inbound.ts b/src/service/inbound.ts index 6ddd980..b89ec35 100644 --- a/src/service/inbound.ts +++ b/src/service/inbound.ts @@ -9,6 +9,7 @@ import type { import type { RocketChatClient } from "../client/rest.js"; import type { GroupHistoryEntry } from "./group-history.js"; import { parsePluginConfig } from "../config/schema.js"; +import { dirname } from "node:path"; const DEFAULT_OWNER_ONLY_SKILLS = ["email"]; @@ -241,21 +242,53 @@ async function buildMediaContext( const mediaUrls: string[] = []; const mediaPaths: string[] = []; const mediaTypes: string[] = []; + const attachmentPaths: string[] = []; + const attachmentUrls: string[] = []; + const attachmentContentTypes: string[] = []; + const attachmentDirs: string[] = []; + const attachmentIndexes: number[] = []; + let index = 0; for (const r of results) { if (!r) continue; if (r.kind === "path") { mediaPaths.push(r.value); + attachmentPaths.push(r.value); + attachmentDirs.push(dirname(r.value)); } else { mediaUrls.push(r.value); + attachmentUrls.push(r.value); } - if (r.mimeType) mediaTypes.push(r.mimeType); + if (r.mimeType) { + mediaTypes.push(r.mimeType); + attachmentContentTypes.push(r.mimeType); + } + attachmentIndexes.push(index); + index += 1; } return { ...(mediaUrls.length > 0 ? { MediaUrl: mediaUrls[0], MediaUrls: mediaUrls } : {}), ...(mediaPaths.length > 0 ? { MediaPath: mediaPaths[0], MediaPaths: mediaPaths } : {}), ...(mediaTypes.length > 0 ? { MediaType: mediaTypes[0], MediaTypes: mediaTypes } : {}), + ...(attachmentUrls.length > 0 + ? { AttachmentUrl: attachmentUrls[0], AttachmentUrls: attachmentUrls } + : {}), + ...(attachmentPaths.length > 0 + ? { AttachmentPath: attachmentPaths[0], AttachmentPaths: attachmentPaths } + : {}), + ...(attachmentContentTypes.length > 0 + ? { + AttachmentContentType: attachmentContentTypes[0], + AttachmentContentTypes: attachmentContentTypes, + } + : {}), + ...(attachmentDirs.length > 0 + ? { AttachmentDir: attachmentDirs[0], AttachmentDirs: attachmentDirs } + : {}), + ...(attachmentIndexes.length > 0 + ? { AttachmentIndex: attachmentIndexes[0], AttachmentIndexes: attachmentIndexes } + : {}), }; } From d632ef8769d2764d08c8c6232de25518dc876e50 Mon Sep 17 00:00:00 2001 From: dodaa08 Date: Wed, 9 Sep 2026 17:17:42 +0530 Subject: [PATCH 2/7] Removed email from command menu skills, and moved out non native skills from plugin command runner --- docs/ARCHITECTURE.md | 63 +++++++- docs/COMMANDS.md | 51 +------ docs/SETUP.md | 204 +++++-------------------- docs/SKILLS/{CronJobs.md => Cron.md} | 0 docs/SKILLS/Email.md | 3 +- docs/SKILLS/{Collections.md => doc.md} | 10 +- src/service/channel.ts | 119 +-------------- src/service/skill-commands.ts | 4 +- 8 files changed, 104 insertions(+), 350 deletions(-) rename docs/SKILLS/{CronJobs.md => Cron.md} (100%) rename docs/SKILLS/{Collections.md => doc.md} (79%) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e9b2207..0d5eead 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -130,13 +130,13 @@ attachments to temp paths (or keeps URLs) and exposes them to OpenClaw core medi understanding. It emits **both** the legacy `Media*` fields and the newer `Attachment*` compatibility names, so all the following context keys are available: -| Family | Keys | -| --------------- | --------------------------------------------------------------------- | -| **Path** | `MediaPath`/`MediaPaths`, `AttachmentPath`/`AttachmentPaths` | -| **URL** | `MediaUrl`/`MediaUrls`, `AttachmentUrl`/`AttachmentUrls` | -| **Type/MIME** | `MediaType`/`MediaTypes`, `AttachmentContentType`/`AttachmentContentTypes` | -| **Directory** | `AttachmentDir`/`AttachmentDirs` (path dirname) | -| **Index** | `AttachmentIndex`/`AttachmentIndexes` | +| Family | Keys | +| ------------- | -------------------------------------------------------------------------- | +| **Path** | `MediaPath`/`MediaPaths`, `AttachmentPath`/`AttachmentPaths` | +| **URL** | `MediaUrl`/`MediaUrls`, `AttachmentUrl`/`AttachmentUrls` | +| **Type/MIME** | `MediaType`/`MediaTypes`, `AttachmentContentType`/`AttachmentContentTypes` | +| **Directory** | `AttachmentDir`/`AttachmentDirs` (path dirname) | +| **Index** | `AttachmentIndex`/`AttachmentIndexes` | Media understanding in core reads the `MediaPath`/`MediaUrls`/`MediaType` family via `normalizeAttachments()`; the `Attachment*` names are the current CLI-template @@ -262,6 +262,55 @@ Rocket.Chat Server - Access control per-bot - Scale horizontally (add more bots as needed) +## Per-Agent (Per-Bot) Config + +### Location + +Agent-level config lives in two places: + +``` +~/.openclaw/ +│ +├─ openclaw.json # agents.list[] entries: per-agent model selection +└─ agents// + └─ agent/ + └─ models.json # Per-agent provider/model catalog +``` + +### Structure + +Each entry in `agents.list[]` in `openclaw.json` can carry its own `model` selector: + +```json +{ + "agents": { + "list": [ + { + "id": "rc-openclaw2nd", + "workspace": "/home/me/.openclaw/agents/rc-openclaw2nd", + "agentDir": "/home/me/.openclaw/agents/rc-openclaw2nd/agent", + "model": { + "primary": "nvidia-nim/claude-3-freecc-no-thinking/nvidia_nim/nvidia/nemotron-3-super-120b-a12b", + "fallbacks": ["openrouter/google/gemini-2.0-flash-thinking-exp:free", "ollama/mistral:7b"] + } + } + ] + } +} +``` + +Field meanings: + +- `model.primary` — the provider/model ref used first for that agent's replies. Convention: `/`. +- `model.fallbacks` — ordered list of alternate provider/model refs tried automatically on overload, timeout, or availability errors. When a primary fails, OpenClaw walks this chain instead of surfacing the error. +- Omit `model` to inherit `agents.defaults.model` (the global primary). + +Per-agent behavior is then resolved as: + +1. Agent's own `model` (if set) → overrides `agents.defaults.model` +2. Each ref is a provider/model in `agents.defaults.models` or the agent's own catalog +3. On failure, the runtime advances through `fallbacks` (log marker `model_fallback_decision`) + ## Data Deduplication Prevents message replay after restart or duplicate receipt: diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 580ceb0..6f44891 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -92,14 +92,10 @@ Control how the agent responds. ## Tools & Skills -| Command | Description | -| ------------------ | ------------------------------------- | -| `!tools` | List tools available to the agent | -| `!skills` | List installed skills with usage info | -| `!skill ` | Run a specific skill | -| `!skill cron` | Show cron skill help | -| `!skill email` | Show email skill help | -| `!skill configure` | Show setup status for skills | +| Command | Description | +| --------------- | -------------------------------------- | +| `!tools` | List tools available to the agent | +| `!skill ` | Run a skill (handled by OpenClaw core) | ## Cron Jobs @@ -124,43 +120,6 @@ Schedule one-shot reminders or repeating tasks. !cron stop check disk space ``` -## Email - -Send, fetch, and summarize emails. Requires env vars — see [SETUP.md](SETUP.md#email-skills). - -| Command | Description | -| --------------------------------------- | ------------------------------------ | -| `!email send : : ` | Send an email | -| `!email fetch [account]` | Fetch recent emails (max 100) | -| `!email summarize [account]` | Fetch + AI-summarize emails (max 10) | -| `!email` or `!email help` | Show email usage | - -**Examples:** - -``` -!email send alice@example.com : Meeting : Let's meet at 3pm -!email fetch 5 -!email fetch 10 user@gmail.com -!email summarize 5 -``` - -**Requirements:** - -- **Send:** `AGENTMAIL_API_KEY` or `EMAIL_SMTP_USER` + `EMAIL_SMTP_PASS` env var -- **Fetch:** `GMAIL_APP_PASSWORD` env var + `GMAIL_ACCOUNT` (or pass account as arg) - -See [SETUP.md](./SETUP.md) for full reference. - -## Configure - -Check skill setup status and get configuration steps. - -| Command | Description | -| ------------ | ------------------------------------------------------- | -| `!configure` | Show which skills are configured and how to set them up | - -Returns the status of email send/fetch and shows the env vars needed for each. - ## Permission Model Commands are split into two tiers: @@ -170,7 +129,7 @@ Commands are split into two tiers: | **Public** | Anyone in a room where the bot is present | | **Owner** | Only the bot owner (set in `openclaw.json` under `accounts..owner`) | -Owner-only commands: `add-bot`, `remove-bot`, `add-group`, `revoke`, `access`, `bots`, `email`, `configure` +Owner-only commands: `add-bot`, `remove-bot`, `add-group`, `revoke`, `access`, `bots` Non-owners see a permission error when trying owner-only commands. diff --git a/docs/SETUP.md b/docs/SETUP.md index 5975a7b..d2f6a2c 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -102,11 +102,10 @@ This deletes: - Bot user from Rocket.Chat server - Bot config + credentials - Agent workspace -- All associated data ## Clean Everything Up -To completely remove the plugin from your machine: +To completely remove the plugin from your machine so it doesn't take space in your machine: ```bash # Delete all Rocket.Chat plugin data @@ -124,6 +123,33 @@ rm -rf ~/.openclaw/media/inbound/ agents/rc-/ # Dedicated agent config + sessions ``` +### Per-Bot (Per-Agent) Model Config + +Each bot is backed by an agent entry in `agents.list[]` inside `~/.openclaw/openclaw.json`. You can give each bot its own primary model and an automatic fallback chain (tried on overload/timeout) without touching the global default: + +```json +{ + "agents": { + "list": [ + { + "id": "rc-openclaw2nd", + "model": { + "primary": "nvidia-nim/claude-3-freecc-no-thinking/nvidia_nim/nvidia/nemotron-3-super-120b-a12b", + "fallbacks": ["openrouter/google/gemini-2.0-flash-thinking-exp:free", "ollama/mistral:7b"] + } + } + ] + } +} +``` + +- `model.primary` → `/` used for that bot's replies (omit to inherit `agents.defaults.model`). +- `model.fallbacks` → ordered refs tried automatically when the primary is overloaded or times out. +- The per-agent provider/model catalog lives at `agents//agent/models.json`. +- Restart the gateway after edits: `openclaw gateway restart`. + +See ARCHITECTURE.md → "Per-Agent (Per-Bot) Config" for the full structure. + ## Media (Temporary) ``` @@ -147,172 +173,14 @@ Optional overrides for paths and email skills. Set these in your shell before st --- -## Email Setup (for command menu email skill) - -Email skills enable the `!email send` and `!email fetch` commands in Rocket.Chat. -Without the correct credentials, these commands will not work. - -### Overview - -| Purpose | Option 1 (Simplest) | Option 2 (Recommended / more robust) | -| --------- | --------------------- | --------------------------------------- | -| **Send** | Environment variables | `~/.netrc` (Linux/macOS) | -| **Fetch** | Environment variables | systemd / shell profile / permanent env | - ---- - -### Option 1 – Environment Variables Only (works on all OS) - -This is the quickest way and works on Linux, macOS, and Windows. - -```bash -# Send + Fetch (Gmail App Password) -export EMAIL_SMTP_USER="you@gmail.com" -export EMAIL_SMTP_PASS="xxxx xxxx xxxx xxxx" # Gmail App Password -export GMAIL_APP_PASSWORD="xxxx xxxx xxxx xxxx" -export GMAIL_ACCOUNT="you@gmail.com" -export EMAIL_FROM="you@gmail.com" -``` - -**Windows (PowerShell):** - -```powershell -$env:EMAIL_SMTP_USER = "you@gmail.com" -$env:EMAIL_SMTP_PASS = "xxxx xxxx xxxx xxxx" -$env:GMAIL_APP_PASSWORD = "xxxx xxxx xxxx xxxx" -$env:GMAIL_ACCOUNT = "you@gmail.com" -$env:EMAIL_FROM = "you@gmail.com" -``` - -Then restart the gateway: - -```bash -openclaw gateway restart -``` - -> Tip: To make these permanent, add them to your shell profile (`~/.zshrc`, `~/.bashrc`, `$PROFILE`) or System Environment Variables on Windows. - ---- - -### Option 2 – OS-native / recommended methods - -#### Sending Emails - -**Linux & macOS (recommended)** – use `~/.netrc` (no environment variables needed): - -```bash -nano ~/.netrc -``` - -Add: - -``` -machine smtp.gmail.com login you@gmail.com password "xxxx xxxx xxxx xxxx" -``` - -Lock the file: - -```bash -chmod 0600 ~/.netrc -``` - -`s-nail` will pick this up automatically. - -**Windows** – stick with Option 1 (environment variables). There is no clean equivalent of `~/.netrc` for this use case. - -#### Fetching Emails (Gmail App Password) - -You still need a Gmail **App Password** (not your normal password). -Generate one at: Google Account → Security → 2-Step Verification → App passwords → “Mail”. - -**Linux (systemd – best for production):** - -```bash -systemctl --user edit --full openclaw-gateway.service -``` - -Add under `[Service]`: - -``` -Environment="GMAIL_APP_PASSWORD=xxxx xxxx xxxx xxxx" -Environment=GMAIL_ACCOUNT=you@gmail.com -Environment=EMAIL_FROM=you@gmail.com -``` - -**Important:** Quote the entire `KEY=value` pair because Gmail app passwords contain spaces. - -Reload & restart: - -```bash -systemctl --user daemon-reload -systemctl --user restart openclaw-gateway.service -``` - -Verify the password loaded: - -```bash -systemctl --user show openclaw-gateway.service -p Environment | grep GMAIL -``` - -> Note: OpenClaw may regenerate the service file on updates. Re-check and re-add these lines after each upgrade. - -**macOS / Linux (shell profile):** - -```bash -# ~/.zshrc, ~/.bashrc or ~/.profile -export GMAIL_APP_PASSWORD="xxxx xxxx xxxx xxxx" -export GMAIL_ACCOUNT="you@gmail.com" -export EMAIL_FROM="you@gmail.com" -``` - -Then: - -```bash -source ~/.zshrc # or the file you edited -openclaw gateway restart -``` - -**Windows** – use Option 1 (environment variables) and make them permanent via System Properties or `$PROFILE`. - ---- - -### Getting the Keys - -- **Gmail App Password** (for fetch): - Google Account → Security → 2-Step Verification → App passwords → generate one for "Mail". - -- **SMTP credentials** (for send): - Use `smtp.gmail.com` with the same App Password. Prefer `~/.netrc` on Linux/macOS. - -### Verifying Setup - -In Rocket.Chat run: - -``` -!configure -``` - -You should see something like: - -``` -Email Configuration: -Send: ✅ netrc / SMTP configured -Fetch: ✅ Gmail app password configured -``` - -If a skill shows ❌, set the corresponding credentials and restart the gateway. - ---- - ## Troubleshooting Setup -| Issue | Fix | -| ------------------------- | ------------------------------------------------------------------------------------- | -| "Can't connect to server" | Check server URL is correct + reachable | -| "Admin login failed" | Verify admin username/password; try deleting `admin.json` and re-running setup | -| "Bot creation failed" | Check you have admin rights; try manual `!add-bot` after setup | -| "2FA keeps failing" | Check TOTP app time is synced; email OTP expires after ~5 min | -| Email send/fetch fails | Run `!configure` and confirm both show ✅. Restart gateway after changing credentials | +| Issue | Fix | +| ------------------------- | ------------------------------------------------------------------------------ | +| "Can't connect to server" | Check server URL is correct + reachable | +| "Admin login failed" | Verify admin username/password; try deleting `admin.json` and re-running setup | +| "Bot creation failed" | Check you have admin rights; try manual `!add-bot` after setup | +| "2FA keeps failing" | Check TOTP app time is synced; email OTP expires after ~5 min | --- @@ -368,7 +236,3 @@ failed_messages (message_id, room_id, reason) -- debugging: what went wrong ``` Limits: 250 seen messages, 100 failed records per bot (auto-pruned). - -``` - -``` diff --git a/docs/SKILLS/CronJobs.md b/docs/SKILLS/Cron.md similarity index 100% rename from docs/SKILLS/CronJobs.md rename to docs/SKILLS/Cron.md diff --git a/docs/SKILLS/Email.md b/docs/SKILLS/Email.md index 7f9dfb7..f8ebac4 100644 --- a/docs/SKILLS/Email.md +++ b/docs/SKILLS/Email.md @@ -14,7 +14,7 @@ metadata: himalaya is blocked for sending. Use s-nail instead. -Sending requires SMTP or Agentmail credentials configured on the gateway (see `!configure`). +Sending requires SMTP or Agentmail credentials configured on the gateway (via env vars, `~/.netrc`, or the OpenClaw core skill config). ```bash echo "Body text here" | s-nail -s "Subject" recipient@example.com @@ -38,7 +38,6 @@ fetch-emails 5 ## CRITICAL rules -- NEVER use himalaya for sending — it's blocked - NEVER pass $GMAIL_APP_PASSWORD in any command — fetch-emails reads it from the environment (or `~/.config/gmail/`) internally - Pass an explicit `` unless `GMAIL_ACCOUNT` is set — never assume a default - Present the fetched result exactly ONCE — do not re-fetch or re-summarize the same data diff --git a/docs/SKILLS/Collections.md b/docs/SKILLS/doc.md similarity index 79% rename from docs/SKILLS/Collections.md rename to docs/SKILLS/doc.md index 3dfc063..71c9da4 100644 --- a/docs/SKILLS/Collections.md +++ b/docs/SKILLS/doc.md @@ -39,19 +39,15 @@ Two ready-made example skills are included to get you started: - **`email`** : send/read email via `s-nail` and `fetch-emails` Reference: [SKILLS/Email.md](./Email.md) - **`cron`** : schedule one-shot and recurring reminders via the `openclaw cron` CLI - Reference: [SKILLS/CronJobs.md](./CronJobs.md) + Reference: [SKILLS/Cron.md](./Cron.md) -These two are directly accessible from the **command menu**. +Of these, the `cron` skill is exposed natively in the command menu (`!cron`). The `email` skill is used like any other skill via OpenClaw core (`!skill `). > Copy the contents of the reference file into `~/.openclaw/workspace/skills//SKILL.md` to use it as-is, or edit it to fit your setup. -## 4. Skills beyond the command menu - -Not every skill needs to live in the command menu. right now other skills aren't directly triggered from the command menu instead, they're picked up and used automatically through OpenClaw's **inbound message handling**, exactly as OpenClaw is designed to work. - ## 5. Explore and add more skills -Want more skills? Check out: +Want more skills and install them like npm packages ? Check out: - 🔗 Awesome OpenClaw Skills (community repo): [https://github.com/VoltAgent/awesome-openclaw-skills](https://github.com/VoltAgent/awesome-openclaw-skills) - 🔗 Skills website: [https://clawskills.sh/](https://clawskills.sh/) diff --git a/src/service/channel.ts b/src/service/channel.ts index b071992..15c1789 100644 --- a/src/service/channel.ts +++ b/src/service/channel.ts @@ -1,10 +1,8 @@ import type { InboundEvent } from "../types.js"; import type { ChannelRuleOptions } from "../types.js"; -import { DM_SCOPE, CommandParser, resolveOpenClawDir } from "../utils.js"; +import { DM_SCOPE, CommandParser } from "../utils.js"; import { RocketChatClient } from "../client/rest.js"; import type { RCLoginResult } from "../types.js"; -import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; -import { resolve, join } from "node:path"; import { readConfig, readDefaultModel, @@ -39,17 +37,7 @@ import { loadAdmin, removeBotCredentials } from "../cli/credentials.js"; import { startGateway } from "./gateway.js"; import { activeClients, connectionStatus } from "./runtime-state.js"; import { AccessStore } from "../config/access-store.js"; -import { - runCronCommand, - runEmailCommand, - runConfigureCommand, - CRON_USAGE, - CRON_HEADING, - EMAIL_USAGE, - EMAIL_HEADING, - CONFIGURE_USAGE, - CONFIGURE_HEADING, -} from "./skill-commands.js"; +import { runCronCommand } from "./skill-commands.js"; const BROADCAST_MENTIONS = new Set(["here", "all", "everyone"]); @@ -152,8 +140,6 @@ const OWNER_ONLY_COMMANDS = new Set([ "revoke", "access", "bots", - "email", - "configure", ]); function isOwner(ctx: CommandContext): boolean { @@ -217,37 +203,8 @@ async function runCommand( return await runModel(argStr); case "tools": return { action: "openclaw-command", command: `/tools${argStr ? " " + argStr : ""}` }; - case "skill": { - const skillName = argStr.trim().split(/\s+/)[0] ?? ""; - const owner = isOwner(ctx); - if (skillName.toLowerCase() === "cron") { - return { action: "reply", replyText: [CRON_HEADING, CRON_USAGE].join("\n") }; - } - if ( - (skillName.toLowerCase() === "email" || skillName.toLowerCase() === "configure") && - !owner - ) { - return { - action: "reply", - replyText: `\`!skill ${skillName}\` is owner-only. Contact ${ctx.account.owner ? `@${ctx.account.owner}` : "the bot owner"}.`, - }; - } - if (skillName.toLowerCase() === "email") { - return { action: "reply", replyText: [EMAIL_HEADING, EMAIL_USAGE].join("\n") }; - } - if (skillName.toLowerCase() === "configure") { - return { action: "reply", replyText: [CONFIGURE_HEADING, CONFIGURE_USAGE].join("\n") }; - } - return { action: "openclaw-command", command: `/skill${argStr ? " " + argStr : ""}` }; - } - case "skills": - return { action: "reply", replyText: runSkills(isOwner(ctx)) }; case "cron": return { action: "reply", replyText: await runCronCommand(ctx, argStr) }; - case "email": - return { action: "reply", replyText: await runEmailCommand(ctx, argStr) }; - case "configure": - return { action: "reply", replyText: runConfigureCommand() }; case "think": return { action: "openclaw-command", command: `/think${argStr ? " " + argStr : ""}` }; case "abort": @@ -310,14 +267,7 @@ function buildHelpText(showAll: boolean): string { ["verbose on/off", "debug details"], ], ], - [ - "Tools & Skills", - [ - ["tools", "list agent tools"], - ["skills", "installed skills"], - ["skill ", "run a skill"], - ], - ], + ["Cron", [["cron ", "one-shot reminder; run `!cron` for full usage"]]], ]; const visibleGroups = groups @@ -477,69 +427,6 @@ function runBots(): string { return ["**Bot accounts**", ...lines].join("\n"); } -function parseSkillFrontmatter(content: string): { name?: string; description?: string } { - const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); - if (!fmMatch) return {}; - const fm = fmMatch[1]!; - const result: { name?: string; description?: string } = {}; - const nameLine = fm.match(/^name:\s*(.+)$/m); - if (nameLine) result.name = nameLine[1]!.trim().replace(/^["']|["']$/g, ""); - const descLine = fm.match(/^description:\s*(.+)$/m); - if (descLine) result.description = descLine[1]!.trim().replace(/^["']|["']$/g, ""); - return result; -} - -function runSkills(showOwnerOnly: boolean): string { - const skillsDir = join(resolveOpenClawDir(), "workspace", "skills"); - if (!existsSync(skillsDir)) { - return "No skills installed (expected at ~/.openclaw/workspace/skills)."; - } - const entries = readdirSync(skillsDir).filter((name) => { - const full = resolve(skillsDir, name); - try { - return statSync(full).isDirectory() || statSync(full).isSymbolicLink(); - } catch { - return false; - } - }); - const skills: Array<{ name: string; description: string }> = []; - for (const name of entries) { - const skillMd = resolve(skillsDir, name, "SKILL.md"); - if (!existsSync(skillMd)) continue; - let content = ""; - try { - content = readFileSync(skillMd, "utf8"); - } catch { - continue; - } - const fm = parseSkillFrontmatter(content); - if (!fm.name) continue; - skills.push({ name: fm.name, description: fm.description ?? "" }); - } - if (skills.length === 0) { - return "No skills installed (expected at ~/.openclaw/workspace/skills)."; - } - const cap = (s: string, n = 80): string => (s.length > n ? s.slice(0, n).trimEnd() + "…" : s); - const lines = ["**Skills**"]; - const has = (name: string): boolean => skills.some((s) => s.name === name); - lines.push("", CRON_HEADING, CRON_USAGE); - if (showOwnerOnly) { - if (has("email") || has("agentmail")) { - lines.push("", EMAIL_HEADING, EMAIL_USAGE); - } - lines.push("", CONFIGURE_HEADING, CONFIGURE_USAGE); - } - for (const s of skills) { - if (s.name === "cron" || s.name === "email" || s.name === "agentmail") continue; - if (!showOwnerOnly && s.name === "configure") continue; - const title = s.name.charAt(0).toUpperCase() + s.name.slice(1); - lines.push("", `**${title}**`); - lines.push(`• ${s.description ? cap(s.description) : "No description available."}`); - lines.push(`• Run with: \`!skill ${s.name}\``); - } - return lines.join("\n"); -} - async function runGroups(ctx: CommandContext): Promise { try { const subs = await ctx.client.listSubscriptions(null); diff --git a/src/service/skill-commands.ts b/src/service/skill-commands.ts index 43275e7..4f8f6db 100644 --- a/src/service/skill-commands.ts +++ b/src/service/skill-commands.ts @@ -26,8 +26,8 @@ export const EMAIL_USAGE = [ "• Fetch via email skill: `!email fetch [account]` (max 20)", "• Summarize via email skill + agent: `!email summarize [account]` (max 10)", "", - "ℹ️ `!email send` is for quick, simple emails. For professional emails, use inbound chat.", - "⚠️ Don't use ` : ` (space-colon-space) in the subject — it's the separator for the format.", + "`!email send` is for quick, simple emails. For professional emails, use inbound chat.", + "Don't use ` : ` (space-colon-space) in the subject it's the separator for the format.", ].join("\n"); export const CONFIGURE_HEADING = "**Configure**"; From c11e012aba522a7705acbaf0cc86f45eefcb2b4d Mon Sep 17 00:00:00 2001 From: dodaa08 Date: Wed, 9 Sep 2026 17:29:48 +0530 Subject: [PATCH 3/7] Added skill listing back --- docs/COMMANDS.md | 8 ++--- src/service/channel.ts | 67 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 6f44891..9ae8ab7 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -92,10 +92,10 @@ Control how the agent responds. ## Tools & Skills -| Command | Description | -| --------------- | -------------------------------------- | -| `!tools` | List tools available to the agent | -| `!skill ` | Run a skill (handled by OpenClaw core) | +| Command | Description | +| --------- | -------------------------------------------- | +| `!tools` | List tools available to the agent | +| `!skills` | List installed skills (use via inbound chat) | ## Cron Jobs diff --git a/src/service/channel.ts b/src/service/channel.ts index 15c1789..b79bce0 100644 --- a/src/service/channel.ts +++ b/src/service/channel.ts @@ -1,8 +1,10 @@ import type { InboundEvent } from "../types.js"; import type { ChannelRuleOptions } from "../types.js"; -import { DM_SCOPE, CommandParser } from "../utils.js"; +import { DM_SCOPE, CommandParser, resolveOpenClawDir } from "../utils.js"; import { RocketChatClient } from "../client/rest.js"; import type { RCLoginResult } from "../types.js"; +import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; +import { resolve, join } from "node:path"; import { readConfig, readDefaultModel, @@ -203,6 +205,8 @@ async function runCommand( return await runModel(argStr); case "tools": return { action: "openclaw-command", command: `/tools${argStr ? " " + argStr : ""}` }; + case "skills": + return { action: "reply", replyText: runSkills() }; case "cron": return { action: "reply", replyText: await runCronCommand(ctx, argStr) }; case "think": @@ -267,7 +271,13 @@ function buildHelpText(showAll: boolean): string { ["verbose on/off", "debug details"], ], ], - ["Cron", [["cron ", "one-shot reminder; run `!cron` for full usage"]]], + [ + "Cron & Skills", + [ + ["cron ", "one-shot reminder; run `!cron` for full usage"], + ["skills", "list installed skills (use via inbound chat)"], + ], + ], ]; const visibleGroups = groups @@ -427,6 +437,59 @@ function runBots(): string { return ["**Bot accounts**", ...lines].join("\n"); } +function runSkills(): string { + const skillsDir = join(resolveOpenClawDir(), "workspace", "skills"); + if (!existsSync(skillsDir)) { + return "No skills installed (expected at ~/.openclaw/workspace/skills)."; + } + const entries = readdirSync(skillsDir).filter((name) => { + const full = resolve(skillsDir, name); + try { + return statSync(full).isDirectory() || statSync(full).isSymbolicLink(); + } catch { + return false; + } + }); + const skills: Array<{ name: string; description: string }> = []; + for (const name of entries) { + const skillMd = resolve(skillsDir, name, "SKILL.md"); + if (!existsSync(skillMd)) continue; + let content = ""; + try { + content = readFileSync(skillMd, "utf8"); + } catch { + continue; + } + const fm = parseSkillFrontmatter(content); + if (!fm.name) continue; + skills.push({ name: fm.name, description: fm.description ?? "" }); + } + if (skills.length === 0) { + return "No skills installed (expected at ~/.openclaw/workspace/skills)."; + } + const cap = (s: string, n = 200): string => (s.length > n ? s.slice(0, n).trimEnd() + "…" : s); + const lines = ["**Installed skills**", ""]; + lines.push("Use a skill via inbound chat with the agent."); + for (const s of skills) { + const title = s.name.charAt(0).toUpperCase() + s.name.slice(1); + lines.push("", `**${title}**`); + lines.push(`• ${s.description ? cap(s.description) : "No description available."}`); + } + return lines.join("\n"); +} + +function parseSkillFrontmatter(content: string): { name?: string; description?: string } { + const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!fmMatch) return {}; + const fm = fmMatch[1]!; + const result: { name?: string; description?: string } = {}; + const nameLine = fm.match(/^name:\s*(.+)$/m); + if (nameLine) result.name = nameLine[1]!.trim().replace(/^["']|["']$/g, ""); + const descLine = fm.match(/^description:\s*(.+)$/m); + if (descLine) result.description = descLine[1]!.trim().replace(/^["']|["']$/g, ""); + return result; +} + async function runGroups(ctx: CommandContext): Promise { try { const subs = await ctx.client.listSubscriptions(null); From 7bae6239761ac8042e85150de0cc26acff0c320b Mon Sep 17 00:00:00 2001 From: dodaa08 Date: Wed, 9 Sep 2026 23:33:27 +0530 Subject: [PATCH 4/7] Removed dead code and imports for email skill runner --- docs/SETUP.md | 1 - docs/SKILLS/Email.md | 4 +- package.json | 3 +- pnpm-lock.yaml | 13 +- src/service/channel.ts | 22 +- src/service/skill-commands.ts | 423 +--------------------------------- 6 files changed, 25 insertions(+), 441 deletions(-) diff --git a/docs/SETUP.md b/docs/SETUP.md index d2f6a2c..37e271d 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -78,7 +78,6 @@ rocketchat/ ├── access.db # Access grants (who can use which bot/room) ├── .db # Seen messages + failures (per bot) ├── rate-limit.json # Bot creation cooldown state -└── skills-status.json # Email skill setup status ``` ### Credentials: What's Safe to Delete diff --git a/docs/SKILLS/Email.md b/docs/SKILLS/Email.md index f8ebac4..4f59199 100644 --- a/docs/SKILLS/Email.md +++ b/docs/SKILLS/Email.md @@ -10,9 +10,7 @@ metadata: # Email -## Send (use s-nail, NOT himalaya) - -himalaya is blocked for sending. Use s-nail instead. +## Send (use s-nail) Sending requires SMTP or Agentmail credentials configured on the gateway (via env vars, `~/.netrc`, or the OpenClaw core skill config). diff --git a/package.json b/package.json index 433336c..2bd7e6f 100644 --- a/package.json +++ b/package.json @@ -68,11 +68,12 @@ "typescript": "^6.0.3" }, "dependencies": { - "@rocket.chat/ddp-client": "^1.1.1", "@clack/prompts": "^1.7.0", + "@rocket.chat/ddp-client": "^1.1.1", "commander": "^15.0.0", "json5": "^2.2.3", "picocolors": "^1.1.1", + "yaml": "^2.9.0", "zod": "^4.4.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f32f518..21ee08a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: picocolors: specifier: ^1.1.1 version: 1.1.1 + yaml: + specifier: ^2.9.0 + version: 2.9.0 zod: specifier: ^4.4.3 version: 4.5.4 @@ -1957,7 +1960,7 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@google/genai@2.18.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))': + '@google/genai@2.18.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))': dependencies: google-auth-library: 10.9.1 p-retry: 4.6.2 @@ -2105,10 +2108,10 @@ snapshots: '@mozilla/readability@0.6.0': {} - '@openclaw/ai@2026.9.2(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + '@openclaw/ai@2026.9.2(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.120.0(zod@4.4.3) - '@google/genai': 2.18.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)) + '@google/genai': 2.18.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4)) '@mistralai/mistralai': 2.6.4 openai: 7.5.0(ws@8.21.3)(zod@4.4.3) partial-json: 0.1.7 @@ -3052,7 +3055,7 @@ snapshots: '@clack/core': 1.4.3 '@clack/prompts': 1.7.0 '@earendil-works/pi-tui': 0.84.3 - '@google/genai': 2.18.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)) + '@google/genai': 2.18.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4)) '@grammyjs/runner': 2.0.3(grammy@1.46.0) '@grammyjs/transformer-throttler': 1.2.1(grammy@1.46.0) '@homebridge/ciao': 1.3.12 @@ -3060,7 +3063,7 @@ snapshots: '@mistralai/mistralai': 2.6.4 '@modelcontextprotocol/sdk': 1.30.0(zod@4.4.3) '@mozilla/readability': 0.6.0 - '@openclaw/ai': 2026.9.2(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@openclaw/ai': 2026.9.2(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3) '@openclaw/fs-safe': 0.8.1 '@openclaw/proxyline': 0.3.7(undici@8.10.2) '@silvia-odwyer/photon-node': 0.3.4 diff --git a/src/service/channel.ts b/src/service/channel.ts index b79bce0..8023199 100644 --- a/src/service/channel.ts +++ b/src/service/channel.ts @@ -5,6 +5,7 @@ import { RocketChatClient } from "../client/rest.js"; import type { RCLoginResult } from "../types.js"; import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; import { resolve, join } from "node:path"; +import { parse as parseYaml } from "yaml"; import { readConfig, readDefaultModel, @@ -479,15 +480,18 @@ function runSkills(): string { } function parseSkillFrontmatter(content: string): { name?: string; description?: string } { - const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); - if (!fmMatch) return {}; - const fm = fmMatch[1]!; - const result: { name?: string; description?: string } = {}; - const nameLine = fm.match(/^name:\s*(.+)$/m); - if (nameLine) result.name = nameLine[1]!.trim().replace(/^["']|["']$/g, ""); - const descLine = fm.match(/^description:\s*(.+)$/m); - if (descLine) result.description = descLine[1]!.trim().replace(/^["']|["']$/g, ""); - return result; + const lines = content.split("\n"); + const openIdx = lines[0]?.trim() === "---" ? 0 : -1; + if (openIdx === -1) return {}; + const closeIdx = lines.findIndex((l, i) => i > openIdx && l.trim() === "---"); + if (closeIdx === -1) return {}; + const data = parseYaml(lines.slice(openIdx + 1, closeIdx).join("\n")) as + { name?: unknown; description?: unknown } | null | undefined; + if (typeof data !== "object" || data === null) return {}; + return { + ...(typeof data.name === "string" ? { name: data.name } : {}), + ...(typeof data.description === "string" ? { description: data.description } : {}), + }; } async function runGroups(ctx: CommandContext): Promise { diff --git a/src/service/skill-commands.ts b/src/service/skill-commands.ts index 4f8f6db..fd6f197 100644 --- a/src/service/skill-commands.ts +++ b/src/service/skill-commands.ts @@ -1,16 +1,9 @@ -import { execFile, spawn } from "node:child_process"; -import { readdirSync, existsSync, readFileSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { homedir } from "node:os"; +import { execFile } from "node:child_process"; import { promisify } from "node:util"; -import { resolveOpenClawDir } from "../utils.js"; import type { CommandContext } from "./channel.js"; const execFileAsync = promisify(execFile); -const EMAIL_DOCS = - "https://github.com/RocketChat/Openclaw/blob/main/docs/SETUP.md#email-setup-for-command-menu-email-skill"; - export const CRON_HEADING = "**Cron jobs**"; export const CRON_USAGE = [ "• `!cron ` one-shot reminder (30s | 5m | 2h | 1d)", @@ -19,125 +12,6 @@ export const CRON_USAGE = [ "• `!cron stop ` stop a repeating job", "• Examples: `!cron 30m stretch` · `!cron --every 1h check disk space`", ].join("\n"); - -export const EMAIL_HEADING = "**Email**"; -export const EMAIL_USAGE = [ - "• Send quick emails: `!email send : : `", - "• Fetch via email skill: `!email fetch [account]` (max 20)", - "• Summarize via email skill + agent: `!email summarize [account]` (max 10)", - "", - "`!email send` is for quick, simple emails. For professional emails, use inbound chat.", - "Don't use ` : ` (space-colon-space) in the subject it's the separator for the format.", -].join("\n"); - -export const CONFIGURE_HEADING = "**Configure**"; -export const CONFIGURE_USAGE = ["• `!configure` check skill setup and get setup steps"].join("\n"); - -type AuthStatus = { ok: boolean; hint: string }; - -function skillStatusFile(): string { - return resolve(resolveOpenClawDir(), "rocketchat", "skills-status.json"); -} - -function persistStatus(send: boolean, fetch: boolean): void { - const data = { - sendConfigured: send, - fetchConfigured: fetch, - checkedAt: new Date().toISOString(), - }; - try { - writeFileSync(skillStatusFile(), JSON.stringify(data, null, 2), "utf8"); - } catch { - /* best-effort; non-fatal */ - } -} - -function readStatus(): { send: boolean; fetch: boolean } | undefined { - try { - const parsed = JSON.parse(readFileSync(skillStatusFile(), "utf8")) as Partial<{ - sendConfigured?: boolean; - fetchConfigured?: boolean; - }>; - return { send: parsed.sendConfigured === true, fetch: parsed.fetchConfigured === true }; - } catch { - return undefined; - } -} - -export function fetchAuthStatus(): AuthStatus { - const password = process.env.GMAIL_APP_PASSWORD?.trim(); - if (password) return { ok: true, hint: "Gmail app password (env `GMAIL_APP_PASSWORD`)" }; - const gmailDir = resolve(homedir(), ".config", "gmail"); - const files = existsSync(gmailDir) - ? readdirSync(gmailDir).filter((f) => f.startsWith("app_password")) - : []; - if (files.length > 0) return { ok: true, hint: "Gmail app password file(s)" }; - return { - ok: false, - hint: "fetch needs a Gmail app password. Set `GMAIL_APP_PASSWORD` env, or add one under `~/.config/gmail/`.", - }; -} - -export function sendAuthStatus(): AuthStatus { - if (process.env.AGENTMAIL_API_KEY?.trim()) { - return { ok: true, hint: "Agentmail API key (env `AGENTMAIL_API_KEY`)" }; - } - if (process.env.EMAIL_SMTP_USER?.trim() && process.env.EMAIL_SMTP_PASS?.trim()) { - return { ok: true, hint: "SMTP credentials (`EMAIL_SMTP_USER` + `EMAIL_SMTP_PASS`)" }; - } - const mailrcPath = resolve(homedir(), ".mailrc"); - if (existsSync(mailrcPath)) { - try { - const content = readFileSync(mailrcPath, "utf8"); - const hasMta = /mta\s*=\s*smtps?:\/\/[^\s]*@[^\s]+/.test(content); - const usesNetrc = /netrc-lookup/.test(content); - const credsReady = usesNetrc - ? existsSync(resolve(homedir(), ".netrc")) - : /mta\s*=\s*smtps?:\/\/[^\s]*:[^\s]*@[^\s]+/.test(content); - if (hasMta && credsReady) { - return { ok: true, hint: "s-nail config (`~/.mailrc` SMTP `mta`)" }; - } - } catch { - /* fall through to "not configured" */ - } - } - return { - ok: false, - hint: "send needs Agentmail or SMTP creds. Set `AGENTMAIL_API_KEY`, or `EMAIL_SMTP_USER` + `EMAIL_SMTP_PASS`, or configure s-nail in `~/.mailrc`.", - }; -} - -export function runConfigureCommand(): string { - const send = sendAuthStatus(); - const fetch = fetchAuthStatus(); - persistStatus(send.ok, fetch.ok); - const emailOk = send.ok || fetch.ok; - - const lines = ["**Setup status**"]; - lines.push(`- Email: ${emailOk ? "**configured**" : "**not configured**"}`); - lines.push(`- Send: ${send.ok ? "**configured**" : "**not configured**"} ${send.hint}`); - lines.push(`- Fetch: ${fetch.ok ? "**configured**" : "**not configured**"} ${fetch.hint}`); - lines.push("- Cron: **always available** (uses OpenClaw account config)"); - - if (!emailOk) { - lines.push("", "### To set up email"); - lines.push( - "• Send: set the `AGENTMAIL_API_KEY` env var, or `EMAIL_SMTP_USER` + `EMAIL_SMTP_PASS`, on the gateway or configure s-nail in `~/.mailrc`.", - ); - lines.push( - "• Fetch: set the `GMAIL_APP_PASSWORD` env var (a Gmail app password), or add a file under `~/.config/gmail/`.", - ); - lines.push("• After adding creds, run `!configure` again to re-check."); - } - - lines.push("", `Docs: ${EMAIL_DOCS}`); - return lines.join("\n"); -} - -function resolveFetchAccount(): string { - return process.env.GMAIL_ACCOUNT?.trim() ?? ""; -} - const INTERVAL_RE = /^(\d+(?:\.\d+)?)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/i; @@ -425,298 +299,3 @@ export async function runCronCommand(ctx: CommandContext, argStr: string): Promi "Reminder set. It will appear in this chat.", ].join("\n"); } - -function resolveFetchEmailsBin(): string { - return process.env.FETCH_EMAILS_BIN ?? "fetch-emails"; -} - -function resolveSNailBin(): string { - return process.env.SNAIL_BIN ?? "s-nail"; -} - -function formatEmailHelp(): string { - return [EMAIL_HEADING, EMAIL_USAGE].join("\n"); -} - -async function fetchEmailRaw(count: number, account: string): Promise { - try { - const res = await execFileAsync(resolveFetchEmailsBin(), [`${count}`, account], { - timeout: 60000, - maxBuffer: 2 * 1024 * 1024, - }); - return (res.stdout || "").trim(); - } catch (e) { - const error = e as { stdout?: string; stderr?: string; message?: string }; - throw new Error( - String(error?.stderr ?? error?.stdout ?? error?.message ?? "unknown error").trim(), - ); - } -} - -async function fetchEmail(countInput: string, accountInput: string | undefined): Promise { - const count = parseInt(countInput, 10); - if (!Number.isInteger(count) || count < 1 || count > 20) { - return [ - "Invalid count for fetching emails.", - "Correct format:", - "• `!email fetch [account]`", - "• Example: `!email fetch 5`", - ].join("\n"); - } - const status = readStatus(); - if (!status?.fetch) { - return [ - "Email isn't set up yet.", - "Run `!configure` to check and set up email creds, then try again.", - `Docs: ${EMAIL_DOCS}`, - ].join("\n"); - } - const account = accountInput?.trim() || resolveFetchAccount(); - if (!account) { - return [ - "No Gmail account specified.", - "Pass one: `!email fetch `, or set the `GMAIL_ACCOUNT` env var on the gateway.", - `Docs: ${EMAIL_DOCS}`, - ].join("\n"); - } - try { - const out = await fetchEmailRaw(count, account); - if (!out) return "Fetched, but the inbox returned no content."; - return out; - } catch (e) { - const error = e instanceof Error ? e.message : String(e); - return ["Failed to fetch email. Details:", "```", error, "```"].join("\n"); - } -} - -const EMAIL_SUMMARY_MAX = 10; - -async function summarizeEmail( - ctx: CommandContext, - countInput: string, - accountInput: string | undefined, -): Promise { - const count = parseInt(countInput, 10); - if (!Number.isInteger(count) || count < 1 || count > EMAIL_SUMMARY_MAX) { - return [ - `Invalid count for summarizing emails (max ${EMAIL_SUMMARY_MAX}).`, - "Correct format:", - `• \`!email summarize [account]\` (max ${EMAIL_SUMMARY_MAX})`, - "• Example: `!email summarize 5`", - ].join("\n"); - } - const status = readStatus(); - if (!status?.fetch) { - return [ - "Email isn't set up yet.", - "Run `!configure` to check and set up email creds, then try again.", - `Docs: ${EMAIL_DOCS}`, - ].join("\n"); - } - const account = accountInput?.trim() || resolveFetchAccount(); - if (!account) { - return [ - "No Gmail account specified.", - "Pass one: `!email summarize `, or set the `GMAIL_ACCOUNT` env var on the gateway.", - `Docs: ${EMAIL_DOCS}`, - ].join("\n"); - } - - let emails: string; - try { - emails = await fetchEmailRaw(count, account); - } catch (e) { - const error = e instanceof Error ? e.message : String(e); - return ["Failed to fetch email. Details:", "```", error, "```"].join("\n"); - } - if (!emails) { - return "Fetched, but the inbox returned no content to summarize."; - } - - const agentId = `rc-${ctx.accountId}`; - const prompt = [ - "Summarize the following email inbox in a concise, natural way.", - "Group similar items (newsletters, alerts, personal), note senders and subjects,", - "and flag anything that needs attention. Keep it short and skimmable.", - "", - "--- INBOX START ---", - emails, - "--- INBOX END ---", - ].join("\n"); - - const argv = ["agent", "--agent", agentId, "--message", prompt, "--json", "--timeout", "120"]; - - const extractSummary = (raw: string): string | undefined => { - try { - const parsed = JSON.parse(raw) as { - text?: unknown; - result?: unknown; - response?: unknown; - error?: unknown; - }; - for (const v of [parsed.text, parsed.response]) { - if (typeof v === "string" && v.trim()) return v.trim(); - } - const result = parsed.result; - if (result && typeof result === "object") { - const payloads = (result as { payloads?: Array<{ text?: unknown }> }).payloads; - for (const p of payloads ?? []) { - if (typeof p.text === "string" && p.text.trim()) return p.text.trim(); - } - } - if (typeof parsed.error === "string") return undefined; - return undefined; - } catch { - return undefined; - } - }; - - const summarizeErr = (raw: string): string => - ["Failed to summarize email. Details:", "```", raw || "unknown error", "```"].join("\n"); - - try { - const res = await execFileAsync(resolveOpenClawBin(), argv, { - timeout: 125000, - maxBuffer: 4 * 1024 * 1024, - }); - const summary = extractSummary(res.stdout ?? ""); - if (summary !== undefined) return summary; - return "The agent produced no summary."; - } catch (e) { - const error = e as { stdout?: string; stderr?: string; message?: string }; - const raw = String(error?.stderr ?? error?.stdout ?? error?.message ?? "").trim(); - const summary = extractSummary(raw); - if (summary !== undefined) return summary; - const msg = ((err: unknown) => { - try { - const parsed = JSON.parse(String(err)) as { error?: string }; - return typeof parsed.error === "string" ? parsed.error : undefined; - } catch { - return undefined; - } - })(raw); - if (msg) return `Summarization failed: ${msg}`; - return summarizeErr(raw); - } -} - -function sendEmail(to: string, subject: string, body: string): Promise { - // Input validation - prevent injection attacks - if (!to || !subject || !body) { - return Promise.resolve("Email parameters cannot be empty."); - } - - // Basic email validation - if (!to.includes("@") || to.includes("\n") || to.includes("\0")) { - return Promise.resolve(`Invalid email address: ${to}`); - } - - // Sanitize subject and body to prevent shell injection - if (subject.includes("\n") || subject.includes("\0")) { - return Promise.resolve("Subject contains invalid characters."); - } - - const from = process.env.EMAIL_FROM?.trim(); - const argv = from ? ["-s", subject, "-S", `from=${from}`, to] : ["-s", subject, to]; - - return new Promise((resolvePromise) => { - try { - const child = spawn(resolveSNailBin(), argv, { - stdio: ["pipe", "pipe", "pipe"], - timeout: 30000, - }); - - const out: string[] = []; - const err: string[] = []; - - child.stdout?.on("data", (d) => out.push(String(d))); - child.stderr?.on("data", (d) => err.push(String(d))); - - child.on("error", (e) => - resolvePromise(["Failed to send email.", "```", String(e.message || e), "```"].join("\n")), - ); - - child.on("close", (code) => { - if (code === 0) { - resolvePromise(`Email sent to ${to}.`); - } else { - resolvePromise( - [ - `s-nail exited with code ${code}.`, - "```", - (err.join("") || out.join("") || "no output").trim(), - "```", - ].join("\n"), - ); - } - }); - - child.stdin?.write(body); - child.stdin?.end(); - } catch (e) { - resolvePromise(["Failed to send email.", "```", String(e), "```"].join("\n")); - } - }); -} -export async function runEmailCommand(ctx: CommandContext, argStr: string): Promise { - const trimmed = argStr.trim(); - if (!trimmed || trimmed === "help") { - return formatEmailHelp(); - } - - const firstSpace = trimmed.search(/\s/); - const sub = (firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace)).toLowerCase(); - const rest = firstSpace === -1 ? "" : trimmed.slice(firstSpace).trim(); - - if (sub === "fetch") { - return fetchEmail( - rest.split(/\s+/)[0] ?? "", - rest.split(/\s+/).slice(1).join(" ") || undefined, - ); - } - - if (sub === "summarize") { - return summarizeEmail( - ctx, - rest.split(/\s+/)[0] ?? "", - rest.split(/\s+/).slice(1).join(" ") || undefined, - ); - } - - if (sub === "send") { - const sepIdx = rest.indexOf(" : "); - if (sepIdx === -1) { - return [ - "Invalid send format.", - "Correct format:", - "• `!email send : : `", - "• Example: `!email send friend@example.com : Hello : Check this out`", - ].join("\n"); - } - const afterFirst = rest.slice(sepIdx + 3); - const secondSepIdx = afterFirst.indexOf(" : "); - const to = rest.slice(0, sepIdx).trim(); - const subject = - secondSepIdx === -1 ? afterFirst.trim() : afterFirst.slice(0, secondSepIdx).trim(); - const body = secondSepIdx === -1 ? "" : afterFirst.slice(secondSepIdx + 3).trim(); - if (!to || !subject || !body) { - return [ - "Invalid send format.", - "Correct format:", - "• `!email send : : `", - "• Example: `!email send friend@example.com : Hello : Check this out`", - ].join("\n"); - } - const status = readStatus(); - if (!status?.send) { - return [ - "Email isn't set up yet (send).", - "Run `!configure` to check and set up email creds, then try again.", - `Docs: ${EMAIL_DOCS}`, - ].join("\n"); - } - return sendEmail(to, subject, body); - } - - return ["Unknown email action. Usage:", formatEmailHelp()].join("\n"); -} From 3fb49f1c976e40252cdc48a6c4f0136fab0e3a28 Mon Sep 17 00:00:00 2001 From: dodaa08 Date: Mon, 14 Sep 2026 09:18:49 +0530 Subject: [PATCH 5/7] Added new file structure for bot creation --- docs/COMMANDS.md | 25 --- docs/SKILLS/Cron.md | 8 +- docs/SKILLS/Email.md | 42 ----- docs/SKILLS/doc.md | 10 +- openclaw.examples.json | 13 +- openclaw.plugin.json | 9 +- package.json | 12 +- pnpm-lock.yaml | 19 +- src/cli/admin-api.ts | 30 +++- src/cli/bot.ts | 13 +- src/cli/config-updater.ts | 317 +++++++++++++++++++++++++++------- src/cli/rate-limiter.ts | 21 +-- src/cli/setup.ts | 17 +- src/cli/ui.ts | 29 +--- src/client/rest.ts | 4 +- src/config/access-store.ts | 2 +- src/config/schema.ts | 2 +- src/index.ts | 2 +- src/plugin.ts | 2 +- src/service/channel.ts | 118 +++++++------ src/service/gateway.ts | 11 +- src/service/inbound.ts | 54 +----- src/service/skill-commands.ts | 45 +++-- src/types.ts | 8 +- 24 files changed, 432 insertions(+), 381 deletions(-) delete mode 100644 docs/SKILLS/Email.md diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 9ae8ab7..ea584fa 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -133,32 +133,7 @@ Owner-only commands: `add-bot`, `remove-bot`, `add-group`, `revoke`, `access`, ` Non-owners see a permission error when trying owner-only commands. -### Owner-only skills (natural language) - -Beyond `!commands`, a lent/granted user can also ask the bot to perform actions in natural -language (e.g. "send an email to ..."). To block owner-level skills from non-owners, each bot -carries a guardrail instruction that tells the agent to refuse those skills unless the requester -is the bot owner. - -Configure the list per account in `openclaw.json` under `channels.rocketchat.accounts.`: - -```json -{ - "channels": { - "rocketchat": { - "accounts": { - "": { - "owner": "adminusername", - "ownerOnlySkills": ["email"] - } - } - } - } -} -``` -If `ownerOnlySkills` is omitted, it defaults to `["email"]`. The guardrail is injected only for -non-owner senders with valid access; the owner's messages are unaffected. ## Unknown Command diff --git a/docs/SKILLS/Cron.md b/docs/SKILLS/Cron.md index 5108c78..74c0134 100644 --- a/docs/SKILLS/Cron.md +++ b/docs/SKILLS/Cron.md @@ -28,7 +28,7 @@ If the `add` command errors, paste the real error and stop. Do not paper over fa - NEVER call the `cron.add` tool. Only `openclaw cron add` (CLI) is allowed. - NEVER set `trigger.script` (or `payload.kind: "systemEvent"`). `trigger.script` is executed as **JavaScript (code-mode)**, so a shell command like `echo '...'` throws `SyntaxError: expecting ';'`. Put the reminder text in `--message "..."` instead — that is the agent payload and is delivered correctly. - NEVER use `--session main` for non-default agents; use `--session isolated`. -- NEVER let the reminder payload trigger an action. A reminder is a NOTE to the user, not a task. Use `--command 'echo ""'` so the text is relayed verbatim via `--announce`. Do NOT use `--message` with an instruction the agent will try to execute (e.g. `--message "check your email"` made the agent actually try to fetch email and fail). For a plain "remind me to X" reminder, always use the `--command echo` form below. +- NEVER let the reminder payload trigger an action. A reminder is a NOTE to the user, not a task. Use `--command 'echo ""'` so the text is relayed verbatim via `--announce`. Do NOT use `--message` with an instruction the agent will try to execute. For a plain "remind me to X" reminder, always use the `--command echo` form below. - NEVER use a `sleep`/background-process workaround (`sleep 120 && echo ...`, `nohup ... &`, `at`, shell loops, etc.). A background `sleep` does NOT deliver to chat — it only echoes into a detached shell and is NOT a reminder. Always create the reminder with `openclaw cron add`. - NEVER claim "cron is disabled" / "cron is globally disabled" / "cron triggers are disabled". Cron is ENABLED. Only the agent's built-in `cron` _tool_ is denied (that is exactly why you must use the `openclaw cron add` CLI). If `openclaw cron add` errors, report the REAL error — do not invent a workaround or a disabled-system excuse. @@ -48,15 +48,15 @@ Users will phrase requests unpredictably ("remind me to X in 2 minutes", "ping m ### Examples (patterns — adapt to the actual words) -Relative one-shot ("remind me to check email after 2 minutes"): +Relative one-shot ("remind me to check server status after 2 minutes"): ```bash openclaw cron add \ - --name "Check email" \ + --name "Check server" \ --at "+2m" \ --agent rc-ocrcbot \ --session isolated \ - --command 'echo "Time to check your email!"' \ + --command 'echo "Time to check the server status!"' \ --announce --channel rocketchat --to "" \ --delete-after-run --json ``` diff --git a/docs/SKILLS/Email.md b/docs/SKILLS/Email.md deleted file mode 100644 index 4f59199..0000000 --- a/docs/SKILLS/Email.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: email -description: "Send emails via s-nail, read inbox via the fetch-emails script (reads password from env/config)." -metadata: - openclaw: - emoji: "📧" - requires: - bins: ["s-nail", "fetch-emails"] ---- - -# Email - -## Send (use s-nail) - -Sending requires SMTP or Agentmail credentials configured on the gateway (via env vars, `~/.netrc`, or the OpenClaw core skill config). - -```bash -echo "Body text here" | s-nail -s "Subject" recipient@example.com -``` - -Send from a specific account (set `EMAIL_FROM` on the gateway): - -```bash -echo "Body" | s-nail -s "Subject" -S from= recipient@example.com -``` - -## Read inbox (single method: fetch-emails) - -Run `fetch-emails ` — the account is required unless `GMAIL_ACCOUNT` is set on the gateway. - -The script reads the Gmail app password from `GMAIL_APP_PASSWORD` env (or a file under `~/.config/gmail/`). - -```bash -fetch-emails 5 -``` - -## CRITICAL rules - -- NEVER pass $GMAIL_APP_PASSWORD in any command — fetch-emails reads it from the environment (or `~/.config/gmail/`) internally -- Pass an explicit `` unless `GMAIL_ACCOUNT` is set — never assume a default -- Present the fetched result exactly ONCE — do not re-fetch or re-summarize the same data -- Use `-S from=` with s-nail when sending from a specific account diff --git a/docs/SKILLS/doc.md b/docs/SKILLS/doc.md index 71c9da4..366cf64 100644 --- a/docs/SKILLS/doc.md +++ b/docs/SKILLS/doc.md @@ -1,6 +1,6 @@ # OpenClaw Skills -Skills let you extend what your OpenClaw agent can do from sending emails to scheduling reminders to integrating with third-party services. +Skills let you extend what your OpenClaw agent can do from scheduling reminders to integrating with third-party services. ## 1. Create the skills folder @@ -18,8 +18,7 @@ Inside each skill's folder, add a `SKILL.md` file describing what the skill does ``` ~/.openclaw/workspace/skills/ -├── email/ -│ └── SKILL.md + ├── cron/ │ └── SKILL.md └── agentmail/ @@ -36,12 +35,11 @@ A `SKILL.md` typically includes: Two ready-made example skills are included to get you started: -- **`email`** : send/read email via `s-nail` and `fetch-emails` - Reference: [SKILLS/Email.md](./Email.md) + - **`cron`** : schedule one-shot and recurring reminders via the `openclaw cron` CLI Reference: [SKILLS/Cron.md](./Cron.md) -Of these, the `cron` skill is exposed natively in the command menu (`!cron`). The `email` skill is used like any other skill via OpenClaw core (`!skill `). +The `cron` skill is exposed natively in the command menu (`!cron`). Other skills are used via OpenClaw core (`!skill `). > Copy the contents of the reference file into `~/.openclaw/workspace/skills//SKILL.md` to use it as-is, or edit it to fit your setup. diff --git a/openclaw.examples.json b/openclaw.examples.json index 5c8c1b7..107d092 100644 --- a/openclaw.examples.json +++ b/openclaw.examples.json @@ -271,14 +271,7 @@ "maxConcurrent": 8 }, "imageModel": "nvidia-nim/anthropic/nvidia_nim/meta/llama-3.2-11b-vision-instruct" - }, - "list": [ - { - "id": "main", - "default": true, - "workspace": "/home/USERNAME/.openclaw/workspace" - } - ] + } }, "tools": { "profile": "full", @@ -398,9 +391,7 @@ "cron": { "enabled": true }, - "email": { - "enabled": true - } + } }, "bindings": [], diff --git a/openclaw.plugin.json b/openclaw.plugin.json index bc55a32..1f3d62b 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "rocketchat", "name": "Rocket.Chat", "description": "Rocket.Chat channel plugin with DDP/websocket outbound/inbound", - "version": "1.2.10", + "version": "1.2.17", "kind": "channel", "channels": [ "rocketchat" @@ -117,13 +117,6 @@ "owner": { "type": "string", "minLength": 1 - }, - "ownerOnlySkills": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - } } }, "required": [ diff --git a/package.json b/package.json index 2bd7e6f..9cb368d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dodaa08/openclaw-plugin-test", - "version": "1.2.10", + "version": "1.2.17", "description": "A fully unified plugin for integrating Rocket.Chat with OpenClaw. This plugin eliminates the need for an external bridging server, providing a direct, single-place architecture for inbounds, outbounds, session management, and CLI configuration.", "main": "dist/index.js", "peerDependencies": { @@ -20,13 +20,13 @@ }, "type": "module", "scripts": { - "build": "rm -rf dist && npm run manifest && tsc && npm run bundle", - "bundle": "esbuild src/client/ddp.ts --bundle --format=esm --platform=node --outfile=dist/client/ddp.js --sourcemap", + "build": "rm -rf dist && npm run manifest && tsc && npm run bundle && chmod -R 755 dist", + "bundle": "esbuild src/client/ddp.ts --bundle --format=esm --platform=node --outfile=dist/client/ddp.js --sourcemap --external:openclaw --external:@openclaw/*", "manifest": "tsx scripts/gen-manifest.mts", "format": "prettier --write .", "format:check": "prettier --check .", "start": "node dist/index.js", - "setup": "openclaw rocket-chat setup", + "setup": "openclaw rocketchat setup", "test": "echo '✅ No tests yet'" }, "repository": { @@ -58,9 +58,7 @@ "access": "public" }, "devDependencies": { - "@clack/core": "1.4.3", "@types/node": "^25.9.1", - "@types/ws": "^8.18.1", "esbuild": "^0.28.1", "openclaw": "^2026.7.1-2", "prettier": "^3.9.6", @@ -68,9 +66,9 @@ "typescript": "^6.0.3" }, "dependencies": { + "@clack/core": "1.4.3", "@clack/prompts": "^1.7.0", "@rocket.chat/ddp-client": "^1.1.1", - "commander": "^15.0.0", "json5": "^2.2.3", "picocolors": "^1.1.1", "yaml": "^2.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21ee08a..72820ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,15 @@ importers: .: dependencies: + '@clack/core': + specifier: 1.4.3 + version: 1.4.3 '@clack/prompts': specifier: ^1.7.0 version: 1.7.0 '@rocket.chat/ddp-client': specifier: ^1.1.1 version: 1.1.2(@rocket.chat/emitter@0.33.0)(@rocket.chat/icons@0.49.0) - commander: - specifier: ^15.0.0 - version: 15.0.0 json5: specifier: ^2.2.3 version: 2.2.3 @@ -30,15 +30,9 @@ importers: specifier: ^4.4.3 version: 4.5.4 devDependencies: - '@clack/core': - specifier: 1.4.3 - version: 1.4.3 '@types/node': specifier: ^25.9.1 version: 25.9.5 - '@types/ws': - specifier: ^8.18.1 - version: 8.18.1 esbuild: specifier: ^0.28.1 version: 0.28.2 @@ -607,9 +601,6 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@ubjs/core@0.31.0-3': resolution: {integrity: sha512-39XrJgUZ2VVb561sSnkXPhczNoeBsNiSRArecsV0JE7CJq69ajFkcn9/tBAUS2NpgHkLIDU+z6Ks2+1wXnboxg==} @@ -2300,10 +2291,6 @@ snapshots: '@types/retry@0.12.0': {} - '@types/ws@8.18.1': - dependencies: - '@types/node': 25.9.5 - '@ubjs/core@0.31.0-3': {} '@ubjs/node-darwin-arm64@0.31.0-3': diff --git a/src/cli/admin-api.ts b/src/cli/admin-api.ts index c21c389..71da81f 100644 --- a/src/cli/admin-api.ts +++ b/src/cli/admin-api.ts @@ -4,6 +4,12 @@ import type { RCLoginResult, RCUser, JsonObject } from "../types.js"; const REQUEST_TIMEOUT_MS = 15_000; +/** + * users.create is a heavyweight endpoint (account + roles + email/avatar work on + * the server); give it a generous window so slow servers don't abort mid-request. + */ +const BOT_CREATE_TIMEOUT_MS = 60_000; + export function isTimeoutError(e: unknown): boolean { return e instanceof DOMException && (e.name === "TimeoutError" || e.name === "AbortError"); } @@ -40,6 +46,7 @@ type RCFetchOpts = { userId?: string; authToken?: string; raw?: boolean; + timeoutMs?: number; }; async function adminFetch( @@ -52,11 +59,15 @@ async function adminFetch( headers["X-Auth-Token"] = opts.authToken; headers["X-User-Id"] = opts.userId; } - const res = await fetchWithTimeout(new URL(path, baseUrl), { - method: opts.method ?? "POST", - headers, - ...(opts.body ? { body: JSON.stringify(opts.body) } : {}), - }); + const res = await fetchWithTimeout( + new URL(path, baseUrl), + { + method: opts.method ?? "POST", + headers, + ...(opts.body ? { body: JSON.stringify(opts.body) } : {}), + }, + opts.timeoutMs, + ); const json = (await res.json()) as JsonObject; if (!opts.raw && (!res.ok || json.success === false)) { const msg = getErrorMessage(json, res.statusText); @@ -146,11 +157,18 @@ export async function loginAs( export async function createBotUser( baseUrl: string, auth: RCLoginResult, - opts: { username: string; name: string; password: string; email: string }, + opts: { + username: string; + name: string; + password: string; + email: string; + timeoutMs?: number; + }, ): Promise { const json = await adminFetch(baseUrl, "/api/v1/users.create", { userId: auth.userId, authToken: auth.authToken, + timeoutMs: opts.timeoutMs ?? BOT_CREATE_TIMEOUT_MS, body: { username: opts.username, name: opts.name, diff --git a/src/cli/bot.ts b/src/cli/bot.ts index 47f3a6f..133f506 100644 --- a/src/cli/bot.ts +++ b/src/cli/bot.ts @@ -1,4 +1,4 @@ -import { createBotUser, getUserInfo } from "./admin-api.js"; +import { createBotUser, getUserInfo, isTimeoutError } from "./admin-api.js"; import { checkBotCreationLimit, recordBotCreation } from "./rate-limiter.js"; import { readChannelLimits } from "./config-updater.js"; import { saveBotCredentials, loadBotCredentials } from "./credentials.js"; @@ -88,7 +88,7 @@ async function createNewBot( botUsername: string, ): Promise { const limits = readChannelLimits(); - const limitCheck = checkBotCreationLimit("cli", { + const limitCheck = checkBotCreationLimit({ serverUrl: rcUrl, maxAccounts: limits.maxAccounts, maxBotsPerServer: limits.maxBotsPerServer, @@ -116,7 +116,14 @@ async function createNewBot( email: botEmail, }); } catch (e: unknown) { - p.log.error(`Failed to create bot: ${e instanceof Error ? e.message : String(e)}`); + if (isTimeoutError(e)) { + p.log.error( + "Failed to create bot: the Rocket.Chat server did not respond within 60s. " + + "The bot may have been created anyway - check @${botUsername} in Rocket.Chat, or re-run setup.", + ); + } else { + p.log.error(`Failed to create bot: ${e instanceof Error ? e.message : String(e)}`); + } return null; } }); diff --git a/src/cli/config-updater.ts b/src/cli/config-updater.ts index 28d673b..7058b4d 100644 --- a/src/cli/config-updater.ts +++ b/src/cli/config-updater.ts @@ -7,13 +7,15 @@ import { rmSync, mkdirSync, } from "node:fs"; -import { resolve } from "node:path"; +import { resolve, dirname } from "node:path"; import { homedir } from "node:os"; import { createHash } from "node:crypto"; import JSON5 from "json5"; +import { OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness-runtime"; import type { AuthCredentials, JsonObject } from "../types.js"; export const OC_CONFIG_PATH = resolve(homedir(), ".openclaw", "openclaw.json"); +const SHARED_WORKSPACE_DIR = resolve(homedir(), ".openclaw", "workspace"); export type TokenAuth = Extract; @@ -22,12 +24,162 @@ export function readConfig(): JsonObject { return JSON5.parse(readFileSync(OC_CONFIG_PATH, "utf-8")); } +/** + * The OpenClaw core that supports `agents.entries` shipped as 2026.8.1 + * (the v2026.7.2 betas were released as 2026.8.1). Earlier cores (2026.7.x) + * reject `agents.entries` and instead accept the legacy `agents.list` array, + * which is only treated as an internal projection. + */ +const AGENTS_ENTRIES_MIN_CORE = "2026.8.0"; + +function parseCoreVersion(version: string): { major: number; minor: number; patch: number } | null { + const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version ?? ""); + if (!m) return null; + return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) }; +} + +function coreSupportsAgentsEntries(): boolean { + const current = parseCoreVersion(OPENCLAW_VERSION); + const min = parseCoreVersion(AGENTS_ENTRIES_MIN_CORE); + if (!current || !min) return false; + if (current.major !== min.major) return current.major > min.major; + if (current.minor !== min.minor) return current.minor > min.minor; + return current.patch >= min.patch; +} + function writeConfig(cfg: JsonObject): void { + reconcileAgentListings(cfg as Record); + const dir = dirname(OC_CONFIG_PATH); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); const tmp = OC_CONFIG_PATH + ".tmp"; writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", "utf-8"); renameSync(tmp, OC_CONFIG_PATH); } +/** + * Keep the persisted `agents` block compatible with the running OpenClaw core. + * + * - Cores >= 2026.8.0 (e.g. 2026.9.2) validate that every non-"main" binding + * `agentId` resolves to a `agents.entries` entry and reject the legacy + * `agents.list` key. Dedicated `rc-*` agents are discovered on disk, but the + * core's config validation does not scan `~/.openclaw/agents/` — it only + * reads `agents.entries`. So each dedicated agent the plugin binds must be + * declared here, otherwise the config is rejected with + * `Unknown agent id "rc-..." (not in agents.entries)` and the bot never + * comes online. + * - Cores < 2026.8.0 (2026.7.x) reject `agents.entries` outright, so those keys + * must be scrubbed to keep the whole config valid for them. + */ +function reconcileAgentListings(cfg: Record): void { + const agents = cfg?.agents; + + if (!coreSupportsAgentsEntries()) { + if (agents && typeof agents === "object") { + delete agents.entries; + delete agents.list; + } + return; + } + + if (agents && typeof agents === "object" && agents.list !== undefined) { + delete agents.list; + } + + const entries = collectRequiredAgentEntries(cfg); + if (Object.keys(entries).length === 0) { + if (agents && typeof agents === "object") { + cfg.agents.entries = { main: {} }; + // Do not set agents.ownership — unrecognized by core 2026.9.x + } + return; + } + + if (!cfg.agents || typeof cfg.agents !== "object") cfg.agents = {}; + cfg.agents.entries = entries; + // Note: do NOT set agents.ownership here — the key is unrecognized by the core + // schema validator and causes gateway reload to be skipped with "Unrecognized key". +} + +/** + * Compute the `agents.entries` record that must be persisted for the new-style + * cores: every non-"main" agent referenced by a rocketchat binding is declared + * (rc-* agents pin `workspace` to the shared ~/.openclaw/workspace), while + * user-defined entries are preserved. Plugin-owned `rc-*` entries whose + * Rocket.Chat account was removed and that no binding references are pruned. + */ +function collectRequiredAgentEntries(cfg: Record): Record { + const result: Record = {}; + const existing = cfg?.agents?.entries; + if (existing && typeof existing === "object" && !Array.isArray(existing)) { + Object.assign(result, existing); + } + + const bindings = cfg?.bindings; + const rocketchatBindings = Array.isArray(bindings) + ? bindings.filter( + (b) => + b && + typeof b === "object" && + b.match && + typeof b.match === "object" && + b.match.channel === "rocketchat", + ) + : []; + + const boundAgentIds = new Set( + rocketchatBindings + .map((b) => (typeof b.agentId === "string" ? b.agentId : "")) + .filter((id) => id.length > 0 && id !== "main"), + ); + + const accountIds = new Set( + (() => { + const accounts = cfg?.channels?.rocketchat?.accounts; + if (typeof accounts !== "object" || accounts === null) return []; + return Object.keys(accounts).filter((id) => id.length > 0); + })(), + ); + + for (const agentId of boundAgentIds) { + if (!(agentId in result)) result[agentId] = {}; + } + + for (const id of Object.keys(result)) { + if (!id.startsWith("rc-")) continue; + const accountId = id.slice("rc-".length); + if (!boundAgentIds.has(id) && !accountIds.has(accountId)) { + delete result[id]; + continue; + } + const raw = result[id]; + result[id] = pinRcAgentWorkspace(raw && typeof raw === "object" ? raw : {}, id); + } + + return result; +} + +export function getAgentWorkspaceDir(agentId: string): string { + return resolve(SHARED_WORKSPACE_DIR, agentId); +} + +/** Dedicated rc-* agents use isolated workspace ~/.openclaw/workspace/. */ +function pinRcAgentWorkspace(entry: Record, agentId: string): Record { + const targetWs = getAgentWorkspaceDir(agentId); + const current = typeof entry.workspace === "string" ? entry.workspace : ""; + if (!current || resolve(current) === SHARED_WORKSPACE_DIR) { + try { + mkdirSync(targetWs, { recursive: true }); + } catch { + /* best-effort */ + } + return { ...entry, workspace: targetWs }; + } + return entry; +} + +// ensureSystemAgent was removed: agents.defaults.systemAgent is a legacy retired key +// in OpenClaw 2026.9.x and causes "Unrecognized key" config validation errors. + export type ExistingAccount = { accountId: string; serverUrl: string; @@ -169,6 +321,7 @@ export function updateConfig(opts: { mentionNames?: string[]; auth: TokenAuth; owner?: string; + agentId?: string; replaceConnection?: boolean; }) { @@ -224,32 +377,53 @@ export function updateConfig(opts: { ...(opts.owner ? { owner: opts.owner.trim().replace(/^@+/, "") } : {}), }; + if (opts.agentId) { + applyBinding(cfg, { + channel: "rocketchat", + accountId: opts.accountId, + agentId: opts.agentId, + }); + } + writeConfig(cfg); } -export function readAgentsList(): Array<{ id: string; name?: string }> { +function readAgentsList(): Array<{ id: string; name?: string }> { const cfg = readConfig() as Record; - const list = cfg?.agents?.list; const agents: Array<{ id: string; name?: string }> = []; + const push = (id: string, name?: string): void => { + if (agents.some((a) => a.id === id)) return; + agents.push(name !== undefined ? { id, name } : { id }); + }; - if (Array.isArray(list)) { - for (const a of list) { + const entries = cfg?.agents?.entries; + if (entries && typeof entries === "object" && !Array.isArray(entries)) { + for (const [id, rawEntry] of Object.entries(entries)) { + if (!rawEntry || typeof rawEntry !== "object") continue; + const entry = rawEntry as Record; + const name = typeof entry.name === "string" ? entry.name : undefined; + push(id, name); + } + } + + const legacy = cfg?.agents?.list; + if (Array.isArray(legacy)) { + for (const a of legacy) { if (!a || typeof a !== "object") continue; const id = typeof a.id === "string" ? a.id : ""; if (!id) continue; const name = typeof a.name === "string" ? a.name : undefined; - agents.push(name !== undefined ? { id, name } : { id }); + push(id, name); } } const agentsDir = resolve(homedir(), ".openclaw", "agents"); if (existsSync(agentsDir)) { try { - const entries = readdirSync(agentsDir, { withFileTypes: true }); - for (const e of entries) { + const entries2 = readdirSync(agentsDir, { withFileTypes: true }); + for (const e of entries2) { if (!e.isDirectory()) continue; - if (agents.some((a) => a.id === e.name)) continue; - agents.push({ id: e.name }); + push(e.name); } } catch { // fall through @@ -285,6 +459,7 @@ export function addAccount(opts: { mentionNames: string[]; transport?: { mode: "websocket" }; owner?: string; + agentId?: string; }): void { const cfg = readConfig() as Record; @@ -309,6 +484,14 @@ export function addAccount(opts: { ...(owner ? { owner } : {}), }; + if (opts.agentId) { + applyBinding(cfg, { + channel: "rocketchat", + accountId: opts.accountId, + agentId: opts.agentId, + }); + } + writeConfig(cfg); } @@ -326,36 +509,25 @@ export function ensureAgentForBot(accountId: string): { reason?: string; } { const dedicatedId = `rc-${accountId}`; - if (readAgentsList().some((a) => a.id === dedicatedId)) { - return { agentId: dedicatedId, created: false, fallback: false }; - } + const existed = readAgentsList().some((a) => a.id === dedicatedId); try { - const workspace = resolve(homedir(), ".openclaw", "agents", dedicatedId); - mkdirSync(resolve(workspace, "agent"), { recursive: true }); - mkdirSync(resolve(workspace, "sessions"), { recursive: true }); - - const stateFile = resolve(workspace, "openclaw-workspace-state.json"); - if (!existsSync(stateFile)) { - writeFileSync( - stateFile, - JSON.stringify({ version: 1, bootstrapSeededAt: new Date().toISOString() }, null, 2) + "\n", - ); + const agentDir = resolve(homedir(), ".openclaw", "agents", dedicatedId); + mkdirSync(resolve(agentDir, "agent"), { recursive: true }); + mkdirSync(resolve(agentDir, "sessions"), { recursive: true }); + + const staleStateFile = resolve(agentDir, "openclaw-workspace-state.json"); + if (existsSync(staleStateFile)) { + try { + rmSync(staleStateFile); + } catch { + /* best-effort */ + } } - const cfg = readConfig() as Record; - if (!cfg.agents) cfg.agents = {}; - if (!Array.isArray(cfg.agents.list)) cfg.agents.list = []; - if (!cfg.agents.list.some((a: any) => a?.id === dedicatedId)) { - cfg.agents.list.push({ - id: dedicatedId, - name: dedicatedId, - workspace, - agentDir: resolve(workspace, "agent"), - }); - } - writeConfig(cfg); - return { agentId: dedicatedId, created: true, fallback: false }; + removeLegacyPerBotWorkspace(dedicatedId); + + return { agentId: dedicatedId, created: !existed, fallback: false }; } catch (err) { return { agentId: "main", @@ -382,14 +554,15 @@ function normalizeAgentId(id: string): string { return id.trim().toLowerCase(); } -export function addBinding(opts: { - channel: string; - accountId: string; - agentId: string; - peer?: { kind: string; id: string }; -}): void { - const cfg = readConfig() as Record; - +function applyBinding( + cfg: Record, + opts: { + channel: string; + accountId: string; + agentId: string; + peer?: { kind: string; id: string }; + }, +): void { if (!cfg.bindings) cfg.bindings = []; const bindings = cfg.bindings as Array>; @@ -417,20 +590,16 @@ export function addBinding(opts: { } else { bindings.push(binding); } - - writeConfig(cfg); } -export function removeBindingsForAccount(accountId: string): void { - const cfg = readConfig() as Record; + + +function stripBindingsForAccount(cfg: Record, accountId: string): void { const bindings = cfg?.bindings as Array> | undefined; if (!bindings) return; - cfg.bindings = bindings.filter( (b) => !(b.match?.channel === "rocketchat" && b.match?.accountId === accountId), ); - - writeConfig(cfg); } export function removeAccount(accountId: string): void { @@ -439,24 +608,52 @@ export function removeAccount(accountId: string): void { if (accounts) { delete accounts[accountId]; } + stripBindingsForAccount(cfg, accountId); writeConfig(cfg); } export function removeAgentDir(accountId: string): void { const dir = resolve(homedir(), ".openclaw", "agents", `rc-${accountId}`); + const agentWs = resolve(dir, "workspace"); if (existsSync(dir)) rmSync(dir, { recursive: true, force: true }); removeWorkspaceAttestations(dir); + removeWorkspaceAttestations(agentWs); + removeLegacyPerBotWorkspace(`rc-${accountId}`); +} - const cfg = readConfig() as Record; - const list = cfg?.agents?.list; - if (Array.isArray(list)) { - const next = list.filter( - (a: any) => !(a && typeof a.id === "string" && a.id === `rc-${accountId}`), - ); - if (next.length !== list.length) { - cfg.agents.list = next; - writeConfig(cfg); +function removeLegacyPerBotWorkspace(agentId: string): void { + const wsDir = getAgentWorkspaceDir(agentId); + if (existsSync(wsDir) && resolve(wsDir) !== SHARED_WORKSPACE_DIR) { + rmSync(wsDir, { recursive: true, force: true }); + removeWorkspaceAttestations(wsDir); + } +} + +let didMigrateRcWorkspaces = false; + +/** Pin rc-* agents to the shared workspace and drop leftover workspace/rc-* dirs. */ +export function migrateRcWorkspacesIfNeeded(): void { + if (didMigrateRcWorkspaces) return; + didMigrateRcWorkspaces = true; + + try { + if (existsSync(SHARED_WORKSPACE_DIR)) { + const entries = readdirSync(SHARED_WORKSPACE_DIR, { withFileTypes: true }); + for (const e of entries) { + if (e.isDirectory() && e.name.startsWith("rc-")) { + removeLegacyPerBotWorkspace(e.name); + } + } } + } catch { + /* best-effort */ + } + + const cfg = readConfig() as Record; + const before = JSON.stringify(cfg); + reconcileAgentListings(cfg); + if (JSON.stringify(cfg) !== before) { + writeConfig(cfg); } } diff --git a/src/cli/rate-limiter.ts b/src/cli/rate-limiter.ts index a4e9fe2..dd18a82 100644 --- a/src/cli/rate-limiter.ts +++ b/src/cli/rate-limiter.ts @@ -3,9 +3,9 @@ import { homedir } from "node:os"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { readAllAccounts } from "./config-updater.js"; -export const DEFAULT_MAX_ACCOUNTS = 10; -export const DEFAULT_MAX_BOTS_PER_SERVER = 5; -export const DEFAULT_COOLDOWN_MS = 60_000; +const DEFAULT_MAX_ACCOUNTS = 10; +const DEFAULT_MAX_BOTS_PER_SERVER = 5; +const DEFAULT_COOLDOWN_MS = 60_000; export const DEFAULT_MAX_RECONNECTS = 20; const STATE_DIR = resolve(homedir(), ".openclaw", "rocketchat"); @@ -50,15 +50,12 @@ export interface BotCreationLimit { remainingCooldownMs?: number; } -export function checkBotCreationLimit( - source: "cli" | "inline", - options?: { - maxAccounts?: number | undefined; - maxBotsPerServer?: number | undefined; - cooldownMs?: number | undefined; - serverUrl?: string; - }, -): BotCreationLimit { +export function checkBotCreationLimit(options?: { + maxAccounts?: number | undefined; + maxBotsPerServer?: number | undefined; + cooldownMs?: number | undefined; + serverUrl?: string; +}): BotCreationLimit { const maxAccounts = options?.maxAccounts ?? DEFAULT_MAX_ACCOUNTS; const maxBotsPerServer = options?.maxBotsPerServer ?? DEFAULT_MAX_BOTS_PER_SERVER; const cooldownMs = options?.cooldownMs ?? DEFAULT_COOLDOWN_MS; diff --git a/src/cli/setup.ts b/src/cli/setup.ts index 8bbc09d..6fd48ba 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -10,14 +10,7 @@ import { inviteToGroup, sendMessage, } from "./admin-api.js"; -import { - addBinding, - ensureAgentForBot, - isAgentBound, - readAllAccounts, - updateConfig, - type ExistingAccount, -} from "./config-updater.js"; +import { ensureAgentForBot, isAgentBound, readAllAccounts, updateConfig } from "./config-updater.js"; import { loadAdmin } from "./credentials.js"; import { resolveAdminAuth } from "./auth.js"; import { resolveBotAuth } from "./bot.js"; @@ -325,9 +318,11 @@ export async function runSetup(): Promise { auth: { mode: "token", userId: botAuth.userId, accessToken: botAuth.authToken }, replaceConnection: !serverAccounts || !serverAccounts.some((a) => a.serverUrl === rcUrl), ...(ownerUsername ? { owner: ownerUsername } : {}), + agentId: agentResult.agentId, }); }); p.log.success(`Updated ${color.cyan(OC_CONFIG_PATH)}`); + p.log.success(`Bound @${botUsername} to agent '${agentResult.agentId}'`); } catch (e: unknown) { p.log.warn(`Config update skipped: ${e instanceof Error ? e.message : String(e)}`); } @@ -344,12 +339,6 @@ export async function runSetup(): Promise { } else { p.log.success(`agent ${agentResult.agentId}`); } - try { - addBinding({ channel: "rocketchat", accountId, agentId: agentResult.agentId }); - p.log.success(`Bound @${botUsername} to agent '${agentResult.agentId}'`); - } catch (e: unknown) { - p.log.warn(`Could not create binding: ${e instanceof Error ? e.message : String(e)}`); - } const addToGroup = await promptConfirm({ message: `Add @${botUsername} to a Rocket.Chat group/channel?`, diff --git a/src/cli/ui.ts b/src/cli/ui.ts index fc59d24..6418ae9 100644 --- a/src/cli/ui.ts +++ b/src/cli/ui.ts @@ -1,7 +1,6 @@ import * as p from "@clack/prompts"; import color from "picocolors"; import { Prompt, isCancel, type PromptOptions } from "@clack/core"; -import { isPrivateOrLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime"; export function normalizeRocketChatUrl(input: string): string | null { const trimmed = input.trim(); @@ -20,17 +19,7 @@ export function normalizeRocketChatUrl(input: string): string | null { return pathname.length > 1 ? `${url.origin}${pathname}` : url.origin; } -export function isLocalRocketChatUrl(input: string): boolean { - let url: URL; - try { - url = new URL(input); - } catch { - return false; - } - return isPrivateOrLoopbackHost(url.hostname.toLowerCase()); -} - -export function handleCancel(value: unknown): never | void { +function handleCancel(value: unknown): never | void { if (p.isCancel(value)) { p.cancel("Setup cancelled."); process.exit(0); @@ -188,22 +177,6 @@ export async function promptSelect(opts: Parameters[0]): Pro return value as T; } -export async function promptAutocomplete( - opts: Parameters[0], -): Promise { - const value = await p.autocomplete(opts); - handleCancel(value); - return value as T; -} - -export async function promptAutocompleteMultiselect( - opts: Parameters[0], -): Promise { - const value = await p.autocompleteMultiselect(opts); - handleCancel(value); - return value as T[]; -} - export async function withSpinner(message: string, task: () => Promise): Promise { const spinner = p.spinner(); spinner.start(message); diff --git a/src/client/rest.ts b/src/client/rest.ts index 7c9b102..6da2501 100644 --- a/src/client/rest.ts +++ b/src/client/rest.ts @@ -29,7 +29,7 @@ export class RocketChatClientError extends Error { } } -export class RocketChatRateLimitError extends RocketChatClientError { +class RocketChatRateLimitError extends RocketChatClientError { readonly retryAfterMs: number; constructor(message: string, options: { retryAfterMs: number }) { @@ -295,7 +295,7 @@ export class RocketChatClient { const maxRetries = 3; for (let attempt = 0; attempt <= maxRetries; attempt++) { await this.ensureInitialized(); - const signal = init.signal ?? AbortSignal.timeout(15_000); + const signal = init.signal ?? AbortSignal.timeout(30_000); const response = await this.fetchFn(url.toString(), { ...init, signal, diff --git a/src/config/access-store.ts b/src/config/access-store.ts index 829f309..60f92ba 100644 --- a/src/config/access-store.ts +++ b/src/config/access-store.ts @@ -14,7 +14,7 @@ export type AccessGrant = { const SCHEMA_VERSION = "1"; -export function getAccessDbPath(): string { +function getAccessDbPath(): string { return resolve(homedir(), ".openclaw", "rocketchat", "access.db"); } diff --git a/src/config/schema.ts b/src/config/schema.ts index 46d0ee1..6e16491 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -37,7 +37,7 @@ const accountSchema = z mentionNames: z.array(z.string().min(1)).default([]), agent: z.string().min(1).optional(), owner: z.string().min(1).optional(), - ownerOnlySkills: z.array(z.string().min(1)).optional(), + }) .strict(); diff --git a/src/index.ts b/src/index.ts index 9524f33..4a5969d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { rocketchatPlugin, startGateway } from "./plugin.js"; +import { rocketchatPlugin } from "./plugin.js"; export { startGateway } from "./plugin.js"; diff --git a/src/plugin.ts b/src/plugin.ts index 27b452b..bda6812 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -14,7 +14,7 @@ import { import { collectBotUsernamesForServer } from "./cli/config-updater.js"; import type { ResolvedAccount } from "./types.js"; -export { startGateway, resolveAccount, listAccountIds, isConfigured }; +export { startGateway, isConfigured }; export const rocketchatPlugin = createChatChannelPlugin({ base: { diff --git a/src/service/channel.ts b/src/service/channel.ts index 8023199..21ff33d 100644 --- a/src/service/channel.ts +++ b/src/service/channel.ts @@ -15,17 +15,17 @@ import { readOwner, readAccount, addAccount, - addBinding, ensureAgentForBot, - removeBindingsForAccount, removeAccount, removeAgentDir, + getAgentWorkspaceDir, type ExistingAccount, type TokenAuth, } from "../cli/config-updater.js"; import { createBotUser, loginAs, + isTimeoutError, getGroupByName, createDirectMessage, sendMessage, @@ -207,7 +207,7 @@ async function runCommand( case "tools": return { action: "openclaw-command", command: `/tools${argStr ? " " + argStr : ""}` }; case "skills": - return { action: "reply", replyText: runSkills() }; + return { action: "reply", replyText: runSkills(ctx) }; case "cron": return { action: "reply", replyText: await runCronCommand(ctx, argStr) }; case "think": @@ -438,42 +438,61 @@ function runBots(): string { return ["**Bot accounts**", ...lines].join("\n"); } -function runSkills(): string { - const skillsDir = join(resolveOpenClawDir(), "workspace", "skills"); - if (!existsSync(skillsDir)) { - return "No skills installed (expected at ~/.openclaw/workspace/skills)."; +function runSkills(ctx?: CommandContext): string { + const scannedDirs: Array<{ path: string; scope: "Private" | "Global" }> = []; + + if (ctx?.accountId) { + const agentId = `rc-${ctx.accountId}`; + const agentWs = getAgentWorkspaceDir(agentId); + scannedDirs.push({ path: join(agentWs, "skills"), scope: "Private" }); + scannedDirs.push({ path: resolve(resolveOpenClawDir(), "agents", agentId, "skills"), scope: "Private" }); } - const entries = readdirSync(skillsDir).filter((name) => { - const full = resolve(skillsDir, name); - try { - return statSync(full).isDirectory() || statSync(full).isSymbolicLink(); - } catch { - return false; - } - }); - const skills: Array<{ name: string; description: string }> = []; - for (const name of entries) { - const skillMd = resolve(skillsDir, name, "SKILL.md"); - if (!existsSync(skillMd)) continue; - let content = ""; - try { - content = readFileSync(skillMd, "utf8"); - } catch { - continue; + + scannedDirs.push({ path: join(resolveOpenClawDir(), "workspace", "skills"), scope: "Global" }); + scannedDirs.push({ path: join(resolveOpenClawDir(), "skills"), scope: "Global" }); + + const skillsMap = new Map(); + + for (const { path: skillsDir, scope } of scannedDirs) { + if (!existsSync(skillsDir)) continue; + const entries = readdirSync(skillsDir).filter((name) => { + const full = resolve(skillsDir, name); + try { + return statSync(full).isDirectory() || statSync(full).isSymbolicLink(); + } catch { + return false; + } + }); + + for (const name of entries) { + const skillMd = resolve(skillsDir, name, "SKILL.md"); + if (!existsSync(skillMd)) continue; + let content = ""; + try { + content = readFileSync(skillMd, "utf8"); + } catch { + continue; + } + const fm = parseSkillFrontmatter(content); + if (!fm.name) continue; + const key = fm.name.toLowerCase(); + if (!skillsMap.has(key)) { + skillsMap.set(key, { name: fm.name, description: fm.description ?? "", scope }); + } } - const fm = parseSkillFrontmatter(content); - if (!fm.name) continue; - skills.push({ name: fm.name, description: fm.description ?? "" }); } + + const skills = Array.from(skillsMap.values()); if (skills.length === 0) { - return "No skills installed (expected at ~/.openclaw/workspace/skills)."; + return "No skills installed."; } const cap = (s: string, n = 200): string => (s.length > n ? s.slice(0, n).trimEnd() + "…" : s); const lines = ["**Installed skills**", ""]; lines.push("Use a skill via inbound chat with the agent."); for (const s of skills) { const title = s.name.charAt(0).toUpperCase() + s.name.slice(1); - lines.push("", `**${title}**`); + const scopeTag = `\`[${s.scope}]\``; + lines.push("", `**${title}** ${scopeTag}`); lines.push(`• ${s.description ? cap(s.description) : "No description available."}`); } return lines.join("\n"); @@ -693,7 +712,7 @@ async function runAddBot(ctx: CommandContext, argStr: string): Promise { } try { - const limitCheck = checkBotCreationLimit("inline", { + const limitCheck = checkBotCreationLimit({ serverUrl: ctx.account.serverUrl, maxAccounts: ctx.limits?.maxAccounts, maxBotsPerServer: ctx.limits?.maxBotsPerServer, @@ -725,8 +744,8 @@ async function runAddBot(ctx: CommandContext, argStr: string): Promise { auth: { mode: "token", userId: botAuth.userId, accessToken: botAuth.authToken } as TokenAuth, mentionNames: [username], ...(ctx.account.owner ? { owner: ctx.account.owner } : {}), + agentId: agent, }); - addBinding({ channel: "rocketchat", accountId, agentId: agent }); const owner = ctx.account.owner?.trim().replace(/^@+/, "") || undefined; @@ -762,6 +781,13 @@ async function runAddBot(ctx: CommandContext, argStr: string): Promise { ...(dmNote ? [dmNote] : []), ].join("\n"); } catch (e: unknown) { + if (isTimeoutError(e)) { + return ( + `Failed to create bot: the Rocket.Chat server did not respond in time. ` + + `It may have been created anyway - check the user \`${username}\` on the server, ` + + `or use \`!remove-bot ${username}\` before retrying.` + ); + } return `Failed to create bot: ${e instanceof Error ? e.message : String(e)}`; } } @@ -813,7 +839,6 @@ async function notifyAccessChange( roomId: string, scopeLabel: string, action: "granted" | "revoked", - targetIsBot: boolean, ): Promise { const botMention = ctx.account.mentionNames[0] ?? ctx.accountId; const how = @@ -889,14 +914,7 @@ async function runLend(ctx: CommandContext, argStr: string): Promise { const scope = roomId === DM_SCOPE ? "direct messages" : `#${roomName}`; if (ok) { - const notice = await notifyAccessChange( - ctx, - cleanUser, - roomId, - scope, - "granted", - !!targetUser.roles?.includes("bot"), - ); + const notice = await notifyAccessChange(ctx, cleanUser, roomId, scope, "granted"); return `Granted ${cleanUser} access to ${ctx.account.mentionNames[0] ?? ctx.accountId} in ${scope}.${notice ?? ""}`; } return `That grant already exists.`; @@ -975,14 +993,7 @@ async function runRevoke(ctx: CommandContext, argStr: string): Promise { const scope = roomId === DM_SCOPE ? "direct messages" : `#${roomName}`; if (ok) { - const notice = await notifyAccessChange( - ctx, - cleanUser, - roomId, - scope, - "revoked", - !!targetUser.roles?.includes("bot"), - ); + const notice = await notifyAccessChange(ctx, cleanUser, roomId, scope, "revoked"); return `Revoked ${cleanUser}'s access to ${ctx.account.mentionNames[0] ?? ctx.accountId} in ${scope}.${notice ?? ""}`; } return `No such grant found. ${cleanUser} did not have access in ${scope}.`; @@ -1067,16 +1078,9 @@ async function removeSingleBot( const steps: string[] = []; - try { - removeBindingsForAccount(username); - steps.push("OpenClaw binding removed"); - } catch (e: unknown) { - steps.push(`binding cleanup failed: ${e instanceof Error ? e.message : String(e)}`); - } - try { removeAccount(username); - steps.push("OpenClaw account removed"); + steps.push("OpenClaw account and binding removed"); } catch (e: unknown) { steps.push(`account cleanup failed: ${e instanceof Error ? e.message : String(e)}`); } @@ -1096,7 +1100,7 @@ async function removeSingleBot( if (ownsDedicatedAgent) { try { removeAgentDir(username); - steps.push(`workspace \`rc-${username}\` removed`); + steps.push(`agent \`rc-${username}\` removed`); } catch (e: unknown) { steps.push(`workspace cleanup failed: ${e instanceof Error ? e.message : String(e)}`); } diff --git a/src/service/gateway.ts b/src/service/gateway.ts index 8859abe..f46db0c 100644 --- a/src/service/gateway.ts +++ b/src/service/gateway.ts @@ -11,7 +11,7 @@ import type { RocketChatMessageRecord, } from "../types.js"; import { shouldHandleInboundEvent, matchCommand } from "./channel.js"; -import { readAccount } from "../cli/config-updater.js"; +import { migrateRcWorkspacesIfNeeded, readAccount } from "../cli/config-updater.js"; import { collectBotUserIdsForServer, collectBotUsernamesForServer } from "../cli/config-updater.js"; import { AccessStore } from "../config/access-store.js"; import { appendGroupHistory, getAndClearGroupHistory } from "./group-history.js"; @@ -29,8 +29,8 @@ import type { const MAX_MESSAGE_LENGTH = 4000; const MAX_ATTACHMENTS = 5; -import { activeClients, connectionStatus, type ClientEntry } from "./runtime-state.js"; -export { activeClients, type ClientEntry } from "./runtime-state.js"; +import { activeClients, connectionStatus } from "./runtime-state.js"; +export { activeClients } from "./runtime-state.js"; let nextGeneration = 0; const threadRoots = new Map(); @@ -425,7 +425,7 @@ function stripEmojis(text: string): string { const SEND_RETRY_DELAY_MS = 500; -export async function postMessageWithRetry( +async function postMessageWithRetry( client: Pick, accountId: string, roomId: string, @@ -445,7 +445,7 @@ export async function postMessageWithRetry( } } -export function resolveReplyTmid(params: { +function resolveReplyTmid(params: { roomType: string; tmid?: string | undefined; messageId: string; @@ -550,6 +550,7 @@ async function sendMessageChunks( } export async function startGateway(ctx: GatewayContext): Promise { + migrateRcWorkspacesIfNeeded(); const account = ctx.account ?? resolveAccount(ctx.cfg ?? {}, ctx.accountId); if (!account || !account.enabled) { ctx.setStatus?.("disabled"); diff --git a/src/service/inbound.ts b/src/service/inbound.ts index b89ec35..d4c036b 100644 --- a/src/service/inbound.ts +++ b/src/service/inbound.ts @@ -8,55 +8,9 @@ import type { } from "../types.js"; import type { RocketChatClient } from "../client/rest.js"; import type { GroupHistoryEntry } from "./group-history.js"; -import { parsePluginConfig } from "../config/schema.js"; import { dirname } from "node:path"; -const DEFAULT_OWNER_ONLY_SKILLS = ["email"]; - -function readAccountPolicy( - cfg: OpenClawConfigLike, - accountId: string, -): { owner: string | undefined; ownerOnlySkills: string[] } { - try { - const nested = cfg.channels?.rocketchat; - const parsed = nested - ? parsePluginConfig(nested as never) - : cfg && typeof cfg === "object" && "accounts" in cfg - ? parsePluginConfig(cfg as never) - : { accounts: {} }; - const account = parsed.accounts[accountId]; - return { - owner: account?.owner, - ownerOnlySkills: - account?.ownerOnlySkills && account.ownerOnlySkills.length > 0 - ? account.ownerOnlySkills - : DEFAULT_OWNER_ONLY_SKILLS, - }; - } catch { - return { owner: undefined, ownerOnlySkills: [] }; - } -} -function normalizeName(name: string): string { - return name.trim().replace(/^@+/, "").toLowerCase(); -} - -function buildOwnerOnlyGuardrail( - senderName: string, - owner: string | undefined, - ownerOnlySkills: string[], -): string { - if (!owner || ownerOnlySkills.length === 0) return ""; - if (normalizeName(senderName) === normalizeName(owner)) return ""; - return [ - ``, - `[SECURITY POLICY]`, - `The sender (@${senderName}) is NOT the bot owner (@${owner}).`, - `You MUST refuse any request that uses the following owner-only skills: ${ownerOnlySkills.join(", ")}.`, - `If the user asks for any of these, politely decline and explain that only @${owner} can do it.`, - `[/SECURITY POLICY]`, - ].join("\n"); -} export async function dispatchInboundEventWithChannelRuntime(params: { cfg: OpenClawConfigLike; @@ -103,23 +57,19 @@ export async function dispatchInboundEventWithChannelRuntime(params: { const bodyForAgent = buildBodyForAgent(params.event, params.groupHistory); - const { owner, ownerOnlySkills } = readAccountPolicy(params.cfg, params.accountId); - const guardrail = buildOwnerOnlyGuardrail(params.event.senderName, owner, ownerOnlySkills); - const bodyForAgentWithGuardrail = guardrail ? `${bodyForAgent}\n\n${guardrail}` : bodyForAgent; - const body = params.channelRuntime.reply.formatAgentEnvelope({ channel: "Rocket.Chat", from: buildConversationLabel(params.event), timestamp, previousTimestamp, envelope: envelopeOptions, - body: bodyForAgentWithGuardrail, + body: bodyForAgent, }); const isCommand = params.event.text.startsWith("/"); const ctxPayload = params.channelRuntime.reply.finalizeInboundContext({ Body: body, - BodyForAgent: bodyForAgentWithGuardrail, + BodyForAgent: bodyForAgent, RawBody: params.event.text, CommandBody: params.event.text, From: buildSenderAddress(params.event), diff --git a/src/service/skill-commands.ts b/src/service/skill-commands.ts index fd6f197..d55399d 100644 --- a/src/service/skill-commands.ts +++ b/src/service/skill-commands.ts @@ -4,8 +4,8 @@ import type { CommandContext } from "./channel.js"; const execFileAsync = promisify(execFile); -export const CRON_HEADING = "**Cron jobs**"; -export const CRON_USAGE = [ +const CRON_HEADING = "**Cron jobs**"; +const CRON_USAGE = [ "• `!cron ` one-shot reminder (30s | 5m | 2h | 1d)", "• `!cron --every ` repeat every interval until stopped", "• `!cron list` list running jobs", @@ -36,7 +36,7 @@ const UNIT_MAP: Record = { days: "d", }; -export function parseInterval( +function parseInterval( input: string, ): { ok: true; seconds: number; at: string } | { ok: false; error: string } { const match = input.trim().match(INTERVAL_RE); @@ -53,7 +53,7 @@ export function parseInterval( return { ok: true, seconds, at: `+${match[1]}${unit}` }; } -export function deriveName(task: string): string { +function deriveName(task: string): string { const slug = task .replace(/\s+/g, " ") .trim() @@ -143,11 +143,24 @@ function parseCronArgs(trimmed: string): CronSubcommand | { ok: false; error: st return { type: "schedule", every: false, intervalInput, interval, task }; } +function parseCronJobsResponse(stdout: string): Array> { + if (!stdout || !stdout.trim()) return []; + try { + const parsed = JSON.parse(stdout); + if (Array.isArray(parsed)) return parsed; + if (parsed && typeof parsed === "object" && Array.isArray((parsed as Record).jobs)) { + return (parsed as Record).jobs as Array>; + } + } catch { + /* best effort */ + } + return []; +} + async function cronList(ctx: CommandContext): Promise { try { const res = await runOpenClaw(["cron", "list", "--agent", `rc-${ctx.accountId}`, "--json"]); - const parsed = JSON.parse(res.stdout || "{}") as { jobs?: Array> }; - const jobs = parsed.jobs ?? []; + const jobs = parseCronJobsResponse(res.stdout); if (jobs.length === 0) { return "No cron jobs for this bot."; } @@ -155,7 +168,8 @@ async function cronList(ctx: CommandContext): Promise { for (const job of jobs) { const name = String(job.name ?? job.id ?? "unknown"); const schedule = job.schedule as - { kind?: string; everyMs?: number; at?: string; cron?: string } | undefined; + | { kind?: string; everyMs?: number; at?: string; cron?: string } + | undefined; const everyMs = schedule?.everyMs; const scheduleDesc = schedule?.kind === "every" @@ -182,20 +196,21 @@ async function cronList(ctx: CommandContext): Promise { async function cronStop(ctx: CommandContext, name: string): Promise { try { const listRes = await runOpenClaw(["cron", "list", "--agent", `rc-${ctx.accountId}`, "--json"]); - const parsed = JSON.parse(listRes.stdout || "{}") as { - jobs?: Array>; - }; - const jobs = parsed.jobs ?? []; + const jobs = parseCronJobsResponse(listRes.stdout); const target = jobs.find((j) => { - const jobName = String(j.name ?? ""); - return jobName.toLowerCase() === name.trim().toLowerCase(); + const jobName = String(j.name ?? j.id ?? ""); + const search = name.trim().toLowerCase(); + return ( + jobName.toLowerCase() === search || + String(j.id ?? "").toLowerCase() === search + ); }); if (!target) { - return `No repeating job named \`${name}\` found for this bot. Use \`!cron list\` to see jobs.`; + return `No job named or matching ID \`${name}\` found for this bot. Use \`!cron list\` to see jobs.`; } const id = String(target.id ?? ""); await runOpenClaw(["cron", "rm", id, "--json"]); - return `Stopped cron job \`${id}\` (\`${String(target.name ?? "")}\`).`; + return `Stopped cron job \`${id}\` (\`${String(target.name ?? id)}\`).`; } catch (e) { const error = e as { stdout?: string; stderr?: string; message?: string }; return [ diff --git a/src/types.ts b/src/types.ts index bd30b23..46f4abe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import type { PluginAccountConfig } from "./config/schema.js"; -export type { PluginConfig, PluginAccountConfig } from "./config/schema.js"; +export type { PluginAccountConfig } from "./config/schema.js"; export type InboundAttachmentKind = "image" | "audio" | "document" | "video" | "unknown"; @@ -96,19 +96,19 @@ export type InboundEvent = { export type OpenClawConfigLike = OpenClawConfig; -export type RoutePeer = { +type RoutePeer = { kind: InboundEvent["roomType"]; id: string; }; -export type ResolvedAgentRoute = { +type ResolvedAgentRoute = { agentId: string; sessionKey: string; accountId?: string; mainSessionKey?: string; }; -export type FinalizedContext = Record & { +type FinalizedContext = Record & { SessionKey?: string; }; From 69b4c3896aad8b3cd2a775c2c8cd869a4709212a Mon Sep 17 00:00:00 2001 From: dodaa08 Date: Mon, 14 Sep 2026 13:37:37 +0530 Subject: [PATCH 6/7] Added updated docs and test for bot's post message, fetching rooms and mention stripping --- .github/workflows/ci.yml | 4 + .gitignore | 5 +- README.md | 14 + docs/COMMANDS.md | 6 + docs/SETUP.md | 16 +- openclaw.examples.json | 433 ++--------------- package.json | 6 +- pnpm-lock.yaml | 856 +++++++++++++++++++++++++++++++++ src/service/inbound.ts | 8 +- tests/integration/rest.test.ts | 51 ++ vitest.config.ts | 31 ++ 11 files changed, 1013 insertions(+), 417 deletions(-) create mode 100644 tests/integration/rest.test.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3bde2e..352ed83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,10 @@ jobs: - run: pnpm run test name: Run tests + env: + ROCKETCHAT_URL: ${{ secrets.ROCKETCHAT_URL }} + ROCKETCHAT_USER_ID: ${{ secrets.ROCKETCHAT_USER_ID }} + ROCKETCHAT_TOKEN: ${{ secrets.ROCKETCHAT_TOKEN }} - run: pnpm dlx clawhub package publish . --dry-run --json --owner @dodaa08 name: Validate ClawHub publish diff --git a/.gitignore b/.gitignore index 8b8fd54..83a5dce 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,4 @@ package-lock.json .vscode node_modules dist -.pnpm-store/ -ROADMAP.md -preview.md -revert.txt +.pnpm-store/ \ No newline at end of file diff --git a/README.md b/README.md index fb2e58b..40fd1d4 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,17 @@ You should see `gateway - online` and `runtime - ready`. | [ARCHITECTURE.md](https://github.com/RocketChat/OpenClaw.Plugin/blob/main/docs/ARCHITECTURE.md) | How the plugin works (DDP + REST, multi-bot, security) | | [COMMANDS.md](https://github.com/RocketChat/OpenClaw.Plugin/blob/main/docs/COMMANDS.md) | Complete command reference | | [CONTRIBUTING.md](https://github.com/RocketChat/OpenClaw.Plugin/blob/main/CONTRIBUTING.md) | Contributors guide | + +## Media Storage & Handling +When users send media (images, audio, etc.) in Rocket.Chat, the plugin downloads the files locally to `~/.openclaw/media/inbound/`. +- **Why locally?** This allows the OpenClaw agent to reliably process the actual file bytes from the filesystem rather than struggling with URL authentication or timeouts. +- **Limits**: The plugin currently caps downloads at **20MB** per file and supports `image/`, `audio/`, `video/`, and `application/` MIME types. +- **Cleanup**: Currently, there is no automatic auto-prune for these files. We recommend users set up a cron job to clean up the folder periodically, e.g.: `find ~/.openclaw/media/inbound -type f -mtime +7 -delete`. + +## Roadmap / Leftovers +*Future enhancements currently being tracked:* +- [ ] Expanding End-to-End (E2E) and integration test coverage across the repository. +- [ ] Preparing project for official v1 release. +- [ ] Addressing remaining bugs and structural updates from our internal trackers: + - [Notion Bug Tracker](https://deserted-education-78a.notion.site/Bugs-to-solve-3cf53cee1e07801b8a25d518f956af23) + - [GSOC Submission Gist](https://gist.github.com/dodaa08/883e8d7d5e2e2d17dd345dfafe918eb6) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index ea584fa..f2668b8 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -97,6 +97,12 @@ Control how the agent responds. | `!tools` | List tools available to the agent | | `!skills` | List installed skills (use via inbound chat) | +### Owner-Only Skills +Certain powerful skills (e.g. bash execution, file manipulation) are restricted strictly to the **Owner** of the bot for security reasons. +- You must be listed in `openclaw.json` under `accounts..owner` (e.g., `"owner": "admin-user"`). +- To use an owner-only skill, simply instruct the bot in your DM or a private channel where the bot is present. +- Non-owner users who try to invoke owner-only skills will receive an unauthorized error from the bot. + ## Cron Jobs Schedule one-shot reminders or repeating tasks. diff --git a/docs/SETUP.md b/docs/SETUP.md index 37e271d..d9729c9 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -96,25 +96,29 @@ Use the command (owner-only): !remove-bot ``` -This deletes: +This deletes the bot from the server and cleans up its local files automatically. -- Bot user from Rocket.Chat server -- Bot config + credentials -- Agent workspace +If you need to manually delete a bot's files (for example, if the command fails), you must remove these specific files and directories: + +1. **Credentials**: `~/.openclaw/credentials/rocketchat/bot-.json` +2. **Database**: `~/.openclaw/rocketchat/.db` +3. **Workspace/Memory**: `~/.openclaw/workspace/rc-/` (if using the default rc- agent) +4. **Agent Config**: `~/.openclaw/agents/rc-/` ## Clean Everything Up -To completely remove the plugin from your machine so it doesn't take space in your machine: +To completely remove the plugin's data and cache from your machine so it doesn't take space: ```bash # Delete all Rocket.Chat plugin data rm -rf ~/.openclaw/credentials/rocketchat/ rm -rf ~/.openclaw/rocketchat/ rm -rf ~/.openclaw/agents/rc-*/ +rm -rf ~/.openclaw/workspace/rc-*/ rm -rf ~/.openclaw/media/inbound/ ``` -**Important:** This does **not** delete bot users from your Rocket.Chat server. Use `!remove-bot` first, or manually delete them via Rocket.Chat admin panel. +**Important:** This does **not** delete bot users from your Rocket.Chat server. Use `!remove-bot` first, or manually delete them via the Rocket.Chat admin panel. ## Agent Workspaces diff --git a/openclaw.examples.json b/openclaw.examples.json index 107d092..5457507 100644 --- a/openclaw.examples.json +++ b/openclaw.examples.json @@ -1,419 +1,46 @@ { "meta": { - "lastTouchedVersion": "2026.7.1-2", - "lastTouchedAt": "2026-09-02T12:39:44.692Z" + "lastTouchedVersion": "2026.7.1-2" }, - "wizard": { - "lastRunAt": "2026-09-02T12:39:44.660Z", - "lastRunVersion": "2026.7.1-2", - "lastRunCommand": "onboard", - "lastRunMode": "local", - "securityAcknowledgedAt": "2026-09-02T12:38:15.663Z" - }, - "models": { - "mode": "merge", - "providers": { - "nvidia-nim": { - "baseUrl": "http://127.0.0.1:8082", - "apiKey": "YOUR_NIM_API_KEY", - "api": "anthropic-messages", - "timeoutSeconds": 600, - "models": [ - { - "id": "anthropic/nvidia_nim/nvidia/nemotron-3-super-120b-a12b", - "name": "Nemotron 3 Super 120B (NVIDIA NIM)", - "contextWindow": 1048576, - "maxTokens": 32000 - }, - { - "id": "anthropic/nvidia_nim/meta/llama-3.1-70b-instruct", - "name": "Llama 3.1 70B Instruct (NVIDIA NIM)", - "contextWindow": 131072, - "maxTokens": 32000 - }, - { - "id": "anthropic/nvidia_nim/meta/llama-3.2-11b-vision-instruct", - "name": "Llama 3.2 11B Vision (NVIDIA NIM)", - "contextWindow": 131072, - "maxTokens": 32000, - "input": ["text", "image"] - }, - { - "id": "anthropic/nvidia_nim/meta/llama-3.2-90b-vision-instruct", - "name": "Llama 3.2 90B Vision (NVIDIA NIM)", - "contextWindow": 131072, - "maxTokens": 32000, - "input": ["text", "image"] - } - ] - }, - "anthropic": { - "baseUrl": "http://127.0.0.1:8082", - "apiKey": "YOUR_ANTHROPIC_API_KEY", - "api": "anthropic-messages", - "timeoutSeconds": 600, - "models": [ - { - "id": "nvidia_nim/meta/llama-3.2-11b-vision-instruct", - "name": "Llama 3.2 11B Vision (NVIDIA NIM)", - "contextWindow": 131072, - "maxTokens": 32000, - "input": ["text", "image"] - }, - { - "id": "nvidia_nim/meta/llama-3.2-90b-vision-instruct", - "name": "Llama 3.2 90B Vision (NVIDIA NIM)", - "contextWindow": 131072, - "maxTokens": 32000, - "input": ["text", "image"] - }, - { - "id": "nvidia_nim/meta/llama-3.1-70b-instruct", - "name": "Llama 3.1 70B Instruct (NVIDIA NIM)", - "contextWindow": 131072, - "maxTokens": 32000, - "input": ["text"] - } - ] - }, - "nvidia-direct": { - "baseUrl": "https://integrate.api.nvidia.com/v1", - "apiKey": "YOUR_NVIDIA_API_KEY", - "api": "openai-completions", - "models": [ - { - "id": "nvidia/nemotron-3.5-lightning-30b-a3b", - "name": "Nemotron 3.5 Lightning 30B (fast)", - "contextWindow": 131072, - "maxTokens": 32768 - }, - { - "id": "nv-mistralai/mistral-nemo-12b-instruct", - "name": "Mistral Nemo 12B", - "contextWindow": 131072, - "maxTokens": 32768 - }, - { - "id": "mistralai/mistral-large", - "name": "Mistral Large", - "contextWindow": 32768, - "maxTokens": 32768 - }, - { - "id": "nvidia/llama-3.1-nemotron-70b-instruct", - "name": "Nemotron 70B Instruct", - "contextWindow": 131072, - "maxTokens": 32768 - }, - { - "id": "meta/llama-3.2-11b-vision-instruct", - "name": "Llama 3.2 11B Vision", - "contextWindow": 131072, - "maxTokens": 32768 - }, - { - "id": "nvidia/nemotron-3-ultra-550b-a55b", - "name": "Nemotron 3 Ultra 550B-A55B", - "contextWindow": 262144, - "maxTokens": 32768 - }, - { - "id": "deepseek-ai/deepseek-v4-flash-0731", - "name": "DeepSeek V4 Flash", - "contextWindow": 131072, - "maxTokens": 32768 - } - ] - }, - "openrouter": { - "baseUrl": "https://openrouter.ai/api/v1", - "apiKey": "YOUR_OPENROUTER_API_KEY", - "api": "openai-completions", - "models": [ - { - "id": "nvidia/nemotron-3-super-120b-a12b:free", - "name": "Nemotron 3 Super (OpenRouter Free)", - "contextWindow": 1048576, - "maxTokens": 32000 - }, - { - "id": "nvidia/nemotron-3-ultra-550b-a55b:free", - "name": "Nemotron 3 Ultra (OpenRouter Free)", - "contextWindow": 1048576, - "maxTokens": 32000, - "reasoning": true - }, - { - "id": "meta-llama/llama-3.3-70b:free", - "name": "Llama 3.3 70B (OpenRouter Free)", - "contextWindow": 131072, - "maxTokens": 32000 - }, - { - "id": "openrouter/free", - "name": "OpenRouter Auto Free", - "contextWindow": 131072, - "maxTokens": 32000 - }, - { - "id": "google/gemini-2.0-flash-thinking-exp:free", - "name": "Gemini 2.0 Flash Thinking (OpenRouter Free)", - "contextWindow": 1048576, - "maxTokens": 32000, - "reasoning": true, - "input": ["text", "image"] - }, - { - "id": "meta-llama/llama-3.2-11b-vision-instruct:free", - "name": "Llama 3.2 11B Vision (OpenRouter Free)", - "contextWindow": 131072, - "maxTokens": 32000, - "input": ["text", "image"] - } - ] - }, - "ollama": { - "baseUrl": "http://127.0.0.1:11434", - "apiKey": "ollama", - "api": "ollama", - "models": [ - { - "id": "llama3.2:3b", - "name": "Llama 3.2 3B (Ollama)", - "contextWindow": 16000, - "maxTokens": 32000, - "reasoning": false, - "input": ["text"], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "api": "ollama", - "params": { - "num_ctx": 16000 - } - }, - { - "id": "qwen2.5:3b", - "name": "qwen2.5:3b (Ollama)", - "contextWindow": 16000, - "maxTokens": 32000, - "reasoning": false, - "input": ["text"], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "api": "ollama", - "params": { - "num_ctx": 16000 - } - }, - { - "id": "mistral:7b", - "name": "Mistral 7B (Ollama)", - "contextWindow": 32000, - "maxTokens": 32000, - "reasoning": false, - "input": ["text"], - "cost": { - "input": 0, - "output": 0, - "cacheRead": 0, - "cacheWrite": 0 - }, - "api": "ollama", - "params": { - "num_ctx": 32000 - } - } - ] - } - } - }, - "agents": { - "defaults": { - "model": { - "primary": "nvidia-direct/nvidia/nemotron-3.5-lightning-30b-a3b" - }, - "models": { - "nvidia-nim/anthropic/nvidia_nim/nvidia/nemotron-3-super-120b-a12b": {}, - "nvidia-direct/nvidia/nemotron-3-ultra-550b-a55b": {}, - "nvidia-nim/anthropic/nvidia_nim/meta/llama-3.2-90b-vision-instruct": {}, - "nvidia-nim/anthropic/nvidia_nim/meta/llama-3.2-11b-vision-instruct": {}, - "nvidia-direct/meta/llama-3.1-70b-instruct": {}, - "nvidia-direct/meta/llama-3.2-90b-vision-instruct": {}, - "openrouter/nvidia/nemotron-3-super-120b-a12b:free": {}, - "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free": {}, - "openrouter/meta-llama/llama-3.3-70b:free": {}, - "openrouter/free": {}, - "openrouter/google/gemini-2.0-flash-thinking-exp:free": {}, - "openrouter/meta-llama/llama-3.2-11b-vision-instruct:free": {}, - "ollama/llama3.2:3b": {}, - "ollama/qwen2.5:3b": {}, - "ollama/mistral:7b": {}, - "nvidia-direct/nvidia/nemotron-3.5-lightning-30b-a3b": {} - }, - "workspace": "/home/USERNAME/.openclaw/workspace", - "memorySearch": { - "enabled": true - }, - "compaction": { - "mode": "safeguard" - }, - "timeoutSeconds": 300, - "maxConcurrent": 4, - "subagents": { - "maxConcurrent": 8 - }, - "imageModel": "nvidia-nim/anthropic/nvidia_nim/meta/llama-3.2-11b-vision-instruct" - } - }, - "tools": { - "profile": "full", - "web": { - "search": { - "enabled": false - }, - "fetch": { - "enabled": false - } - }, - "exec": { - "host": "gateway", - "mode": "full" - }, - "media": { - "audio": { - "enabled": true, - "models": [ - { - "type": "cli", - "command": "whisper-cli", - "args": [ - "-m", - "/home/USERNAME/.local/share/whisper-cpp/ggml-base.en.bin", - "--no-timestamps", - "-otxt", - "-of", - "{{OutputBase}}", - "{{MediaPath}}" - ], - "timeoutSeconds": 60 - } - ] - }, - "video": { - "enabled": false - } - }, - "deny": [] - }, - "commands": { - "native": true, - "nativeSkills": false, - "restart": true, - "ownerDisplay": "raw", - "ownerAllowFrom": ["rocketchat:CHANNEL_ID"] - }, - "session": { - "dmScope": "per-channel-peer", - "maintenance": { - "pruneAfter": "7d", - "maxEntries": 200 - } - }, - "messages": { - "groupChat": { - "historyLimit": 20 - } - }, - "hooks": { - "enabled": true, - "token": "YOUR_HOOK_TOKEN", - "allowRequestSessionKey": false, - "allowedSessionKeyPrefixes": ["hook:"], - "internal": { - "enabled": true, - "entries": { - "session-memory": { - "enabled": true + "channels": { + "rocketchat": { + "accounts": { + "botname": { + "enabled": true, + "serverUrl": "http://localhost:3000", + "auth": { + "mode": "token", + "userId": "YOUR_ROCKETCHAT_BOT_USER_ID", + "accessToken": "YOUR_ROCKETCHAT_BOT_ACCESS_TOKEN" + }, + "transport": { + "mode": "websocket" + }, + "mentionNames": [ + "botname" + ], + "owner": "admin-user" } } } }, - "gateway": { - "port": 18789, - "mode": "local", - "bind": "loopback", - "auth": { - "mode": "token", - "token": "YOUR_GATEWAY_AUTH_TOKEN" - }, - "tailscale": { - "mode": "off", - "resetOnExit": false - }, - "controlUi": { - "allowInsecureAuth": true + "bindings": [ + { + "agentId": "main", + "match": { + "channel": "rocketchat", + "accountId": "botname" + } } - }, + ], "plugins": { - "allow": ["memory-core", "document-extract", "anthropic", "rocketchat"], - "bundledDiscovery": "allowlist", + "allow": [ + "rocketchat" + ], "entries": { - "document-extract": { - "enabled": true - }, - "anthropic": { - "enabled": true - }, "rocketchat": { "enabled": true } - }, - "load": { - "paths": ["/home/USERNAME/personal/GSOC_project/Openclaw"] - } - }, - "skills": { - "entries": { - "himalaya": { - "enabled": true - }, - "github": { - "enabled": true - }, - "cron": { - "enabled": true - }, - - } - }, - "bindings": [], - "mcp": { - "servers": { - "excalidraw": { - "url": "https://mcp.excalidraw.com/mcp", - "transport": "streamable-http", - "connect_timeout": 30000, - "requestTimeoutMs": 180000, - "timeout": 180000 - } - } - }, - "cron": { - "triggers": { - "enabled": true - } - }, - "channels": { - "rocketchat": { - "accounts": {} } } } diff --git a/package.json b/package.json index 9cb368d..c9f5ca6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "format:check": "prettier --check .", "start": "node dist/index.js", "setup": "openclaw rocketchat setup", - "test": "echo '✅ No tests yet'" + "test": "vitest run", + "test:integration": "vitest run tests/integration" }, "repository": { "type": "git", @@ -63,7 +64,8 @@ "openclaw": "^2026.7.1-2", "prettier": "^3.9.6", "tsx": "^4.22.4", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^2.0.0" }, "dependencies": { "@clack/core": "1.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72820ef..dc18eae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@25.9.5) packages: @@ -84,102 +87,204 @@ packages: resolution: {integrity: sha512-fS6OEQKEEALnKa6Uw8LcgZZ+9CWck7f3MQSCETQp6leUgIFwMEDtKmOUnL9nsYm+RIPmy7OmplVxYRbV6hiaFg==} engines: {node: '>=22.19.0'} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.28.2': resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.28.2': resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.28.2': resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.28.2': resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.28.2': resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.28.2': resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.28.2': resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.28.2': resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.28.2': resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.28.2': resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.28.2': resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.28.2': resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.28.2': resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.28.2': resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} @@ -192,6 +297,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} @@ -204,6 +315,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} @@ -216,24 +333,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.28.2': resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.28.2': resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.28.2': resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.28.2': resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} @@ -278,6 +419,9 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@koromix/koffi-darwin-arm64@3.1.6': resolution: {integrity: sha512-8FHyXGCZN7/iQf4f7W5BRysmtdlAFvSx6FpmX4u6wmkZiX/2e9hIRdGLiZYlHGudlcA18UmXB/cMiyhJ7fJkzA==} cpu: [arm64] @@ -417,6 +561,13 @@ packages: resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} engines: {node: '>=14.0.0'} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + '@openclaw/ai@2026.9.2': resolution: {integrity: sha512-VsRzawylkkKTvzKgVM3XrRSe6LVqKM2t8M25TfiK114MB3lRRDqVwE8eGZ4mF+w3IKPwPDqvpzir0ipiP0EVCQ==} engines: {node: '>=22.19.0'} @@ -540,6 +691,144 @@ packages: peerDependencies: '@rocket.chat/icons': '*' + '@rollup/rollup-android-arm-eabi@4.63.2': + resolution: {integrity: sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.2': + resolution: {integrity: sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.2': + resolution: {integrity: sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.2': + resolution: {integrity: sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.2': + resolution: {integrity: sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.2': + resolution: {integrity: sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.2': + resolution: {integrity: sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.2': + resolution: {integrity: sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.2': + resolution: {integrity: sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.2': + resolution: {integrity: sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.2': + resolution: {integrity: sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.2': + resolution: {integrity: sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.2': + resolution: {integrity: sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.2': + resolution: {integrity: sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.2': + resolution: {integrity: sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.2': + resolution: {integrity: sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.2': + resolution: {integrity: sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.2': + resolution: {integrity: sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.2': + resolution: {integrity: sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.2': + resolution: {integrity: sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.2': + resolution: {integrity: sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.2': + resolution: {integrity: sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.2': + resolution: {integrity: sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.2': + resolution: {integrity: sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.2': + resolution: {integrity: sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==} + cpu: [x64] + os: [win32] + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -595,6 +884,9 @@ packages: '@trycua/cua-driver@0.22.0': resolution: {integrity: sha512-NElsgryvNTl7arPdx7trvYhz4geMhPztsi8RFeECY2XhuuhWWzSPPPHTC4cBVsb/gprqqPtb+Hy2I1kGGEGddg==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@25.9.5': resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} @@ -651,6 +943,35 @@ packages: '@ubjs/node@0.31.0-3': resolution: {integrity: sha512-qNMpi2LICNwxGXZyRF8fSDBSpbezyZbEsydrbiMPJOmtOWr4tmZIEl7jkWGHVGShoBvHfFo4eHp5B4UVP928Cg==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -690,6 +1011,10 @@ packages: asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -728,6 +1053,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -740,10 +1069,18 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@6.0.0: resolution: {integrity: sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==} engines: {node: '>=22'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -842,6 +1179,10 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -923,10 +1264,18 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -939,6 +1288,9 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -959,6 +1311,10 @@ packages: resolution: {integrity: sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==} engines: {node: '>=22'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.7.0: resolution: {integrity: sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==} engines: {node: '>= 16'} @@ -1234,10 +1590,16 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@18.0.5: resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} engines: {node: '>= 20'} @@ -1284,6 +1646,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@1.1.0: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} @@ -1420,6 +1787,13 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1436,6 +1810,10 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + prettier@3.9.6: resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} @@ -1506,6 +1884,11 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + rollup@4.63.2: + resolution: {integrity: sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -1565,6 +1948,9 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -1572,6 +1958,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -1615,6 +2005,9 @@ packages: sqlite-vec@0.1.9: resolution: {integrity: sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.1.1: resolution: {integrity: sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==} @@ -1622,6 +2015,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -1649,6 +2045,24 @@ packages: resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} @@ -1731,6 +2145,67 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + web-push@3.6.7: resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} engines: {node: '>= 16'} @@ -1762,6 +2237,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -1873,81 +2353,150 @@ snapshots: get-east-asian-width: 1.6.0 marked: 18.0.5 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.28.2': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.28.2': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.28.2': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.28.2': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.28.2': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.28.2': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.28.2': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.28.2': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.28.2': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.28.2': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.28.2': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.28.2': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.28.2': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.28.2': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.28.2': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.28.2': optional: true '@esbuild/netbsd-arm64@0.28.2': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.28.2': optional: true '@esbuild/openbsd-arm64@0.28.2': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.28.2': optional: true '@esbuild/openharmony-arm64@0.28.2': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.28.2': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.28.2': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.28.2': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.28.2': optional: true @@ -1993,6 +2542,8 @@ snapshots: dependencies: minipass: 7.1.3 + '@jridgewell/sourcemap-codec@1.6.0': {} + '@koromix/koffi-darwin-arm64@3.1.6': optional: true @@ -2099,6 +2650,9 @@ snapshots: '@mozilla/readability@0.6.0': {} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@openclaw/ai@2026.9.2(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.120.0(zod@4.4.3) @@ -2238,6 +2792,81 @@ snapshots: '@rocket.chat/icons': 0.49.0 typia: link:patch:typia@npm%3A9.7.2#~/.yarn/patches/typia-npm-9.7.2-5c5d9c80b4.patch + '@rollup/rollup-android-arm-eabi@4.63.2': + optional: true + + '@rollup/rollup-android-arm64@4.63.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.2': + optional: true + + '@rollup/rollup-darwin-x64@4.63.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.2': + optional: true + '@sec-ant/readable-stream@0.4.1': {} '@silvia-odwyer/photon-node@0.3.4': {} @@ -2285,6 +2914,8 @@ snapshots: '@trycua/cua-driver-win32-arm64-msvc': 0.22.0 '@trycua/cua-driver-win32-x64-msvc': 0.22.0 + '@types/estree@1.0.9': {} + '@types/node@25.9.5': dependencies: undici-types: 7.24.6 @@ -2328,6 +2959,46 @@ snapshots: '@ubjs/node-win32-arm64-msvc': 0.31.0-3 '@ubjs/node-win32-x64-msvc': 0.31.0-3 + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@25.9.5))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@25.9.5) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -2365,6 +3036,8 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 + assertion-error@2.0.1: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} @@ -2401,6 +3074,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2413,8 +3088,18 @@ snapshots: camelcase@5.3.1: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@6.0.0: {} + check-error@2.1.3: {} + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -2490,6 +3175,8 @@ snapshots: decode-uri-component@0.2.2: {} + deep-eql@5.0.2: {} + depd@2.0.0: {} diff@9.0.0: {} @@ -2560,10 +3247,38 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -2597,6 +3312,10 @@ snapshots: escape-html@1.0.3: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + etag@1.8.1: {} event-target-shim@5.0.1: {} @@ -2622,6 +3341,8 @@ snapshots: which-command: 0.1.0 yoctocolors: 2.2.0 + expect-type@1.4.0: {} + express-rate-limit@8.7.0(express@5.2.1): dependencies: debug: 4.4.3 @@ -2947,8 +3668,14 @@ snapshots: long@5.3.2: {} + loupe@3.2.1: {} + lru-cache@11.5.2: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + marked@18.0.5: {} math-intrinsics@1.1.0: {} @@ -2979,6 +3706,8 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.19: {} + negotiator@1.1.0: dependencies: content-type: 2.1.0 @@ -3157,6 +3886,10 @@ snapshots: path-to-regexp@8.4.2: {} + pathe@1.1.2: {} + + pathval@2.0.1: {} + picocolors@1.1.1: {} pkce-challenge@5.0.1: {} @@ -3165,6 +3898,12 @@ snapshots: pngjs@5.0.0: {} + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@3.9.6: {} pretty-ms@9.3.0: @@ -3245,6 +3984,38 @@ snapshots: retry@0.13.1: {} + rollup@4.63.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.2 + '@rollup/rollup-android-arm64': 4.63.2 + '@rollup/rollup-darwin-arm64': 4.63.2 + '@rollup/rollup-darwin-x64': 4.63.2 + '@rollup/rollup-freebsd-arm64': 4.63.2 + '@rollup/rollup-freebsd-x64': 4.63.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.2 + '@rollup/rollup-linux-arm-musleabihf': 4.63.2 + '@rollup/rollup-linux-arm64-gnu': 4.63.2 + '@rollup/rollup-linux-arm64-musl': 4.63.2 + '@rollup/rollup-linux-loong64-gnu': 4.63.2 + '@rollup/rollup-linux-loong64-musl': 4.63.2 + '@rollup/rollup-linux-ppc64-gnu': 4.63.2 + '@rollup/rollup-linux-ppc64-musl': 4.63.2 + '@rollup/rollup-linux-riscv64-gnu': 4.63.2 + '@rollup/rollup-linux-riscv64-musl': 4.63.2 + '@rollup/rollup-linux-s390x-gnu': 4.63.2 + '@rollup/rollup-linux-x64-gnu': 4.63.2 + '@rollup/rollup-linux-x64-musl': 4.63.2 + '@rollup/rollup-openbsd-x64': 4.63.2 + '@rollup/rollup-openharmony-arm64': 4.63.2 + '@rollup/rollup-win32-arm64-msvc': 4.63.2 + '@rollup/rollup-win32-ia32-msvc': 4.63.2 + '@rollup/rollup-win32-x64-gnu': 4.63.2 + '@rollup/rollup-win32-x64-msvc': 4.63.2 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -3328,10 +4099,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} sisteransi@1.0.5: {} + source-map-js@1.2.1: {} + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -3367,6 +4142,8 @@ snapshots: sqlite-vec-windows-x64: 0.1.9 optional: true + stackback@0.0.2: {} + standardwebhooks@1.1.1: dependencies: '@stablelib/base64': 1.0.1 @@ -3374,6 +4151,8 @@ snapshots: statuses@2.0.2: {} + std-env@3.10.0: {} + strict-uri-encode@2.0.0: {} string-width@4.2.3: @@ -3404,6 +4183,16 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + tldts-core@6.1.86: {} tldts@6.1.86: @@ -3463,6 +4252,68 @@ snapshots: vary@1.1.2: {} + vite-node@2.1.9(@types/node@25.9.5): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@25.9.5) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@25.9.5): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.28 + rollup: 4.63.2 + optionalDependencies: + '@types/node': 25.9.5 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@25.9.5): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@25.9.5)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@25.9.5) + vite-node: 2.1.9(@types/node@25.9.5) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.5 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + web-push@3.6.7: dependencies: asn1.js: 5.4.1 @@ -3492,6 +4343,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/src/service/inbound.ts b/src/service/inbound.ts index d4c036b..02b6553 100644 --- a/src/service/inbound.ts +++ b/src/service/inbound.ts @@ -91,7 +91,7 @@ export async function dispatchInboundEventWithChannelRuntime(params: { OriginatingChannel: "rocketchat", OriginatingTo: to, ...(isCommand ? { CommandSource: "text" as const, CommandAuthorized: true } : {}), - ...(await buildMediaContext(params.event.attachments, params.client)), + ...(await buildMediaContext(params.event.attachments, params.event.roomId, params.client)), }); await params.channelRuntime.session.recordInboundSession({ @@ -163,6 +163,7 @@ function buildRecipientAddress(event: InboundEvent): string { async function buildMediaContext( attachments: InboundAttachment[], + roomId: string, client?: RocketChatClient, ): Promise> { if (attachments.length === 0) return {}; @@ -176,7 +177,10 @@ async function buildMediaContext( attachment.fileName ? { fileName: attachment.fileName } : undefined, ); return { kind: "path" as const, value: filePath, mimeType: attachment.mimeType }; - } catch { + } catch (error: any) { + if (client && roomId) { + client.postMessage(roomId, `⚠️ ${error.message || "Failed to download attachment."}`).catch(() => {}); + } return null; } } diff --git a/tests/integration/rest.test.ts b/tests/integration/rest.test.ts new file mode 100644 index 0000000..1b49227 --- /dev/null +++ b/tests/integration/rest.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { RocketChatClient } from "../../src/client/rest.js"; + +const SERVER_URL = process.env.ROCKETCHAT_URL; +const USER_ID = process.env.ROCKETCHAT_USER_ID; +const TOKEN = process.env.ROCKETCHAT_TOKEN; + +const skip = !SERVER_URL || !USER_ID || !TOKEN; + +if (skip) { + console.warn( + "[integration] Skipping – set ROCKETCHAT_URL, ROCKETCHAT_USER_ID, and ROCKETCHAT_TOKEN to run.", + ); +} + +describe.skipIf(skip)("RocketChatClient – Integration", () => { + let client: RocketChatClient; + + beforeAll(() => { + client = new RocketChatClient({ + serverUrl: SERVER_URL!, + auth: { mode: "token", userId: USER_ID!, accessToken: TOKEN! }, + }); + }); + + it("should post a message (tests mention-stripping + markdown fallback logic)", async () => { + const subs = await client.listSubscriptions(null); + expect(subs.length).toBeGreaterThan(0); + + const dmRoom = subs.find((s) => s.t === "d") ?? subs[0]!; + const messageId = await client.postMessage(dmRoom.rid, "[integration-test] E2E postMessage check"); + + expect(typeof messageId).toBe("string"); + expect(messageId.length).toBeGreaterThan(0); + console.log(`[integration] postMessage → messageId: ${messageId} in room: ${dmRoom.name ?? dmRoom.rid}`); + }); + + it("should resolve bot identity", async () => { + const identity = await client.getIdentity(); + expect(typeof identity.userId).toBe("string"); + expect(identity.userId.length).toBeGreaterThan(0); + expect(typeof identity.username).toBe("string"); + console.log(`[integration] Connected as @${identity.username} (${identity.userId})`); + }); + + it("should fetch subscriptions", async () => { + const subs = await client.listSubscriptions(null); + expect(Array.isArray(subs)).toBe(true); + console.log(`[integration] Bot is in ${subs.length} room(s)`); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..bd17fdf --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from "vitest/config"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +// Manually parse .env so integration tests can pick up ROCKETCHAT_* vars +// without requiring the `vite` package to be installed. +function loadDotEnv(): Record { + try { + const raw = readFileSync(resolve(process.cwd(), ".env"), "utf-8"); + const env: Record = {}; + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const value = trimmed.slice(eqIdx + 1).trim(); + env[key] = value; + } + return env; + } catch { + return {}; + } +} + +export default defineConfig({ + test: { + env: loadDotEnv(), + }, +}); + From 2266930d8bd7adcd3bb3701d662cfe393ff90500 Mon Sep 17 00:00:00 2001 From: dodaa08 Date: Mon, 14 Sep 2026 13:38:37 +0530 Subject: [PATCH 7/7] formatting --- README.md | 8 ++++++-- docs/COMMANDS.md | 8 ++++---- docs/SETUP.md | 2 +- docs/SKILLS/doc.md | 1 - openclaw.examples.json | 8 ++------ package.json | 2 +- src/cli/config-updater.ts | 2 -- src/cli/setup.ts | 7 ++++++- src/config/schema.ts | 1 - src/service/channel.ts | 10 ++++++++-- src/service/inbound.ts | 6 +++--- src/service/skill-commands.ts | 14 +++++++------- tests/integration/rest.test.ts | 9 +++++++-- vitest.config.ts | 1 - 14 files changed, 45 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 40fd1d4..6cc85a9 100644 --- a/README.md +++ b/README.md @@ -72,13 +72,17 @@ You should see `gateway - online` and `runtime - ready`. | [CONTRIBUTING.md](https://github.com/RocketChat/OpenClaw.Plugin/blob/main/CONTRIBUTING.md) | Contributors guide | ## Media Storage & Handling -When users send media (images, audio, etc.) in Rocket.Chat, the plugin downloads the files locally to `~/.openclaw/media/inbound/`. + +When users send media (images, audio, etc.) in Rocket.Chat, the plugin downloads the files locally to `~/.openclaw/media/inbound/`. + - **Why locally?** This allows the OpenClaw agent to reliably process the actual file bytes from the filesystem rather than struggling with URL authentication or timeouts. - **Limits**: The plugin currently caps downloads at **20MB** per file and supports `image/`, `audio/`, `video/`, and `application/` MIME types. - **Cleanup**: Currently, there is no automatic auto-prune for these files. We recommend users set up a cron job to clean up the folder periodically, e.g.: `find ~/.openclaw/media/inbound -type f -mtime +7 -delete`. ## Roadmap / Leftovers -*Future enhancements currently being tracked:* + +_Future enhancements currently being tracked:_ + - [ ] Expanding End-to-End (E2E) and integration test coverage across the repository. - [ ] Preparing project for official v1 release. - [ ] Addressing remaining bugs and structural updates from our internal trackers: diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index f2668b8..b49e4e8 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -98,9 +98,11 @@ Control how the agent responds. | `!skills` | List installed skills (use via inbound chat) | ### Owner-Only Skills -Certain powerful skills (e.g. bash execution, file manipulation) are restricted strictly to the **Owner** of the bot for security reasons. + +Certain powerful skills (e.g. bash execution, file manipulation) are restricted strictly to the **Owner** of the bot for security reasons. + - You must be listed in `openclaw.json` under `accounts..owner` (e.g., `"owner": "admin-user"`). -- To use an owner-only skill, simply instruct the bot in your DM or a private channel where the bot is present. +- To use an owner-only skill, simply instruct the bot in your DM or a private channel where the bot is present. - Non-owner users who try to invoke owner-only skills will receive an unauthorized error from the bot. ## Cron Jobs @@ -139,8 +141,6 @@ Owner-only commands: `add-bot`, `remove-bot`, `add-group`, `revoke`, `access`, ` Non-owners see a permission error when trying owner-only commands. - - ## Unknown Command If you type a command that doesn't exist, the bot replies: diff --git a/docs/SETUP.md b/docs/SETUP.md index d9729c9..3cdee0b 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -96,7 +96,7 @@ Use the command (owner-only): !remove-bot ``` -This deletes the bot from the server and cleans up its local files automatically. +This deletes the bot from the server and cleans up its local files automatically. If you need to manually delete a bot's files (for example, if the command fails), you must remove these specific files and directories: diff --git a/docs/SKILLS/doc.md b/docs/SKILLS/doc.md index 366cf64..aa71a86 100644 --- a/docs/SKILLS/doc.md +++ b/docs/SKILLS/doc.md @@ -35,7 +35,6 @@ A `SKILL.md` typically includes: Two ready-made example skills are included to get you started: - - **`cron`** : schedule one-shot and recurring reminders via the `openclaw cron` CLI Reference: [SKILLS/Cron.md](./Cron.md) diff --git a/openclaw.examples.json b/openclaw.examples.json index 5457507..c147c99 100644 --- a/openclaw.examples.json +++ b/openclaw.examples.json @@ -16,9 +16,7 @@ "transport": { "mode": "websocket" }, - "mentionNames": [ - "botname" - ], + "mentionNames": ["botname"], "owner": "admin-user" } } @@ -34,9 +32,7 @@ } ], "plugins": { - "allow": [ - "rocketchat" - ], + "allow": ["rocketchat"], "entries": { "rocketchat": { "enabled": true diff --git a/package.json b/package.json index c9f5ca6..3e28ebc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "type": "module", "scripts": { - "build": "rm -rf dist && npm run manifest && tsc && npm run bundle && chmod -R 755 dist", + "build": "rm -rf dist && npm run manifest && tsc && npm run bundle && chmod -R 755 dist && npm run format", "bundle": "esbuild src/client/ddp.ts --bundle --format=esm --platform=node --outfile=dist/client/ddp.js --sourcemap --external:openclaw --external:@openclaw/*", "manifest": "tsx scripts/gen-manifest.mts", "format": "prettier --write .", diff --git a/src/cli/config-updater.ts b/src/cli/config-updater.ts index 7058b4d..cacc96e 100644 --- a/src/cli/config-updater.ts +++ b/src/cli/config-updater.ts @@ -592,8 +592,6 @@ function applyBinding( } } - - function stripBindingsForAccount(cfg: Record, accountId: string): void { const bindings = cfg?.bindings as Array> | undefined; if (!bindings) return; diff --git a/src/cli/setup.ts b/src/cli/setup.ts index 6fd48ba..4a97d20 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -10,7 +10,12 @@ import { inviteToGroup, sendMessage, } from "./admin-api.js"; -import { ensureAgentForBot, isAgentBound, readAllAccounts, updateConfig } from "./config-updater.js"; +import { + ensureAgentForBot, + isAgentBound, + readAllAccounts, + updateConfig, +} from "./config-updater.js"; import { loadAdmin } from "./credentials.js"; import { resolveAdminAuth } from "./auth.js"; import { resolveBotAuth } from "./bot.js"; diff --git a/src/config/schema.ts b/src/config/schema.ts index 6e16491..754bd53 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -37,7 +37,6 @@ const accountSchema = z mentionNames: z.array(z.string().min(1)).default([]), agent: z.string().min(1).optional(), owner: z.string().min(1).optional(), - }) .strict(); diff --git a/src/service/channel.ts b/src/service/channel.ts index 21ff33d..63e0680 100644 --- a/src/service/channel.ts +++ b/src/service/channel.ts @@ -445,13 +445,19 @@ function runSkills(ctx?: CommandContext): string { const agentId = `rc-${ctx.accountId}`; const agentWs = getAgentWorkspaceDir(agentId); scannedDirs.push({ path: join(agentWs, "skills"), scope: "Private" }); - scannedDirs.push({ path: resolve(resolveOpenClawDir(), "agents", agentId, "skills"), scope: "Private" }); + scannedDirs.push({ + path: resolve(resolveOpenClawDir(), "agents", agentId, "skills"), + scope: "Private", + }); } scannedDirs.push({ path: join(resolveOpenClawDir(), "workspace", "skills"), scope: "Global" }); scannedDirs.push({ path: join(resolveOpenClawDir(), "skills"), scope: "Global" }); - const skillsMap = new Map(); + const skillsMap = new Map< + string, + { name: string; description: string; scope: "Private" | "Global" } + >(); for (const { path: skillsDir, scope } of scannedDirs) { if (!existsSync(skillsDir)) continue; diff --git a/src/service/inbound.ts b/src/service/inbound.ts index 02b6553..a1f8183 100644 --- a/src/service/inbound.ts +++ b/src/service/inbound.ts @@ -10,8 +10,6 @@ import type { RocketChatClient } from "../client/rest.js"; import type { GroupHistoryEntry } from "./group-history.js"; import { dirname } from "node:path"; - - export async function dispatchInboundEventWithChannelRuntime(params: { cfg: OpenClawConfigLike; accountId: string; @@ -179,7 +177,9 @@ async function buildMediaContext( return { kind: "path" as const, value: filePath, mimeType: attachment.mimeType }; } catch (error: any) { if (client && roomId) { - client.postMessage(roomId, `⚠️ ${error.message || "Failed to download attachment."}`).catch(() => {}); + client + .postMessage(roomId, `⚠️ ${error.message || "Failed to download attachment."}`) + .catch(() => {}); } return null; } diff --git a/src/service/skill-commands.ts b/src/service/skill-commands.ts index d55399d..eadb4f9 100644 --- a/src/service/skill-commands.ts +++ b/src/service/skill-commands.ts @@ -148,7 +148,11 @@ function parseCronJobsResponse(stdout: string): Array> { try { const parsed = JSON.parse(stdout); if (Array.isArray(parsed)) return parsed; - if (parsed && typeof parsed === "object" && Array.isArray((parsed as Record).jobs)) { + if ( + parsed && + typeof parsed === "object" && + Array.isArray((parsed as Record).jobs) + ) { return (parsed as Record).jobs as Array>; } } catch { @@ -168,8 +172,7 @@ async function cronList(ctx: CommandContext): Promise { for (const job of jobs) { const name = String(job.name ?? job.id ?? "unknown"); const schedule = job.schedule as - | { kind?: string; everyMs?: number; at?: string; cron?: string } - | undefined; + { kind?: string; everyMs?: number; at?: string; cron?: string } | undefined; const everyMs = schedule?.everyMs; const scheduleDesc = schedule?.kind === "every" @@ -200,10 +203,7 @@ async function cronStop(ctx: CommandContext, name: string): Promise { const target = jobs.find((j) => { const jobName = String(j.name ?? j.id ?? ""); const search = name.trim().toLowerCase(); - return ( - jobName.toLowerCase() === search || - String(j.id ?? "").toLowerCase() === search - ); + return jobName.toLowerCase() === search || String(j.id ?? "").toLowerCase() === search; }); if (!target) { return `No job named or matching ID \`${name}\` found for this bot. Use \`!cron list\` to see jobs.`; diff --git a/tests/integration/rest.test.ts b/tests/integration/rest.test.ts index 1b49227..dac1bf8 100644 --- a/tests/integration/rest.test.ts +++ b/tests/integration/rest.test.ts @@ -28,11 +28,16 @@ describe.skipIf(skip)("RocketChatClient – Integration", () => { expect(subs.length).toBeGreaterThan(0); const dmRoom = subs.find((s) => s.t === "d") ?? subs[0]!; - const messageId = await client.postMessage(dmRoom.rid, "[integration-test] E2E postMessage check"); + const messageId = await client.postMessage( + dmRoom.rid, + "[integration-test] E2E postMessage check", + ); expect(typeof messageId).toBe("string"); expect(messageId.length).toBeGreaterThan(0); - console.log(`[integration] postMessage → messageId: ${messageId} in room: ${dmRoom.name ?? dmRoom.rid}`); + console.log( + `[integration] postMessage → messageId: ${messageId} in room: ${dmRoom.name ?? dmRoom.rid}`, + ); }); it("should resolve bot identity", async () => { diff --git a/vitest.config.ts b/vitest.config.ts index bd17fdf..f081ba2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -28,4 +28,3 @@ export default defineConfig({ env: loadDotEnv(), }, }); -