diff --git a/.agent/workflows/update_clawdbot.md b/.agent/workflows/update_clawdbot.md index 692ee84e4..04a079aab 100644 --- a/.agent/workflows/update_clawdbot.md +++ b/.agent/workflows/update_clawdbot.md @@ -29,10 +29,12 @@ git log --oneline --left-right main...upstream/main | head -20 ``` This shows: + - `<` = your local commits (ahead) - `>` = upstream commits you're missing (behind) **Decision point:** + - Few local commits, many upstream → **Rebase** (cleaner history) - Many local commits or shared branch → **Merge** (preserves history) @@ -70,12 +72,12 @@ git rebase --abort ### Common Conflict Patterns -| File | Resolution | -|------|------------| -| `package.json` | Take upstream deps, keep local scripts if needed | -| `pnpm-lock.yaml` | Accept upstream, regenerate with `pnpm install` | -| `*.patch` files | Usually take upstream version | -| Source files | Merge logic carefully, prefer upstream structure | +| File | Resolution | +| ---------------- | ------------------------------------------------ | +| `package.json` | Take upstream deps, keep local scripts if needed | +| `pnpm-lock.yaml` | Accept upstream, regenerate with `pnpm install` | +| `*.patch` files | Usually take upstream version | +| Source files | Merge logic carefully, prefer upstream structure | --- @@ -88,6 +90,7 @@ git merge upstream/main --no-edit ``` Resolve conflicts same as rebase, then: + ```bash git add git commit @@ -170,6 +173,7 @@ pnpm clawdbot agent --message "Verification: macOS app rebuild successful - agen Upstream updates may introduce Swift 6.2 / macOS 26 SDK incompatibilities. Use analyze-mode for systematic debugging: ### Analyze-Mode Investigation + ```bash # Gather context with parallel agents morph-mcp_warpgrep_codebase_search search_string="Find deprecated FileManager.default and Thread.isMainThread usages in Swift files" repo_path="/Volumes/Main SSD/Developer/clawdis" @@ -179,6 +183,7 @@ morph-mcp_warpgrep_codebase_search search_string="Locate Peekaboo submodule and ### Common Swift 6.2 Fixes **FileManager.default Deprecation:** + ```bash # Search for deprecated usage grep -r "FileManager\.default" src/ apps/ --include="*.swift" @@ -189,6 +194,7 @@ grep -r "FileManager\.default" src/ apps/ --include="*.swift" ``` **Thread.isMainThread Deprecation:** + ```bash # Search for deprecated usage grep -r "Thread\.isMainThread" src/ apps/ --include="*.swift" @@ -199,6 +205,7 @@ grep -r "Thread\.isMainThread" src/ apps/ --include="*.swift" ``` ### Peekaboo Submodule Fixes + ```bash # Check Peekaboo for concurrency issues cd src/canvas-host/a2ui @@ -210,6 +217,7 @@ pnpm canvas:a2ui:bundle ``` ### macOS App Concurrency Fixes + ```bash # Check macOS app for issues grep -r "Thread\.isMainThread\|FileManager\.default" apps/macos/ --include="*.swift" @@ -220,7 +228,9 @@ cd apps/macos && rm -rf .build .swiftpm ``` ### Model Configuration Updates + If upstream introduced new model configurations: + ```bash # Check for OpenRouter API key requirements grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js" @@ -265,6 +275,7 @@ Common issue: `fetch.preconnect` type mismatch. Fix by using `FetchLike` type in ### macOS App Crashes on Launch Usually resource bundle mismatch. Full rebuild required: + ```bash cd apps/macos && rm -rf .build .swiftpm ./scripts/restart-mac.sh @@ -285,12 +296,14 @@ pnpm install 2>&1 | grep -i patch **Symptoms:** Build fails with deprecation warnings about `FileManager.default` or `Thread.isMainThread` **Search-Mode Investigation:** + ```bash # Exhaustive search for deprecated APIs morph-mcp_warpgrep_codebase_search search_string="Find all Swift files using deprecated FileManager.default or Thread.isMainThread" repo_path="/Volumes/Main SSD/Developer/clawdis" ``` **Quick Fix Commands:** + ```bash # Find all affected files find . -name "*.swift" -exec grep -l "FileManager\.default\|Thread\.isMainThread" {} \; @@ -303,6 +316,7 @@ grep -rn "Thread\.isMainThread" --include="*.swift" . ``` **Rebuild After Fixes:** + ```bash # Clean all build artifacts rm -rf apps/macos/.build apps/macos/.swiftpm diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f6fca8c5e..082086ea0 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ['https://github.com/sponsors/steipete'] +custom: ["https://github.com/sponsors/steipete"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 46ee3da04..82b560c47 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,23 +6,29 @@ labels: bug --- ## Summary + What went wrong? ## Steps to reproduce + 1. 2. 3. ## Expected behavior + What did you expect to happen? ## Actual behavior + What actually happened? ## Environment + - Clawdbot version: - OS: - Install method (pnpm/npx/docker/etc): ## Logs or screenshots + Paste relevant logs or add screenshots (redact secrets). diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 742bf184e..7b33641dc 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,13 +6,17 @@ labels: enhancement --- ## Summary + Describe the problem you are trying to solve or the opportunity you see. ## Proposed solution + What would you like Clawdbot to do? ## Alternatives considered + Any other approaches you have considered? ## Additional context + Links, screenshots, or related issues. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index bcfdefcdd..e660d2a97 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -12,6 +12,6 @@ paths: .github/workflows/**/*.yml: ignore: # Ignore shellcheck warnings (we run shellcheck separately) - - 'shellcheck reported issue.+' + - "shellcheck reported issue.+" # Ignore intentional if: false for disabled jobs - 'constant expression "false" in condition' diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index 6d9f55903..d443ebc79 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -2,25 +2,26 @@ name: Auto response on: issues: - types: [labeled] + types: [opened, edited, labeled] pull_request_target: types: [labeled] -permissions: - issues: write - pull-requests: write +permissions: {} jobs: auto-response: + permissions: + issues: write + pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/create-github-app-token@v1 + - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token with: app-id: "2729701" private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Handle labeled items - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ steps.app-token.outputs.token }} script: | @@ -44,8 +45,35 @@ jobs: message: "This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.molt.bot/plugin.", }, + { + label: "r: moltbook", + close: true, + lock: true, + lockReason: "off-topic", + message: + "OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.", + }, ]; + const issue = context.payload.issue; + if (issue) { + const title = issue.title ?? ""; + const body = issue.body ?? ""; + const haystack = `${title}\n${body}`.toLowerCase(); + const hasLabel = (issue.labels ?? []).some((label) => + typeof label === "string" ? label === "r: moltbook" : label?.name === "r: moltbook", + ); + if (haystack.includes("moltbook") && !hasLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ["r: moltbook"], + }); + return; + } + } + const labelName = context.payload.label?.name; if (!labelName) { return; @@ -76,3 +104,12 @@ jobs: state: "closed", }); } + + if (rule.lock) { + await github.rest.issues.lock({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + lock_reason: rule.lockReason ?? "resolved", + }); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 885d87fcb..071d16cd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,15 +71,15 @@ jobs: fail-fast: false matrix: include: + - runtime: node + task: tsgo + command: pnpm tsgo - runtime: node task: lint - command: pnpm lint + command: pnpm build && pnpm lint - runtime: node task: test command: pnpm canvas:a2ui:bundle && pnpm test - - runtime: node - task: build - command: pnpm build - runtime: node task: protocol command: pnpm protocol:check @@ -91,7 +91,7 @@ jobs: command: pnpm canvas:a2ui:bundle && bunx vitest run - runtime: bun task: build - command: bunx tsc -p tsconfig.json + command: bunx tsc -p tsconfig.json --noEmit false steps: - name: Checkout uses: actions/checkout@v4 @@ -197,14 +197,11 @@ jobs: matrix: include: - runtime: node - task: lint - command: pnpm lint + task: build & lint + command: pnpm build && pnpm lint - runtime: node task: test command: pnpm canvas:a2ui:bundle && pnpm test - - runtime: node - task: build - command: pnpm build - runtime: node task: protocol command: pnpm protocol:check diff --git a/.github/workflows/formal-conformance.yml b/.github/workflows/formal-conformance.yml new file mode 100644 index 000000000..04a5db553 --- /dev/null +++ b/.github/workflows/formal-conformance.yml @@ -0,0 +1,89 @@ +name: Formal models (informational conformance) + +on: + pull_request: + +jobs: + formal_conformance: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout openclaw (PR) + uses: actions/checkout@v4 + with: + path: openclaw + + - name: Checkout formal models + uses: actions/checkout@v4 + with: + repository: vignesh07/clawdbot-formal-models + path: clawdbot-formal-models + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Regenerate extracted constants from openclaw + run: | + set -euo pipefail + cd clawdbot-formal-models + export OPENCLAW_REPO_DIR="${GITHUB_WORKSPACE}/openclaw" + node scripts/extract-tool-groups.mjs + node scripts/check-tool-group-alias.mjs + + - name: Compute drift + id: drift + run: | + set -euo pipefail + cd clawdbot-formal-models + + if git diff --quiet; then + echo "drift=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "drift=true" >> "$GITHUB_OUTPUT" + git diff > "${GITHUB_WORKSPACE}/formal-models-drift.diff" + + - name: Upload drift diff artifact + if: steps.drift.outputs.drift == 'true' + uses: actions/upload-artifact@v4 + with: + name: formal-models-conformance-drift + path: formal-models-drift.diff + + - name: Comment on PR (informational) + if: steps.drift.outputs.drift == 'true' + uses: actions/github-script@v7 + with: + script: | + const body = [ + '⚠️ **Formal models conformance drift detected**', + '', + 'The formal models extracted constants (`generated/*`) do not match this openclaw PR.', + '', + 'This check is **informational** (not blocking merges yet).', + 'See the `formal-models-conformance-drift` artifact for the diff.', + '', + 'If this change is intentional, follow up by updating the formal models repo or regenerating the extracted artifacts there.', + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); + + - name: Summary + run: | + if [ "${{ steps.drift.outputs.drift }}" = "true" ]; then + echo "Formal conformance drift detected (informational)." + else + echo "Formal conformance: no drift." + fi diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 0347c7810..d5d2ef515 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -32,8 +32,8 @@ jobs: - name: Run installer docker tests env: - CLAWDBOT_INSTALL_URL: https://clawd.bot/install.sh - CLAWDBOT_INSTALL_CLI_URL: https://clawd.bot/install-cli.sh + CLAWDBOT_INSTALL_URL: https://openclaw.ai/install.sh + CLAWDBOT_INSTALL_CLI_URL: https://openclaw.ai/install-cli.sh CLAWDBOT_NO_ONBOARD: "1" CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1" CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2b2f80130..32d103718 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -4,20 +4,21 @@ on: pull_request_target: types: [opened, synchronize, reopened] -permissions: - contents: read - pull-requests: write +permissions: {} jobs: label: + permissions: + contents: read + pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/create-github-app-token@v1 + - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 id: app-token with: app-id: "2729701" private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - uses: actions/labeler@v5 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 with: configuration-path: .github/labeler.yml repo-token: ${{ steps.app-token.outputs.token }} diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index 7d80600ac..f7208b4da 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -1,5 +1,20 @@ { "$schema": "./node_modules/oxfmt/configuration_schema.json", - "indentWidth": 2, - "printWidth": 100 + "experimentalSortImports": { + "newlinesBetween": false, + }, + "experimentalSortPackageJson": { + "sortScripts": true, + }, + "ignorePatterns": [ + "apps/", + "assets/", + "dist/", + "docs/_layouts/", + "node_modules/", + "patches/", + "pnpm-lock.yaml/", + "Swabble/", + "vendor/", + ], } diff --git a/.oxlintrc.json b/.oxlintrc.json index 3876f8e04..d5dfde5b4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,12 +1,36 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": [ - "unicorn", - "typescript", - "oxc" - ], + "plugins": ["unicorn", "typescript", "oxc"], "categories": { - "correctness": "error" + "correctness": "error", + "perf": "error", + "suspicious": "error" }, - "ignorePatterns": ["src/canvas-host/a2ui/a2ui.bundle.js"] + "rules": { + "curly": "error", + "eslint-plugin-unicorn/prefer-array-find": "off", + "eslint/no-await-in-loop": "off", + "eslint/no-new": "off", + "oxc/no-accumulating-spread": "off", + "oxc/no-async-endpoint-handlers": "off", + "oxc/no-map-spread": "off", + "typescript/no-extraneous-class": "off", + "typescript/no-unnecessary-template-expression": "off", + "typescript/no-unsafe-type-assertion": "off", + "unicorn/consistent-function-scoping": "off", + "unicorn/require-post-message-target-origin": "off" + }, + "ignorePatterns": [ + "assets/", + "dist/", + "docs/_layouts/", + "node_modules/", + "patches/", + "pnpm-lock.yaml/", + "skills/", + "src/canvas-host/a2ui/a2ui.bundle.js", + "Swabble/", + "vendor/", + "ui/" + ] } diff --git a/.pi/extensions/diff.ts b/.pi/extensions/diff.ts new file mode 100644 index 000000000..037fa240a --- /dev/null +++ b/.pi/extensions/diff.ts @@ -0,0 +1,195 @@ +/** + * Diff Extension + * + * /diff command shows modified/deleted/new files from git status and opens + * the selected file in VS Code's diff view. + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { DynamicBorder } from "@mariozechner/pi-coding-agent"; +import { + Container, + Key, + matchesKey, + type SelectItem, + SelectList, + Text, +} from "@mariozechner/pi-tui"; + +interface FileInfo { + status: string; + statusLabel: string; + file: string; +} + +export default function (pi: ExtensionAPI) { + pi.registerCommand("diff", { + description: "Show git changes and open in VS Code diff view", + handler: async (_args, ctx) => { + if (!ctx.hasUI) { + ctx.ui.notify("No UI available", "error"); + return; + } + + // Get changed files from git status + const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd }); + + if (result.code !== 0) { + ctx.ui.notify(`git status failed: ${result.stderr}`, "error"); + return; + } + + if (!result.stdout || !result.stdout.trim()) { + ctx.ui.notify("No changes in working tree", "info"); + return; + } + + // Parse git status output + // Format: XY filename (where XY is two-letter status, then space, then filename) + const lines = result.stdout.split("\n"); + const files: FileInfo[] = []; + + for (const line of lines) { + if (line.length < 4) { + continue; + } // Need at least "XY f" + + const status = line.slice(0, 2); + const file = line.slice(2).trimStart(); + + // Translate status codes to short labels + let statusLabel: string; + if (status.includes("M")) { + statusLabel = "M"; + } else if (status.includes("A")) { + statusLabel = "A"; + } else if (status.includes("D")) { + statusLabel = "D"; + } else if (status.includes("?")) { + statusLabel = "?"; + } else if (status.includes("R")) { + statusLabel = "R"; + } else if (status.includes("C")) { + statusLabel = "C"; + } else { + statusLabel = status.trim() || "~"; + } + + files.push({ status: statusLabel, statusLabel, file }); + } + + if (files.length === 0) { + ctx.ui.notify("No changes found", "info"); + return; + } + + const openSelected = async (fileInfo: FileInfo): Promise => { + try { + // Open in VS Code diff view. + // For untracked files, git difftool won't work, so fall back to just opening the file. + if (fileInfo.status === "?") { + await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd }); + return; + } + + const diffResult = await pi.exec( + "git", + ["difftool", "-y", "--tool=vscode", fileInfo.file], + { + cwd: ctx.cwd, + }, + ); + if (diffResult.code !== 0) { + await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd }); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + ctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, "error"); + } + }; + + // Show file picker with SelectList + await ctx.ui.custom((tui, theme, _kb, done) => { + const container = new Container(); + + // Top border + container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s))); + + // Title + container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0)); + + // Build select items with colored status + const items: SelectItem[] = files.map((f) => { + let statusColor: string; + switch (f.status) { + case "M": + statusColor = theme.fg("warning", f.status); + break; + case "A": + statusColor = theme.fg("success", f.status); + break; + case "D": + statusColor = theme.fg("error", f.status); + break; + case "?": + statusColor = theme.fg("muted", f.status); + break; + default: + statusColor = theme.fg("dim", f.status); + } + return { + value: f, + label: `${statusColor} ${f.file}`, + }; + }); + + const visibleRows = Math.min(files.length, 15); + let currentIndex = 0; + + const selectList = new SelectList(items, visibleRows, { + selectedPrefix: (t) => theme.fg("accent", t), + selectedText: (t) => t, // Keep existing colors + description: (t) => theme.fg("muted", t), + scrollInfo: (t) => theme.fg("dim", t), + noMatch: (t) => theme.fg("warning", t), + }); + selectList.onSelect = (item) => { + void openSelected(item.value as FileInfo); + }; + selectList.onCancel = () => done(); + selectList.onSelectionChange = (item) => { + currentIndex = items.indexOf(item); + }; + container.addChild(selectList); + + // Help text + container.addChild( + new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0), + ); + + // Bottom border + container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s))); + + return { + render: (w) => container.render(w), + invalidate: () => container.invalidate(), + handleInput: (data) => { + // Add paging with left/right + if (matchesKey(data, Key.left)) { + // Page up - clamp to 0 + currentIndex = Math.max(0, currentIndex - visibleRows); + selectList.setSelectedIndex(currentIndex); + } else if (matchesKey(data, Key.right)) { + // Page down - clamp to last + currentIndex = Math.min(items.length - 1, currentIndex + visibleRows); + selectList.setSelectedIndex(currentIndex); + } else { + selectList.handleInput(data); + } + tui.requestRender(); + }, + }; + }); + }, + }); +} diff --git a/.pi/extensions/files.ts b/.pi/extensions/files.ts new file mode 100644 index 000000000..bba2760d0 --- /dev/null +++ b/.pi/extensions/files.ts @@ -0,0 +1,194 @@ +/** + * Files Extension + * + * /files command lists all files the model has read/written/edited in the active session branch, + * coalesced by path and sorted newest first. Selecting a file opens it in VS Code. + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { DynamicBorder } from "@mariozechner/pi-coding-agent"; +import { + Container, + Key, + matchesKey, + type SelectItem, + SelectList, + Text, +} from "@mariozechner/pi-tui"; + +interface FileEntry { + path: string; + operations: Set<"read" | "write" | "edit">; + lastTimestamp: number; +} + +type FileToolName = "read" | "write" | "edit"; + +export default function (pi: ExtensionAPI) { + pi.registerCommand("files", { + description: "Show files read/written/edited in this session", + handler: async (_args, ctx) => { + if (!ctx.hasUI) { + ctx.ui.notify("No UI available", "error"); + return; + } + + // Get the current branch (path from leaf to root) + const branch = ctx.sessionManager.getBranch(); + + // First pass: collect tool calls (id -> {path, name}) from assistant messages + const toolCalls = new Map(); + + for (const entry of branch) { + if (entry.type !== "message") { + continue; + } + const msg = entry.message; + + if (msg.role === "assistant" && Array.isArray(msg.content)) { + for (const block of msg.content) { + if (block.type === "toolCall") { + const name = block.name; + if (name === "read" || name === "write" || name === "edit") { + const path = block.arguments?.path; + if (path && typeof path === "string") { + toolCalls.set(block.id, { path, name, timestamp: msg.timestamp }); + } + } + } + } + } + } + + // Second pass: match tool results to get the actual execution timestamp + const fileMap = new Map(); + + for (const entry of branch) { + if (entry.type !== "message") { + continue; + } + const msg = entry.message; + + if (msg.role === "toolResult") { + const toolCall = toolCalls.get(msg.toolCallId); + if (!toolCall) { + continue; + } + + const { path, name } = toolCall; + const timestamp = msg.timestamp; + + const existing = fileMap.get(path); + if (existing) { + existing.operations.add(name); + if (timestamp > existing.lastTimestamp) { + existing.lastTimestamp = timestamp; + } + } else { + fileMap.set(path, { + path, + operations: new Set([name]), + lastTimestamp: timestamp, + }); + } + } + } + + if (fileMap.size === 0) { + ctx.ui.notify("No files read/written/edited in this session", "info"); + return; + } + + // Sort by most recent first + const files = Array.from(fileMap.values()).toSorted( + (a, b) => b.lastTimestamp - a.lastTimestamp, + ); + + const openSelected = async (file: FileEntry): Promise => { + try { + await pi.exec("code", ["-g", file.path], { cwd: ctx.cwd }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error"); + } + }; + + // Show file picker with SelectList + await ctx.ui.custom((tui, theme, _kb, done) => { + const container = new Container(); + + // Top border + container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s))); + + // Title + container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0)); + + // Build select items with colored operations + const items: SelectItem[] = files.map((f) => { + const ops: string[] = []; + if (f.operations.has("read")) { + ops.push(theme.fg("muted", "R")); + } + if (f.operations.has("write")) { + ops.push(theme.fg("success", "W")); + } + if (f.operations.has("edit")) { + ops.push(theme.fg("warning", "E")); + } + const opsLabel = ops.join(""); + return { + value: f, + label: `${opsLabel} ${f.path}`, + }; + }); + + const visibleRows = Math.min(files.length, 15); + let currentIndex = 0; + + const selectList = new SelectList(items, visibleRows, { + selectedPrefix: (t) => theme.fg("accent", t), + selectedText: (t) => t, // Keep existing colors + description: (t) => theme.fg("muted", t), + scrollInfo: (t) => theme.fg("dim", t), + noMatch: (t) => theme.fg("warning", t), + }); + selectList.onSelect = (item) => { + void openSelected(item.value as FileEntry); + }; + selectList.onCancel = () => done(); + selectList.onSelectionChange = (item) => { + currentIndex = items.indexOf(item); + }; + container.addChild(selectList); + + // Help text + container.addChild( + new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0), + ); + + // Bottom border + container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s))); + + return { + render: (w) => container.render(w), + invalidate: () => container.invalidate(), + handleInput: (data) => { + // Add paging with left/right + if (matchesKey(data, Key.left)) { + // Page up - clamp to 0 + currentIndex = Math.max(0, currentIndex - visibleRows); + selectList.setSelectedIndex(currentIndex); + } else if (matchesKey(data, Key.right)) { + // Page down - clamp to last + currentIndex = Math.min(items.length - 1, currentIndex + visibleRows); + selectList.setSelectedIndex(currentIndex); + } else { + selectList.handleInput(data); + } + tui.requestRender(); + }, + }; + }); + }, + }); +} diff --git a/.pi/extensions/prompt-url-widget.ts b/.pi/extensions/prompt-url-widget.ts new file mode 100644 index 000000000..2bb56b104 --- /dev/null +++ b/.pi/extensions/prompt-url-widget.ts @@ -0,0 +1,193 @@ +import { + DynamicBorder, + type ExtensionAPI, + type ExtensionContext, +} from "@mariozechner/pi-coding-agent"; +import { Container, Text } from "@mariozechner/pi-tui"; + +const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im; +const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im; + +type PromptMatch = { + kind: "pr" | "issue"; + url: string; +}; + +type GhMetadata = { + title?: string; + author?: { + login?: string; + name?: string | null; + }; +}; + +function extractPromptMatch(prompt: string): PromptMatch | undefined { + const prMatch = prompt.match(PR_PROMPT_PATTERN); + if (prMatch?.[1]) { + return { kind: "pr", url: prMatch[1].trim() }; + } + + const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN); + if (issueMatch?.[1]) { + return { kind: "issue", url: issueMatch[1].trim() }; + } + + return undefined; +} + +async function fetchGhMetadata( + pi: ExtensionAPI, + kind: PromptMatch["kind"], + url: string, +): Promise { + const args = + kind === "pr" + ? ["pr", "view", url, "--json", "title,author"] + : ["issue", "view", url, "--json", "title,author"]; + + try { + const result = await pi.exec("gh", args); + if (result.code !== 0 || !result.stdout) { + return undefined; + } + return JSON.parse(result.stdout) as GhMetadata; + } catch { + return undefined; + } +} + +function formatAuthor(author?: GhMetadata["author"]): string | undefined { + if (!author) { + return undefined; + } + const name = author.name?.trim(); + const login = author.login?.trim(); + if (name && login) { + return `${name} (@${login})`; + } + if (login) { + return `@${login}`; + } + if (name) { + return name; + } + return undefined; +} + +export default function promptUrlWidgetExtension(pi: ExtensionAPI) { + const setWidget = ( + ctx: ExtensionContext, + match: PromptMatch, + title?: string, + authorText?: string, + ) => { + ctx.ui.setWidget("prompt-url", (_tui, thm) => { + const titleText = title ? thm.fg("accent", title) : thm.fg("accent", match.url); + const authorLine = authorText ? thm.fg("muted", authorText) : undefined; + const urlLine = thm.fg("dim", match.url); + + const lines = [titleText]; + if (authorLine) { + lines.push(authorLine); + } + lines.push(urlLine); + + const container = new Container(); + container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s))); + container.addChild(new Text(lines.join("\n"), 1, 0)); + return container; + }); + }; + + const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => { + const label = match.kind === "pr" ? "PR" : "Issue"; + const trimmedTitle = title?.trim(); + const fallbackName = `${label}: ${match.url}`; + const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${match.url})` : fallbackName; + const currentName = pi.getSessionName()?.trim(); + if (!currentName) { + pi.setSessionName(desiredName); + return; + } + if (currentName === match.url || currentName === fallbackName) { + pi.setSessionName(desiredName); + } + }; + + pi.on("before_agent_start", async (event, ctx) => { + if (!ctx.hasUI) { + return; + } + const match = extractPromptMatch(event.prompt); + if (!match) { + return; + } + + setWidget(ctx, match); + applySessionName(ctx, match); + void fetchGhMetadata(pi, match.kind, match.url).then((meta) => { + const title = meta?.title?.trim(); + const authorText = formatAuthor(meta?.author); + setWidget(ctx, match, title, authorText); + applySessionName(ctx, match, title); + }); + }); + + pi.on("session_switch", async (_event, ctx) => { + rebuildFromSession(ctx); + }); + + const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => { + if (!content) { + return ""; + } + if (typeof content === "string") { + return content; + } + return ( + content + .filter((block): block is { type: "text"; text: string } => block.type === "text") + .map((block) => block.text) + .join("\n") ?? "" + ); + }; + + const rebuildFromSession = (ctx: ExtensionContext) => { + if (!ctx.hasUI) { + return; + } + + const entries = ctx.sessionManager.getEntries(); + const lastMatch = [...entries].toReversed().find((entry) => { + if (entry.type !== "message" || entry.message.role !== "user") { + return false; + } + const text = getUserText(entry.message.content); + return !!extractPromptMatch(text); + }); + + const content = + lastMatch?.type === "message" && lastMatch.message.role === "user" + ? lastMatch.message.content + : undefined; + const text = getUserText(content); + const match = text ? extractPromptMatch(text) : undefined; + if (!match) { + ctx.ui.setWidget("prompt-url", undefined); + return; + } + + setWidget(ctx, match); + applySessionName(ctx, match); + void fetchGhMetadata(pi, match.kind, match.url).then((meta) => { + const title = meta?.title?.trim(); + const authorText = formatAuthor(meta?.author); + setWidget(ctx, match, title, authorText); + applySessionName(ctx, match, title); + }); + }; + + pi.on("session_start", async (_event, ctx) => { + rebuildFromSession(ctx); + }); +} diff --git a/.pi/extensions/redraws.ts b/.pi/extensions/redraws.ts new file mode 100644 index 000000000..6331f5eab --- /dev/null +++ b/.pi/extensions/redraws.ts @@ -0,0 +1,26 @@ +/** + * Redraws Extension + * + * Exposes /tui to show TUI redraw stats. + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { Text } from "@mariozechner/pi-tui"; + +export default function (pi: ExtensionAPI) { + pi.registerCommand("tui", { + description: "Show TUI stats", + handler: async (_args, ctx) => { + if (!ctx.hasUI) { + return; + } + let redraws = 0; + await ctx.ui.custom((tui, _theme, _keybindings, done) => { + redraws = tui.fullRedraws; + done(undefined); + return new Text("", 0, 0); + }); + ctx.ui.notify(`TUI full redraws: ${redraws}`, "info"); + }, + }); +} diff --git a/.pi/git/.gitignore b/.pi/git/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/.pi/git/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.pi/prompts/cl.md b/.pi/prompts/cl.md new file mode 100644 index 000000000..6d79ecda6 --- /dev/null +++ b/.pi/prompts/cl.md @@ -0,0 +1,58 @@ +--- +description: Audit changelog entries before release +--- + +Audit changelog entries for all commits since the last release. + +## Process + +1. **Find the last release tag:** + + ```bash + git tag --sort=-version:refname | head -1 + ``` + +2. **List all commits since that tag:** + + ```bash + git log ..HEAD --oneline + ``` + +3. **Read each package's [Unreleased] section:** + - packages/ai/CHANGELOG.md + - packages/tui/CHANGELOG.md + - packages/coding-agent/CHANGELOG.md + +4. **For each commit, check:** + - Skip: changelog updates, doc-only changes, release housekeeping + - Determine which package(s) the commit affects (use `git show --stat`) + - Verify a changelog entry exists in the affected package(s) + - For external contributions (PRs), verify format: `Description ([#N](url) by [@user](url))` + +5. **Cross-package duplication rule:** + Changes in `ai`, `agent` or `tui` that affect end users should be duplicated to `coding-agent` changelog, since coding-agent is the user-facing package that depends on them. + +6. **Add New Features section after changelog fixes:** + - Insert a `### New Features` section at the start of `## [Unreleased]` in `packages/coding-agent/CHANGELOG.md`. + - Propose the top new features to the user for confirmation before writing them. + - Link to relevant docs and sections whenever possible. + +7. **Report:** + - List commits with missing entries + - List entries that need cross-package duplication + - Add any missing entries directly + +## Changelog Format Reference + +Sections (in order): + +- `### Breaking Changes` - API changes requiring migration +- `### Added` - New features +- `### Changed` - Changes to existing functionality +- `### Fixed` - Bug fixes +- `### Removed` - Removed features + +Attribution: + +- Internal: `Fixed foo ([#123](https://github.com/badlogic/pi-mono/issues/123))` +- External: `Added bar ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@user](https://github.com/user))` diff --git a/.pi/prompts/is.md b/.pi/prompts/is.md new file mode 100644 index 000000000..cc8f603ad --- /dev/null +++ b/.pi/prompts/is.md @@ -0,0 +1,22 @@ +--- +description: Analyze GitHub issues (bugs or feature requests) +--- + +Analyze GitHub issue(s): $ARGUMENTS + +For each issue: + +1. Read the issue in full, including all comments and linked issues/PRs. + +2. **For bugs**: + - Ignore any root cause analysis in the issue (likely wrong) + - Read all related code files in full (no truncation) + - Trace the code path and identify the actual root cause + - Propose a fix + +3. **For feature requests**: + - Read all related code files in full (no truncation) + - Propose the most concise implementation approach + - List affected files and changes needed + +Do NOT implement unless explicitly asked. Analyze and propose only. diff --git a/.pi/prompts/pr.md b/.pi/prompts/pr.md new file mode 100644 index 000000000..f16236566 --- /dev/null +++ b/.pi/prompts/pr.md @@ -0,0 +1,36 @@ +--- +description: Review PRs from URLs with structured issue and code analysis +--- + +You are given one or more GitHub PR URLs: $@ + +For each PR URL, do the following in order: + +1. Read the PR page in full. Include description, all comments, all commits, and all changed files. +2. Identify any linked issues referenced in the PR body, comments, commit messages, or cross links. Read each issue in full, including all comments. +3. Analyze the PR diff. Read all relevant code files in full with no truncation from the current main branch and compare against the diff. Do not fetch PR file blobs unless a file is missing on main or the diff context is insufficient. Include related code paths that are not in the diff but are required to validate behavior. +4. Check if docs/\*.md require modification. This is usually the case when existing features have been changed, or new features have been added. +5. Provide a structured review with these sections: + - Good: solid choices or improvements + - Bad: concrete issues, regressions, missing tests, or risks + - Ugly: subtle or high impact problems +6. Add Questions or Assumptions if anything is unclear. +7. Add Change summary and Tests. + +Output format per PR: +PR: +Good: + +- ... + Bad: +- ... + Ugly: +- ... + Questions or Assumptions: +- ... + Change summary: +- ... + Tests: +- ... + +If no issues are found, say so under Bad and Ugly. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80813a0d3..e946d18c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Pre-commit hooks for clawdbot +# Pre-commit hooks for openclaw # Install: prek install # Run manually: prek run --all-files # @@ -51,9 +51,9 @@ repos: rev: v0.11.0 hooks: - id: shellcheck - args: [--severity=error] # Only fail on errors, not warnings/info + args: [--severity=error] # Only fail on errors, not warnings/info # Exclude vendor and scripts with embedded code or known issues - exclude: '^(vendor/|scripts/e2e/)' + exclude: "^(vendor/|scripts/e2e/)" # GitHub Actions linting - repo: https://github.com/rhysd/actionlint @@ -67,7 +67,7 @@ repos: hooks: - id: zizmor args: [--persona=regular, --min-severity=medium, --min-confidence=medium] - exclude: '^(vendor/|Swabble/)' + exclude: "^(vendor/|Swabble/)" # Project checks (same commands as CI) - repo: local diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 6333f297c..000000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -src/canvas-host/a2ui/a2ui.bundle.js diff --git a/AGENTS.md b/AGENTS.md index 44b0149fd..4f99bdeca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,14 +1,16 @@ # Repository Guidelines -- Repo: https://github.com/moltbot/moltbot + +- Repo: https://github.com/openclaw/openclaw - GitHub issues/comments/PR comments: use literal multiline strings or `-F - <<'EOF'` (or $'...') for real newlines; never embed "\\n". ## Project Structure & Module Organization + - Source code: `src/` (CLI wiring in `src/cli`, commands in `src/commands`, web provider in `src/provider-web.ts`, infra in `src/infra`, media pipeline in `src/media`). - Tests: colocated `*.test.ts`. - Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`. - Plugins/extensions: live under `extensions/*` (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them. -- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `moltbot` in `devDependencies` or `peerDependencies` instead (runtime resolves `clawdbot/plugin-sdk` via jiti alias). -- Installers served from `https://molt.bot/*`: live in the sibling repo `../molt.bot` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`). +- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `openclaw` in `devDependencies` or `peerDependencies` instead (runtime resolves `openclaw/plugin-sdk` via jiti alias). +- Installers served from `https://openclaw.ai/*`: live in the sibling repo `../openclaw.ai` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`). - Messaging channels: always consider **all** built-in + extension channels when refactoring shared logic (routing, allowlists, pairing, command gating, onboarding, docs). - Core channel docs: `docs/channels/` - Core channel code: `src/telegram`, `src/discord`, `src/slack`, `src/signal`, `src/imessage`, `src/web` (WhatsApp web), `src/channels`, `src/routing` @@ -16,62 +18,69 @@ - When adding channels/extensions/apps/docs, review `.github/labeler.yml` for label coverage. ## Docs Linking (Mintlify) -- Docs are hosted on Mintlify (docs.molt.bot). + +- Docs are hosted on Mintlify (docs.openclaw.ai). - Internal doc links in `docs/**/*.md`: root-relative, no `.md`/`.mdx` (example: `[Config](/configuration)`). - Section cross-references: use anchors on root-relative paths (example: `[Hooks](/configuration#hooks)`). - Doc headings and anchors: avoid em dashes and apostrophes in headings because they break Mintlify anchor links. -- When Peter asks for links, reply with full `https://docs.molt.bot/...` URLs (not root-relative). -- When you touch docs, end the reply with the `https://docs.molt.bot/...` URLs you referenced. -- README (GitHub): keep absolute docs URLs (`https://docs.molt.bot/...`) so links work on GitHub. +- When Peter asks for links, reply with full `https://docs.openclaw.ai/...` URLs (not root-relative). +- When you touch docs, end the reply with the `https://docs.openclaw.ai/...` URLs you referenced. +- README (GitHub): keep absolute docs URLs (`https://docs.openclaw.ai/...`) so links work on GitHub. - Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”. ## exe.dev VM ops (general) + - Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set). - SSH flaky: use exe.dev web terminal or Shelley (web agent); keep a tmux session for long ops. -- Update: `sudo npm i -g moltbot@latest` (global install needs root on `/usr/lib/node_modules`). -- Config: use `moltbot config set ...`; ensure `gateway.mode=local` is set. +- Update: `sudo npm i -g openclaw@latest` (global install needs root on `/usr/lib/node_modules`). +- Config: use `openclaw config set ...`; ensure `gateway.mode=local` is set. - Discord: store raw token only (no `DISCORD_BOT_TOKEN=` prefix). - Restart: stop old gateway and run: - `pkill -9 -f moltbot-gateway || true; nohup moltbot gateway run --bind loopback --port 18789 --force > /tmp/moltbot-gateway.log 2>&1 &` -- Verify: `moltbot channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/moltbot-gateway.log`. + `pkill -9 -f openclaw-gateway || true; nohup openclaw gateway run --bind loopback --port 18789 --force > /tmp/openclaw-gateway.log 2>&1 &` +- Verify: `openclaw channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/openclaw-gateway.log`. ## Build, Test, and Development Commands + - Runtime baseline: Node **22+** (keep Node + Bun paths working). - Install deps: `pnpm install` - Pre-commit hooks: `prek install` (runs same checks as CI) - Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches). - Prefer Bun for TypeScript execution (scripts, dev, tests): `bun ` / `bunx `. -- Run CLI in dev: `pnpm moltbot ...` (bun) or `pnpm dev`. +- Run CLI in dev: `pnpm openclaw ...` (bun) or `pnpm dev`. - Node remains supported for running built output (`dist/*`) and production installs. - Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`. -- Type-check/build: `pnpm build` (tsc) +- Type-check/build: `pnpm build` - Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt) - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` ## Coding Style & Naming Conventions + - Language: TypeScript (ESM). Prefer strict typing; avoid `any`. - Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits. - Add brief code comments for tricky or non-obvious logic. - Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`. - Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability. -- Naming: use **Moltbot** for product/app/docs headings; use `moltbot` for CLI command, package/binary, paths, and config keys. +- Naming: use **OpenClaw** for product/app/docs headings; use `openclaw` for CLI command, package/binary, paths, and config keys. ## Release Channels (Naming) + - stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`. - beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app). - dev: moving head on `main` (no tag; git checkout main). ## Testing Guidelines + - Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements). - Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`. - Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic. - Do not set test workers above 16; tried already. -- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (Moltbot-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`. +- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`. - Full kit + what’s covered: `docs/testing.md`. - Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one. - Mobile: before using a simulator, check for connected real devices (iOS + Android) and prefer them when available. ## Commit & Pull Request Guidelines + - Create commits with `scripts/committer "" `; avoid manual `git add`/`git commit` so staging stays scoped. - Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`). - Group related changes; avoid bundling unrelated refactors. @@ -90,26 +99,31 @@ - After merging a PR: run `bun scripts/update-clawtributors.ts` if the contributor is missing, then commit the regenerated README. ## Shorthand Commands + - `sync`: if working tree is dirty, commit all changes (pick a sensible Conventional Commit message), then `git pull --rebase`; if rebase conflicts and cannot resolve, stop; otherwise `git push`. ### PR Workflow (Review vs Land) + - **Review mode (PR link only):** read `gh pr view/diff`; **do not** switch branches; **do not** change code. - **Landing mode:** create an integration branch from `main`, bring in PR commits (**prefer rebase** for linear history; **merge allowed** when complexity/conflicts make it safer), apply fixes, add changelog (+ thanks + PR #), run full gate **locally before committing** (`pnpm lint && pnpm build && pnpm test`), commit, merge back to `main`, then `git switch main` (never stay on a topic branch after landing). Important: contributor needs to be in git graph after this! ## Security & Configuration Tips -- Web provider stores creds at `~/.clawdbot/credentials/`; rerun `moltbot login` if logged out. -- Pi sessions live under `~/.clawdbot/sessions/` by default; the base directory is not configurable. + +- Web provider stores creds at `~/.openclaw/credentials/`; rerun `openclaw login` if logged out. +- Pi sessions live under `~/.openclaw/sessions/` by default; the base directory is not configurable. - Environment variables: see `~/.profile`. - Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples. - - Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them. +- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them. ## Troubleshooting -- Rebrand/migration issues or legacy config/service warnings: run `moltbot doctor` (see `docs/gateway/doctor.md`). + +- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`). ## Agent-Specific Notes + - Vocabulary: "makeup" = "mac app". - Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`. -- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/moltbot && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`. +- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/openclaw && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`. - When working on a GitHub Issue or PR, print the full URL at the end of the task. - When answering questions, respond with high-confidence answers only: verify in code; do not guess. - Never update the Carbon dependency. @@ -117,12 +131,12 @@ - Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default. - CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars. - Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes. -- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Moltbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep moltbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.** -- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the Moltbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`. +- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the OpenClaw Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep openclaw` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.** +- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the OpenClaw subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`. - If shared guardrails are available locally, review them; otherwise follow this repo's guidance. - SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code. - Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync. -- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/Moltbot/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), `docs/platforms/mac/release.md` (APP_VERSION/APP_BUILD examples), Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION). +- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/OpenClaw/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), `docs/platforms/mac/release.md` (APP_VERSION/APP_BUILD examples), Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION). - **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch. - **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators. - iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`. @@ -145,16 +159,17 @@ - Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed). - Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`. - Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema. -- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents//sessions/*.jsonl` (use the `agent=` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there. +- When asked to open a “session” file, open the Pi session logs under `~/.openclaw/agents//sessions/*.jsonl` (use the `agent=` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there. - Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac. - Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel. - Voice wake forwarding tips: - - Command template should stay `moltbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes. - - launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`moltbot` binaries resolve when invoked via `moltbot-mac`. -- For manual `moltbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping. + - Command template should stay `openclaw-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes. + - launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`openclaw` binaries resolve when invoked via `openclaw-mac`. +- For manual `openclaw message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping. - Release guardrails: do not change version numbers without operator’s explicit consent; always ask permission before running any npm publish/release step. ## NPM + 1Password (publish/verify) + - Use the 1password skill; all `op` commands must run inside a fresh tmux session. - Sign in: `eval "$(op signin --account my.1password.com)"` (app unlocked + integration on). - OTP: `op read 'op://Private/Npmjs/one-time password?attribute=otp'`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ae5fdf2..dc8222dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,108 @@ # Changelog -Docs: https://docs.molt.bot +Docs: https://docs.openclaw.ai -## 2026.1.27-beta.1 -Status: beta. +## 2026.1.31 ### Changes -- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope. -- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. -- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). -- macOS: finish Moltbot app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3. -- Branding: update launchd labels, mobile bundle IDs, and logging subsystems to bot.molt (legacy com.clawdbot migrations). Thanks @thewilloftheshadow. -- Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt. -- Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47. -- Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204. -- Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281) -- Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk. -- Docs: tighten Fly private deployment steps. (#2289) Thanks @dguido. -- Docs: add migration guide for moving to a new machine. (#2381) -- Docs: add Northflank one-click deployment guide. (#2167) Thanks @AdeboyeDN. + +### Fixes + +- Discord: resolve PluralKit proxied senders for allowlists and labels. (#5838) Thanks @thewilloftheshadow. +- Telegram: restore draft streaming partials. (#5543) Thanks @obviyus. +- fix(lobster): block arbitrary exec via lobsterPath/cwd injection (GHSA-4mhr-g7xj-cg8j). (#5335) Thanks @vignesh07. + +## 2026.1.30 + +### Changes + +- CLI: add `completion` command (Zsh/Bash/PowerShell/Fish) and auto-setup during postinstall/onboarding. +- CLI: add per-agent `models status` (`--agent` filter). (#4780) Thanks @jlowin. +- Agents: add Kimi K2.5 to the synthetic model catalog. (#4407) Thanks @manikv12. +- Auth: switch Kimi Coding to built-in provider; normalize OAuth profile email. +- Auth: add MiniMax OAuth plugin + onboarding option. (#4521) Thanks @Maosghoul. +- Agents: update pi SDK/API usage and dependencies. +- Gateway: inject timestamps into agent and chat.send messages. (#3705) Thanks @conroywhitney, @CashWilliams. +- Web UI: refresh sessions after chat commands and improve session display names. +- Build: move TypeScript builds to `tsdown` + `tsgo` (faster builds, CI typechecks), update tsconfig target, and clean up lint rules. +- Build: align npm tar override and bin metadata so the `openclaw` CLI entrypoint is preserved in npm publishes. +- Docs: add pi/pi-dev docs and update OpenClaw branding + install links. + +### Fixes + +- Security: restrict local path extraction in media parser to prevent LFI. (#4880) +- Gateway: prevent token defaults from becoming the literal "undefined". (#4873) Thanks @Hisleren. +- Control UI: fix assets resolution for npm global installs. (#4909) Thanks @YuriNachos. +- macOS: avoid stderr pipe backpressure in gateway discovery. (#3304) Thanks @abhijeet117. +- Telegram: normalize account token lookup for non-normalized IDs. (#5055) Thanks @jasonsschin. +- Telegram: preserve delivery thread fallback and fix threadId handling in delivery context. +- Telegram: fix HTML nesting for overlapping styles/links. (#4578) Thanks @ThanhNguyxn. +- Telegram: accept numeric messageId/chatId in react actions. (#4533) Thanks @Ayush10. +- Telegram: honor per-account proxy dispatcher via undici fetch. (#4456) Thanks @spiceoogway. +- Telegram: scope skill commands to bound agent per bot. (#4360) Thanks @robhparker. +- BlueBubbles: debounce by messageId to preserve attachments in text+image messages. (#4984) +- Routing: prefer requesterOrigin over stale session entries for sub-agent announce delivery. (#4957) +- Extensions: restore embedded extension discovery typings. +- CLI: fix `tui:dev` port resolution. +- LINE: fix status command TypeError. (#4651) +- OAuth: skip expired-token warnings when refresh tokens are still valid. (#4593) +- Build: skip redundant UI install step in Dockerfile. (#4584) Thanks @obviyus. + +## 2026.1.29 + +### Changes + +- Rebrand: rename the npm package/CLI to `openclaw`, add a `openclaw` compatibility shim, and move extensions to the `@openclaw/*` scope. +- Onboarding: strengthen security warning copy for beta + access control expectations. +- Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub. +- Config: auto-migrate legacy state/config paths and keep config resolution consistent across legacy filenames. - Gateway: warn on hook tokens via query params; document header auth preference. (#2200) Thanks @YuriNachos. - Gateway: add dangerous Control UI device auth bypass flag + audit warnings. (#2248) - Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz. -- Config: auto-migrate legacy state/config paths and keep config resolution consistent across legacy filenames. +- Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7. +- Browser: route browser control via gateway/node; remove standalone browser control command and control URL config. +- Browser: route `browser.request` via node proxies when available; honor proxy timeouts; derive browser ports from `gateway.port`. +- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev. +- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra. +- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon. +- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco. +- Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma. +- Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21. +- Telegram: support quote replies for message tool and inbound context. (#2900) Thanks @aduk059. +- Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos. +- Telegram: send sticker pixels to vision models. (#2650) +- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. - Discord: add configurable privileged gateway intents for presences/members. (#2266) Thanks @kentaro. -- Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng. +- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. +- Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk. +- Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a. +- Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt. +- Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47. - Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr. +- Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281) +- Memory Search: allow extra paths for memory indexing (ignores symlinks). (#3600) Thanks @kira-ariaki. +- Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204. - Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger. +- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. +- Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam. +- Routing: precompile session key regexes. (#1697) Thanks @Ray0907. +- CLI: use Node's module compile cache for faster startup. (#2808) Thanks @pi0. +- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. +- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. +- macOS: finish OpenClaw app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3. +- Branding: update launchd labels, mobile bundle IDs, and logging subsystems to bot.molt (legacy bundle ID migrations). Thanks @thewilloftheshadow. +- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). +- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. +- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn. +- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg. +- Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro. +- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi. +- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. +- Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst. +- Docs: tighten Fly private deployment steps. (#2289) Thanks @dguido. +- Docs: add migration guide for moving to a new machine. (#2381) +- Docs: add Northflank one-click deployment guide. (#2167) Thanks @AdeboyeDN. +- Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng. - Docs: add Render deployment guide. (#1975) Thanks @anurag. - Docs: add Claude Max API Proxy guide. (#1875) Thanks @atalovesyou. - Docs: add DigitalOcean deployment guide. (#1870) Thanks @0xJonHoldsCrypto. @@ -35,48 +111,24 @@ Status: beta. - Docs: add GCP Compute Engine deployment guide. (#1848) Thanks @hougangdev. - Docs: add LINE channel guide. Thanks @thewilloftheshadow. - Docs: credit both contributors for Control UI refresh. (#1852) Thanks @EnzeD. -- Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub. -- Onboarding: strengthen security warning copy for beta + access control expectations. -- Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a. -- Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst. -- Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7. -- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi. -- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn. -- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev. -- Browser: route browser control via gateway/node; remove standalone browser control command and control URL config. -- Browser: route `browser.request` via node proxies when available; honor proxy timeouts; derive browser ports from `gateway.port`. -- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg. -- Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro. -- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra. -- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon. -- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco. - Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99. -- Docs: update exe.dev install instructions. (#https://github.com/moltbot/moltbot/pull/3047) Thanks @zackerthescar. -- Security: use Windows ACLs for permission audits and fixes on Windows. (#1957) -- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. -- Routing: precompile session key regexes. (#1697) Thanks @Ray0907. -- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. -- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. -- Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma. -- Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21. -- Telegram: support quote replies for message tool and inbound context. (#2900) Thanks @aduk059. -- Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos. -- Telegram: send sticker pixels to vision models. (#2650) -- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. -- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. -- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. -- CLI: use Node's module compile cache for faster startup. (#2808) Thanks @pi0. -- Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam. +- Docs: update exe.dev install instructions. (#https://github.com/openclaw/openclaw/pull/3047) Thanks @zackerthescar. ### Breaking + - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes + +- Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796) +- Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R. - Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald. - Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald. - Agents: prevent retries on oversized image errors and surface size limits. (#2871) Thanks @Suksham-sharma. - Agents: inherit provider baseUrl/api for inline models. (#2740) Thanks @lploc94. - Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355. +- Telegram: include AccountId in native command context for multi-agent routing. (#2942) Thanks @Chloe-VP. +- Telegram: handle video note attachments in media extraction. (#2905) Thanks @mylukin. - TTS: read OPENAI_TTS_BASE_URL at runtime instead of module load to honor config.env. (#3341) Thanks @hclsys. - macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee. - Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101. @@ -107,6 +159,7 @@ Status: beta. - Telegram: centralize API error logging for delivery and bot calls. (#2492) Thanks @altryne. - Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default. - Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers. +- Media: fix text attachment MIME misclassification with CSV/TSV inference and UTF-16 detection; add XML attribute escaping for file output. (#3628) Thanks @frankekn. - Build: align memory-core peer dependency with lockfile. - Security: add mDNS discovery mode with minimal default to reduce information disclosure. (#1882) Thanks @orlyjamie. - Security: harden URL fetches with DNS pinning to reduce rebinding risk. Thanks Chris Zheng. @@ -119,6 +172,7 @@ Status: beta. ## 2026.1.24-3 ### Fixes + - Slack: fix image downloads failing due to missing Authorization header on cross-origin redirects. (#1936) Thanks @sanderhelgesen. - Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie. - Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie. @@ -127,52 +181,57 @@ Status: beta. ## 2026.1.24-2 ### Fixes + - Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install). ## 2026.1.24-1 ### Fixes + - Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install). ## 2026.1.24 ### Highlights -- Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.molt.bot/providers/ollama https://docs.molt.bot/providers/venice + +- Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.openclaw.ai/providers/ollama https://docs.openclaw.ai/providers/venice - Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg. -- TTS: Edge fallback (keyless) + `/tts` auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.molt.bot/tts -- Exec approvals: approve in-chat via `/approve` across all channels (including plugins). (#1621) Thanks @czekaj. https://docs.molt.bot/tools/exec-approvals https://docs.molt.bot/tools/slash-commands -- Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.molt.bot/channels/telegram +- TTS: Edge fallback (keyless) + `/tts` auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.openclaw.ai/tts +- Exec approvals: approve in-chat via `/approve` across all channels (including plugins). (#1621) Thanks @czekaj. https://docs.openclaw.ai/tools/exec-approvals https://docs.openclaw.ai/tools/slash-commands +- Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.openclaw.ai/channels/telegram ### Changes + - Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg. -- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.molt.bot/tts -- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.molt.bot/tts +- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.openclaw.ai/tts +- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.openclaw.ai/tts - Telegram: treat DM topics as separate sessions and keep DM history limits stable with thread suffixes. (#1597) Thanks @rohannagpal. -- Telegram: add `channels.telegram.linkPreview` to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.molt.bot/channels/telegram -- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.molt.bot/tools/web +- Telegram: add `channels.telegram.linkPreview` to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.openclaw.ai/channels/telegram +- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.openclaw.ai/tools/web - UI: refresh Control UI dashboard design system (colors, icons, typography). (#1745, #1786) Thanks @EnzeD, @mousberg. -- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.molt.bot/tools/exec-approvals https://docs.molt.bot/tools/slash-commands +- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.openclaw.ai/tools/exec-approvals https://docs.openclaw.ai/tools/slash-commands - Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg. -- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.molt.bot/diagnostics/flags +- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.openclaw.ai/diagnostics/flags - Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround). - Docs: add verbose installer troubleshooting guidance. - Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua. -- Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.molt.bot/bedrock +- Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.openclaw.ai/bedrock - Docs: update Fly.io guide notes. - Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido. ### Fixes + - Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589. - Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent. - Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg. - Web UI: hide internal `message_id` hints in chat bubbles. - Gateway: allow Control UI token-only auth to skip device pairing even when device identity is present (`gateway.controlUi.allowInsecureAuth`). (#1679) Thanks @steipete. - Matrix: decrypt E2EE media attachments with preflight size guard. (#1744) Thanks @araa47. -- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.molt.bot/channels/bluebubbles +- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.openclaw.ai/channels/bluebubbles - BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing. - iMessage: normalize chat_id/chat_guid/chat_identifier prefixes case-insensitively and keep service-prefixed handles stable. (#1708) Thanks @aaronn. - Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev. -- Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.molt.bot/channels/signal +- Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.openclaw.ai/channels/signal - Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338. - Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639) - Telegram: honor per-account proxy for outbound API calls. (#1774) Thanks @radek-paclt. @@ -205,32 +264,36 @@ Status: beta. ## 2026.1.23-1 ### Fixes + - Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js). ## 2026.1.23 ### Highlights -- TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.molt.bot/tts -- Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.molt.bot/gateway/tools-invoke-http-api -- Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.molt.bot/gateway/heartbeat -- Deploy: add Fly.io deployment support + guide. (#1570) https://docs.molt.bot/platforms/fly -- Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.molt.bot/channels/tlon + +- TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.openclaw.ai/tts +- Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.openclaw.ai/gateway/tools-invoke-http-api +- Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.openclaw.ai/gateway/heartbeat +- Deploy: add Fly.io deployment support + guide. (#1570) https://docs.openclaw.ai/platforms/fly +- Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.openclaw.ai/channels/tlon ### Changes -- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.molt.bot/multi-agent-sandbox-tools -- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.molt.bot/bedrock -- CLI: add `moltbot system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.molt.bot/cli/system -- CLI: add live auth probes to `moltbot models status` for per-profile verification. (commit 40181afde) https://docs.molt.bot/cli/models -- CLI: restart the gateway by default after `moltbot update`; add `--no-restart` to skip it. (commit 2c85b1b40) + +- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.openclaw.ai/multi-agent-sandbox-tools +- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.openclaw.ai/bedrock +- CLI: add `openclaw system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.openclaw.ai/cli/system +- CLI: add live auth probes to `openclaw models status` for per-profile verification. (commit 40181afde) https://docs.openclaw.ai/cli/models +- CLI: restart the gateway by default after `openclaw update`; add `--no-restart` to skip it. (commit 2c85b1b40) - Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node). (commit c3cb26f7c) -- Plugins: add optional `llm-task` JSON-only tool for workflows. (#1498) Thanks @vignesh07. https://docs.molt.bot/tools/llm-task +- Plugins: add optional `llm-task` JSON-only tool for workflows. (#1498) Thanks @vignesh07. https://docs.openclaw.ai/tools/llm-task - Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0. - Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits. (commit 66eec295b) - Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman. -- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc. https://docs.molt.bot/automation/cron-vs-heartbeat -- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.molt.bot/gateway/heartbeat +- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc. https://docs.openclaw.ai/automation/cron-vs-heartbeat +- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.openclaw.ai/gateway/heartbeat ### Fixes + - Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518) - Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints. - Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3) @@ -261,7 +324,7 @@ Status: beta. - UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast. - UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank. (commit d57cb2e1a) - TUI: forward unknown slash commands, include Gateway commands in autocomplete, and render slash replies as system output. (commit 1af227b61, commit 8195497ce, commit 6fba598ea) -- CLI: auth probe output polish (table output, inline errors, reduced noise, and wrap fixes in `moltbot models status`). (commit da3f2b489, commit 00ae21bed, commit 31e59cd58, commit f7dc27f2d, commit 438e782f8, commit 886752217, commit aabe0bed3, commit 81535d512, commit c63144ab1) +- CLI: auth probe output polish (table output, inline errors, reduced noise, and wrap fixes in `openclaw models status`). (commit da3f2b489, commit 00ae21bed, commit 31e59cd58, commit f7dc27f2d, commit 438e782f8, commit 886752217, commit aabe0bed3, commit 81535d512, commit c63144ab1) - Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206) - Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla. - Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest. @@ -269,6 +332,7 @@ Status: beta. ## 2026.1.22 ### Changes + - Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer. - Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren. - Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu. @@ -276,6 +340,7 @@ Status: beta. - Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link. ### Fixes + - BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell. - Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky. - Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla. @@ -306,38 +371,42 @@ Status: beta. ## 2026.1.21-2 ### Fixes -- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.molt.bot/cli/agents https://docs.molt.bot/web/control-ui + +- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.openclaw.ai/cli/agents https://docs.openclaw.ai/web/control-ui - Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447) ## 2026.1.21 ### Changes -- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.molt.bot/tools/lobster -- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.molt.bot/tools/lobster + +- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.openclaw.ai/tools/lobster +- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.openclaw.ai/tools/lobster - Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker. - CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output. - CLI: exec approvals mutations render tables instead of raw JSON. - Exec approvals: support wildcard agent allowlists (`*`) across all agents. - Exec approvals: allowlist matches resolved binary paths only, add safe stdin-only bins, and tighten allowlist shell parsing. - Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution. -- CLI: flatten node service commands under `moltbot node` and remove `service node` docs. -- CLI: move gateway service commands under `moltbot gateway` and add `gateway probe` for reachability. +- CLI: flatten node service commands under `openclaw node` and remove `service node` docs. +- CLI: move gateway service commands under `openclaw gateway` and add `gateway probe` for reachability. - Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot. - Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. - UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla. -- CLI: add `moltbot update wizard` for interactive channel selection and restart prompts. https://docs.molt.bot/cli/update +- CLI: add `openclaw update wizard` for interactive channel selection and restart prompts. https://docs.openclaw.ai/cli/update - Signal: add typing indicators and DM read receipts via signal-cli. - MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. - Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead). -- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.molt.bot/gateway/troubleshooting +- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.openclaw.ai/gateway/troubleshooting - Docs: add /model allowlist troubleshooting note. (#1405) - Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky. ### Breaking -- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.molt.bot/web/control-ui#insecure-http + +- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http - **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. ### Fixes + - Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman. - Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380) - Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early. @@ -360,68 +429,69 @@ Status: beta. ## 2026.1.20 ### Changes -- Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.molt.bot/web/control-ui -- Control UI: drop the legacy list view. (#1345) https://docs.molt.bot/web/control-ui -- TUI: add syntax highlighting for code blocks. (#1200) https://docs.molt.bot/tui -- TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview. (#1271) https://docs.molt.bot/tui -- TUI: add a searchable model picker for quicker model selection. (#1198) https://docs.molt.bot/tui -- TUI: add input history (up/down) for submitted messages. (#1348) https://docs.molt.bot/tui -- ACP: add `moltbot acp` for IDE integrations. https://docs.molt.bot/cli/acp -- ACP: add `moltbot acp client` interactive harness for debugging. https://docs.molt.bot/cli/acp -- Skills: add download installs with OS-filtered options. https://docs.molt.bot/tools/skills -- Skills: add the local sherpa-onnx-tts skill. https://docs.molt.bot/tools/skills -- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. https://docs.molt.bot/concepts/memory -- Memory: add SQLite embedding cache to speed up reindexing and frequent updates. https://docs.molt.bot/concepts/memory -- Memory: add OpenAI batch indexing for embeddings when configured. https://docs.molt.bot/concepts/memory -- Memory: enable OpenAI batch indexing by default for OpenAI embeddings. https://docs.molt.bot/concepts/memory -- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). https://docs.molt.bot/concepts/memory -- Memory: render progress immediately, color batch statuses in verbose logs, and poll OpenAI batch status every 2s by default. https://docs.molt.bot/concepts/memory -- Memory: add `--verbose` logging for memory status + batch indexing details. https://docs.molt.bot/concepts/memory -- Memory: add native Gemini embeddings provider for memory search. (#1151) https://docs.molt.bot/concepts/memory -- Browser: allow config defaults for efficient snapshots in the tool/CLI. (#1336) https://docs.molt.bot/tools/browser -- Nostr: add the Nostr channel plugin with profile management + onboarding defaults. (#1323) https://docs.molt.bot/channels/nostr -- Matrix: migrate to matrix-bot-sdk with E2EE support, location handling, and group allowlist upgrades. (#1298) https://docs.molt.bot/channels/matrix -- Slack: add HTTP webhook mode via Bolt HTTP receiver. (#1143) https://docs.molt.bot/channels/slack -- Telegram: enrich forwarded-message context with normalized origin details + legacy fallback. (#1090) https://docs.molt.bot/channels/telegram + +- Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.openclaw.ai/web/control-ui +- Control UI: drop the legacy list view. (#1345) https://docs.openclaw.ai/web/control-ui +- TUI: add syntax highlighting for code blocks. (#1200) https://docs.openclaw.ai/tui +- TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview. (#1271) https://docs.openclaw.ai/tui +- TUI: add a searchable model picker for quicker model selection. (#1198) https://docs.openclaw.ai/tui +- TUI: add input history (up/down) for submitted messages. (#1348) https://docs.openclaw.ai/tui +- ACP: add `openclaw acp` for IDE integrations. https://docs.openclaw.ai/cli/acp +- ACP: add `openclaw acp client` interactive harness for debugging. https://docs.openclaw.ai/cli/acp +- Skills: add download installs with OS-filtered options. https://docs.openclaw.ai/tools/skills +- Skills: add the local sherpa-onnx-tts skill. https://docs.openclaw.ai/tools/skills +- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. https://docs.openclaw.ai/concepts/memory +- Memory: add SQLite embedding cache to speed up reindexing and frequent updates. https://docs.openclaw.ai/concepts/memory +- Memory: add OpenAI batch indexing for embeddings when configured. https://docs.openclaw.ai/concepts/memory +- Memory: enable OpenAI batch indexing by default for OpenAI embeddings. https://docs.openclaw.ai/concepts/memory +- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). https://docs.openclaw.ai/concepts/memory +- Memory: render progress immediately, color batch statuses in verbose logs, and poll OpenAI batch status every 2s by default. https://docs.openclaw.ai/concepts/memory +- Memory: add `--verbose` logging for memory status + batch indexing details. https://docs.openclaw.ai/concepts/memory +- Memory: add native Gemini embeddings provider for memory search. (#1151) https://docs.openclaw.ai/concepts/memory +- Browser: allow config defaults for efficient snapshots in the tool/CLI. (#1336) https://docs.openclaw.ai/tools/browser +- Nostr: add the Nostr channel plugin with profile management + onboarding defaults. (#1323) https://docs.openclaw.ai/channels/nostr +- Matrix: migrate to matrix-bot-sdk with E2EE support, location handling, and group allowlist upgrades. (#1298) https://docs.openclaw.ai/channels/matrix +- Slack: add HTTP webhook mode via Bolt HTTP receiver. (#1143) https://docs.openclaw.ai/channels/slack +- Telegram: enrich forwarded-message context with normalized origin details + legacy fallback. (#1090) https://docs.openclaw.ai/channels/telegram - Discord: fall back to `/skill` when native command limits are exceeded. (#1287) - Discord: expose `/skill` globally. (#1287) -- Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) https://docs.molt.bot/plugins/zalouser -- Plugins: require manifest-embedded config schemas with preflight validation warnings. (#1272) https://docs.molt.bot/plugins/manifest -- Plugins: move channel catalog metadata into plugin manifests. (#1290) https://docs.molt.bot/plugins/manifest -- Plugins: align Nextcloud Talk policy helpers with core patterns. (#1290) https://docs.molt.bot/plugins/manifest -- Plugins/UI: let channel plugin metadata drive UI labels/icons and cron channel options. (#1306) https://docs.molt.bot/web/control-ui -- Agents/UI: add agent avatar support in identity config, IDENTITY.md, and the Control UI. (#1329) https://docs.molt.bot/gateway/configuration -- Plugins: add plugin slots with a dedicated memory slot selector. https://docs.molt.bot/plugins/agent-tools -- Plugins: ship the bundled BlueBubbles channel plugin (disabled by default). https://docs.molt.bot/channels/bluebubbles +- Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) https://docs.openclaw.ai/plugins/zalouser +- Plugins: require manifest-embedded config schemas with preflight validation warnings. (#1272) https://docs.openclaw.ai/plugins/manifest +- Plugins: move channel catalog metadata into plugin manifests. (#1290) https://docs.openclaw.ai/plugins/manifest +- Plugins: align Nextcloud Talk policy helpers with core patterns. (#1290) https://docs.openclaw.ai/plugins/manifest +- Plugins/UI: let channel plugin metadata drive UI labels/icons and cron channel options. (#1306) https://docs.openclaw.ai/web/control-ui +- Agents/UI: add agent avatar support in identity config, IDENTITY.md, and the Control UI. (#1329) https://docs.openclaw.ai/gateway/configuration +- Plugins: add plugin slots with a dedicated memory slot selector. https://docs.openclaw.ai/plugins/agent-tools +- Plugins: ship the bundled BlueBubbles channel plugin (disabled by default). https://docs.openclaw.ai/channels/bluebubbles - Plugins: migrate bundled messaging extensions to the plugin SDK and resolve plugin-sdk imports in the loader. -- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime. https://docs.molt.bot/channels/zalo -- Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime. https://docs.molt.bot/plugins/zalouser -- Plugins: allow optional agent tools with explicit allowlists and add the plugin tool authoring guide. https://docs.molt.bot/plugins/agent-tools +- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime. https://docs.openclaw.ai/channels/zalo +- Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime. https://docs.openclaw.ai/plugins/zalouser +- Plugins: allow optional agent tools with explicit allowlists and add the plugin tool authoring guide. https://docs.openclaw.ai/plugins/agent-tools - Plugins: auto-enable bundled channel/provider plugins when configuration is present. -- Plugins: sync plugin sources on channel switches and update npm-installed plugins during `moltbot update`. -- Plugins: share npm plugin update logic between `moltbot update` and `moltbot plugins update`. +- Plugins: sync plugin sources on channel switches and update npm-installed plugins during `openclaw update`. +- Plugins: share npm plugin update logic between `openclaw update` and `openclaw plugins update`. - Gateway/API: add `/v1/responses` (OpenResponses) with item-based input + semantic streaming events. (#1229) - Gateway/API: expand `/v1/responses` to support file/image inputs, tool_choice, usage, and output limits. (#1229) -- Usage: add `/usage cost` summaries and macOS menu cost charts. https://docs.molt.bot/reference/api-usage-costs -- Security: warn when <=300B models run without sandboxing while web tools are enabled. https://docs.molt.bot/cli/security -- Exec: add host/security/ask routing for gateway + node exec. https://docs.molt.bot/tools/exec -- Exec: add `/exec` directive for per-session exec defaults (host/security/ask/node). https://docs.molt.bot/tools/exec -- Exec approvals: migrate approvals to `~/.clawdbot/exec-approvals.json` with per-agent allowlists + skill auto-allow toggle, and add approvals UI + node exec lifecycle events. https://docs.molt.bot/tools/exec-approvals -- Nodes: add headless node host (`moltbot node start`) for `system.run`/`system.which`. https://docs.molt.bot/cli/node -- Nodes: add node daemon service install/status/start/stop/restart. https://docs.molt.bot/cli/node +- Usage: add `/usage cost` summaries and macOS menu cost charts. https://docs.openclaw.ai/reference/api-usage-costs +- Security: warn when <=300B models run without sandboxing while web tools are enabled. https://docs.openclaw.ai/cli/security +- Exec: add host/security/ask routing for gateway + node exec. https://docs.openclaw.ai/tools/exec +- Exec: add `/exec` directive for per-session exec defaults (host/security/ask/node). https://docs.openclaw.ai/tools/exec +- Exec approvals: migrate approvals to `~/.openclaw/exec-approvals.json` with per-agent allowlists + skill auto-allow toggle, and add approvals UI + node exec lifecycle events. https://docs.openclaw.ai/tools/exec-approvals +- Nodes: add headless node host (`openclaw node start`) for `system.run`/`system.which`. https://docs.openclaw.ai/cli/node +- Nodes: add node daemon service install/status/start/stop/restart. https://docs.openclaw.ai/cli/node - Bridge: add `skills.bins` RPC to support node host auto-allow skill bins. -- Sessions: add daily reset policy with per-type overrides and idle windows (default 4am local), preserving legacy idle-only configs. (#1146) https://docs.molt.bot/concepts/session -- Sessions: allow `sessions_spawn` to override thinking level for sub-agent runs. https://docs.molt.bot/tools/subagents -- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. https://docs.molt.bot/concepts/groups -- Models: add Qwen Portal OAuth provider support. (#1120) https://docs.molt.bot/providers/qwen -- Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels. https://docs.molt.bot/start/onboarding -- Docs: clarify allowlist input types and onboarding behavior for messaging channels. https://docs.molt.bot/start/onboarding -- Docs: refresh Android node discovery docs for the Gateway WS service type. https://docs.molt.bot/platforms/android -- Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) https://docs.molt.bot/bedrock -- Docs: clarify WhatsApp voice notes. https://docs.molt.bot/channels/whatsapp -- Docs: clarify Windows WSL portproxy LAN access notes. https://docs.molt.bot/platforms/windows -- Docs: refresh bird skill install metadata and usage notes. (#1302) https://docs.molt.bot/tools/browser-login +- Sessions: add daily reset policy with per-type overrides and idle windows (default 4am local), preserving legacy idle-only configs. (#1146) https://docs.openclaw.ai/concepts/session +- Sessions: allow `sessions_spawn` to override thinking level for sub-agent runs. https://docs.openclaw.ai/tools/subagents +- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. https://docs.openclaw.ai/concepts/groups +- Models: add Qwen Portal OAuth provider support. (#1120) https://docs.openclaw.ai/providers/qwen +- Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels. https://docs.openclaw.ai/start/onboarding +- Docs: clarify allowlist input types and onboarding behavior for messaging channels. https://docs.openclaw.ai/start/onboarding +- Docs: refresh Android node discovery docs for the Gateway WS service type. https://docs.openclaw.ai/platforms/android +- Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) https://docs.openclaw.ai/bedrock +- Docs: clarify WhatsApp voice notes. https://docs.openclaw.ai/channels/whatsapp +- Docs: clarify Windows WSL portproxy LAN access notes. https://docs.openclaw.ai/platforms/windows +- Docs: refresh bird skill install metadata and usage notes. (#1302) https://docs.openclaw.ai/tools/browser-login - Agents: add local docs path resolution and include docs/mirror/source/community pointers in the system prompt. - Agents: clarify node_modules read-only guidance in agent instructions. - Config: stamp last-touched metadata on write and warn if the config is newer than the running build. @@ -431,16 +501,18 @@ Status: beta. - Android: remove legacy bridge transport code now that nodes use the gateway protocol. - Android: bump okhttp + dnsjava to satisfy lint dependency checks. - Build: update workspace + core/plugin deps. -- Build: use tsgo for dev/watch builds by default (opt out with `CLAWDBOT_TS_COMPILER=tsc`). +- Build: use tsgo for dev/watch builds by default (opt out with `OPENCLAW_TS_COMPILER=tsc`). - Repo: remove the Peekaboo git submodule now that the SPM release is used. - macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release. - macOS: stop syncing Peekaboo in postinstall. - Swabble: use the tagged Commander Swift package release. ### Breaking -- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `moltbot doctor --fix` to repair, then update plugins (`moltbot plugins update`) if you use any. + +- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any. ### Fixes + - Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs. - Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry. - Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244) @@ -468,7 +540,7 @@ Status: beta. - Plugins: add Nextcloud Talk manifest for plugin config validation. (#1297) - Plugins: surface plugin load/register/config errors in gateway logs with plugin/source context. - CLI: preserve cron delivery settings when editing message payloads. (#1322) -- CLI: keep `moltbot logs` output resilient to broken pipes while preserving progress output. +- CLI: keep `openclaw logs` output resilient to broken pipes while preserving progress output. - CLI: avoid duplicating --profile/--dev flags when formatting commands. - CLI: centralize CLI command registration to keep fast-path routing and program wiring in sync. (#1207) - CLI: keep banners on routed commands, restore config guarding outside fast-path routing, and tighten fast-path flag parsing while skipping console capture for extra speed. (#1195) @@ -486,7 +558,7 @@ Status: beta. - TUI: show generic empty-state text for searchable pickers. (#1201) - TUI: highlight model search matches and stabilize search ordering. - Configure: hide OpenRouter auto routing model from the model picker. (#1182) -- Memory: show total file counts + scan issues in `moltbot memory status`. +- Memory: show total file counts + scan issues in `openclaw memory status`. - Memory: fall back to non-batch embeddings after repeated batch failures. - Memory: apply OpenAI batch defaults even without explicit remote config. - Memory: index atomically so failed reindex preserves the previous memory database. (#1151) @@ -496,7 +568,7 @@ Status: beta. - Memory: split overly long lines to keep embeddings under token limits. - Memory: skip empty chunks to avoid invalid embedding inputs. - Memory: split embedding batches to avoid OpenAI token limits during indexing. -- Memory: probe sqlite-vec availability in `moltbot memory status`. +- Memory: probe sqlite-vec availability in `openclaw memory status`. - Exec approvals: enforce allowlist when ask is off. - Exec approvals: prefer raw command for node approvals/events. - Tools: show exec elevated flag before the command and keep it outside markdown in tool summaries. @@ -540,28 +612,32 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.16-2 ### Changes + - CLI: stamp build commit into dist metadata so banners show the commit in npm installs. - CLI: close memory manager after memory commands to avoid hanging processes. (#1127) — thanks @NicholasSpisak. ## 2026.1.16-1 ### Highlights -- Hooks: add hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake. https://docs.molt.bot/hooks -- Media: add inbound media understanding (image/audio/video) with provider + CLI fallbacks. https://docs.molt.bot/nodes/media-understanding -- Plugins: add Zalo Personal plugin (`@moltbot/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh. https://docs.molt.bot/plugins/zalouser -- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins. https://docs.molt.bot/providers/vercel-ai-gateway -- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.molt.bot/concepts/session -- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.molt.bot/tools/web + +- Hooks: add hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake. https://docs.openclaw.ai/hooks +- Media: add inbound media understanding (image/audio/video) with provider + CLI fallbacks. https://docs.openclaw.ai/nodes/media-understanding +- Plugins: add Zalo Personal plugin (`@openclaw/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh. https://docs.openclaw.ai/plugins/zalouser +- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins. https://docs.openclaw.ai/providers/vercel-ai-gateway +- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.openclaw.ai/concepts/session +- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.openclaw.ai/tools/web ### Breaking -- **BREAKING:** `moltbot message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan. + +- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan. - **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow. - **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`. - **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups. -- **BREAKING:** `moltbot hooks` is now `moltbot webhooks`; hooks live under `moltbot hooks`. https://docs.molt.bot/cli/webhooks -- **BREAKING:** `moltbot plugins install ` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading). +- **BREAKING:** `openclaw hooks` is now `openclaw webhooks`; hooks live under `openclaw hooks`. https://docs.openclaw.ai/cli/webhooks +- **BREAKING:** `openclaw plugins install ` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading). ### Changes + - Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO. - Plugins: add bundled Antigravity + Gemini CLI OAuth + Copilot Proxy provider plugins. (#1066) — thanks @ItzR3NO. - Tools: improve `web_fetch` extraction using Readability (with fallback). @@ -569,7 +645,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Tools: send Chrome-like headers by default for `web_fetch` to improve extraction on bot-sensitive sites. - Tools: Firecrawl fallback now uses bot-circumvention + cache by default; remove basic HTML fallback when extraction fails. - Tools: default `exec` exit notifications and auto-migrate legacy `tools.bash` to `tools.exec`. -- Tools: add `exec` PTY support for interactive sessions. https://docs.molt.bot/tools/exec +- Tools: add `exec` PTY support for interactive sessions. https://docs.openclaw.ai/tools/exec - Tools: add tmux-style `process send-keys` and bracketed paste helpers for PTY sessions. - Tools: add `process submit` helper to send CR for PTY sessions. - Tools: respond to PTY cursor position queries to unblock interactive TUIs. @@ -577,7 +653,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Skills: update coding-agent guidance to prefer PTY-enabled exec runs and simplify tmux usage. - TUI: refresh session token counts after runs complete or fail. (#1079) — thanks @d-ploutarchos. - Status: trim `/status` to current-provider usage only and drop the OAuth/token block. -- Directory: unify `moltbot directory` across channels and plugin channels. +- Directory: unify `openclaw directory` across channels and plugin channels. - UI: allow deleting sessions from the Control UI. - Memory: add sqlite-vec vector acceleration with CLI status details. - Memory: add experimental session transcript indexing for memory_search (opt-in via memorySearch.experimental.sessionMemory + sources). @@ -593,10 +669,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Docs: add `/help` hub, Node/npm PATH guide, and expand directory CLI docs. - Config: support env var substitution in config values. (#1044) — thanks @sebslight. - Health: add per-agent session summaries and account-level health details, and allow selective probes. (#1047) — thanks @gumadeiras. -- Hooks: add hook pack installs (npm/path/zip/tar) with `moltbot.hooks` manifests and `moltbot hooks install/update`. +- Hooks: add hook pack installs (npm/path/zip/tar) with `openclaw.hooks` manifests and `openclaw hooks install/update`. - Plugins: add zip installs and `--link` to avoid copying local paths. ### Fixes + - macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash. - Verbose: wrap tool summaries/output in markdown only for markdown-capable channels. - Tools: include provider/session context in elevated exec denial errors. @@ -624,10 +701,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Sessions: hard-stop `sessions.delete` cleanup. - Channels: treat replies to the bot as implicit mentions across supported channels. - Channels: normalize object-format capabilities in channel capability parsing. -- Security: default-deny slash/control commands unless a channel computed `CommandAuthorized` (fixes accidental “open” behavior), and ensure WhatsApp + Zalo plugin channels gate inline `/…` tokens correctly. https://docs.molt.bot/gateway/security +- Security: default-deny slash/control commands unless a channel computed `CommandAuthorized` (fixes accidental “open” behavior), and ensure WhatsApp + Zalo plugin channels gate inline `/…` tokens correctly. https://docs.openclaw.ai/gateway/security - Security: redact sensitive text in gateway WS logs. - Tools: cap pending `exec` process output to avoid unbounded buffers. -- CLI: speed up `moltbot sandbox-explain` by avoiding heavy plugin imports when normalizing channel ids. +- CLI: speed up `openclaw sandbox-explain` by avoiding heavy plugin imports when normalizing channel ids. - Browser: remote profile tab operations prefer persistent Playwright and avoid silent HTTP fallbacks. (#1057) — thanks @mukhtharcm. - Browser: remote profile tab ops follow-up: shared Playwright loader, Playwright-based focus, and more coverage (incl. opt-in live Browserless test). (follow-up to #1057) — thanks @mukhtharcm. - Browser: refresh extension relay tab metadata after navigation so `/json/list` stays current. (#1073) — thanks @roshanasingh4. @@ -653,19 +730,22 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.15 ### Highlights -- Plugins: add provider auth registry + `moltbot models auth login` for plugin-driven OAuth/API key flows. + +- Plugins: add provider auth registry + `openclaw models auth login` for plugin-driven OAuth/API key flows. - Browser: improve remote CDP/Browserless support (auth passthrough, `wss` upgrade, timeouts, clearer errors). - Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf. - Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs). ### Breaking + - **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702) -- **BREAKING:** Microsoft Teams is now a plugin; install `@moltbot/msteams` via `moltbot plugins install @moltbot/msteams`. +- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`. - **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow. ### Changes + - UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow. -- CLI: set process titles to `moltbot-` for clearer process listings. +- CLI: set process titles to `openclaw-` for clearer process listings. - CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware). - Telegram: scope inline buttons with allowlist default + callback gating in DMs/groups. - Telegram: default reaction notifications to own. @@ -673,13 +753,13 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf. - Repo: ignore local identity files to avoid accidental commits. (#1001) — thanks @gerardward2007. - Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee. -- Plugins: add provider auth registry + `moltbot models auth login` for plugin-driven OAuth/API key flows. +- Plugins: add provider auth registry + `openclaw models auth login` for plugin-driven OAuth/API key flows. - Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker. - TUI: show provider/model labels for the active session and default model. - Heartbeat: add per-agent heartbeat configuration and multi-agent docs example. - UI: show gateway auth guidance + doc link on unauthorized Control UI connections. - UI: add session deletion action in Control UI sessions list. (#1017) — thanks @Szpadel. -- Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in `moltbot security audit`. +- Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in `openclaw security audit`. - Apps: store node auth tokens encrypted (Keychain/SecurePrefs). - Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts. - Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter. @@ -689,8 +769,8 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Docs: add Date & Time guide and update prompt/timezone configuration docs. - Messages: debounce rapid inbound messages across channels with per-connector overrides. (#971) — thanks @juanpablodlc. - Messages: allow media-only sends (CLI/tool) and show Telegram voice recording status for voice notes. (#957) — thanks @rdev. -- Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in `/status` and `moltbot models status`, and update docs. -- CLI: add `--json` output for `moltbot daemon` lifecycle/install commands. +- Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in `/status` and `openclaw models status`, and update docs. +- CLI: add `--json` output for `openclaw daemon` lifecycle/install commands. - Memory: make `node-llama-cpp` an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors. - Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` → `act`. - Browser: `profile="chrome"` now defaults to host control and returns clearer “attach a tab” errors. @@ -703,6 +783,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE. ### Fixes + - Messages: make `/stop` clear queued followups and pending session lane work for a hard abort. - Messages: make `/stop` abort active sub-agent runs spawned from the requester session and report how many were stopped. - WhatsApp: report linked status consistently in channel status. (#1050) — thanks @YuriNachos. @@ -713,10 +794,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - iMessage: treat missing `imsg rpc` support as fatal to avoid restart loops. - Auth: merge main auth profiles into per-agent stores for sub-agents and document inheritance. (#1013) — thanks @marcmarg. - Agents: avoid JSON Schema `format` collisions in tool params by renaming snapshot format fields. (#1013) — thanks @marcmarg. -- Fix: make `moltbot update` auto-update global installs when installed via a package manager. +- Fix: make `openclaw update` auto-update global installs when installed via a package manager. - Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj. - Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr. -- Fix: persist `gateway.mode=local` after selecting Local run mode in `moltbot configure`, even if no other sections are chosen. +- Fix: persist `gateway.mode=local` after selecting Local run mode in `openclaw configure`, even if no other sections are chosen. - Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter. - Agents: avoid false positives when logging unsupported Google tool schema keywords. - Agents: skip Gemini history downgrades for google-antigravity to preserve tool calls. (#894) — thanks @mukhtharcm. @@ -739,16 +820,18 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.14-1 ### Highlights + - Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure. - Browser control: Chrome extension relay takeover mode + remote browser control support. - Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba. -- Security: expanded `moltbot security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy. +- Security: expanded `openclaw security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy. ### Changes + - Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics. - Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors. - Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging. -- Security: expand `moltbot security audit` checks (model hygiene, config includes, plugin allowlists, exposure matrix) and extend `--fix` to tighten more sensitive state paths. +- Security: expand `openclaw security audit` checks (model hygiene, config includes, plugin allowlists, exposure matrix) and extend `--fix` to tighten more sensitive state paths. - Security: add `SECURITY.md` reporting policy. - Channels: add Matrix plugin (external) with docs + onboarding hooks. - Plugins: add Zalo channel plugin with gateway HTTP hooks and onboarding install prompt. (#854) — thanks @longmaba. @@ -758,9 +841,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia. - Tools: add `web_search`/`web_fetch` (Brave API), auto-enable `web_fetch` for sandboxed sessions, and remove the `brave-search` skill. - CLI/Docs: add a web tools configure section for storing Brave API keys and update onboarding tips. -- Browser: add Chrome extension relay takeover mode (toolbar button), plus `moltbot browser extension install/path` and remote browser control (standalone server + token auth). +- Browser: add Chrome extension relay takeover mode (toolbar button), plus `openclaw browser extension install/path` and remote browser control (standalone server + token auth). ### Fixes + - Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204. - Browser: add tests for snapshot labels/efficient query params and labeled image responses. - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. @@ -782,6 +866,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.14 ### Changes + - Usage: add MiniMax coding plan usage tracking. - Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR. - Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915) @@ -789,14 +874,16 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Config: add `channels..configWrites` gating for channel-initiated config writes; migrate Slack channel IDs. ### Fixes - - Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor. - - UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor. + +- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor. +- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor. - TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank. - TUI: add a bright spinner + elapsed time in the status line for send/stream/run states. - TUI: show LLM error messages (rate limits, auth, etc.) instead of `(no output)`. -- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`). +- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.openclaw-dev`). #### Agents / Auth / Tools / Sandbox + - Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams. - Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994. - Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli. @@ -809,6 +896,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. #### macOS / Apps + - macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4. - macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75. - macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor. @@ -828,12 +916,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.13 ### Fixes + - Postinstall: treat already-applied pnpm patches as no-ops to avoid npm/bun install failures. - Packaging: pin `@mariozechner/pi-ai` to 0.45.7 and refresh patched dependency to match npm resolution. ## 2026.1.12-2 ### Fixes + - Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`). - Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4. - Agents: strip invalid Gemini thought signatures from OpenRouter history to avoid 400s. (#841, #845) — thanks @MatthieuBizien. @@ -841,11 +931,13 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.12-1 ### Fixes + - Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`). ## 2026.1.12 ### Highlights + - **BREAKING:** rename chat “providers” (Slack/Telegram/WhatsApp/…) to **channels** across CLI/RPC/config; legacy config keys auto-migrate on load (and are written back as `channels.*`). - Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback. - Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI). @@ -854,22 +946,25 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Agents: add compaction mode config with optional safeguard summarization and per-agent model fallbacks. (#700) — thanks @thewilloftheshadow; (#583) — thanks @mitschabaude-bot. ### New & Improved + - Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI. (#819) — thanks @mukhtharcm. -- Memory: new `moltbot memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default. +- Memory: new `openclaw memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.openclaw/memory/{agentId}.sqlite` with watch-on-by-default. - Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config. - Tools/Sandbox: add tool profiles + group shorthands; support tool-policy groups in `tools.sandbox.tools`; drop legacy `memory` shorthand; allow Docker bind mounts via `docker.binds`. (#790) — thanks @akonyer. - Tools: add provider/model-specific tool policy overrides (`tools.byProvider`) to trim tool exposure per provider. - Tools: add browser `scrollintoview` action; allow Claude/Gemini tool param aliases; allow thinking `xhigh` for GPT-5.2/Codex with safe downgrades. (#793) — thanks @hsrvc; (#444) — thanks @grp06. -- Gateway/CLI: add Tailscale binary discovery, custom bind mode, and probe auth retry; add `moltbot dashboard` auto-open flow; default native slash commands to `"auto"` with per-provider overrides. (#740) — thanks @jeffersonwarrior. +- Gateway/CLI: add Tailscale binary discovery, custom bind mode, and probe auth retry; add `openclaw dashboard` auto-open flow; default native slash commands to `"auto"` with per-provider overrides. (#740) — thanks @jeffersonwarrior. - Auth/Onboarding: add Chutes OAuth (PKCE + refresh + onboarding choice); normalize API key inputs; default TUI onboarding to `deliver: false`. (#726) — thanks @FrieSei; (#791) — thanks @roshanasingh4. - Providers: add `discord.allowBots`; trim legacy MiniMax M2 from default catalogs; route MiniMax vision to the Coding Plan VLM endpoint (also accepts `@/path/to/file.png` inputs). (#802) — thanks @zknicker. - Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer. (#823) — thanks @roshanasingh4; (#786) — thanks @meaningfool. - Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal. ### Installer -- Install: run `moltbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. + +- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. ### Fixes + - Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds. - Tools: apply global tool allow/deny even when agent-specific tool policy is set. - Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. (#822) — thanks @sebslight; (#705) — thanks @TAGOOZ. @@ -888,12 +983,13 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Telegram: preserve forum topic thread ids, persist polling offsets, respect account bindings in webhook mode, and show typing indicator in General topics. (#727, #739) — thanks @thewilloftheshadow; (#821) — thanks @gumadeiras; (#779) — thanks @azade-c. - Slack: accept slash commands with or without leading `/` for custom command configs. (#798) — thanks @thewilloftheshadow. - Cron: persist disabled jobs correctly; accept `jobId` aliases for update/run/remove params. (#205, #252) — thanks @thewilloftheshadow. -- Gateway/CLI: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides; `agents.list` respects explicit config; reduce noisy loopback WS logs during tests; run `moltbot doctor --non-interactive` during updates. (#781) — thanks @ronyrus. +- Gateway/CLI: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides; `agents.list` respects explicit config; reduce noisy loopback WS logs during tests; run `openclaw doctor --non-interactive` during updates. (#781) — thanks @ronyrus. - Onboarding/Control UI: refuse invalid configs (run doctor first); quote Windows browser URLs for OAuth; keep chat scroll position unless the user is near the bottom. (#764) — thanks @mukhtharcm; (#794) — thanks @roshanasingh4; (#217) — thanks @thewilloftheshadow. - Tools/UI: harden tool input schemas for strict providers; drop null-only union variants for Gemini schema cleanup; treat `maxChars: 0` as unlimited; keep TUI last streamed response instead of "(no output)". (#782) — thanks @AbhisekBasu1; (#796) — thanks @gabriel-trigo; (#747) — thanks @thewilloftheshadow. - Connections UI: polish multi-account account cards. (#816) — thanks @steipete. ### Maintenance + - Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai. - Testing: update Vitest + browser-playwright to 4.0.17. - Docs: add Amazon Bedrock provider notes and link from models/FAQ. @@ -901,12 +997,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.11 ### Highlights + - Plugins are now first-class: loader + CLI management, plus the new Voice Call plugin. - Config: modular `$include` support for split config files. (#731) — thanks @pasogott. - Agents/Pi: reserve compaction headroom so pre-compaction memory writes can run before auto-compaction. - Agents: automatic pre-compaction memory flush turn to store durable memories before compaction. ### Changes + - CLI/Onboarding: simplify MiniMax auth choice to a single M2.1 option. - CLI: configure section selection now loops until Continue. - Docs: explain MiniMax vs MiniMax Lightning (speed vs cost) and restore LM Studio example. @@ -917,7 +1015,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Auto-reply: add compact `/model` picker (models + available providers) and show provider endpoints in `/model status`. - Control UI: add Config tab model presets (MiniMax M2.1, GLM 4.7, Kimi) for one-click setup. - Plugins: add extension loader (tools/RPC/CLI/services), discovery paths, and config schema + Control UI labels (uiHints). -- Plugins: add `moltbot plugins install` (path/tgz/npm), plus `list|info|enable|disable|doctor` UX. +- Plugins: add `openclaw plugins install` (path/tgz/npm), plus `list|info|enable|disable|doctor` UX. - Plugins: voice-call plugin now real (Twilio/log), adds start/status RPC/CLI/tool + tests. - Docs: add plugins doc + cross-links from tools/skills/gateway config. - Docs: add beginner-friendly plugin quick start + expand Voice Call plugin docs. @@ -930,7 +1028,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Agents: add pre-compaction memory flush config (`agents.defaults.compaction.*`) with a soft threshold + system prompt. - Config: add `$include` directive for modular config files. (#731) — thanks @pasogott. - Build: set pnpm minimum release age to 2880 minutes (2 days). (#718) — thanks @dan-dr. -- macOS: prompt to install the global `moltbot` CLI when missing in local mode; install via `molt.bot/install-cli.sh` (no onboarding) and use external launchd/CLI instead of the embedded gateway runtime. +- macOS: prompt to install the global `openclaw` CLI when missing in local mode; install via `openclaw.ai/install-cli.sh` (no onboarding) and use external launchd/CLI instead of the embedded gateway runtime. - Docs: add gog calendar event color IDs from `gog calendar colors`. (#715) — thanks @mjrussell. - Cron/CLI: add `--model` flag to cron add/edit commands. (#711) — thanks @mjrussell. - Cron/CLI: trim model overrides on cron edits and document main-session guidance. (#711) — thanks @mjrussell. @@ -942,14 +1040,16 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present. ### Installer + - Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests. - Postinstall: skip pnpm patch fallback when the new patcher is active. -- Installer tests: add root+non-root docker smokes, CI workflow to fetch molt.bot scripts and run install sh/cli with onboarding skipped. +- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped. - Installer UX: support `CLAWDBOT_NO_ONBOARD=1` for non-interactive installs; fix npm prefix on Linux and auto-install git. - Installer UX: add `install.sh --help` with flags/env and git install hint. - Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm). ### Fixes + - Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias). - Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete. - CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete. @@ -960,7 +1060,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Auth: read Codex keychain credentials and make the lookup platform-aware. - macOS/Release: avoid bundling dist artifacts in relay builds and generate appcasts from zip-only sources. - Doctor: surface plugin diagnostics in the report. -- Plugins: treat `plugins.load.paths` directory entries as package roots when they contain `package.json` + `moltbot.extensions`; load plugin packages from config dirs; extract archives without system tar. +- Plugins: treat `plugins.load.paths` directory entries as package roots when they contain `package.json` + `openclaw.extensions`; load plugin packages from config dirs; extract archives without system tar. - Config: expand `~` in `CLAWDBOT_CONFIG_PATH` and common path-like config fields (including `plugins.load.paths`); guard invalid `$include` paths. (#731) — thanks @pasogott. - Agents: stop pre-creating session transcripts so first user messages persist in JSONL history. - Agents: skip pre-compaction memory flush when the session workspace is read-only. @@ -989,28 +1089,31 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.10 ### Highlights -- CLI: `moltbot status` now table-based + shows OS/update/gateway/daemon/agents/sessions; `status --all` adds a full read-only debug report (tables, log tails, Tailscale summary, and scan progress via OSC-9 + spinner). + +- CLI: `openclaw status` now table-based + shows OS/update/gateway/daemon/agents/sessions; `status --all` adds a full read-only debug report (tables, log tails, Tailscale summary, and scan progress via OSC-9 + spinner). - CLI Backends: add Codex CLI fallback with resume support (text output) and JSONL parsing for new runs, plus a live CLI resume probe. -- CLI: add `moltbot update` (safe-ish git checkout update) + `--update` shorthand. (#673) — thanks @fm1randa. +- CLI: add `openclaw update` (safe-ish git checkout update) + `--update` shorthand. (#673) — thanks @fm1randa. - Gateway: add OpenAI-compatible `/v1/chat/completions` HTTP endpoint (auth, SSE streaming, per-agent routing). (#680). ### Changes + - Onboarding/Models: add first-class Z.AI (GLM) auth choice (`zai-api-key`) + `--zai-api-key` flag. - CLI/Onboarding: add OpenRouter API key auth option in configure/onboard. (#703) — thanks @mteam88. - Agents: add human-delay pacing between block replies (modes: off/natural/custom, per-agent configurable). (#446) — thanks @tony-freedomology. - Agents/Browser: add `browser.target` (sandbox/host/custom) with sandbox host-control gating via `agents.defaults.sandbox.browser.allowHostControl`, allowlists for custom control URLs/hosts/ports, and expand browser tool docs (remote control, profiles, internals). - Onboarding/Models: add catalog-backed default model picker to onboarding + configure. (#611) — thanks @jonasjancarik. - Agents/OpenCode Zen: update fallback models + defaults, keep legacy alias mappings. (#669) — thanks @magimetal. -- CLI: add `moltbot reset` and `moltbot uninstall` flows (interactive + non-interactive) plus docker cleanup smoke test. +- CLI: add `openclaw reset` and `openclaw uninstall` flows (interactive + non-interactive) plus docker cleanup smoke test. - Providers: move provider wiring to a plugin architecture. (#661). - Providers: unify group history context wrappers across providers with per-provider/per-account `historyLimit` overrides (fallback to `messages.groupChat.historyLimit`). Set `0` to disable. (#672). - Gateway/Heartbeat: optionally deliver heartbeat `Reasoning:` output (`agents.defaults.heartbeat.includeReasoning`). (#690) - Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo. ### Fixes + - Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesn’t leak partial output. - CLI/Status: expand tables to full terminal width; clarify provider setup vs runtime warnings; richer per-provider detail; token previews in `status` while keeping `status --all` redacted; add troubleshooting link footer; keep log tails pasteable; show gateway auth used when reachable; surface provider runtime errors (Signal/iMessage/Slack); harden `tailscale status --json` parsing; make `status --all` scan progress determinate; and replace the footer with a 3-line “Next steps” recommendation (share/debug/probe). -- CLI/Gateway: clarify that `moltbot gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures. +- CLI/Gateway: clarify that `openclaw gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures. - CLI/Update: gate progress spinner on stdout TTY and align clean-check step label. (#701) — thanks @bjesuiter. - Telegram: add `/whoami` + `/id` commands to reveal sender id for allowlists; allow `@username` and prefixed ids in `allowFrom` prompts (with stability warning). - Heartbeat: strip markup-wrapped `HEARTBEAT_OK` so acks don’t leak to external providers (e.g., Telegram). @@ -1023,7 +1126,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Agents/Pi: inject config `temperature`/`maxTokens` into streaming without replacing the session streamFn; cover with live maxTokens probe. (#732) — thanks @peschee. - macOS: clear unsigned launchd overrides on signed restarts and warn via doctor when attach-only/disable markers are set. (#695) — thanks @jeffersonwarrior. - Agents: enforce single-writer session locks and drop orphan tool results to prevent tool-call ID failures (MiniMax/Anthropic-compatible APIs). -- Docs: make `moltbot status` the first diagnostic step, clarify `status --deep` behavior, and document `/whoami` + `/id`. +- Docs: make `openclaw status` the first diagnostic step, clarify `status --deep` behavior, and document `/whoami` + `/id`. - Docs/Testing: clarify live tool+image probes and how to list your testable `provider/model` ids. - Tests/Live: make gateway bash+read probes resilient to provider formatting while still validating real tool calls. - WhatsApp: detect @lid mentions in groups using authDir reverse mapping + resolve self JID E.164 for mention gating. (#692) — thanks @peschee. @@ -1043,7 +1146,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - WhatsApp: expose group participant IDs to the model so reactions can target the right sender. - Cron: `wakeMode: "now"` waits for heartbeat completion (and retries when the main lane is busy). (#666) — thanks @roshanasingh4. - Agents/OpenAI: fix Responses tool-only → follow-up turn handling (avoid standalone `reasoning` items that trigger 400 “required following item”) and replay reasoning items in Responses/Codex Responses history for tool-call-only turns. -- Sandbox: add `moltbot sandbox explain` (effective policy inspector + fix-it keys); improve “sandbox jail” tool-policy/elevated errors with actionable config key paths; link to docs. +- Sandbox: add `openclaw sandbox explain` (effective policy inspector + fix-it keys); improve “sandbox jail” tool-policy/elevated errors with actionable config key paths; link to docs. - Hooks/Gmail: keep Tailscale serve path at `/` while preserving the public path. (#668) — thanks @antons. - Hooks/Gmail: allow Tailscale target URLs to preserve internal serve paths. - Auth: update Claude Code keychain credentials in-place during refresh sync; share JSON file helpers; add CLI fallback coverage. @@ -1055,12 +1158,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Gateway/Control UI: sniff image attachments for chat.send, drop non-images, and log mismatches. (#670) — thanks @cristip73. - macOS: force `restart-mac.sh --sign` to require identities and keep bundled Node signed for relay verification. (#580) — thanks @jeffersonwarrior. - Gateway/Agent: accept image attachments on `agent` (multimodal message) and add live gateway image probe (`CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1`). -- CLI: `moltbot sessions` now includes `elev:*` + `usage:*` flags in the table output. +- CLI: `openclaw sessions` now includes `elev:*` + `usage:*` flags in the table output. - CLI/Pairing: accept positional provider for `pairing list|approve` (npm-run compatible); update docs/bot hints. -- Branding: normalize legacy casing/branding to “Moltbot” (CLI, status, docs). +- Branding: normalize legacy casing/branding to “OpenClaw” (CLI, status, docs). - Auto-reply: fix native `/model` not updating the actual chat session (Telegram/Slack/Discord). (#646) -- Doctor: offer to run `moltbot update` first on git installs (keeps doctor output aligned with latest). -- Doctor: avoid false legacy workspace warning when install dir is `~/moltbot`. (#660) +- Doctor: offer to run `openclaw update` first on git installs (keeps doctor output aligned with latest). +- Doctor: avoid false legacy workspace warning when install dir is `~/openclaw`. (#660) - iMessage: fix reasoning persistence across DMs; avoid partial/duplicate replies when reasoning is enabled. (#655) — thanks @antons. - Models/Auth: allow MiniMax API configs without `models.providers.minimax.apiKey` (auth profiles / `MINIMAX_API_KEY`). (#656) — thanks @mneves75. - Agents: avoid duplicate replies when the message tool sends. (#659) — thanks @mickahouan. @@ -1079,10 +1182,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Agents: repair session transcripts by dropping duplicate tool results across the whole history (unblocks Anthropic-compatible APIs after retries). - Tests/Live: reset the gateway session between model runs to avoid cross-provider transcript incompatibilities (notably OpenAI Responses reasoning replay rules). - ## 2026.1.9 ### Highlights + - Microsoft Teams provider: polling, attachments, outbound CLI send, per-channel policy. - Models/Auth expansion: OpenCode Zen + MiniMax API onboarding; token auth profiles + auth order; OAuth health in doctor/status. - CLI/Gateway UX: message subcommands, gateway discover/status/SSH, /config + /debug, sandbox CLI. @@ -1091,19 +1194,21 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX. ### Breaking -- CLI: `moltbot message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured. + +- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured. - Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`. ### New Features and Changes + - Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff. -- Models/Auth: setup-token + token auth profiles; `moltbot models auth order {get,set,clear}`; per-agent auth candidates in `/model status`; OAuth expiry checks in doctor/status. +- Models/Auth: setup-token + token auth profiles; `openclaw models auth order {get,set,clear}`; per-agent auth candidates in `/model status`; OAuth expiry checks in doctor/status. - Agent/System: claude-cli runner; `session_status` tool (and sandbox allow); adaptive context pruning default; system prompt messaging guidance + no auto self-update; eligible skills list injection; sub-agent context trimmed. - Commands: `/commands` list; `/models` alias; `/usage` alias; `/debug` runtime overrides + effective config view; `/config` chat updates + `/config get`; `config --section`. - CLI/Gateway: unified message tool + message subcommands; gateway discover (local + wide-area DNS-SD) with JSON/timeout; gateway status human-readable + JSON + SSH loopback; wide-area records include gatewayPort/sshPort/cliPath + tailnet DNS fallback. - CLI UX: logs output modes (pretty/plain/JSONL) + colorized health/daemon output; global `--no-color`; lobster palette in onboarding/config. - Dev ergonomics: gateway `--dev/--reset` + dev profile auto-config; C-3PO dev templates; dev gateway/TUI helper scripts. - Sandbox/Workspace: sandbox list/recreate commands; sync skills into sandbox workspace; sandbox browser auto-start. -- Config/Onboarding: inline env vars; OpenAI API key flow to shared `~/.clawdbot/.env`; Opus 4.5 default prompt for Anthropic auth; QuickStart auto-install gateway (Node-only) + provider picker tweaks + skip-systemd flags; TUI bootstrap prompt (`tui --message`); remove Bun runtime choice. +- Config/Onboarding: inline env vars; OpenAI API key flow to shared `~/.openclaw/.env`; Opus 4.5 default prompt for Anthropic auth; QuickStart auto-install gateway (Node-only) + provider picker tweaks + skip-systemd flags; TUI bootstrap prompt (`tui --message`); remove Bun runtime choice. - Providers: Microsoft Teams provider (polling, attachments, outbound sends, requireMention, config reload/DM policy). (#404) — thanks @onutc - Providers: WhatsApp broadcast groups for multi-agent replies (#547) — thanks @pasogott; inbound media size cap configurable (#505) — thanks @koala73; identity-based message prefixes (#578) — thanks @p6l-richard. - Providers: Telegram inline keyboard buttons + callback payload routing (#491) — thanks @azade-c; cron topic delivery targets (#474/#478) — thanks @mitschabaude-bot, @nachoiacovino; `[[audio_as_voice]]` tag support (#490) — thanks @jarvis-medmatic. @@ -1116,6 +1221,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Apps/Branding: refreshed iOS/Android/macOS icons (#521) — thanks @fishfisher. ### Fixes + - Packaging: include MS Teams send module in npm tarball. - Sandbox/Browser: auto-start CDP endpoint; proxy CDP out of container for attachOnly; relax Bun fetch typing; align sandbox list output with config images. - Agents/Runtime: gate heartbeat prompt to default sessions; /stop aborts between tool calls; require explicit system-event session keys; guard small context windows; fix model fallback stringification; sessions_spawn inherits provider; failover on billing/credits; respect auth cooldown ordering; restore Anthropic OAuth tool dispatch + tool-name bypass; avoid OpenAI invalid reasoning replay; harden Gmail hook model defaults. @@ -1133,7 +1239,8 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag. ### Maintenance -- Dependencies: bump pi-* stack to 0.42.2. + +- Dependencies: bump pi-\* stack to 0.42.2. - Dependencies: Pi 0.40.0 bump (#543) — thanks @mcinteerj. - Build: Docker build cache layer (#605) — thanks @zknicker. @@ -1142,6 +1249,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.8 ### Highlights + - Security: DMs locked down by default across providers; pairing-first + allowlist guidance. - Sandbox: per-agent scope defaults + workspace access controls; tool/session isolation tuned. - Agent loop: compaction, pruning, streaming, and error handling hardened. @@ -1150,11 +1258,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded. ### Breaking + - **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack. - Previously, if you didn’t configure an allowlist, your bot could be **open to anyone** (especially discoverable Telegram bots). - New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`). - To keep old “open to everyone” behavior: set `dmPolicy="open"` and include `"*"` in the relevant `allowFrom` (Discord/Slack: `discord.dm.allowFrom` / `slack.dm.allowFrom`). - - Approve requests via `moltbot pairing list ` + `moltbot pairing approve `. + - Approve requests via `openclaw pairing list ` + `openclaw pairing approve `. - Sandbox: default `agent.sandbox.scope` to `"agent"` (one container/workspace per agent). Use `"session"` for per-session isolation; `"shared"` disables cross-session isolation. - Timestamps in agent envelopes are now UTC (compact `YYYY-MM-DDTHH:mmZ`); removed `messages.timestampPrefix`. Add `agent.userTimezone` to tell the model the user’s local time (system prompt only). - Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup. @@ -1164,24 +1273,27 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops. ### Fixes + - **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints. - **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking. - **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification. - **Providers (Telegram/WhatsApp/Discord/Slack/Signal/iMessage):** retry/backoff, threading, reactions, media groups/attachments, mention gating, typing behavior, and error/log stability; long polling + forum topic isolation for Telegram. -- **Gateway/CLI UX:** `moltbot logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification. +- **Gateway/CLI UX:** `openclaw logs`, cron list colors/aliases, docs search, agents list/add/delete flows, status usage snapshots, runtime/auth source display, and `/status`/commands auth unification. - **Control UI/Web:** logs tab, focus mode polish, config form resilience, streaming stability, tool output caps, windowed chat history, and reconnect/password URL auth. - **macOS/Android/TUI/Build:** macOS gateway races, QR bundling, JSON5 config safety, Voice Wake hardening; Android EXIF rotation + APK naming/versioning; TUI key handling; tooling/bundling fixes. - **Packaging/compat:** npm dist folder coverage, Node 25 qrcode-terminal import fixes, Bun/Playwright/WebSocket patches, and Docker Bun install. -- **Docs:** new FAQ/ClawdHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs. +- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs. ### Maintenance + - Skills additions (Himalaya email, CodexBar, 1Password). -- Dependency refreshes (pi-* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite). +- Dependency refreshes (pi-\* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite). - Refactors: centralized group allowlist/mention policy; lint/import cleanup; switch tsx → bun for TS execution. ## 2026.1.5 ### Highlights + - Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support. - Agent tools: new `image` tool routed to the image model (when configured). - Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`). @@ -1189,6 +1301,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Bun: optional local install/build workflow without maintaining a Bun lockfile (see `docs/bun.md`). ### Fixes + - Control UI: render Markdown in tool result cards. - Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts. - Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids @@ -1202,7 +1315,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - WhatsApp: mark offline history sync messages as read without auto-reply. (#193) — thanks @mcinteerj - Discord: avoid duplicate replies when a provider emits late streaming `text_end` events (OpenAI/GPT). - CLI: use tailnet IP for local gateway calls when bind is tailnet/auto (fixes #176). -- Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`. +- Env: load global `$OPENCLAW_STATE_DIR/.env` (`~/.openclaw/.env`) as a fallback after CWD `.env`. - Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env). - Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas). - Onboarding: when running from source, auto-build missing Control UI assets (`bun run ui:build`). @@ -1210,4 +1323,4 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Agent tools: honor `agent.tools` allow/deny policy even when sandbox is off. - Discord: avoid duplicate replies when OpenAI emits repeated `message_end` events. - Commands: unify /status (inline) and command auth across providers; group bypass for authorized control commands; remove Discord /clawd slash handler. -- CLI: run `moltbot agent` via the Gateway by default; use `--local` to force embedded mode. +- CLI: run `openclaw agent` via the Gateway by default; use `--local` to force embedded mode. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 509d5b11a..1bf0c961c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,12 @@ -# Contributing to Moltbot +# Contributing to OpenClaw Welcome to the lobster tank! 🦞 ## Quick Links -- **GitHub:** https://github.com/moltbot/moltbot + +- **GitHub:** https://github.com/openclaw/openclaw - **Discord:** https://discord.gg/qkhbAGHRBT -- **X/Twitter:** [@steipete](https://x.com/steipete) / [@moltbot](https://x.com/moltbot) +- **X/Twitter:** [@steipete](https://x.com/steipete) / [@openclaw](https://x.com/openclaw) ## Maintainers @@ -19,13 +20,15 @@ Welcome to the lobster tank! 🦞 - GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes) ## How to Contribute + 1. **Bugs & small fixes** → Open a PR! -2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/moltbot/moltbot/discussions) or ask in Discord first +2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/openclaw/openclaw/discussions) or ask in Discord first 3. **Questions** → Discord #setup-help ## Before You PR -- Test locally with your Moltbot instance -- Run linter: `npm run lint` + +- Test locally with your OpenClaw instance +- Run tests: `pnpm tsgo && pnpm format && pnpm lint && pnpm build && pnpm test` - Keep PRs focused (one thing per PR) - Describe what & why @@ -34,6 +37,7 @@ Welcome to the lobster tank! 🦞 Built with Codex, Claude, or other AI tools? **Awesome - just mark it!** Please include in your PR: + - [ ] Mark as AI-assisted in the PR title or description - [ ] Note the degree of testing (untested / lightly tested / fully tested) - [ ] Include prompts or session logs if possible (super helpful!) @@ -44,9 +48,10 @@ AI PRs are first-class citizens here. We just want transparency so reviewers kno ## Current Focus & Roadmap 🗺 We are currently prioritizing: + - **Stability**: Fixing edge cases in channel connections (WhatsApp/Telegram). - **UX**: Improving the onboarding wizard and error messages. - **Skills**: Expanding the library of bundled skills and improving the Skill Creation developer experience. - **Performance**: Optimizing token usage and compaction logic. -Check the [GitHub Issues](https://github.com/moltbot/moltbot/issues) for "good first issue" labels! +Check the [GitHub Issues](https://github.com/openclaw/openclaw/issues) for "good first issue" labels! diff --git a/Dockerfile b/Dockerfile index 9c6aa7036..ad08bd37c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,10 @@ RUN corepack enable WORKDIR /app -ARG CLAWDBOT_DOCKER_APT_PACKAGES="" -RUN if [ -n "$CLAWDBOT_DOCKER_APT_PACKAGES" ]; then \ +ARG OPENCLAW_DOCKER_APT_PACKAGES="" +RUN if [ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]; then \ apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $CLAWDBOT_DOCKER_APT_PACKAGES && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $OPENCLAW_DOCKER_APT_PACKAGES && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \ fi @@ -24,10 +24,9 @@ COPY scripts ./scripts RUN pnpm install --frozen-lockfile COPY . . -RUN CLAWDBOT_A2UI_SKIP_MISSING=1 pnpm build +RUN OPENCLAW_A2UI_SKIP_MISSING=1 pnpm build # Force pnpm for UI build (Bun may fail on ARM/Synology architectures) -ENV CLAWDBOT_PREFER_PNPM=1 -RUN pnpm ui:install +ENV OPENCLAW_PREFER_PNPM=1 RUN pnpm ui:build ENV NODE_ENV=production diff --git a/Dockerfile.sandbox-browser b/Dockerfile.sandbox-browser index 8d8a08701..05090881e 100644 --- a/Dockerfile.sandbox-browser +++ b/Dockerfile.sandbox-browser @@ -20,9 +20,9 @@ RUN apt-get update \ xvfb \ && rm -rf /var/lib/apt/lists/* -COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/moltbot-sandbox-browser -RUN chmod +x /usr/local/bin/moltbot-sandbox-browser +COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/openclaw-sandbox-browser +RUN chmod +x /usr/local/bin/openclaw-sandbox-browser EXPOSE 9222 5900 6080 -CMD ["moltbot-sandbox-browser"] +CMD ["openclaw-sandbox-browser"] diff --git a/README.md b/README.md index 7e884be33..d375461ec 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# 🦞 Moltbot — Personal AI Assistant +# 🦞 OpenClaw — Personal AI Assistant

- Clawdbot + + + OpenClaw +

@@ -9,68 +12,67 @@

- CI status - GitHub release - DeepWiki + CI status + GitHub release Discord MIT License

-**Moltbot** is a *personal AI assistant* you run on your own devices. +**OpenClaw** is a _personal AI assistant_ you run on your own devices. It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant. If you want a personal, single-user assistant that feels local, fast, and always-on, this is it. -[Website](https://molt.bot) · [Docs](https://docs.molt.bot) · [Getting Started](https://docs.molt.bot/start/getting-started) · [Updating](https://docs.molt.bot/install/updating) · [Showcase](https://docs.molt.bot/start/showcase) · [FAQ](https://docs.molt.bot/start/faq) · [Wizard](https://docs.molt.bot/start/wizard) · [Nix](https://github.com/moltbot/nix-clawdbot) · [Docker](https://docs.molt.bot/install/docker) · [Discord](https://discord.gg/clawd) +[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-clawdbot) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd) -Preferred setup: run the onboarding wizard (`moltbot onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**. +Preferred setup: run the onboarding wizard (`openclaw onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**. Works with npm, pnpm, or bun. -New install? Start here: [Getting started](https://docs.molt.bot/start/getting-started) +New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started) **Subscriptions (OAuth):** + - **[Anthropic](https://www.anthropic.com/)** (Claude Pro/Max) - **[OpenAI](https://openai.com/)** (ChatGPT/Codex) -Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.molt.bot/start/onboarding). +Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.openclaw.ai/start/onboarding). ## Models (selection + auth) -- Models config + CLI: [Models](https://docs.molt.bot/concepts/models) -- Auth profile rotation (OAuth vs API keys) + fallbacks: [Model failover](https://docs.molt.bot/concepts/model-failover) +- Models config + CLI: [Models](https://docs.openclaw.ai/concepts/models) +- Auth profile rotation (OAuth vs API keys) + fallbacks: [Model failover](https://docs.openclaw.ai/concepts/model-failover) ## Install (recommended) Runtime: **Node ≥22**. ```bash -npm install -g moltbot@latest -# or: pnpm add -g moltbot@latest +npm install -g openclaw@latest +# or: pnpm add -g openclaw@latest -moltbot onboard --install-daemon +openclaw onboard --install-daemon ``` The wizard installs the Gateway daemon (launchd/systemd user service) so it stays running. -Legacy note: `clawdbot` remains available as a compatibility shim. ## Quick start (TL;DR) Runtime: **Node ≥22**. -Full beginner guide (auth, pairing, channels): [Getting started](https://docs.molt.bot/start/getting-started) +Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started) ```bash -moltbot onboard --install-daemon +openclaw onboard --install-daemon -moltbot gateway --port 18789 --verbose +openclaw gateway --port 18789 --verbose # Send a message -moltbot message send --to +1234567890 --message "Hello from Moltbot" +openclaw message send --to +1234567890 --message "Hello from OpenClaw" # Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/Microsoft Teams/Matrix/Zalo/Zalo Personal/WebChat) -moltbot agent --message "Ship checklist" --thinking high +openclaw agent --message "Ship checklist" --thinking high ``` -Upgrading? [Updating guide](https://docs.molt.bot/install/updating) (and run `moltbot doctor`). +Upgrading? [Updating guide](https://docs.openclaw.ai/install/updating) (and run `openclaw doctor`). ## Development channels @@ -78,94 +80,101 @@ Upgrading? [Updating guide](https://docs.molt.bot/install/updating) (and run `mo - **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing). - **dev**: moving head of `main`, npm dist-tag `dev` (when published). -Switch channels (git + npm): `moltbot update --channel stable|beta|dev`. -Details: [Development channels](https://docs.molt.bot/install/development-channels). +Switch channels (git + npm): `openclaw update --channel stable|beta|dev`. +Details: [Development channels](https://docs.openclaw.ai/install/development-channels). ## From source (development) Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly. ```bash -git clone https://github.com/moltbot/moltbot.git -cd moltbot +git clone https://github.com/openclaw/openclaw.git +cd openclaw pnpm install pnpm ui:build # auto-installs UI deps on first run pnpm build -pnpm moltbot onboard --install-daemon +pnpm openclaw onboard --install-daemon # Dev loop (auto-reload on TS changes) pnpm gateway:watch ``` -Note: `pnpm moltbot ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `moltbot` binary. +Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `openclaw` binary. ## Security defaults (DM access) -Moltbot connects to real messaging surfaces. Treat inbound DMs as **untrusted input**. +OpenClaw connects to real messaging surfaces. Treat inbound DMs as **untrusted input**. -Full security guide: [Security](https://docs.molt.bot/gateway/security) +Full security guide: [Security](https://docs.openclaw.ai/gateway/security) Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack: + - **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message. -- Approve with: `moltbot pairing approve ` (then the sender is added to a local allowlist store). +- Approve with: `openclaw pairing approve ` (then the sender is added to a local allowlist store). - Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`). -Run `moltbot doctor` to surface risky/misconfigured DM policies. +Run `openclaw doctor` to surface risky/misconfigured DM policies. ## Highlights -- **[Local-first Gateway](https://docs.molt.bot/gateway)** — single control plane for sessions, channels, tools, and events. -- **[Multi-channel inbox](https://docs.molt.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android. -- **[Multi-agent routing](https://docs.molt.bot/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions). -- **[Voice Wake](https://docs.molt.bot/nodes/voicewake) + [Talk Mode](https://docs.molt.bot/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs. -- **[Live Canvas](https://docs.molt.bot/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.molt.bot/platforms/mac/canvas#canvas-a2ui). -- **[First-class tools](https://docs.molt.bot/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions. -- **[Companion apps](https://docs.molt.bot/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.molt.bot/nodes). -- **[Onboarding](https://docs.molt.bot/start/wizard) + [skills](https://docs.molt.bot/tools/skills)** — wizard-driven setup with bundled/managed/workspace skills. +- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events. +- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android. +- **[Multi-agent routing](https://docs.openclaw.ai/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions). +- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs. +- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui). +- **[First-class tools](https://docs.openclaw.ai/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions. +- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes). +- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — wizard-driven setup with bundled/managed/workspace skills. ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=moltbot/moltbot&type=date&legend=top-left)](https://www.star-history.com/#moltbot/moltbot&type=date&legend=top-left) +[![Star History Chart](https://api.star-history.com/svg?repos=openclaw/openclaw&type=date&legend=top-left)](https://www.star-history.com/#openclaw/openclaw&type=date&legend=top-left) ## Everything we built so far ### Core platform -- [Gateway WS control plane](https://docs.molt.bot/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.molt.bot/web), and [Canvas host](https://docs.molt.bot/platforms/mac/canvas#canvas-a2ui). -- [CLI surface](https://docs.molt.bot/tools/agent-send): gateway, agent, send, [wizard](https://docs.molt.bot/start/wizard), and [doctor](https://docs.molt.bot/gateway/doctor). -- [Pi agent runtime](https://docs.molt.bot/concepts/agent) in RPC mode with tool streaming and block streaming. -- [Session model](https://docs.molt.bot/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.molt.bot/concepts/groups). -- [Media pipeline](https://docs.molt.bot/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.molt.bot/nodes/audio). + +- [Gateway WS control plane](https://docs.openclaw.ai/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.openclaw.ai/web), and [Canvas host](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui). +- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [wizard](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor). +- [Pi agent runtime](https://docs.openclaw.ai/concepts/agent) in RPC mode with tool streaming and block streaming. +- [Session model](https://docs.openclaw.ai/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.openclaw.ai/concepts/groups). +- [Media pipeline](https://docs.openclaw.ai/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.openclaw.ai/nodes/audio). ### Channels -- [Channels](https://docs.molt.bot/channels): [WhatsApp](https://docs.molt.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.molt.bot/channels/telegram) (grammY), [Slack](https://docs.molt.bot/channels/slack) (Bolt), [Discord](https://docs.molt.bot/channels/discord) (discord.js), [Google Chat](https://docs.molt.bot/channels/googlechat) (Chat API), [Signal](https://docs.molt.bot/channels/signal) (signal-cli), [iMessage](https://docs.molt.bot/channels/imessage) (imsg), [BlueBubbles](https://docs.molt.bot/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.molt.bot/channels/msteams) (extension), [Matrix](https://docs.molt.bot/channels/matrix) (extension), [Zalo](https://docs.molt.bot/channels/zalo) (extension), [Zalo Personal](https://docs.molt.bot/channels/zalouser) (extension), [WebChat](https://docs.molt.bot/web/webchat). -- [Group routing](https://docs.molt.bot/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.molt.bot/channels). + +- [Channels](https://docs.openclaw.ai/channels): [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) (Baileys), [Telegram](https://docs.openclaw.ai/channels/telegram) (grammY), [Slack](https://docs.openclaw.ai/channels/slack) (Bolt), [Discord](https://docs.openclaw.ai/channels/discord) (discord.js), [Google Chat](https://docs.openclaw.ai/channels/googlechat) (Chat API), [Signal](https://docs.openclaw.ai/channels/signal) (signal-cli), [iMessage](https://docs.openclaw.ai/channels/imessage) (imsg), [BlueBubbles](https://docs.openclaw.ai/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.openclaw.ai/channels/msteams) (extension), [Matrix](https://docs.openclaw.ai/channels/matrix) (extension), [Zalo](https://docs.openclaw.ai/channels/zalo) (extension), [Zalo Personal](https://docs.openclaw.ai/channels/zalouser) (extension), [WebChat](https://docs.openclaw.ai/web/webchat). +- [Group routing](https://docs.openclaw.ai/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.openclaw.ai/channels). ### Apps + nodes -- [macOS app](https://docs.molt.bot/platforms/macos): menu bar control plane, [Voice Wake](https://docs.molt.bot/nodes/voicewake)/PTT, [Talk Mode](https://docs.molt.bot/nodes/talk) overlay, [WebChat](https://docs.molt.bot/web/webchat), debug tools, [remote gateway](https://docs.molt.bot/gateway/remote) control. -- [iOS node](https://docs.molt.bot/platforms/ios): [Canvas](https://docs.molt.bot/platforms/mac/canvas), [Voice Wake](https://docs.molt.bot/nodes/voicewake), [Talk Mode](https://docs.molt.bot/nodes/talk), camera, screen recording, Bonjour pairing. -- [Android node](https://docs.molt.bot/platforms/android): [Canvas](https://docs.molt.bot/platforms/mac/canvas), [Talk Mode](https://docs.molt.bot/nodes/talk), camera, screen recording, optional SMS. -- [macOS node mode](https://docs.molt.bot/nodes): system.run/notify + canvas/camera exposure. + +- [macOS app](https://docs.openclaw.ai/platforms/macos): menu bar control plane, [Voice Wake](https://docs.openclaw.ai/nodes/voicewake)/PTT, [Talk Mode](https://docs.openclaw.ai/nodes/talk) overlay, [WebChat](https://docs.openclaw.ai/web/webchat), debug tools, [remote gateway](https://docs.openclaw.ai/gateway/remote) control. +- [iOS node](https://docs.openclaw.ai/platforms/ios): [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), [Voice Wake](https://docs.openclaw.ai/nodes/voicewake), [Talk Mode](https://docs.openclaw.ai/nodes/talk), camera, screen recording, Bonjour pairing. +- [Android node](https://docs.openclaw.ai/platforms/android): [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), [Talk Mode](https://docs.openclaw.ai/nodes/talk), camera, screen recording, optional SMS. +- [macOS node mode](https://docs.openclaw.ai/nodes): system.run/notify + canvas/camera exposure. ### Tools + automation -- [Browser control](https://docs.molt.bot/tools/browser): dedicated moltbot Chrome/Chromium, snapshots, actions, uploads, profiles. -- [Canvas](https://docs.molt.bot/platforms/mac/canvas): [A2UI](https://docs.molt.bot/platforms/mac/canvas#canvas-a2ui) push/reset, eval, snapshot. -- [Nodes](https://docs.molt.bot/nodes): camera snap/clip, screen record, [location.get](https://docs.molt.bot/nodes/location-command), notifications. -- [Cron + wakeups](https://docs.molt.bot/automation/cron-jobs); [webhooks](https://docs.molt.bot/automation/webhook); [Gmail Pub/Sub](https://docs.molt.bot/automation/gmail-pubsub). -- [Skills platform](https://docs.molt.bot/tools/skills): bundled, managed, and workspace skills with install gating + UI. + +- [Browser control](https://docs.openclaw.ai/tools/browser): dedicated openclaw Chrome/Chromium, snapshots, actions, uploads, profiles. +- [Canvas](https://docs.openclaw.ai/platforms/mac/canvas): [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui) push/reset, eval, snapshot. +- [Nodes](https://docs.openclaw.ai/nodes): camera snap/clip, screen record, [location.get](https://docs.openclaw.ai/nodes/location-command), notifications. +- [Cron + wakeups](https://docs.openclaw.ai/automation/cron-jobs); [webhooks](https://docs.openclaw.ai/automation/webhook); [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub). +- [Skills platform](https://docs.openclaw.ai/tools/skills): bundled, managed, and workspace skills with install gating + UI. ### Runtime + safety -- [Channel routing](https://docs.molt.bot/concepts/channel-routing), [retry policy](https://docs.molt.bot/concepts/retry), and [streaming/chunking](https://docs.molt.bot/concepts/streaming). -- [Presence](https://docs.molt.bot/concepts/presence), [typing indicators](https://docs.molt.bot/concepts/typing-indicators), and [usage tracking](https://docs.molt.bot/concepts/usage-tracking). -- [Models](https://docs.molt.bot/concepts/models), [model failover](https://docs.molt.bot/concepts/model-failover), and [session pruning](https://docs.molt.bot/concepts/session-pruning). -- [Security](https://docs.molt.bot/gateway/security) and [troubleshooting](https://docs.molt.bot/channels/troubleshooting). + +- [Channel routing](https://docs.openclaw.ai/concepts/channel-routing), [retry policy](https://docs.openclaw.ai/concepts/retry), and [streaming/chunking](https://docs.openclaw.ai/concepts/streaming). +- [Presence](https://docs.openclaw.ai/concepts/presence), [typing indicators](https://docs.openclaw.ai/concepts/typing-indicators), and [usage tracking](https://docs.openclaw.ai/concepts/usage-tracking). +- [Models](https://docs.openclaw.ai/concepts/models), [model failover](https://docs.openclaw.ai/concepts/model-failover), and [session pruning](https://docs.openclaw.ai/concepts/session-pruning). +- [Security](https://docs.openclaw.ai/gateway/security) and [troubleshooting](https://docs.openclaw.ai/channels/troubleshooting). ### Ops + packaging -- [Control UI](https://docs.molt.bot/web) + [WebChat](https://docs.molt.bot/web/webchat) served directly from the Gateway. -- [Tailscale Serve/Funnel](https://docs.molt.bot/gateway/tailscale) or [SSH tunnels](https://docs.molt.bot/gateway/remote) with token/password auth. -- [Nix mode](https://docs.molt.bot/install/nix) for declarative config; [Docker](https://docs.molt.bot/install/docker)-based installs. -- [Doctor](https://docs.molt.bot/gateway/doctor) migrations, [logging](https://docs.molt.bot/logging). + +- [Control UI](https://docs.openclaw.ai/web) + [WebChat](https://docs.openclaw.ai/web/webchat) served directly from the Gateway. +- [Tailscale Serve/Funnel](https://docs.openclaw.ai/gateway/tailscale) or [SSH tunnels](https://docs.openclaw.ai/gateway/remote) with token/password auth. +- [Nix mode](https://docs.openclaw.ai/install/nix) for declarative config; [Docker](https://docs.openclaw.ai/install/docker)-based installs. +- [Doctor](https://docs.openclaw.ai/gateway/doctor) migrations, [logging](https://docs.openclaw.ai/logging). ## How it works (short) @@ -180,7 +189,7 @@ WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage / BlueBu └──────────────┬────────────────┘ │ ├─ Pi agent (RPC) - ├─ CLI (moltbot …) + ├─ CLI (openclaw …) ├─ WebChat UI ├─ macOS app └─ iOS / Android nodes @@ -188,28 +197,29 @@ WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage / BlueBu ## Key subsystems -- **[Gateway WebSocket network](https://docs.molt.bot/concepts/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.molt.bot/gateway)). -- **[Tailscale exposure](https://docs.molt.bot/gateway/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.molt.bot/gateway/remote)). -- **[Browser control](https://docs.molt.bot/tools/browser)** — moltbot‑managed Chrome/Chromium with CDP control. -- **[Canvas + A2UI](https://docs.molt.bot/platforms/mac/canvas)** — agent‑driven visual workspace (A2UI host: [Canvas/A2UI](https://docs.molt.bot/platforms/mac/canvas#canvas-a2ui)). -- **[Voice Wake](https://docs.molt.bot/nodes/voicewake) + [Talk Mode](https://docs.molt.bot/nodes/talk)** — always‑on speech and continuous conversation. -- **[Nodes](https://docs.molt.bot/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `system.run`/`system.notify`. +- **[Gateway WebSocket network](https://docs.openclaw.ai/concepts/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.openclaw.ai/gateway)). +- **[Tailscale exposure](https://docs.openclaw.ai/gateway/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.openclaw.ai/gateway/remote)). +- **[Browser control](https://docs.openclaw.ai/tools/browser)** — openclaw‑managed Chrome/Chromium with CDP control. +- **[Canvas + A2UI](https://docs.openclaw.ai/platforms/mac/canvas)** — agent‑driven visual workspace (A2UI host: [Canvas/A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui)). +- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — always‑on speech and continuous conversation. +- **[Nodes](https://docs.openclaw.ai/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `system.run`/`system.notify`. ## Tailscale access (Gateway dashboard) -Moltbot can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (public) while the Gateway stays bound to loopback. Configure `gateway.tailscale.mode`: +OpenClaw can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (public) while the Gateway stays bound to loopback. Configure `gateway.tailscale.mode`: - `off`: no Tailscale automation (default). - `serve`: tailnet-only HTTPS via `tailscale serve` (uses Tailscale identity headers by default). - `funnel`: public HTTPS via `tailscale funnel` (requires shared password auth). Notes: -- `gateway.bind` must stay `loopback` when Serve/Funnel is enabled (Moltbot enforces this). + +- `gateway.bind` must stay `loopback` when Serve/Funnel is enabled (OpenClaw enforces this). - Serve can be forced to require a password by setting `gateway.auth.mode: "password"` or `gateway.auth.allowTailscale: false`. - Funnel refuses to start unless `gateway.auth.mode: "password"` is set. - Optional: `gateway.tailscale.resetOnExit` to undo Serve/Funnel on shutdown. -Details: [Tailscale guide](https://docs.molt.bot/gateway/tailscale) · [Web surfaces](https://docs.molt.bot/web) +Details: [Tailscale guide](https://docs.openclaw.ai/gateway/tailscale) · [Web surfaces](https://docs.openclaw.ai/web) ## Remote Gateway (Linux is great) @@ -217,9 +227,9 @@ It’s perfectly fine to run the Gateway on a small Linux instance. Clients (mac - **Gateway host** runs the exec tool and channel connections by default. - **Device nodes** run device‑local actions (`system.run`, camera, screen recording, notifications) via `node.invoke`. -In short: exec runs where the Gateway lives; device actions run where the device lives. + In short: exec runs where the Gateway lives; device actions run where the device lives. -Details: [Remote access](https://docs.molt.bot/gateway/remote) · [Nodes](https://docs.molt.bot/nodes) · [Security](https://docs.molt.bot/gateway/security) +Details: [Remote access](https://docs.openclaw.ai/gateway/remote) · [Nodes](https://docs.openclaw.ai/nodes) · [Security](https://docs.openclaw.ai/gateway/security) ## macOS permissions via the Gateway protocol @@ -234,22 +244,22 @@ Elevated bash (host permissions) is separate from macOS TCC: - Use `/elevated on|off` to toggle per‑session elevated access when enabled + allowlisted. - Gateway persists the per‑session toggle via `sessions.patch` (WS method) alongside `thinkingLevel`, `verboseLevel`, `model`, `sendPolicy`, and `groupActivation`. -Details: [Nodes](https://docs.molt.bot/nodes) · [macOS app](https://docs.molt.bot/platforms/macos) · [Gateway protocol](https://docs.molt.bot/concepts/architecture) +Details: [Nodes](https://docs.openclaw.ai/nodes) · [macOS app](https://docs.openclaw.ai/platforms/macos) · [Gateway protocol](https://docs.openclaw.ai/concepts/architecture) -## Agent to Agent (sessions_* tools) +## Agent to Agent (sessions\_\* tools) - Use these to coordinate work across sessions without jumping between chat surfaces. - `sessions_list` — discover active sessions (agents) and their metadata. - `sessions_history` — fetch transcript logs for a session. - `sessions_send` — message another session; optional reply‑back ping‑pong + announce step (`REPLY_SKIP`, `ANNOUNCE_SKIP`). -Details: [Session tools](https://docs.molt.bot/concepts/session-tool) +Details: [Session tools](https://docs.openclaw.ai/concepts/session-tool) -## Skills registry (ClawdHub) +## Skills registry (ClawHub) -ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can search for skills automatically and pull in new ones as needed. +ClawHub is a minimal skill registry. With ClawHub enabled, the agent can search for skills automatically and pull in new ones as needed. -[ClawdHub](https://ClawdHub.com) +[ClawHub](https://clawhub.com) ## Chat commands @@ -270,7 +280,7 @@ The Gateway alone delivers a great experience. All apps are optional and add ext If you plan to build/run companion apps, follow the platform runbooks below. -### macOS (Moltbot.app) (optional) +### macOS (OpenClaw.app) (optional) - Menu bar control for the Gateway and health. - Voice Wake + push-to-talk overlay. @@ -283,35 +293,35 @@ Note: signed builds required for macOS permissions to stick across rebuilds (see - Pairs as a node via the Bridge. - Voice trigger forwarding + Canvas surface. -- Controlled via `moltbot nodes …`. +- Controlled via `openclaw nodes …`. -Runbook: [iOS connect](https://docs.molt.bot/platforms/ios). +Runbook: [iOS connect](https://docs.openclaw.ai/platforms/ios). ### Android node (optional) - Pairs via the same Bridge + pairing flow as iOS. - Exposes Canvas, Camera, and Screen capture commands. -- Runbook: [Android connect](https://docs.molt.bot/platforms/android). +- Runbook: [Android connect](https://docs.openclaw.ai/platforms/android). ## Agent workspace + skills -- Workspace root: `~/clawd` (configurable via `agents.defaults.workspace`). +- Workspace root: `~/.openclaw/workspace` (configurable via `agents.defaults.workspace`). - Injected prompt files: `AGENTS.md`, `SOUL.md`, `TOOLS.md`. -- Skills: `~/clawd/skills//SKILL.md`. +- Skills: `~/.openclaw/workspace/skills//SKILL.md`. ## Configuration -Minimal `~/.clawdbot/moltbot.json` (model + defaults): +Minimal `~/.openclaw/openclaw.json` (model + defaults): ```json5 { agent: { - model: "anthropic/claude-opus-4-5" - } + model: "anthropic/claude-opus-4-5", + }, } ``` -[Full configuration reference (all keys + examples).](https://docs.molt.bot/gateway/configuration) +[Full configuration reference (all keys + examples).](https://docs.openclaw.ai/gateway/configuration) ## Security model (important) @@ -319,15 +329,15 @@ Minimal `~/.clawdbot/moltbot.json` (model + defaults): - **Group/channel safety:** set `agents.defaults.sandbox.mode: "non-main"` to run **non‑main sessions** (groups/channels) inside per‑session Docker sandboxes; bash then runs in Docker for those sessions. - **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`. -Details: [Security guide](https://docs.molt.bot/gateway/security) · [Docker + sandboxing](https://docs.molt.bot/install/docker) · [Sandbox config](https://docs.molt.bot/gateway/configuration) +Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker + sandboxing](https://docs.openclaw.ai/install/docker) · [Sandbox config](https://docs.openclaw.ai/gateway/configuration) -### [WhatsApp](https://docs.molt.bot/channels/whatsapp) +### [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) -- Link the device: `pnpm moltbot channels login` (stores creds in `~/.clawdbot/credentials`). +- Link the device: `pnpm openclaw channels login` (stores creds in `~/.openclaw/credentials`). - Allowlist who can talk to the assistant via `channels.whatsapp.allowFrom`. - If `channels.whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all. -### [Telegram](https://docs.molt.bot/channels/telegram) +### [Telegram](https://docs.openclaw.ai/channels/telegram) - Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` (env wins). - Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` as needed. @@ -336,17 +346,17 @@ Details: [Security guide](https://docs.molt.bot/gateway/security) · [Docker + s { channels: { telegram: { - botToken: "123456:ABCDEF" - } - } + botToken: "123456:ABCDEF", + }, + }, } ``` -### [Slack](https://docs.molt.bot/channels/slack) +### [Slack](https://docs.openclaw.ai/channels/slack) - Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `channels.slack.botToken` + `channels.slack.appToken`). -### [Discord](https://docs.molt.bot/channels/discord) +### [Discord](https://docs.openclaw.ai/channels/discord) - Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins). - Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.dm.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed. @@ -355,27 +365,27 @@ Details: [Security guide](https://docs.molt.bot/gateway/security) · [Docker + s { channels: { discord: { - token: "1234abcd" - } - } + token: "1234abcd", + }, + }, } ``` -### [Signal](https://docs.molt.bot/channels/signal) +### [Signal](https://docs.openclaw.ai/channels/signal) - Requires `signal-cli` and a `channels.signal` config section. -### [iMessage](https://docs.molt.bot/channels/imessage) +### [iMessage](https://docs.openclaw.ai/channels/imessage) - macOS only; Messages must be signed in. - If `channels.imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all. -### [Microsoft Teams](https://docs.molt.bot/channels/msteams) +### [Microsoft Teams](https://docs.openclaw.ai/channels/msteams) - Configure a Teams app + Bot Framework, then add a `msteams` config section. - Allowlist who can talk via `msteams.allowFrom`; group access via `msteams.groupAllowFrom` or `msteams.groupPolicy: "open"`. -### [WebChat](https://docs.molt.bot/web/webchat) +### [WebChat](https://docs.openclaw.ai/web/webchat) - Uses the Gateway WebSocket; no separate WebChat port/config. @@ -385,87 +395,88 @@ Browser control (optional): { browser: { enabled: true, - color: "#FF4500" - } + color: "#FF4500", + }, } ``` ## Docs Use these when you’re past the onboarding flow and want the deeper reference. -- [Start with the docs index for navigation and “what’s where.”](https://docs.molt.bot) -- [Read the architecture overview for the gateway + protocol model.](https://docs.molt.bot/concepts/architecture) -- [Use the full configuration reference when you need every key and example.](https://docs.molt.bot/gateway/configuration) -- [Run the Gateway by the book with the operational runbook.](https://docs.molt.bot/gateway) -- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.molt.bot/web) -- [Understand remote access over SSH tunnels or tailnets.](https://docs.molt.bot/gateway/remote) -- [Follow the onboarding wizard flow for a guided setup.](https://docs.molt.bot/start/wizard) -- [Wire external triggers via the webhook surface.](https://docs.molt.bot/automation/webhook) -- [Set up Gmail Pub/Sub triggers.](https://docs.molt.bot/automation/gmail-pubsub) -- [Learn the macOS menu bar companion details.](https://docs.molt.bot/platforms/mac/menu-bar) -- [Platform guides: Windows (WSL2)](https://docs.molt.bot/platforms/windows), [Linux](https://docs.molt.bot/platforms/linux), [macOS](https://docs.molt.bot/platforms/macos), [iOS](https://docs.molt.bot/platforms/ios), [Android](https://docs.molt.bot/platforms/android) -- [Debug common failures with the troubleshooting guide.](https://docs.molt.bot/channels/troubleshooting) -- [Review security guidance before exposing anything.](https://docs.molt.bot/gateway/security) + +- [Start with the docs index for navigation and “what’s where.”](https://docs.openclaw.ai) +- [Read the architecture overview for the gateway + protocol model.](https://docs.openclaw.ai/concepts/architecture) +- [Use the full configuration reference when you need every key and example.](https://docs.openclaw.ai/gateway/configuration) +- [Run the Gateway by the book with the operational runbook.](https://docs.openclaw.ai/gateway) +- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.openclaw.ai/web) +- [Understand remote access over SSH tunnels or tailnets.](https://docs.openclaw.ai/gateway/remote) +- [Follow the onboarding wizard flow for a guided setup.](https://docs.openclaw.ai/start/wizard) +- [Wire external triggers via the webhook surface.](https://docs.openclaw.ai/automation/webhook) +- [Set up Gmail Pub/Sub triggers.](https://docs.openclaw.ai/automation/gmail-pubsub) +- [Learn the macOS menu bar companion details.](https://docs.openclaw.ai/platforms/mac/menu-bar) +- [Platform guides: Windows (WSL2)](https://docs.openclaw.ai/platforms/windows), [Linux](https://docs.openclaw.ai/platforms/linux), [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android) +- [Debug common failures with the troubleshooting guide.](https://docs.openclaw.ai/channels/troubleshooting) +- [Review security guidance before exposing anything.](https://docs.openclaw.ai/gateway/security) ## Advanced docs (discovery + control) -- [Discovery + transports](https://docs.molt.bot/gateway/discovery) -- [Bonjour/mDNS](https://docs.molt.bot/gateway/bonjour) -- [Gateway pairing](https://docs.molt.bot/gateway/pairing) -- [Remote gateway README](https://docs.molt.bot/gateway/remote-gateway-readme) -- [Control UI](https://docs.molt.bot/web/control-ui) -- [Dashboard](https://docs.molt.bot/web/dashboard) +- [Discovery + transports](https://docs.openclaw.ai/gateway/discovery) +- [Bonjour/mDNS](https://docs.openclaw.ai/gateway/bonjour) +- [Gateway pairing](https://docs.openclaw.ai/gateway/pairing) +- [Remote gateway README](https://docs.openclaw.ai/gateway/remote-gateway-readme) +- [Control UI](https://docs.openclaw.ai/web/control-ui) +- [Dashboard](https://docs.openclaw.ai/web/dashboard) ## Operations & troubleshooting -- [Health checks](https://docs.molt.bot/gateway/health) -- [Gateway lock](https://docs.molt.bot/gateway/gateway-lock) -- [Background process](https://docs.molt.bot/gateway/background-process) -- [Browser troubleshooting (Linux)](https://docs.molt.bot/tools/browser-linux-troubleshooting) -- [Logging](https://docs.molt.bot/logging) +- [Health checks](https://docs.openclaw.ai/gateway/health) +- [Gateway lock](https://docs.openclaw.ai/gateway/gateway-lock) +- [Background process](https://docs.openclaw.ai/gateway/background-process) +- [Browser troubleshooting (Linux)](https://docs.openclaw.ai/tools/browser-linux-troubleshooting) +- [Logging](https://docs.openclaw.ai/logging) ## Deep dives -- [Agent loop](https://docs.molt.bot/concepts/agent-loop) -- [Presence](https://docs.molt.bot/concepts/presence) -- [TypeBox schemas](https://docs.molt.bot/concepts/typebox) -- [RPC adapters](https://docs.molt.bot/reference/rpc) -- [Queue](https://docs.molt.bot/concepts/queue) +- [Agent loop](https://docs.openclaw.ai/concepts/agent-loop) +- [Presence](https://docs.openclaw.ai/concepts/presence) +- [TypeBox schemas](https://docs.openclaw.ai/concepts/typebox) +- [RPC adapters](https://docs.openclaw.ai/reference/rpc) +- [Queue](https://docs.openclaw.ai/concepts/queue) ## Workspace & skills -- [Skills config](https://docs.molt.bot/tools/skills-config) -- [Default AGENTS](https://docs.molt.bot/reference/AGENTS.default) -- [Templates: AGENTS](https://docs.molt.bot/reference/templates/AGENTS) -- [Templates: BOOTSTRAP](https://docs.molt.bot/reference/templates/BOOTSTRAP) -- [Templates: IDENTITY](https://docs.molt.bot/reference/templates/IDENTITY) -- [Templates: SOUL](https://docs.molt.bot/reference/templates/SOUL) -- [Templates: TOOLS](https://docs.molt.bot/reference/templates/TOOLS) -- [Templates: USER](https://docs.molt.bot/reference/templates/USER) +- [Skills config](https://docs.openclaw.ai/tools/skills-config) +- [Default AGENTS](https://docs.openclaw.ai/reference/AGENTS.default) +- [Templates: AGENTS](https://docs.openclaw.ai/reference/templates/AGENTS) +- [Templates: BOOTSTRAP](https://docs.openclaw.ai/reference/templates/BOOTSTRAP) +- [Templates: IDENTITY](https://docs.openclaw.ai/reference/templates/IDENTITY) +- [Templates: SOUL](https://docs.openclaw.ai/reference/templates/SOUL) +- [Templates: TOOLS](https://docs.openclaw.ai/reference/templates/TOOLS) +- [Templates: USER](https://docs.openclaw.ai/reference/templates/USER) ## Platform internals -- [macOS dev setup](https://docs.molt.bot/platforms/mac/dev-setup) -- [macOS menu bar](https://docs.molt.bot/platforms/mac/menu-bar) -- [macOS voice wake](https://docs.molt.bot/platforms/mac/voicewake) -- [iOS node](https://docs.molt.bot/platforms/ios) -- [Android node](https://docs.molt.bot/platforms/android) -- [Windows (WSL2)](https://docs.molt.bot/platforms/windows) -- [Linux app](https://docs.molt.bot/platforms/linux) +- [macOS dev setup](https://docs.openclaw.ai/platforms/mac/dev-setup) +- [macOS menu bar](https://docs.openclaw.ai/platforms/mac/menu-bar) +- [macOS voice wake](https://docs.openclaw.ai/platforms/mac/voicewake) +- [iOS node](https://docs.openclaw.ai/platforms/ios) +- [Android node](https://docs.openclaw.ai/platforms/android) +- [Windows (WSL2)](https://docs.openclaw.ai/platforms/windows) +- [Linux app](https://docs.openclaw.ai/platforms/linux) ## Email hooks (Gmail) -- [docs.molt.bot/gmail-pubsub](https://docs.molt.bot/automation/gmail-pubsub) +- [docs.openclaw.ai/gmail-pubsub](https://docs.openclaw.ai/automation/gmail-pubsub) ## Molty -Moltbot was built for **Molty**, a space lobster AI assistant. 🦞 +OpenClaw was built for **Molty**, a space lobster AI assistant. 🦞 by Peter Steinberger and the community. -- [clawd.me](https://clawd.me) +- [openclaw.ai](https://openclaw.ai) - [soul.md](https://soul.md) - [steipete.me](https://steipete.me) -- [@moltbot](https://x.com/moltbot) +- [@openclaw](https://x.com/openclaw) ## Community @@ -474,41 +485,46 @@ AI/vibe-coded PRs welcome! 🤖 Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for [pi-mono](https://github.com/badlogic/pi-mono). +Special thanks to Adam Doppelt for lobster.bot. Thanks to all clawtributors:

- steipete plum-dawg bohdanpodvirnyi iHildy jaydenfyi joaohlisboa mneves75 MatthieuBizien MaudeBot Glucksberg - rahthakor vrknetha radek-paclt Tobias Bischoff joshp123 vignesh07 czekaj mukhtharcm sebslight maxsumrall - xadenryan rodrigouroz juanpablodlc hsrvc magimetal zerone0x meaningfool tyler6204 patelhiren NicholasSpisak - jonisjongithub abhisekbasu1 jamesgroat claude JustYannicc Hyaxia dantelex SocialNerd42069 daveonkels google-labs-jules[bot] - lc0rp mousberg adam91holt hougangdev gumadeiras mteam88 hirefrank joeynyc orlyjamie dbhurley - Mariano Belinky Eng. Juan Combetto TSavo julianengel bradleypriest benithors rohannagpal timolins f-trycua benostein - nachx639 shakkernerd pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino Vasanth Rao Naik Sabavat petter-b - cpojer scald thewilloftheshadow andranik-sahakyan davidguttman sleontenko denysvitali sircrumpet peschee rafaelreis-r - dominicnunez ratulsarna lutr0 danielz1z AdeboyeDN Alg0rix papago2355 emanuelst KristijanJovanovski rdev - rhuanssauro joshrad-dev kiranjd osolmaz adityashaw2 CashWilliams sheeek artuskg Takhoffman onutc - pauloportella neooriginal manuelhettich minghinmatthewlam myfunc travisirby buddyh connorshea kyleok obviyus - mcinteerj dependabot[bot] John-Rood timkrase uos-status gerardward2007 roshanasingh4 tosh-hamburg azade-c dlauer - JonUleis bjesuiter cheeeee robbyczgw-cla Josh Phillips YuriNachos pookNast Whoaa512 chriseidhof ngutman - ysqander aj47 kennyklee superman32432432 Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] damoahdominic - dan-dr HeimdallStrategy imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse - dougvk erikpr1994 fal3 Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr - neist sibbl chrisrodz Friederike Seiler gabriel-trigo iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 - manmal ogulcancelik pasogott petradonka rubyrunsstuff siddhantjain suminhthanh svkozak VACInc wes-davis - zats 24601 ameno- Chris Taylor dguido Django Navarro evalexpr henrino3 humanwritten larlyssa - odysseus0 oswalpalash pcty-nextgen-service-account rmorse Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot - Clawd ClawdFx EnzeD erik-agens Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira jayhickey - jeffersonwarrior jeffersonwarrior jverdi longmaba mickahouan mjrussell odnxe p6l-richard philipp-spiess Pocket Clawd - robaxelsen Sash Catanzarite T5-AndyML travisp VAC william arzt zknicker 0oAstro abhaymundhara alejandro maza - Alex-Alaniz alexstyl andrewting19 anpoirier arthyn Asleep123 bolismauro chenyuan99 Clawdbot Maintainers conhecendoia - dasilva333 David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen Felix Krause foeken ganghyun kim grrowl gtsifrikas - HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jane Jarvis Jefferson Nunn jogi47 kentaro Kevin Lin - kitze Kiwitwitter levifig Lloyd longjos loukotal louzhixian martinpucik Matt mini mertcicekci0 - Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman nexty5870 Noctivoro ppamment prathamdby ptn1411 - reeltimeapps RLTCmpe Rolf Fredheim Rony Kelner Samrat Jha senoldogann Seredeep sergical shiv19 shiyuanhai - siraht snopoke Suksham-sharma techboss testingabc321 The Admiral thesash Ubuntu voidserf Vultr-Clawd Admin - Wimmie wolfred wstock yazinsai YiWang24 ymat19 Zach Knickerbocker 0xJonHoldsCrypto aaronn Alphonse-arianee - atalovesyou Azade carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder - Quentin Randy Torres rhjoh ronak-guliani William Stock + steipete cpojer plum-dawg bohdanpodvirnyi iHildy jaydenfyi joaohlisboa mneves75 MatthieuBizien MaudeBot + Glucksberg rahthakor vrknetha radek-paclt vignesh07 joshp123 Tobias Bischoff czekaj mukhtharcm sebslight + maxsumrall xadenryan rodrigouroz Mariano Belinky tyler6204 juanpablodlc hsrvc magimetal zerone0x meaningfool + patelhiren NicholasSpisak jonisjongithub abhisekbasu1 jamesgroat claude JustYannicc SocialNerd42069 Hyaxia dantelex + daveonkels google-labs-jules[bot] lc0rp adam91holt mousberg hougangdev gumadeiras shakkernerd mteam88 hirefrank + joeynyc orlyjamie Eng. Juan Combetto dbhurley TSavo julianengel bradleypriest benithors rohannagpal elliotsecops + timolins benostein f-trycua nachx639 pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino + Vasanth Rao Naik Sabavat petter-b thewilloftheshadow scald andranik-sahakyan davidguttman sleontenko denysvitali sircrumpet peschee + rafaelreis-r nonggialiang dominicnunez lploc94 ratulsarna lutr0 kiranjd danielz1z AdeboyeDN Alg0rix + papago2355 emanuelst evanotero KristijanJovanovski CashWilliams jlowin rdev rhuanssauro osolmaz joshrad-dev + adityashaw2 sheeek ryancontent jasonsschin obviyus artuskg Takhoffman onutc pauloportella HirokiKobayashi-R + ThanhNguyxn yuting0624 neooriginal manuelhettich minghinmatthewlam manikv12 myfunc travisirby buddyh connorshea + kyleok mcinteerj dependabot[bot] amitbiswal007 John-Rood timkrase uos-status gerardward2007 roshanasingh4 tosh-hamburg + azade-c dlauer JonUleis shivamraut101 bjesuiter cheeeee robbyczgw-cla YuriNachos badlogic conroywhitney + Josh Phillips pookNast Whoaa512 chriseidhof ngutman ysqander Yurii Chukhlib aj47 kennyklee superman32432432 + grp06 Hisleren antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy imfing jalehman + jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures robhparker Ryan Lisse dougvk erikpr1994 fal3 + Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz + Friederike Seiler gabriel-trigo iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 manmal ogulcancelik pasogott + petradonka rubyrunsstuff siddhantjain spiceoogway suminhthanh svkozak VACInc wes-davis zats 24601 + ameno- Chris Taylor dguido Django Navarro evalexpr henrino3 humanwritten larlyssa Lukavyi odysseus0 + oswalpalash pcty-nextgen-service-account pi0 rmorse Roopak Nijhara Syhids Ubuntu Aaron Konyer aaronveklabs andreabadesso + Andrii cash-echo-bot Clawd ClawdFx EnzeD erik-agens Evizero fcatuhe itsjaydesu ivancasco + ivanrvpereira Jarvis jayhickey jeffersonwarrior jeffersonwarrior jverdi longmaba MarvinCui mjrussell odnxe + optimikelabs p6l-richard philipp-spiess Pocket Clawd robaxelsen Sash Catanzarite Suksham-sharma T5-AndyML tewatia travisp + VAC william arzt zknicker 0oAstro abhaymundhara aduk059 alejandro maza Alex-Alaniz alexstyl andrewting19 + anpoirier araa47 arthyn Asleep123 Ayush Ojha Ayush10 bguidolim bolismauro championswimmer chenyuan99 + Chloe-VP Clawdbot Maintainers conhecendoia dasilva333 David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen dylanneve1 Felix Krause + foeken frankekn ganghyun kim grrowl gtsifrikas HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jane + Jarvis Deploy Jefferson Nunn jogi47 kentaro Kevin Lin kira-ariaki kitze Kiwitwitter levifig Lloyd + longjos loukotal louzhixian martinpucik Matt mini mertcicekci0 Miles mitsuhiko mrdbstn MSch + Mustafa Tag Eldeen mylukin nathanbosse ndraiman nexty5870 Noctivoro ppamment prathamdby ptn1411 reeltimeapps + RLTCmpe Rolf Fredheim Rony Kelner Samrat Jha senoldogann Seredeep sergical shiv19 shiyuanhai siraht + snopoke techboss testingabc321 The Admiral thesash Vibe Kanban voidserf Vultr-Clawd Admin Wimmie wolfred + wstock YangHuang2280 yazinsai yevhen YiWang24 ymat19 Zach Knickerbocker zackerthescar 0xJonHoldsCrypto aaronn + Alphonse-arianee atalovesyou Azade carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik + pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani William Stock

diff --git a/SECURITY.md b/SECURITY.md index 414def17f..92f9d0fa4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,27 +1,38 @@ # Security Policy -If you believe you've found a security issue in Moltbot, please report it privately. +If you believe you've found a security issue in OpenClaw, please report it privately. ## Reporting - Email: `steipete@gmail.com` - What to include: reproduction steps, impact assessment, and (if possible) a minimal PoC. +## Bug Bounties + +OpenClaw is a labor of love. There is no bug bounty program and no budget for paid reports. Please still disclose responsibly so we can fix issues quickly. +The best way to help the project right now is by sending PRs. + +## Out of Scope + +- Public Internet Exposure +- Using OpenClaw in ways that the docs recommend not to +- Prompt injection attacks + ## Operational Guidance -For threat model + hardening guidance (including `moltbot security audit --deep` and `--fix`), see: +For threat model + hardening guidance (including `openclaw security audit --deep` and `--fix`), see: -- `https://docs.molt.bot/gateway/security` +- `https://docs.openclaw.ai/gateway/security` ### Web Interface Safety -Moltbot's web interface is intended for local use only. Do **not** bind it to the public internet; it is not hardened for public exposure. +OpenClaw's web interface is intended for local use only. Do **not** bind it to the public internet; it is not hardened for public exposure. ## Runtime Requirements ### Node.js Version -Moltbot requires **Node.js 22.12.0 or later** (LTS). This version includes important security patches: +OpenClaw requires **Node.js 22.12.0 or later** (LTS). This version includes important security patches: - CVE-2025-59466: async_hooks DoS vulnerability - CVE-2026-21636: Permission model bypass vulnerability @@ -34,7 +45,7 @@ node --version # Should be v22.12.0 or later ### Docker Security -When running Moltbot in Docker: +When running OpenClaw in Docker: 1. The official image runs as a non-root user (`node`) for reduced attack surface 2. Use `--read-only` flag when possible for additional filesystem protection @@ -44,8 +55,8 @@ Example secure Docker run: ```bash docker run --read-only --cap-drop=ALL \ - -v moltbot-data:/app/data \ - moltbot/moltbot:latest + -v openclaw-data:/app/data \ + openclaw/openclaw:latest ``` ## Security Scanning diff --git a/Swabble/Package.resolved b/Swabble/Package.resolved index 24de6ea3a..f52a51fbe 100644 --- a/Swabble/Package.resolved +++ b/Swabble/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c0677e232394b5f6b0191b6dbb5bae553d55264f65ae725cd03a8ffdfda9cdd3", + "originHash" : "24a723309d7a0039d3df3051106f77ac1ed7068a02508e3a6804e41d757e6c72", "pins" : [ { "identity" : "commander", @@ -10,6 +10,24 @@ "version" : "0.2.1" } }, + { + "identity" : "elevenlabskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/steipete/ElevenLabsKit", + "state" : { + "revision" : "7e3c948d8340abe3977014f3de020edf221e9269", + "version" : "0.1.0" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "5a3825302b1a0d744183200915a47b508c828e6f", + "version" : "1.3.2" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -27,6 +45,24 @@ "revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211", "version" : "0.99.0" } + }, + { + "identity" : "swiftui-math", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swiftui-math", + "state" : { + "revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71", + "version" : "0.1.0" + } + }, + { + "identity" : "textual", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/textual", + "state" : { + "revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38", + "version" : "0.3.1" + } } ], "version" : 3 diff --git a/appcast.xml b/appcast.xml index 568632fd7..4aa70fb61 100644 --- a/appcast.xml +++ b/appcast.xml @@ -1,188 +1,199 @@ - Moltbot + OpenClaw + + 2026.1.30 + Sat, 31 Jan 2026 14:29:57 +0100 + https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml + 8469 + 2026.1.30 + 15.0 + OpenClaw 2026.1.30 +

Changes

+
    +
  • CLI: add completion command (Zsh/Bash/PowerShell/Fish) and auto-setup during postinstall/onboarding.
  • +
  • CLI: add per-agent models status (--agent filter). (#4780) Thanks @jlowin.
  • +
  • Agents: add Kimi K2.5 to the synthetic model catalog. (#4407) Thanks @manikv12.
  • +
  • Auth: switch Kimi Coding to built-in provider; normalize OAuth profile email.
  • +
  • Auth: add MiniMax OAuth plugin + onboarding option. (#4521) Thanks @Maosghoul.
  • +
  • Agents: update pi SDK/API usage and dependencies.
  • +
  • Web UI: refresh sessions after chat commands and improve session display names.
  • +
  • Build: move TypeScript builds to tsdown + tsgo (faster builds, CI typechecks), update tsconfig target, and clean up lint rules.
  • +
  • Build: align npm tar override and bin metadata so the openclaw CLI entrypoint is preserved in npm publishes.
  • +
  • Docs: add pi/pi-dev docs and update OpenClaw branding + install links.
  • +
+

Fixes

+
    +
  • Security: restrict local path extraction in media parser to prevent LFI. (#4880)
  • +
  • Gateway: prevent token defaults from becoming the literal "undefined". (#4873) Thanks @Hisleren.
  • +
  • Control UI: fix assets resolution for npm global installs. (#4909) Thanks @YuriNachos.
  • +
  • macOS: avoid stderr pipe backpressure in gateway discovery. (#3304) Thanks @abhijeet117.
  • +
  • Telegram: normalize account token lookup for non-normalized IDs. (#5055) Thanks @jasonsschin.
  • +
  • Telegram: preserve delivery thread fallback and fix threadId handling in delivery context.
  • +
  • Telegram: fix HTML nesting for overlapping styles/links. (#4578) Thanks @ThanhNguyxn.
  • +
  • Telegram: accept numeric messageId/chatId in react actions. (#4533) Thanks @Ayush10.
  • +
  • Telegram: honor per-account proxy dispatcher via undici fetch. (#4456) Thanks @spiceoogway.
  • +
  • Telegram: scope skill commands to bound agent per bot. (#4360) Thanks @robhparker.
  • +
  • BlueBubbles: debounce by messageId to preserve attachments in text+image messages. (#4984)
  • +
  • Routing: prefer requesterOrigin over stale session entries for sub-agent announce delivery. (#4957)
  • +
  • Extensions: restore embedded extension discovery typings.
  • +
  • CLI: fix tui:dev port resolution.
  • +
  • LINE: fix status command TypeError. (#4651)
  • +
  • OAuth: skip expired-token warnings when refresh tokens are still valid. (#4593)
  • +
  • Build: skip redundant UI install step in Dockerfile. (#4584) Thanks @obviyus.
  • +
+

View full changelog

+]]>
+ +
+ + 2026.1.29 + Fri, 30 Jan 2026 06:24:15 +0100 + https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml + 8345 + 2026.1.29 + 15.0 + OpenClaw 2026.1.29 +Status: stable. +

Changes

+
    +
  • Rebrand: rename the npm package/CLI to openclaw, add a openclaw compatibility shim, and move extensions to the @openclaw/* scope.
  • +
  • Onboarding: strengthen security warning copy for beta + access control expectations.
  • +
  • Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub.
  • +
  • Config: auto-migrate legacy state/config paths and keep config resolution consistent across legacy filenames.
  • +
  • Gateway: warn on hook tokens via query params; document header auth preference. (#2200) Thanks @YuriNachos.
  • +
  • Gateway: add dangerous Control UI device auth bypass flag + audit warnings. (#2248)
  • +
  • Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz.
  • +
  • Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7.
  • +
  • Browser: route browser control via gateway/node; remove standalone browser control command and control URL config.
  • +
  • Browser: route browser.request via node proxies when available; honor proxy timeouts; derive browser ports from gateway.port.
  • +
  • Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
  • +
  • Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
  • +
  • Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon.
  • +
  • Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
  • +
  • Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma.
  • +
  • Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21.
  • +
  • Telegram: support quote replies for message tool and inbound context. (#2900) Thanks @aduk059.
  • +
  • Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos.
  • +
  • Telegram: send sticker pixels to vision models. (#2650)
  • +
  • Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc.
  • +
  • Discord: add configurable privileged gateway intents for presences/members. (#2266) Thanks @kentaro.
  • +
  • Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
  • +
  • Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk.
  • +
  • Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a.
  • +
  • Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt.
  • +
  • Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47.
  • +
  • Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr.
  • +
  • Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281)
  • +
  • Memory Search: allow extra paths for memory indexing (ignores symlinks). (#3600) Thanks @kira-ariaki.
  • +
  • Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204.
  • +
  • Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger.
  • +
  • Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
  • +
  • Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam.
  • +
  • Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
  • +
  • CLI: use Node's module compile cache for faster startup. (#2808) Thanks @pi0.
  • +
  • Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
  • +
  • TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein.
  • +
  • macOS: finish OpenClaw app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3.
  • +
  • Branding: update launchd labels, mobile bundle IDs, and logging subsystems to bot.molt (legacy bundle ID migrations). Thanks @thewilloftheshadow.
  • +
  • macOS: limit project-local node_modules/.bin PATH preference to debug builds (reduce PATH hijacking risk).
  • +
  • macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.
  • +
  • macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn.
  • +
  • Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
  • +
  • Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro.
  • +
  • CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi.
  • +
  • Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.
  • +
  • Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst.
  • +
  • Docs: tighten Fly private deployment steps. (#2289) Thanks @dguido.
  • +
  • Docs: add migration guide for moving to a new machine. (#2381)
  • +
  • Docs: add Northflank one-click deployment guide. (#2167) Thanks @AdeboyeDN.
  • +
  • Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng.
  • +
  • Docs: add Render deployment guide. (#1975) Thanks @anurag.
  • +
  • Docs: add Claude Max API Proxy guide. (#1875) Thanks @atalovesyou.
  • +
  • Docs: add DigitalOcean deployment guide. (#1870) Thanks @0xJonHoldsCrypto.
  • +
  • Docs: add Oracle Cloud (OCI) platform guide + cross-links. (#2333) Thanks @hirefrank.
  • +
  • Docs: add Raspberry Pi install guide. (#1871) Thanks @0xJonHoldsCrypto.
  • +
  • Docs: add GCP Compute Engine deployment guide. (#1848) Thanks @hougangdev.
  • +
  • Docs: add LINE channel guide. Thanks @thewilloftheshadow.
  • +
  • Docs: credit both contributors for Control UI refresh. (#1852) Thanks @EnzeD.
  • +
  • Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99.
  • +
  • Docs: update exe.dev install instructions. (#https://github.com/openclaw/openclaw/pull/3047) Thanks @zackerthescar.
  • +
+

Breaking

+
    +
  • BREAKING: Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
  • +
+

Fixes

+
    +
  • Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796)
  • +
  • Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R.
  • +
  • Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald.
  • +
  • Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald.
  • +
  • Agents: prevent retries on oversized image errors and surface size limits. (#2871) Thanks @Suksham-sharma.
  • +
  • Agents: inherit provider baseUrl/api for inline models. (#2740) Thanks @lploc94.
  • +
  • Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355.
  • +
  • Telegram: include AccountId in native command context for multi-agent routing. (#2942) Thanks @Chloe-VP.
  • +
  • Telegram: handle video note attachments in media extraction. (#2905) Thanks @mylukin.
  • +
  • TTS: read OPENAI_TTS_BASE_URL at runtime instead of module load to honor config.env. (#3341) Thanks @hclsys.
  • +
  • macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee.
  • +
  • Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101.
  • +
  • Gateway: prevent crashes on transient network errors (fetch failures, timeouts, DNS). Added fatal error detection to only exit on truly critical errors. Fixes #2895, #2879, #2873. (#2980) Thanks @elliotsecops.
  • +
  • Agents: guard channel tool listActions to avoid plugin crashes. (#2859) Thanks @mbelinky.
  • +
  • Discord: stop resolveDiscordTarget from passing directory params into messaging target parsers. Fixes #3167. Thanks @thewilloftheshadow.
  • +
  • Discord: avoid resolving bare channel names to user DMs when a username matches. Thanks @thewilloftheshadow.
  • +
  • Discord: fix directory config type import for target resolution. Thanks @thewilloftheshadow.
  • +
  • Providers: update MiniMax API endpoint and compatibility mode. (#3064) Thanks @hlbbbbbbb.
  • +
  • Telegram: treat more network errors as recoverable in polling. (#3013) Thanks @ryancontent.
  • +
  • Discord: resolve usernames to user IDs for outbound messages. (#2649) Thanks @nonggialiang.
  • +
  • Providers: update Moonshot Kimi model references to kimi-k2.5. (#2762) Thanks @MarvinCui.
  • +
  • Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.
  • +
  • TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.
  • +
  • Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
  • +
  • Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez.
  • +
  • CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo.
  • +
  • CLI: avoid prompting for gateway runtime under the spinner. (#2874)
  • +
  • BlueBubbles: coalesce inbound URL link preview messages. (#1981) Thanks @tyler6204.
  • +
  • Cron: allow payloads containing "heartbeat" in event filter. (#2219) Thanks @dwfinkelstein.
  • +
  • CLI: avoid loading config for global help/version while registering plugin commands. (#2212) Thanks @dial481.
  • +
  • Agents: include memory.md when bootstrapping memory context. (#2318) Thanks @czekaj.
  • +
  • Agents: release session locks on process termination and cover more signals. (#2483) Thanks @janeexai.
  • +
  • Agents: skip cooldowned providers during model failover. (#2143) Thanks @YiWang24.
  • +
  • Telegram: harden polling + retry behavior for transient network errors and Node 22 transport issues. (#2420) Thanks @techboss.
  • +
  • Telegram: ignore non-forum group message_thread_id while preserving DM thread sessions. (#2731) Thanks @dylanneve1.
  • +
  • Telegram: wrap reasoning italics per line to avoid raw underscores. (#2181) Thanks @YuriNachos.
  • +
  • Telegram: centralize API error logging for delivery and bot calls. (#2492) Thanks @altryne.
  • +
  • Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default.
  • +
  • Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers.
  • +
  • Media: fix text attachment MIME misclassification with CSV/TSV inference and UTF-16 detection; add XML attribute escaping for file output. (#3628) Thanks @frankekn.
  • +
  • Build: align memory-core peer dependency with lockfile.
  • +
  • Security: add mDNS discovery mode with minimal default to reduce information disclosure. (#1882) Thanks @orlyjamie.
  • +
  • Security: harden URL fetches with DNS pinning to reduce rebinding risk. Thanks Chris Zheng.
  • +
  • Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93.
  • +
  • Security: wrap external hook content by default with a per-hook opt-out. (#1827) Thanks @mertcicekci0.
  • +
  • Gateway: default auth now fail-closed (token/password required; Tailscale Serve identity remains allowed).
  • +
  • Gateway: treat loopback + non-local Host connections as remote unless trusted proxy headers are present.
  • +
  • Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags.
  • +
+

View full changelog

+]]>
+ +
2026.1.24-1 Sun, 25 Jan 2026 14:05:25 +0000 - https://raw.githubusercontent.com/moltbot/moltbot/main/appcast.xml + https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml 7952 2026.1.24-1 15.0 - Moltbot 2026.1.24-1 + OpenClaw 2026.1.24-1

Fixes

  • Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install).
-

View full changelog

+

View full changelog

]]>
- -
- - 2026.1.24 - Sun, 25 Jan 2026 13:31:05 +0000 - https://raw.githubusercontent.com/moltbot/moltbot/main/appcast.xml - 7944 - 2026.1.24 - 15.0 - Moltbot 2026.1.24 -

Highlights

-
    -
  • Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.molt.bot/providers/ollama https://docs.molt.bot/providers/venice
  • -
  • Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg.
  • -
  • TTS: Edge fallback (keyless) + /tts auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.molt.bot/tts
  • -
  • Exec approvals: approve in-chat via /approve across all channels (including plugins). (#1621) Thanks @czekaj. https://docs.molt.bot/tools/exec-approvals https://docs.molt.bot/tools/slash-commands
  • -
  • Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.molt.bot/channels/telegram
  • -
-

Changes

-
    -
  • Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg.
  • -
  • TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.molt.bot/tts
  • -
  • TTS: add auto mode enum (off/always/inbound/tagged) with per-session /tts override. (#1667) Thanks @sebslight. https://docs.molt.bot/tts
  • -
  • Telegram: treat DM topics as separate sessions and keep DM history limits stable with thread suffixes. (#1597) Thanks @rohannagpal.
  • -
  • Telegram: add channels.telegram.linkPreview to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.molt.bot/channels/telegram
  • -
  • Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.molt.bot/tools/web
  • -
  • UI: refresh Control UI dashboard design system (typography, colors, spacing). (#1786) Thanks @mousberg.
  • -
  • Exec approvals: forward approval prompts to chat with /approve for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.molt.bot/tools/exec-approvals https://docs.molt.bot/tools/slash-commands
  • -
  • Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
  • -
  • Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.molt.bot/diagnostics/flags
  • -
  • Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
  • -
  • Docs: add verbose installer troubleshooting guidance.
  • -
  • Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua.
  • -
  • Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.molt.bot/bedrock
  • -
  • Docs: update Fly.io guide notes.
  • -
  • Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
  • -
-

Fixes

-
    -
  • Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589.
  • -
  • Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
  • -
  • Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
  • -
  • Web UI: hide internal message_id hints in chat bubbles.
  • -
  • Gateway: allow Control UI token-only auth to skip device pairing even when device identity is present (gateway.controlUi.allowInsecureAuth). (#1679) Thanks @steipete.
  • -
  • Matrix: decrypt E2EE media attachments with preflight size guard. (#1744) Thanks @araa47.
  • -
  • BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.molt.bot/channels/bluebubbles
  • -
  • BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
  • -
  • Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.
  • -
  • Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.molt.bot/channels/signal
  • -
  • Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
  • -
  • Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
  • -
  • Telegram: honor per-account proxy for outbound API calls. (#1774) Thanks @radek-paclt.
  • -
  • Telegram: fall back to text when voice notes are blocked by privacy settings. (#1725) Thanks @foeken.
  • -
  • Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
  • -
  • Voice Call: serialize Twilio TTS playback and cancel on barge-in to prevent overlap. (#1713) Thanks @dguido.
  • -
  • Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
  • -
  • Google Chat: normalize space targets without double spaces/ prefix.
  • -
  • Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
  • -
  • Agents: use the active auth profile for auto-compaction recovery.
  • -
  • Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204.
  • -
  • Models: default missing custom provider fields so minimal configs are accepted.
  • -
  • Messaging: keep newline chunking safe for fenced markdown blocks across channels.
  • -
  • TUI: reload history after gateway reconnect to restore session state. (#1663)
  • -
  • Heartbeat: normalize target identifiers for consistent routing.
  • -
  • Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
  • -
  • Exec: treat Windows platform labels as Windows for node shell selection. (#1760) Thanks @ymat19.
  • -
  • Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.
  • -
  • Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671)
  • -
  • Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
  • -
  • Gateway: clarify Control UI/WebChat auth error hints for missing tokens. (#1690)
  • -
  • Gateway: listen on IPv6 loopback when bound to 127.0.0.1 so localhost webhooks work.
  • -
  • Gateway: store lock files in the temp directory to avoid stale locks on persistent volumes. (#1676)
  • -
  • macOS: default direct-transport ws:// URLs to port 18789; document gateway.remote.transport. (#1603) Thanks @ngutman.
  • -
  • Tests: cap Vitest workers on CI macOS to reduce timeouts. (#1597) Thanks @rohannagpal.
  • -
  • Tests: avoid fake-timer dependency in embedded runner stream mock to reduce CI flakes. (#1597) Thanks @rohannagpal.
  • -
  • Tests: increase embedded runner ordering test timeout to reduce CI flakes. (#1597) Thanks @rohannagpal.
  • -
-

View full changelog

-]]>
- -
- - 2026.1.23 - Sat, 24 Jan 2026 13:02:18 +0000 - https://raw.githubusercontent.com/moltbot/moltbot/main/appcast.xml - 7750 - 2026.1.23 - 15.0 - Moltbot 2026.1.23 -

Highlights

-
    -
  • TTS: allow model-driven TTS tags by default for expressive audio replies (laughter, singing cues, etc.).
  • -
-

Changes

-
    -
  • Gateway: add /tools/invoke HTTP endpoint for direct tool calls and document it. (#1575) Thanks @vignesh07.
  • -
  • Agents: keep system prompt time zone-only and move current time to session_status for better cache hits.
  • -
  • Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman.
  • -
  • Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node).
  • -
  • Heartbeat: add per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer.
  • -
  • Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
  • -
  • CLI: restart the gateway by default after moltbot update; add --no-restart to skip it.
  • -
  • CLI: add live auth probes to moltbot models status for per-profile verification.
  • -
  • CLI: add moltbot system for system events + heartbeat controls; remove standalone wake.
  • -
  • Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
  • -
  • Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
  • -
  • Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
  • -
  • Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
  • -
  • Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
  • -
  • Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
  • -
  • TTS: move Telegram TTS into core with auto-replies, commands, and gateway methods. (#1559) Thanks @Glucksberg.
  • -
-

Fixes

-
    -
  • Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
  • -
  • Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
  • -
  • Messaging: mirror outbound sends into target session keys (threads + dmScope) and create session entries on send. (#1520)
  • -
  • Sessions: normalize session key casing to lowercase for consistent routing.
  • -
  • BlueBubbles: normalize group session keys for outbound mirroring. (#1520)
  • -
  • Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
  • -
  • Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
  • -
  • Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops).
  • -
  • Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
  • -
  • Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4.
  • -
  • Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
  • -
  • Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
  • -
  • Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
  • -
  • UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
  • -
  • UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
  • -
  • Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
  • -
  • Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
  • -
  • Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
  • -
  • Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes.
  • -
  • Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp).
  • -
  • Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
  • -
  • Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
  • -
  • TUI: forward unknown slash commands (for example, /context) to the Gateway.
  • -
  • TUI: include Gateway slash commands in autocomplete and /help.
  • -
  • CLI: skip usage lines in moltbot models status when provider usage is unavailable.
  • -
  • CLI: suppress diagnostic session/run noise during auth probes.
  • -
  • CLI: hide auth probe timeout warnings from embedded runs.
  • -
  • CLI: render auth probe results as a table in moltbot models status.
  • -
  • CLI: suppress probe-only embedded logs unless --verbose is set.
  • -
  • CLI: move auth probe errors below the table to reduce wrapping.
  • -
  • CLI: prevent ANSI color bleed when table cells wrap.
  • -
  • CLI: explain when auth profiles are excluded by auth.order in probe details.
  • -
  • CLI: drop the em dash when the banner tagline wraps to a second line.
  • -
  • CLI: inline auth probe errors in status rows to reduce wrapping.
  • -
  • Telegram: render markdown in media captions. (#1478)
  • -
  • Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
  • -
  • Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
  • -
  • Daemon: use platform PATH delimiters when building minimal service paths.
  • -
  • Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
  • -
  • Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
  • -
  • TUI: render Gateway slash-command replies as system output (for example, /context).
  • -
  • Media: only parse MEDIA: tags when they start the line to avoid stripping prose mentions. (#1206)
  • -
  • Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
  • -
  • Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
  • -
  • Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
  • -
  • MS Teams (plugin): remove .default suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
  • -
  • MS Teams (plugin): remove .default suffix from Bot Framework probe scope to avoid double-appending. (#1574) Thanks @Evizero.
  • -
  • Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
  • -
  • Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)
  • -
-

View full changelog

-]]>
- +
\ No newline at end of file diff --git a/apps/android/README.md b/apps/android/README.md index ca0967643..c2ae5a217 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -1,6 +1,6 @@ -## Clawdbot Node (Android) (internal) +## OpenClaw Node (Android) (internal) -Modern Android node app: connects to the **Gateway WebSocket** (`_clawdbot-gw._tcp`) and exposes **Canvas + Chat + Camera**. +Modern Android node app: connects to the **Gateway WebSocket** (`_openclaw-gw._tcp`) and exposes **Canvas + Chat + Camera**. Notes: - The node keeps the connection alive via a **foreground service** (persistent notification with a Disconnect action). @@ -25,7 +25,7 @@ cd apps/android 1) Start the gateway (on your “master” machine): ```bash -pnpm clawdbot gateway --port 18789 --verbose +pnpm openclaw gateway --port 18789 --verbose ``` 2) In the Android app: @@ -34,8 +34,8 @@ pnpm clawdbot gateway --port 18789 --verbose 3) Approve pairing (on the gateway machine): ```bash -clawdbot nodes pending -clawdbot nodes approve +openclaw nodes pending +openclaw nodes approve ``` More details: `docs/platforms/android.md`. diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index 3ddcb3b81..8279b6bd9 100644 --- a/apps/android/app/build.gradle.kts +++ b/apps/android/app/build.gradle.kts @@ -8,21 +8,21 @@ plugins { } android { - namespace = "bot.molt.android" + namespace = "ai.openclaw.android" compileSdk = 36 sourceSets { getByName("main") { - assets.srcDir(file("../../shared/MoltbotKit/Sources/MoltbotKit/Resources")) + assets.srcDir(file("../../shared/OpenClawKit/Sources/OpenClawKit/Resources")) } } defaultConfig { - applicationId = "bot.molt.android" + applicationId = "ai.openclaw.android" minSdk = 31 targetSdk = 36 - versionCode = 202601260 - versionName = "2026.1.27-beta.1" + versionCode = 202601290 + versionName = "2026.1.30" } buildTypes { @@ -65,7 +65,7 @@ androidComponents { val versionName = output.versionName.orNull ?: "0" val buildType = variant.buildType - val outputFileName = "moltbot-${versionName}-${buildType}.apk" + val outputFileName = "openclaw-${versionName}-${buildType}.apk" output.outputFileName = outputFileName } } diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index e0aab841e..bc0de1f87 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -32,7 +32,7 @@ android:label="@string/app_name" android:supportsRtl="true" android:networkSecurityConfig="@xml/network_security_config" - android:theme="@style/Theme.MoltbotNode"> + android:theme="@style/Theme.OpenClawNode"> Quint(status, server, connected, voiceMode, voiceListening) }.collect { (status, server, connected, voiceMode, voiceListening) -> - val title = if (connected) "Moltbot Node · Connected" else "Moltbot Node" + val title = if (connected) "OpenClaw Node · Connected" else "OpenClaw Node" val voiceSuffix = if (voiceMode == VoiceWakeMode.Always) { if (voiceListening) " · Voice Wake: Listening" else " · Voice Wake: Paused" @@ -91,7 +91,7 @@ class NodeForegroundService : Service() { "Connection", NotificationManager.IMPORTANCE_LOW, ).apply { - description = "Moltbot node connection status" + description = "OpenClaw node connection status" setShowBadge(false) } mgr.createNotificationChannel(channel) @@ -163,7 +163,7 @@ class NodeForegroundService : Service() { private const val CHANNEL_ID = "connection" private const val NOTIFICATION_ID = 1 - private const val ACTION_STOP = "bot.molt.android.action.STOP" + private const val ACTION_STOP = "ai.openclaw.android.action.STOP" fun start(context: Context) { val intent = Intent(context, NodeForegroundService::class.java) diff --git a/apps/android/app/src/main/java/bot/molt/android/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt similarity index 89% rename from apps/android/app/src/main/java/bot/molt/android/NodeRuntime.kt rename to apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt index 5fd429e9e..e6ceae598 100644 --- a/apps/android/app/src/main/java/bot/molt/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt @@ -1,4 +1,4 @@ -package bot.molt.android +package ai.openclaw.android import android.Manifest import android.content.Context @@ -7,35 +7,35 @@ import android.location.LocationManager import android.os.Build import android.os.SystemClock import androidx.core.content.ContextCompat -import bot.molt.android.chat.ChatController -import bot.molt.android.chat.ChatMessage -import bot.molt.android.chat.ChatPendingToolCall -import bot.molt.android.chat.ChatSessionEntry -import bot.molt.android.chat.OutgoingAttachment -import bot.molt.android.gateway.DeviceAuthStore -import bot.molt.android.gateway.DeviceIdentityStore -import bot.molt.android.gateway.GatewayClientInfo -import bot.molt.android.gateway.GatewayConnectOptions -import bot.molt.android.gateway.GatewayDiscovery -import bot.molt.android.gateway.GatewayEndpoint -import bot.molt.android.gateway.GatewaySession -import bot.molt.android.gateway.GatewayTlsParams -import bot.molt.android.node.CameraCaptureManager -import bot.molt.android.node.LocationCaptureManager -import bot.molt.android.BuildConfig -import bot.molt.android.node.CanvasController -import bot.molt.android.node.ScreenRecordManager -import bot.molt.android.node.SmsManager -import bot.molt.android.protocol.MoltbotCapability -import bot.molt.android.protocol.MoltbotCameraCommand -import bot.molt.android.protocol.MoltbotCanvasA2UIAction -import bot.molt.android.protocol.MoltbotCanvasA2UICommand -import bot.molt.android.protocol.MoltbotCanvasCommand -import bot.molt.android.protocol.MoltbotScreenCommand -import bot.molt.android.protocol.MoltbotLocationCommand -import bot.molt.android.protocol.MoltbotSmsCommand -import bot.molt.android.voice.TalkModeManager -import bot.molt.android.voice.VoiceWakeManager +import ai.openclaw.android.chat.ChatController +import ai.openclaw.android.chat.ChatMessage +import ai.openclaw.android.chat.ChatPendingToolCall +import ai.openclaw.android.chat.ChatSessionEntry +import ai.openclaw.android.chat.OutgoingAttachment +import ai.openclaw.android.gateway.DeviceAuthStore +import ai.openclaw.android.gateway.DeviceIdentityStore +import ai.openclaw.android.gateway.GatewayClientInfo +import ai.openclaw.android.gateway.GatewayConnectOptions +import ai.openclaw.android.gateway.GatewayDiscovery +import ai.openclaw.android.gateway.GatewayEndpoint +import ai.openclaw.android.gateway.GatewaySession +import ai.openclaw.android.gateway.GatewayTlsParams +import ai.openclaw.android.node.CameraCaptureManager +import ai.openclaw.android.node.LocationCaptureManager +import ai.openclaw.android.BuildConfig +import ai.openclaw.android.node.CanvasController +import ai.openclaw.android.node.ScreenRecordManager +import ai.openclaw.android.node.SmsManager +import ai.openclaw.android.protocol.OpenClawCapability +import ai.openclaw.android.protocol.OpenClawCameraCommand +import ai.openclaw.android.protocol.OpenClawCanvasA2UIAction +import ai.openclaw.android.protocol.OpenClawCanvasA2UICommand +import ai.openclaw.android.protocol.OpenClawCanvasCommand +import ai.openclaw.android.protocol.OpenClawScreenCommand +import ai.openclaw.android.protocol.OpenClawLocationCommand +import ai.openclaw.android.protocol.OpenClawSmsCommand +import ai.openclaw.android.voice.TalkModeManager +import ai.openclaw.android.voice.VoiceWakeManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -451,38 +451,38 @@ class NodeRuntime(context: Context) { private fun buildInvokeCommands(): List = buildList { - add(MoltbotCanvasCommand.Present.rawValue) - add(MoltbotCanvasCommand.Hide.rawValue) - add(MoltbotCanvasCommand.Navigate.rawValue) - add(MoltbotCanvasCommand.Eval.rawValue) - add(MoltbotCanvasCommand.Snapshot.rawValue) - add(MoltbotCanvasA2UICommand.Push.rawValue) - add(MoltbotCanvasA2UICommand.PushJSONL.rawValue) - add(MoltbotCanvasA2UICommand.Reset.rawValue) - add(MoltbotScreenCommand.Record.rawValue) + add(OpenClawCanvasCommand.Present.rawValue) + add(OpenClawCanvasCommand.Hide.rawValue) + add(OpenClawCanvasCommand.Navigate.rawValue) + add(OpenClawCanvasCommand.Eval.rawValue) + add(OpenClawCanvasCommand.Snapshot.rawValue) + add(OpenClawCanvasA2UICommand.Push.rawValue) + add(OpenClawCanvasA2UICommand.PushJSONL.rawValue) + add(OpenClawCanvasA2UICommand.Reset.rawValue) + add(OpenClawScreenCommand.Record.rawValue) if (cameraEnabled.value) { - add(MoltbotCameraCommand.Snap.rawValue) - add(MoltbotCameraCommand.Clip.rawValue) + add(OpenClawCameraCommand.Snap.rawValue) + add(OpenClawCameraCommand.Clip.rawValue) } if (locationMode.value != LocationMode.Off) { - add(MoltbotLocationCommand.Get.rawValue) + add(OpenClawLocationCommand.Get.rawValue) } if (sms.canSendSms()) { - add(MoltbotSmsCommand.Send.rawValue) + add(OpenClawSmsCommand.Send.rawValue) } } private fun buildCapabilities(): List = buildList { - add(MoltbotCapability.Canvas.rawValue) - add(MoltbotCapability.Screen.rawValue) - if (cameraEnabled.value) add(MoltbotCapability.Camera.rawValue) - if (sms.canSendSms()) add(MoltbotCapability.Sms.rawValue) + add(OpenClawCapability.Canvas.rawValue) + add(OpenClawCapability.Screen.rawValue) + if (cameraEnabled.value) add(OpenClawCapability.Camera.rawValue) + if (sms.canSendSms()) add(OpenClawCapability.Sms.rawValue) if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) { - add(MoltbotCapability.VoiceWake.rawValue) + add(OpenClawCapability.VoiceWake.rawValue) } if (locationMode.value != LocationMode.Off) { - add(MoltbotCapability.Location.rawValue) + add(OpenClawCapability.Location.rawValue) } } @@ -506,7 +506,7 @@ class NodeRuntime(context: Context) { val version = resolvedVersionName() val release = Build.VERSION.RELEASE?.trim().orEmpty() val releaseLabel = if (release.isEmpty()) "unknown" else release - return "MoltbotAndroid/$version (Android $releaseLabel; SDK ${Build.VERSION.SDK_INT})" + return "OpenClawAndroid/$version (Android $releaseLabel; SDK ${Build.VERSION.SDK_INT})" } private fun buildClientInfo(clientId: String, clientMode: String): GatewayClientInfo { @@ -529,7 +529,7 @@ class NodeRuntime(context: Context) { caps = buildCapabilities(), commands = buildInvokeCommands(), permissions = emptyMap(), - client = buildClientInfo(clientId = "moltbot-android", clientMode = "node"), + client = buildClientInfo(clientId = "openclaw-android", clientMode = "node"), userAgent = buildUserAgent(), ) } @@ -541,7 +541,7 @@ class NodeRuntime(context: Context) { caps = emptyList(), commands = emptyList(), permissions = emptyMap(), - client = buildClientInfo(clientId = "moltbot-control-ui", clientMode = "ui"), + client = buildClientInfo(clientId = "openclaw-control-ui", clientMode = "ui"), userAgent = buildUserAgent(), ) } @@ -665,7 +665,7 @@ class NodeRuntime(context: Context) { val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { java.util.UUID.randomUUID().toString() } - val name = MoltbotCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch + val name = OpenClawCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch val surfaceId = (userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" } @@ -675,7 +675,7 @@ class NodeRuntime(context: Context) { val sessionKey = resolveMainSessionKey() val message = - MoltbotCanvasA2UIAction.formatAgentMessage( + OpenClawCanvasA2UIAction.formatAgentMessage( actionName = name, sessionKey = sessionKey, surfaceId = surfaceId, @@ -709,7 +709,7 @@ class NodeRuntime(context: Context) { try { canvas.eval( - MoltbotCanvasA2UIAction.jsDispatchA2UIActionStatus( + OpenClawCanvasA2UIAction.jsDispatchA2UIActionStatus( actionId = actionId, ok = connected && error == null, error = error, @@ -827,10 +827,10 @@ class NodeRuntime(context: Context) { private suspend fun handleInvoke(command: String, paramsJson: String?): GatewaySession.InvokeResult { if ( - command.startsWith(MoltbotCanvasCommand.NamespacePrefix) || - command.startsWith(MoltbotCanvasA2UICommand.NamespacePrefix) || - command.startsWith(MoltbotCameraCommand.NamespacePrefix) || - command.startsWith(MoltbotScreenCommand.NamespacePrefix) + command.startsWith(OpenClawCanvasCommand.NamespacePrefix) || + command.startsWith(OpenClawCanvasA2UICommand.NamespacePrefix) || + command.startsWith(OpenClawCameraCommand.NamespacePrefix) || + command.startsWith(OpenClawScreenCommand.NamespacePrefix) ) { if (!isForeground.value) { return GatewaySession.InvokeResult.error( @@ -839,13 +839,13 @@ class NodeRuntime(context: Context) { ) } } - if (command.startsWith(MoltbotCameraCommand.NamespacePrefix) && !cameraEnabled.value) { + if (command.startsWith(OpenClawCameraCommand.NamespacePrefix) && !cameraEnabled.value) { return GatewaySession.InvokeResult.error( code = "CAMERA_DISABLED", message = "CAMERA_DISABLED: enable Camera in Settings", ) } - if (command.startsWith(MoltbotLocationCommand.NamespacePrefix) && + if (command.startsWith(OpenClawLocationCommand.NamespacePrefix) && locationMode.value == LocationMode.Off ) { return GatewaySession.InvokeResult.error( @@ -855,18 +855,18 @@ class NodeRuntime(context: Context) { } return when (command) { - MoltbotCanvasCommand.Present.rawValue -> { + OpenClawCanvasCommand.Present.rawValue -> { val url = CanvasController.parseNavigateUrl(paramsJson) canvas.navigate(url) GatewaySession.InvokeResult.ok(null) } - MoltbotCanvasCommand.Hide.rawValue -> GatewaySession.InvokeResult.ok(null) - MoltbotCanvasCommand.Navigate.rawValue -> { + OpenClawCanvasCommand.Hide.rawValue -> GatewaySession.InvokeResult.ok(null) + OpenClawCanvasCommand.Navigate.rawValue -> { val url = CanvasController.parseNavigateUrl(paramsJson) canvas.navigate(url) GatewaySession.InvokeResult.ok(null) } - MoltbotCanvasCommand.Eval.rawValue -> { + OpenClawCanvasCommand.Eval.rawValue -> { val js = CanvasController.parseEvalJs(paramsJson) ?: return GatewaySession.InvokeResult.error( @@ -884,7 +884,7 @@ class NodeRuntime(context: Context) { } GatewaySession.InvokeResult.ok("""{"result":${result.toJsonString()}}""") } - MoltbotCanvasCommand.Snapshot.rawValue -> { + OpenClawCanvasCommand.Snapshot.rawValue -> { val snapshotParams = CanvasController.parseSnapshotParams(paramsJson) val base64 = try { @@ -901,7 +901,7 @@ class NodeRuntime(context: Context) { } GatewaySession.InvokeResult.ok("""{"format":"${snapshotParams.format.rawValue}","base64":"$base64"}""") } - MoltbotCanvasA2UICommand.Reset.rawValue -> { + OpenClawCanvasA2UICommand.Reset.rawValue -> { val a2uiUrl = resolveA2uiHostUrl() ?: return GatewaySession.InvokeResult.error( code = "A2UI_HOST_NOT_CONFIGURED", @@ -917,7 +917,7 @@ class NodeRuntime(context: Context) { val res = canvas.eval(a2uiResetJS) GatewaySession.InvokeResult.ok(res) } - MoltbotCanvasA2UICommand.Push.rawValue, MoltbotCanvasA2UICommand.PushJSONL.rawValue -> { + OpenClawCanvasA2UICommand.Push.rawValue, OpenClawCanvasA2UICommand.PushJSONL.rawValue -> { val messages = try { decodeA2uiMessages(command, paramsJson) @@ -940,7 +940,7 @@ class NodeRuntime(context: Context) { val res = canvas.eval(js) GatewaySession.InvokeResult.ok(res) } - MoltbotCameraCommand.Snap.rawValue -> { + OpenClawCameraCommand.Snap.rawValue -> { showCameraHud(message = "Taking photo…", kind = CameraHudKind.Photo) triggerCameraFlash() val res = @@ -954,7 +954,7 @@ class NodeRuntime(context: Context) { showCameraHud(message = "Photo captured", kind = CameraHudKind.Success, autoHideMs = 1600) GatewaySession.InvokeResult.ok(res.payloadJson) } - MoltbotCameraCommand.Clip.rawValue -> { + OpenClawCameraCommand.Clip.rawValue -> { val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false if (includeAudio) externalAudioCaptureActive.value = true try { @@ -973,7 +973,7 @@ class NodeRuntime(context: Context) { if (includeAudio) externalAudioCaptureActive.value = false } } - MoltbotLocationCommand.Get.rawValue -> { + OpenClawLocationCommand.Get.rawValue -> { val mode = locationMode.value if (!isForeground.value && mode != LocationMode.Always) { return GatewaySession.InvokeResult.error( @@ -1026,7 +1026,7 @@ class NodeRuntime(context: Context) { GatewaySession.InvokeResult.error(code = "LOCATION_UNAVAILABLE", message = message) } } - MoltbotScreenCommand.Record.rawValue -> { + OpenClawScreenCommand.Record.rawValue -> { // Status pill mirrors screen recording state so it stays visible without overlay stacking. _screenRecordActive.value = true try { @@ -1042,7 +1042,7 @@ class NodeRuntime(context: Context) { _screenRecordActive.value = false } } - MoltbotSmsCommand.Send.rawValue -> { + OpenClawSmsCommand.Send.rawValue -> { val res = sms.send(paramsJson) if (res.ok) { GatewaySession.InvokeResult.ok(res.payloadJson) @@ -1115,7 +1115,7 @@ class NodeRuntime(context: Context) { val raw = if (nodeRaw.isNotBlank()) nodeRaw else operatorRaw if (raw.isBlank()) return null val base = raw.trimEnd('/') - return "${base}/__moltbot__/a2ui/?platform=android" + return "${base}/__openclaw__/a2ui/?platform=android" } private suspend fun ensureA2uiReady(a2uiUrl: String): Boolean { @@ -1150,7 +1150,7 @@ class NodeRuntime(context: Context) { val jsonlField = (obj["jsonl"] as? JsonPrimitive)?.content?.trim().orEmpty() val hasMessagesArray = obj["messages"] is JsonArray - if (command == MoltbotCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) { + if (command == OpenClawCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) { val jsonl = jsonlField if (jsonl.isBlank()) throw IllegalArgumentException("INVALID_REQUEST: jsonl required") val messages = @@ -1207,7 +1207,8 @@ private const val a2uiReadyCheckJS: String = """ (() => { try { - return !!globalThis.clawdbotA2UI && typeof globalThis.clawdbotA2UI.applyMessages === 'function'; + const host = globalThis.openclawA2UI; + return !!host && typeof host.applyMessages === 'function'; } catch (_) { return false; } @@ -1218,8 +1219,9 @@ private const val a2uiResetJS: String = """ (() => { try { - if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing moltbotA2UI" }; - return globalThis.clawdbotA2UI.reset(); + const host = globalThis.openclawA2UI; + if (!host) return { ok: false, error: "missing openclawA2UI" }; + return host.reset(); } catch (e) { return { ok: false, error: String(e?.message ?? e) }; } @@ -1230,9 +1232,10 @@ private fun a2uiApplyMessagesJS(messagesJson: String): String { return """ (() => { try { - if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing moltbotA2UI" }; + const host = globalThis.openclawA2UI; + if (!host) return { ok: false, error: "missing openclawA2UI" }; const messages = $messagesJson; - return globalThis.clawdbotA2UI.applyMessages(messages); + return host.applyMessages(messages); } catch (e) { return { ok: false, error: String(e?.message ?? e) }; } diff --git a/apps/android/app/src/main/java/bot/molt/android/PermissionRequester.kt b/apps/android/app/src/main/java/ai/openclaw/android/PermissionRequester.kt similarity index 97% rename from apps/android/app/src/main/java/bot/molt/android/PermissionRequester.kt rename to apps/android/app/src/main/java/ai/openclaw/android/PermissionRequester.kt index 78ae0b62b..0ee267b55 100644 --- a/apps/android/app/src/main/java/bot/molt/android/PermissionRequester.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/PermissionRequester.kt @@ -1,4 +1,4 @@ -package bot.molt.android +package ai.openclaw.android import android.content.pm.PackageManager import android.content.Intent @@ -115,7 +115,7 @@ class PermissionRequester(private val activity: ComponentActivity) { private fun buildRationaleMessage(permissions: List): String { val labels = permissions.map { permissionLabel(it) } - return "Moltbot needs ${labels.joinToString(", ")} permissions to continue." + return "OpenClaw needs ${labels.joinToString(", ")} permissions to continue." } private fun buildSettingsMessage(permissions: List): String { diff --git a/apps/android/app/src/main/java/bot/molt/android/ScreenCaptureRequester.kt b/apps/android/app/src/main/java/ai/openclaw/android/ScreenCaptureRequester.kt similarity index 95% rename from apps/android/app/src/main/java/bot/molt/android/ScreenCaptureRequester.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ScreenCaptureRequester.kt index 29d662044..c215103b5 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ScreenCaptureRequester.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ScreenCaptureRequester.kt @@ -1,4 +1,4 @@ -package bot.molt.android +package ai.openclaw.android import android.app.Activity import android.content.Context @@ -55,7 +55,7 @@ class ScreenCaptureRequester(private val activity: ComponentActivity) { suspendCancellableCoroutine { cont -> AlertDialog.Builder(activity) .setTitle("Screen recording required") - .setMessage("Moltbot needs to record the screen for this command.") + .setMessage("OpenClaw needs to record the screen for this command.") .setPositiveButton("Continue") { _, _ -> cont.resume(true) } .setNegativeButton("Not now") { _, _ -> cont.resume(false) } .setOnCancelListener { cont.resume(false) } diff --git a/apps/android/app/src/main/java/bot/molt/android/SecurePrefs.kt b/apps/android/app/src/main/java/ai/openclaw/android/SecurePrefs.kt similarity index 79% rename from apps/android/app/src/main/java/bot/molt/android/SecurePrefs.kt rename to apps/android/app/src/main/java/ai/openclaw/android/SecurePrefs.kt index 7ee3294dc..881d724fd 100644 --- a/apps/android/app/src/main/java/bot/molt/android/SecurePrefs.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/SecurePrefs.kt @@ -1,8 +1,9 @@ @file:Suppress("DEPRECATION") -package bot.molt.android +package ai.openclaw.android import android.content.Context +import android.content.SharedPreferences import androidx.core.content.edit import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey @@ -16,11 +17,12 @@ import java.util.UUID class SecurePrefs(context: Context) { companion object { - val defaultWakeWords: List = listOf("clawd", "claude") + val defaultWakeWords: List = listOf("openclaw", "claude") private const val displayNameKey = "node.displayName" private const val voiceWakeModeKey = "voiceWake.mode" } + private val appContext = context.applicationContext private val json = Json { ignoreUnknownKeys = true } private val masterKey = @@ -28,14 +30,9 @@ class SecurePrefs(context: Context) { .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() - private val prefs = - EncryptedSharedPreferences.create( - context, - "moltbot.node.secure", - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, - ) + private val prefs: SharedPreferences by lazy { + createPrefs(appContext, "openclaw.node.secure") + } private val _instanceId = MutableStateFlow(loadOrCreateInstanceId()) val instanceId: StateFlow = _instanceId @@ -59,28 +56,24 @@ class SecurePrefs(context: Context) { val preventSleep: StateFlow = _preventSleep private val _manualEnabled = - MutableStateFlow(readBoolWithMigration("gateway.manual.enabled", "bridge.manual.enabled", false)) + MutableStateFlow(prefs.getBoolean("gateway.manual.enabled", false)) val manualEnabled: StateFlow = _manualEnabled private val _manualHost = - MutableStateFlow(readStringWithMigration("gateway.manual.host", "bridge.manual.host", "")) + MutableStateFlow(prefs.getString("gateway.manual.host", "") ?: "") val manualHost: StateFlow = _manualHost private val _manualPort = - MutableStateFlow(readIntWithMigration("gateway.manual.port", "bridge.manual.port", 18789)) + MutableStateFlow(prefs.getInt("gateway.manual.port", 18789)) val manualPort: StateFlow = _manualPort private val _manualTls = - MutableStateFlow(readBoolWithMigration("gateway.manual.tls", null, true)) + MutableStateFlow(prefs.getBoolean("gateway.manual.tls", true)) val manualTls: StateFlow = _manualTls private val _lastDiscoveredStableId = MutableStateFlow( - readStringWithMigration( - "gateway.lastDiscoveredStableID", - "bridge.lastDiscoveredStableId", - "", - ), + prefs.getString("gateway.lastDiscoveredStableID", "") ?: "", ) val lastDiscoveredStableId: StateFlow = _lastDiscoveredStableId @@ -158,9 +151,7 @@ class SecurePrefs(context: Context) { fun loadGatewayToken(): String? { val key = "gateway.token.${_instanceId.value}" val stored = prefs.getString(key, null)?.trim() - if (!stored.isNullOrEmpty()) return stored - val legacy = prefs.getString("bridge.token.${_instanceId.value}", null)?.trim() - return legacy?.takeIf { it.isNotEmpty() } + return stored?.takeIf { it.isNotEmpty() } } fun saveGatewayToken(token: String) { @@ -201,6 +192,16 @@ class SecurePrefs(context: Context) { prefs.edit { remove(key) } } + private fun createPrefs(context: Context, name: String): SharedPreferences { + return EncryptedSharedPreferences.create( + context, + name, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + } + private fun loadOrCreateInstanceId(): String { val existing = prefs.getString("node.instanceId", null)?.trim() if (!existing.isNullOrBlank()) return existing @@ -270,39 +271,4 @@ class SecurePrefs(context: Context) { } } - private fun readBoolWithMigration(newKey: String, oldKey: String?, defaultValue: Boolean): Boolean { - if (prefs.contains(newKey)) { - return prefs.getBoolean(newKey, defaultValue) - } - if (oldKey != null && prefs.contains(oldKey)) { - val value = prefs.getBoolean(oldKey, defaultValue) - prefs.edit { putBoolean(newKey, value) } - return value - } - return defaultValue - } - - private fun readStringWithMigration(newKey: String, oldKey: String?, defaultValue: String): String { - if (prefs.contains(newKey)) { - return prefs.getString(newKey, defaultValue) ?: defaultValue - } - if (oldKey != null && prefs.contains(oldKey)) { - val value = prefs.getString(oldKey, defaultValue) ?: defaultValue - prefs.edit { putString(newKey, value) } - return value - } - return defaultValue - } - - private fun readIntWithMigration(newKey: String, oldKey: String?, defaultValue: Int): Int { - if (prefs.contains(newKey)) { - return prefs.getInt(newKey, defaultValue) - } - if (oldKey != null && prefs.contains(oldKey)) { - val value = prefs.getInt(oldKey, defaultValue) - prefs.edit { putInt(newKey, value) } - return value - } - return defaultValue - } } diff --git a/apps/android/app/src/main/java/bot/molt/android/SessionKey.kt b/apps/android/app/src/main/java/ai/openclaw/android/SessionKey.kt similarity index 92% rename from apps/android/app/src/main/java/bot/molt/android/SessionKey.kt rename to apps/android/app/src/main/java/ai/openclaw/android/SessionKey.kt index e64051649..8148a1702 100644 --- a/apps/android/app/src/main/java/bot/molt/android/SessionKey.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/SessionKey.kt @@ -1,4 +1,4 @@ -package bot.molt.android +package ai.openclaw.android internal fun normalizeMainKey(raw: String?): String { val trimmed = raw?.trim() diff --git a/apps/android/app/src/main/java/bot/molt/android/VoiceWakeMode.kt b/apps/android/app/src/main/java/ai/openclaw/android/VoiceWakeMode.kt similarity index 91% rename from apps/android/app/src/main/java/bot/molt/android/VoiceWakeMode.kt rename to apps/android/app/src/main/java/ai/openclaw/android/VoiceWakeMode.kt index e0862cc25..75c2fe344 100644 --- a/apps/android/app/src/main/java/bot/molt/android/VoiceWakeMode.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/VoiceWakeMode.kt @@ -1,4 +1,4 @@ -package bot.molt.android +package ai.openclaw.android enum class VoiceWakeMode(val rawValue: String) { Off("off"), diff --git a/apps/android/app/src/main/java/bot/molt/android/WakeWords.kt b/apps/android/app/src/main/java/ai/openclaw/android/WakeWords.kt similarity index 95% rename from apps/android/app/src/main/java/bot/molt/android/WakeWords.kt rename to apps/android/app/src/main/java/ai/openclaw/android/WakeWords.kt index 56b85a5df..b64cb1dd7 100644 --- a/apps/android/app/src/main/java/bot/molt/android/WakeWords.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/WakeWords.kt @@ -1,4 +1,4 @@ -package bot.molt.android +package ai.openclaw.android object WakeWords { const val maxWords: Int = 32 diff --git a/apps/android/app/src/main/java/bot/molt/android/chat/ChatController.kt b/apps/android/app/src/main/java/ai/openclaw/android/chat/ChatController.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/chat/ChatController.kt rename to apps/android/app/src/main/java/ai/openclaw/android/chat/ChatController.kt index eef66fece..3ed69ee5b 100644 --- a/apps/android/app/src/main/java/bot/molt/android/chat/ChatController.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/chat/ChatController.kt @@ -1,6 +1,6 @@ -package bot.molt.android.chat +package ai.openclaw.android.chat -import bot.molt.android.gateway.GatewaySession +import ai.openclaw.android.gateway.GatewaySession import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope diff --git a/apps/android/app/src/main/java/bot/molt/android/chat/ChatModels.kt b/apps/android/app/src/main/java/ai/openclaw/android/chat/ChatModels.kt similarity index 96% rename from apps/android/app/src/main/java/bot/molt/android/chat/ChatModels.kt rename to apps/android/app/src/main/java/ai/openclaw/android/chat/ChatModels.kt index 340624452..dd17a8c1a 100644 --- a/apps/android/app/src/main/java/bot/molt/android/chat/ChatModels.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/chat/ChatModels.kt @@ -1,4 +1,4 @@ -package bot.molt.android.chat +package ai.openclaw.android.chat data class ChatMessage( val id: String, diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/BonjourEscapes.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/BonjourEscapes.kt similarity index 96% rename from apps/android/app/src/main/java/bot/molt/android/gateway/BonjourEscapes.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/BonjourEscapes.kt index 2c0c34d68..1606df79e 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/BonjourEscapes.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/BonjourEscapes.kt @@ -1,4 +1,4 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway object BonjourEscapes { fun decode(input: String): String { diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/DeviceAuthStore.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/DeviceAuthStore.kt similarity index 90% rename from apps/android/app/src/main/java/bot/molt/android/gateway/DeviceAuthStore.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/DeviceAuthStore.kt index 6b90b4672..810e029fb 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/DeviceAuthStore.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/DeviceAuthStore.kt @@ -1,6 +1,6 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway -import bot.molt.android.SecurePrefs +import ai.openclaw.android.SecurePrefs class DeviceAuthStore(private val prefs: SecurePrefs) { fun loadToken(deviceId: String, role: String): String? { diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/DeviceIdentityStore.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/DeviceIdentityStore.kt similarity index 93% rename from apps/android/app/src/main/java/bot/molt/android/gateway/DeviceIdentityStore.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/DeviceIdentityStore.kt index 58a0aceff..accbb79e4 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/DeviceIdentityStore.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/DeviceIdentityStore.kt @@ -1,4 +1,4 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway import android.content.Context import android.util.Base64 @@ -21,7 +21,7 @@ data class DeviceIdentity( class DeviceIdentityStore(context: Context) { private val json = Json { ignoreUnknownKeys = true } - private val identityFile = File(context.filesDir, "moltbot/identity/device.json") + private val identityFile = File(context.filesDir, "openclaw/identity/device.json") @Synchronized fun loadOrCreate(): DeviceIdentity { @@ -65,9 +65,13 @@ class DeviceIdentityStore(context: Context) { } private fun load(): DeviceIdentity? { + return readIdentity(identityFile) + } + + private fun readIdentity(file: File): DeviceIdentity? { return try { - if (!identityFile.exists()) return null - val raw = identityFile.readText(Charsets.UTF_8) + if (!file.exists()) return null + val raw = file.readText(Charsets.UTF_8) val decoded = json.decodeFromString(DeviceIdentity.serializer(), raw) if (decoded.deviceId.isBlank() || decoded.publicKeyRawBase64.isBlank() || diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayDiscovery.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayDiscovery.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/gateway/GatewayDiscovery.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayDiscovery.kt index 53bdb5588..2ad8ec0cb 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayDiscovery.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayDiscovery.kt @@ -1,4 +1,4 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway import android.content.Context import android.net.ConnectivityManager @@ -51,9 +51,9 @@ class GatewayDiscovery( private val nsd = context.getSystemService(NsdManager::class.java) private val connectivity = context.getSystemService(ConnectivityManager::class.java) private val dns = DnsResolver.getInstance() - private val serviceType = "_moltbot-gw._tcp." - private val wideAreaDomain = "moltbot.internal." - private val logTag = "Moltbot/GatewayDiscovery" + private val serviceType = "_openclaw-gw._tcp." + private val wideAreaDomain = System.getenv("OPENCLAW_WIDE_AREA_DOMAIN") + private val logTag = "OpenClaw/GatewayDiscovery" private val localById = ConcurrentHashMap() private val unicastById = ConcurrentHashMap() @@ -91,7 +91,9 @@ class GatewayDiscovery( init { startLocalDiscovery() - startUnicastDiscovery(wideAreaDomain) + if (!wideAreaDomain.isNullOrBlank()) { + startUnicastDiscovery(wideAreaDomain) + } } private fun startLocalDiscovery() { diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayEndpoint.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayEndpoint.kt similarity index 94% rename from apps/android/app/src/main/java/bot/molt/android/gateway/GatewayEndpoint.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayEndpoint.kt index 2c524cc67..9a3010602 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayEndpoint.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayEndpoint.kt @@ -1,4 +1,4 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway data class GatewayEndpoint( val stableId: String, diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayProtocol.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayProtocol.kt similarity index 52% rename from apps/android/app/src/main/java/bot/molt/android/gateway/GatewayProtocol.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayProtocol.kt index 6836331be..da8fa4c69 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayProtocol.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayProtocol.kt @@ -1,3 +1,3 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway const val GATEWAY_PROTOCOL_VERSION = 3 diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewaySession.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewaySession.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/gateway/GatewaySession.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewaySession.kt index 13074b918..a8979d2e5 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewaySession.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewaySession.kt @@ -1,4 +1,4 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway import android.util.Log import java.util.Locale @@ -148,7 +148,7 @@ class GatewaySession( try { conn.request("node.event", params, timeoutMs = 8_000) } catch (err: Throwable) { - Log.w("MoltbotGateway", "node.event failed: ${err.message ?: err::class.java.simpleName}") + Log.w("OpenClawGateway", "node.event failed: ${err.message ?: err::class.java.simpleName}") } } @@ -181,7 +181,7 @@ class GatewaySession( private val connectNonceDeferred = CompletableDeferred() private val client: OkHttpClient = buildClient() private var socket: WebSocket? = null - private val loggerTag = "MoltbotGateway" + private val loggerTag = "OpenClawGateway" val remoteAddress: String = if (endpoint.host.contains(":")) { diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayTls.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayTls.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/gateway/GatewayTls.kt rename to apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayTls.kt index 673d60c8f..dc17aa732 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayTls.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewayTls.kt @@ -1,4 +1,4 @@ -package bot.molt.android.gateway +package ai.openclaw.android.gateway import android.annotation.SuppressLint import java.security.MessageDigest diff --git a/apps/android/app/src/main/java/bot/molt/android/node/CameraCaptureManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/CameraCaptureManager.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/node/CameraCaptureManager.kt rename to apps/android/app/src/main/java/ai/openclaw/android/node/CameraCaptureManager.kt index cb15a3915..536c8cbda 100644 --- a/apps/android/app/src/main/java/bot/molt/android/node/CameraCaptureManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/CameraCaptureManager.kt @@ -1,4 +1,4 @@ -package bot.molt.android.node +package ai.openclaw.android.node import android.Manifest import android.content.Context @@ -22,7 +22,7 @@ import androidx.camera.video.VideoRecordEvent import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.checkSelfPermission import androidx.core.graphics.scale -import bot.molt.android.PermissionRequester +import ai.openclaw.android.PermissionRequester import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeout @@ -155,7 +155,7 @@ class CameraCaptureManager(private val context: Context) { provider.unbindAll() provider.bindToLifecycle(owner, selector, videoCapture) - val file = File.createTempFile("moltbot-clip-", ".mp4") + val file = File.createTempFile("openclaw-clip-", ".mp4") val outputOptions = FileOutputOptions.Builder(file).build() val finalized = kotlinx.coroutines.CompletableDeferred() @@ -285,7 +285,7 @@ private suspend fun Context.cameraProvider(): ProcessCameraProvider = /** Returns (jpegBytes, exifOrientation) so caller can rotate the decoded bitmap. */ private suspend fun ImageCapture.takeJpegWithExif(executor: Executor): Pair = suspendCancellableCoroutine { cont -> - val file = File.createTempFile("moltbot-snap-", ".jpg") + val file = File.createTempFile("openclaw-snap-", ".jpg") val options = ImageCapture.OutputFileOptions.Builder(file).build() takePicture( options, diff --git a/apps/android/app/src/main/java/bot/molt/android/node/CanvasController.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/CanvasController.kt similarity index 97% rename from apps/android/app/src/main/java/bot/molt/android/node/CanvasController.kt rename to apps/android/app/src/main/java/ai/openclaw/android/node/CanvasController.kt index 4d33ed0a6..c46770a63 100644 --- a/apps/android/app/src/main/java/bot/molt/android/node/CanvasController.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/CanvasController.kt @@ -1,4 +1,4 @@ -package bot.molt.android.node +package ai.openclaw.android.node import android.graphics.Bitmap import android.graphics.Canvas @@ -17,7 +17,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive -import bot.molt.android.BuildConfig +import ai.openclaw.android.BuildConfig import kotlin.coroutines.resume class CanvasController { @@ -84,12 +84,12 @@ class CanvasController { withWebViewOnMain { wv -> if (currentUrl == null) { if (BuildConfig.DEBUG) { - Log.d("MoltbotCanvas", "load scaffold: $scaffoldAssetUrl") + Log.d("OpenClawCanvas", "load scaffold: $scaffoldAssetUrl") } wv.loadUrl(scaffoldAssetUrl) } else { if (BuildConfig.DEBUG) { - Log.d("MoltbotCanvas", "load url: $currentUrl") + Log.d("OpenClawCanvas", "load url: $currentUrl") } wv.loadUrl(currentUrl) } @@ -106,7 +106,7 @@ class CanvasController { val js = """ (() => { try { - const api = globalThis.__moltbot; + const api = globalThis.__openclaw; if (!api) return; if (typeof api.setDebugStatusEnabled === 'function') { api.setDebugStatusEnabled(${if (enabled) "true" else "false"}); diff --git a/apps/android/app/src/main/java/bot/molt/android/node/JpegSizeLimiter.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/JpegSizeLimiter.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/node/JpegSizeLimiter.kt rename to apps/android/app/src/main/java/ai/openclaw/android/node/JpegSizeLimiter.kt index 8fb6c35d4..d6018467e 100644 --- a/apps/android/app/src/main/java/bot/molt/android/node/JpegSizeLimiter.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/JpegSizeLimiter.kt @@ -1,4 +1,4 @@ -package bot.molt.android.node +package ai.openclaw.android.node import kotlin.math.max import kotlin.math.min diff --git a/apps/android/app/src/main/java/bot/molt/android/node/LocationCaptureManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/LocationCaptureManager.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/node/LocationCaptureManager.kt rename to apps/android/app/src/main/java/ai/openclaw/android/node/LocationCaptureManager.kt index c56eee03a..87762e87f 100644 --- a/apps/android/app/src/main/java/bot/molt/android/node/LocationCaptureManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/LocationCaptureManager.kt @@ -1,4 +1,4 @@ -package bot.molt.android.node +package ai.openclaw.android.node import android.Manifest import android.content.Context diff --git a/apps/android/app/src/main/java/bot/molt/android/node/ScreenRecordManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/ScreenRecordManager.kt similarity index 95% rename from apps/android/app/src/main/java/bot/molt/android/node/ScreenRecordManager.kt rename to apps/android/app/src/main/java/ai/openclaw/android/node/ScreenRecordManager.kt index 0e785c245..337a95386 100644 --- a/apps/android/app/src/main/java/bot/molt/android/node/ScreenRecordManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/ScreenRecordManager.kt @@ -1,4 +1,4 @@ -package bot.molt.android.node +package ai.openclaw.android.node import android.content.Context import android.hardware.display.DisplayManager @@ -6,7 +6,7 @@ import android.media.MediaRecorder import android.media.projection.MediaProjectionManager import android.os.Build import android.util.Base64 -import bot.molt.android.ScreenCaptureRequester +import ai.openclaw.android.ScreenCaptureRequester import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -17,13 +17,13 @@ class ScreenRecordManager(private val context: Context) { data class Payload(val payloadJson: String) @Volatile private var screenCaptureRequester: ScreenCaptureRequester? = null - @Volatile private var permissionRequester: bot.molt.android.PermissionRequester? = null + @Volatile private var permissionRequester: ai.openclaw.android.PermissionRequester? = null fun attachScreenCaptureRequester(requester: ScreenCaptureRequester) { screenCaptureRequester = requester } - fun attachPermissionRequester(requester: bot.molt.android.PermissionRequester) { + fun attachPermissionRequester(requester: ai.openclaw.android.PermissionRequester) { permissionRequester = requester } @@ -63,7 +63,7 @@ class ScreenRecordManager(private val context: Context) { val height = metrics.heightPixels val densityDpi = metrics.densityDpi - val file = File.createTempFile("moltbot-screen-", ".mp4") + val file = File.createTempFile("openclaw-screen-", ".mp4") if (includeAudio) ensureMicPermission() val recorder = createMediaRecorder() @@ -90,7 +90,7 @@ class ScreenRecordManager(private val context: Context) { val surface = recorder.surface virtualDisplay = projection.createVirtualDisplay( - "moltbot-screen", + "openclaw-screen", width, height, densityDpi, diff --git a/apps/android/app/src/main/java/bot/molt/android/node/SmsManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/SmsManager.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/node/SmsManager.kt rename to apps/android/app/src/main/java/ai/openclaw/android/node/SmsManager.kt index 0314ee1a7..d727bfd27 100644 --- a/apps/android/app/src/main/java/bot/molt/android/node/SmsManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/SmsManager.kt @@ -1,4 +1,4 @@ -package bot.molt.android.node +package ai.openclaw.android.node import android.Manifest import android.content.Context @@ -11,7 +11,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonObject import kotlinx.serialization.encodeToString -import bot.molt.android.PermissionRequester +import ai.openclaw.android.PermissionRequester /** * Sends SMS messages via the Android SMS API. diff --git a/apps/android/app/src/main/java/bot/molt/android/protocol/ClawdbotCanvasA2UIAction.kt b/apps/android/app/src/main/java/ai/openclaw/android/protocol/OpenClawCanvasA2UIAction.kt similarity index 89% rename from apps/android/app/src/main/java/bot/molt/android/protocol/ClawdbotCanvasA2UIAction.kt rename to apps/android/app/src/main/java/ai/openclaw/android/protocol/OpenClawCanvasA2UIAction.kt index f73879bb2..7e1a5bf12 100644 --- a/apps/android/app/src/main/java/bot/molt/android/protocol/ClawdbotCanvasA2UIAction.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/protocol/OpenClawCanvasA2UIAction.kt @@ -1,9 +1,9 @@ -package bot.molt.android.protocol +package ai.openclaw.android.protocol import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive -object MoltbotCanvasA2UIAction { +object OpenClawCanvasA2UIAction { fun extractActionName(userAction: JsonObject): String? { val name = (userAction["name"] as? JsonPrimitive) @@ -61,6 +61,6 @@ object MoltbotCanvasA2UIAction { val err = (error ?: "").replace("\\", "\\\\").replace("\"", "\\\"") val okLiteral = if (ok) "true" else "false" val idEscaped = actionId.replace("\\", "\\\\").replace("\"", "\\\"") - return "window.dispatchEvent(new CustomEvent('moltbot:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));" + return "window.dispatchEvent(new CustomEvent('openclaw:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));" } } diff --git a/apps/android/app/src/main/java/bot/molt/android/protocol/ClawdbotProtocolConstants.kt b/apps/android/app/src/main/java/ai/openclaw/android/protocol/OpenClawProtocolConstants.kt similarity index 69% rename from apps/android/app/src/main/java/bot/molt/android/protocol/ClawdbotProtocolConstants.kt rename to apps/android/app/src/main/java/ai/openclaw/android/protocol/OpenClawProtocolConstants.kt index 27d46c3f1..ccca40c4c 100644 --- a/apps/android/app/src/main/java/bot/molt/android/protocol/ClawdbotProtocolConstants.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/protocol/OpenClawProtocolConstants.kt @@ -1,6 +1,6 @@ -package bot.molt.android.protocol +package ai.openclaw.android.protocol -enum class MoltbotCapability(val rawValue: String) { +enum class OpenClawCapability(val rawValue: String) { Canvas("canvas"), Camera("camera"), Screen("screen"), @@ -9,7 +9,7 @@ enum class MoltbotCapability(val rawValue: String) { Location("location"), } -enum class MoltbotCanvasCommand(val rawValue: String) { +enum class OpenClawCanvasCommand(val rawValue: String) { Present("canvas.present"), Hide("canvas.hide"), Navigate("canvas.navigate"), @@ -22,7 +22,7 @@ enum class MoltbotCanvasCommand(val rawValue: String) { } } -enum class MoltbotCanvasA2UICommand(val rawValue: String) { +enum class OpenClawCanvasA2UICommand(val rawValue: String) { Push("canvas.a2ui.push"), PushJSONL("canvas.a2ui.pushJSONL"), Reset("canvas.a2ui.reset"), @@ -33,7 +33,7 @@ enum class MoltbotCanvasA2UICommand(val rawValue: String) { } } -enum class MoltbotCameraCommand(val rawValue: String) { +enum class OpenClawCameraCommand(val rawValue: String) { Snap("camera.snap"), Clip("camera.clip"), ; @@ -43,7 +43,7 @@ enum class MoltbotCameraCommand(val rawValue: String) { } } -enum class MoltbotScreenCommand(val rawValue: String) { +enum class OpenClawScreenCommand(val rawValue: String) { Record("screen.record"), ; @@ -52,7 +52,7 @@ enum class MoltbotScreenCommand(val rawValue: String) { } } -enum class MoltbotSmsCommand(val rawValue: String) { +enum class OpenClawSmsCommand(val rawValue: String) { Send("sms.send"), ; @@ -61,7 +61,7 @@ enum class MoltbotSmsCommand(val rawValue: String) { } } -enum class MoltbotLocationCommand(val rawValue: String) { +enum class OpenClawLocationCommand(val rawValue: String) { Get("location.get"), ; diff --git a/apps/android/app/src/main/java/bot/molt/android/tools/ToolDisplay.kt b/apps/android/app/src/main/java/ai/openclaw/android/tools/ToolDisplay.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/tools/ToolDisplay.kt rename to apps/android/app/src/main/java/ai/openclaw/android/tools/ToolDisplay.kt index 6f4862887..1c5561767 100644 --- a/apps/android/app/src/main/java/bot/molt/android/tools/ToolDisplay.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/tools/ToolDisplay.kt @@ -1,4 +1,4 @@ -package bot.molt.android.tools +package ai.openclaw.android.tools import android.content.Context import kotlinx.serialization.Serializable diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/CameraHudOverlay.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/CameraHudOverlay.kt similarity index 97% rename from apps/android/app/src/main/java/bot/molt/android/ui/CameraHudOverlay.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/CameraHudOverlay.kt index 7b45efae9..21043d739 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/CameraHudOverlay.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/CameraHudOverlay.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui +package ai.openclaw.android.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/ChatSheet.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/ChatSheet.kt similarity index 53% rename from apps/android/app/src/main/java/bot/molt/android/ui/ChatSheet.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/ChatSheet.kt index 21af1a4c6..85f20364c 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/ChatSheet.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/ChatSheet.kt @@ -1,8 +1,8 @@ -package bot.molt.android.ui +package ai.openclaw.android.ui import androidx.compose.runtime.Composable -import bot.molt.android.MainViewModel -import bot.molt.android.ui.chat.ChatSheetContent +import ai.openclaw.android.MainViewModel +import ai.openclaw.android.ui.chat.ChatSheetContent @Composable fun ChatSheet(viewModel: MainViewModel) { diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/ClawdbotTheme.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/OpenClawTheme.kt similarity index 92% rename from apps/android/app/src/main/java/bot/molt/android/ui/ClawdbotTheme.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/OpenClawTheme.kt index c292aa25d..aad743a6d 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/ClawdbotTheme.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/OpenClawTheme.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui +package ai.openclaw.android.ui import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -9,7 +9,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @Composable -fun MoltbotTheme(content: @Composable () -> Unit) { +fun OpenClawTheme(content: @Composable () -> Unit) { val context = LocalContext.current val isDark = isSystemInDarkTheme() val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/RootScreen.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/RootScreen.kt similarity index 93% rename from apps/android/app/src/main/java/bot/molt/android/ui/RootScreen.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/RootScreen.kt index 67d76b82f..af0cfe628 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/RootScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/RootScreen.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui +package ai.openclaw.android.ui import android.annotation.SuppressLint import android.Manifest @@ -65,8 +65,8 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import androidx.core.content.ContextCompat -import bot.molt.android.CameraHudKind -import bot.molt.android.MainViewModel +import ai.openclaw.android.CameraHudKind +import ai.openclaw.android.MainViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -333,7 +333,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) disableForceDarkIfSupported(settings) } if (isDebuggable) { - Log.d("MoltbotWebView", "userAgent: ${settings.userAgentString}") + Log.d("OpenClawWebView", "userAgent: ${settings.userAgentString}") } isScrollContainer = true overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS @@ -348,7 +348,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) ) { if (!isDebuggable) return if (!request.isForMainFrame) return - Log.e("MoltbotWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}") + Log.e("OpenClawWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}") } override fun onReceivedHttpError( @@ -359,14 +359,14 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) if (!isDebuggable) return if (!request.isForMainFrame) return Log.e( - "MoltbotWebView", + "OpenClawWebView", "onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}", ) } override fun onPageFinished(view: WebView, url: String?) { if (isDebuggable) { - Log.d("MoltbotWebView", "onPageFinished: $url") + Log.d("OpenClawWebView", "onPageFinished: $url") } viewModel.canvas.onPageFinished() } @@ -377,7 +377,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) ): Boolean { if (isDebuggable) { Log.e( - "MoltbotWebView", + "OpenClawWebView", "onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}", ) } @@ -390,7 +390,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) if (!isDebuggable) return false val msg = consoleMessage ?: return false Log.d( - "MoltbotWebView", + "OpenClawWebView", "console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}", ) return false @@ -403,10 +403,6 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) viewModel.handleCanvasA2UIActionFromWebView(payload) } addJavascriptInterface(a2uiBridge, CanvasA2UIActionBridge.interfaceName) - addJavascriptInterface( - CanvasA2UIActionLegacyBridge(a2uiBridge), - CanvasA2UIActionLegacyBridge.interfaceName, - ) viewModel.canvas.attach(this) } }, @@ -428,22 +424,6 @@ private class CanvasA2UIActionBridge(private val onMessage: (String) -> Unit) { } companion object { - const val interfaceName: String = "moltbotCanvasA2UIAction" - } -} - -private class CanvasA2UIActionLegacyBridge(private val bridge: CanvasA2UIActionBridge) { - @JavascriptInterface - fun canvasAction(payload: String?) { - bridge.postMessage(payload) - } - - @JavascriptInterface - fun postMessage(payload: String?) { - bridge.postMessage(payload) - } - - companion object { - const val interfaceName: String = "Android" + const val interfaceName: String = "openclawCanvasA2UIAction" } } diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/SettingsSheet.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/SettingsSheet.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/ui/SettingsSheet.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/SettingsSheet.kt index f96731acf..fa32f7bb8 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/SettingsSheet.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/SettingsSheet.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui +package ai.openclaw.android.ui import android.Manifest import android.content.Context @@ -58,12 +58,12 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import bot.molt.android.BuildConfig -import bot.molt.android.LocationMode -import bot.molt.android.MainViewModel -import bot.molt.android.NodeForegroundService -import bot.molt.android.VoiceWakeMode -import bot.molt.android.WakeWords +import ai.openclaw.android.BuildConfig +import ai.openclaw.android.LocationMode +import ai.openclaw.android.MainViewModel +import ai.openclaw.android.NodeForegroundService +import ai.openclaw.android.VoiceWakeMode +import ai.openclaw.android.WakeWords @Composable fun SettingsSheet(viewModel: MainViewModel) { @@ -457,7 +457,7 @@ fun SettingsSheet(viewModel: MainViewModel) { Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) { ListItem( headlineContent = { Text("Foreground Only") }, - supportingContent = { Text("Listens only while Moltbot is open.") }, + supportingContent = { Text("Listens only while OpenClaw is open.") }, trailingContent = { RadioButton( selected = voiceWakeMode == VoiceWakeMode.Foreground, @@ -603,7 +603,7 @@ fun SettingsSheet(viewModel: MainViewModel) { ) ListItem( headlineContent = { Text("While Using") }, - supportingContent = { Text("Only while Moltbot is open.") }, + supportingContent = { Text("Only while OpenClaw is open.") }, trailingContent = { RadioButton( selected = locationMode == LocationMode.WhileUsing, @@ -650,7 +650,7 @@ fun SettingsSheet(viewModel: MainViewModel) { item { ListItem( headlineContent = { Text("Prevent Sleep") }, - supportingContent = { Text("Keeps the screen awake while Moltbot is open.") }, + supportingContent = { Text("Keeps the screen awake while OpenClaw is open.") }, trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) }, ) } diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/StatusPill.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/StatusPill.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/ui/StatusPill.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/StatusPill.kt index 199bcbf82..d608fc38a 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/StatusPill.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/StatusPill.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui +package ai.openclaw.android.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/TalkOrbOverlay.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/TalkOrbOverlay.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/ui/TalkOrbOverlay.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/TalkOrbOverlay.kt index 9098c06ff..f89b298d1 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/TalkOrbOverlay.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/TalkOrbOverlay.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui +package ai.openclaw.android.ui import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatComposer.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatComposer.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt index bc0d9917f..492516b51 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatComposer.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui.chat +package ai.openclaw.android.ui.chat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -38,7 +38,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import bot.molt.android.chat.ChatSessionEntry +import ai.openclaw.android.chat.ChatSessionEntry @Composable fun ChatComposer( @@ -143,7 +143,7 @@ fun ChatComposer( value = input, onValueChange = { input = it }, modifier = Modifier.fillMaxWidth(), - placeholder = { Text("Message Clawd…") }, + placeholder = { Text("Message OpenClaw…") }, minLines = 2, maxLines = 6, ) diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMarkdown.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMarkdown.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMarkdown.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMarkdown.kt index 10cf25b81..77dba2275 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMarkdown.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMarkdown.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui.chat +package ai.openclaw.android.ui.chat import android.graphics.BitmapFactory import android.util.Base64 diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMessageListCard.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageListCard.kt similarity index 95% rename from apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMessageListCard.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageListCard.kt index 1091de6c8..d26346372 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMessageListCard.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageListCard.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui.chat +package ai.openclaw.android.ui.chat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -20,8 +20,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp -import bot.molt.android.chat.ChatMessage -import bot.molt.android.chat.ChatPendingToolCall +import ai.openclaw.android.chat.ChatMessage +import ai.openclaw.android.chat.ChatPendingToolCall @Composable fun ChatMessageListCard( @@ -103,7 +103,7 @@ private fun EmptyChatHint(modifier: Modifier = Modifier) { tint = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( - text = "Message Clawd…", + text = "Message OpenClaw…", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMessageViews.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageViews.kt similarity index 97% rename from apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMessageViews.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageViews.kt index 59445be37..1f87db32a 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatMessageViews.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageViews.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui.chat +package ai.openclaw.android.ui.chat import android.graphics.BitmapFactory import android.util.Base64 @@ -31,10 +31,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.foundation.Image -import bot.molt.android.chat.ChatMessage -import bot.molt.android.chat.ChatMessageContent -import bot.molt.android.chat.ChatPendingToolCall -import bot.molt.android.tools.ToolDisplayRegistry +import ai.openclaw.android.chat.ChatMessage +import ai.openclaw.android.chat.ChatMessageContent +import ai.openclaw.android.chat.ChatPendingToolCall +import ai.openclaw.android.tools.ToolDisplayRegistry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import androidx.compose.ui.platform.LocalContext diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatSessionsDialog.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSessionsDialog.kt similarity index 97% rename from apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatSessionsDialog.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSessionsDialog.kt index 377a13daa..56b5cfb1f 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatSessionsDialog.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSessionsDialog.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui.chat +package ai.openclaw.android.ui.chat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import bot.molt.android.chat.ChatSessionEntry +import ai.openclaw.android.chat.ChatSessionEntry @Composable fun ChatSessionsDialog( diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatSheetContent.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSheetContent.kt similarity index 97% rename from apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatSheetContent.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSheetContent.kt index 5632be70f..effee6708 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/chat/ChatSheetContent.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSheetContent.kt @@ -1,4 +1,4 @@ -package bot.molt.android.ui.chat +package ai.openclaw.android.ui.chat import android.content.ContentResolver import android.net.Uri @@ -19,8 +19,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import bot.molt.android.MainViewModel -import bot.molt.android.chat.OutgoingAttachment +import ai.openclaw.android.MainViewModel +import ai.openclaw.android.chat.OutgoingAttachment import java.io.ByteArrayOutputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/chat/SessionFilters.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/SessionFilters.kt similarity index 94% rename from apps/android/app/src/main/java/bot/molt/android/ui/chat/SessionFilters.kt rename to apps/android/app/src/main/java/ai/openclaw/android/ui/chat/SessionFilters.kt index 227fb0a02..4efca2d0c 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/chat/SessionFilters.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/SessionFilters.kt @@ -1,6 +1,6 @@ -package bot.molt.android.ui.chat +package ai.openclaw.android.ui.chat -import bot.molt.android.chat.ChatSessionEntry +import ai.openclaw.android.chat.ChatSessionEntry private const val RECENT_WINDOW_MS = 24 * 60 * 60 * 1000L diff --git a/apps/android/app/src/main/java/bot/molt/android/voice/StreamingMediaDataSource.kt b/apps/android/app/src/main/java/ai/openclaw/android/voice/StreamingMediaDataSource.kt similarity index 98% rename from apps/android/app/src/main/java/bot/molt/android/voice/StreamingMediaDataSource.kt rename to apps/android/app/src/main/java/ai/openclaw/android/voice/StreamingMediaDataSource.kt index 7a7f61165..329707ad5 100644 --- a/apps/android/app/src/main/java/bot/molt/android/voice/StreamingMediaDataSource.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/voice/StreamingMediaDataSource.kt @@ -1,4 +1,4 @@ -package bot.molt.android.voice +package ai.openclaw.android.voice import android.media.MediaDataSource import kotlin.math.min diff --git a/apps/android/app/src/main/java/bot/molt/android/voice/TalkDirectiveParser.kt b/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkDirectiveParser.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/voice/TalkDirectiveParser.kt rename to apps/android/app/src/main/java/ai/openclaw/android/voice/TalkDirectiveParser.kt index 0d969e4d1..5c80cc1f4 100644 --- a/apps/android/app/src/main/java/bot/molt/android/voice/TalkDirectiveParser.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkDirectiveParser.kt @@ -1,4 +1,4 @@ -package bot.molt.android.voice +package ai.openclaw.android.voice import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement diff --git a/apps/android/app/src/main/java/bot/molt/android/voice/TalkModeManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkModeManager.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/voice/TalkModeManager.kt rename to apps/android/app/src/main/java/ai/openclaw/android/voice/TalkModeManager.kt index f050f8bd2..d4ca06f50 100644 --- a/apps/android/app/src/main/java/bot/molt/android/voice/TalkModeManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkModeManager.kt @@ -1,4 +1,4 @@ -package bot.molt.android.voice +package ai.openclaw.android.voice import android.Manifest import android.content.Context @@ -20,9 +20,9 @@ import android.speech.tts.TextToSpeech import android.speech.tts.UtteranceProgressListener import android.util.Log import androidx.core.content.ContextCompat -import bot.molt.android.gateway.GatewaySession -import bot.molt.android.isCanonicalMainSessionKey -import bot.molt.android.normalizeMainKey +import ai.openclaw.android.gateway.GatewaySession +import ai.openclaw.android.isCanonicalMainSessionKey +import ai.openclaw.android.normalizeMainKey import java.net.HttpURLConnection import java.net.URL import java.util.UUID diff --git a/apps/android/app/src/main/java/bot/molt/android/voice/VoiceWakeCommandExtractor.kt b/apps/android/app/src/main/java/ai/openclaw/android/voice/VoiceWakeCommandExtractor.kt similarity index 97% rename from apps/android/app/src/main/java/bot/molt/android/voice/VoiceWakeCommandExtractor.kt rename to apps/android/app/src/main/java/ai/openclaw/android/voice/VoiceWakeCommandExtractor.kt index 8da4e3289..dccd3950c 100644 --- a/apps/android/app/src/main/java/bot/molt/android/voice/VoiceWakeCommandExtractor.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/voice/VoiceWakeCommandExtractor.kt @@ -1,4 +1,4 @@ -package bot.molt.android.voice +package ai.openclaw.android.voice object VoiceWakeCommandExtractor { fun extractCommand(text: String, triggerWords: List): String? { diff --git a/apps/android/app/src/main/java/bot/molt/android/voice/VoiceWakeManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/voice/VoiceWakeManager.kt similarity index 99% rename from apps/android/app/src/main/java/bot/molt/android/voice/VoiceWakeManager.kt rename to apps/android/app/src/main/java/ai/openclaw/android/voice/VoiceWakeManager.kt index b27d0e3c7..334f985a0 100644 --- a/apps/android/app/src/main/java/bot/molt/android/voice/VoiceWakeManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/voice/VoiceWakeManager.kt @@ -1,4 +1,4 @@ -package bot.molt.android.voice +package ai.openclaw.android.voice import android.content.Context import android.content.Intent diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 0aae9e739..0098cee20 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - Moltbot Node + OpenClaw Node diff --git a/apps/android/app/src/main/res/values/themes.xml b/apps/android/app/src/main/res/values/themes.xml index f90f40dc9..3ac5d04d8 100644 --- a/apps/android/app/src/main/res/values/themes.xml +++ b/apps/android/app/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ - - -
+ +
-
Ready
-
Waiting for agent
+
Ready
+
Waiting for agent
diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/Resources/tool-display.json b/apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/Resources/tool-display.json rename to apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/ScreenCommands.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift similarity index 80% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/ScreenCommands.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift index 00c9bb5fc..dfb57ce2a 100644 --- a/apps/shared/MoltbotKit/Sources/MoltbotKit/ScreenCommands.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift @@ -1,10 +1,10 @@ import Foundation -public enum MoltbotScreenCommand: String, Codable, Sendable { +public enum OpenClawScreenCommand: String, Codable, Sendable { case record = "screen.record" } -public struct MoltbotScreenRecordParams: Codable, Sendable, Equatable { +public struct OpenClawScreenRecordParams: Codable, Sendable, Equatable { public var screenIndex: Int? public var durationMs: Int? public var fps: Double? diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/StoragePaths.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/StoragePaths.swift similarity index 80% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/StoragePaths.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/StoragePaths.swift index 437c96777..d75422957 100644 --- a/apps/shared/MoltbotKit/Sources/MoltbotKit/StoragePaths.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/StoragePaths.swift @@ -1,14 +1,14 @@ import Foundation -public enum MoltbotNodeStorage { +public enum OpenClawNodeStorage { public static func appSupportDir() throws -> URL { let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first guard let base else { - throw NSError(domain: "MoltbotNodeStorage", code: 1, userInfo: [ + throw NSError(domain: "OpenClawNodeStorage", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Application Support directory unavailable", ]) } - return base.appendingPathComponent("Moltbot", isDirectory: true) + return base.appendingPathComponent("OpenClaw", isDirectory: true) } public static func canvasRoot(sessionKey: String) throws -> URL { @@ -21,11 +21,11 @@ public enum MoltbotNodeStorage { public static func cachesDir() throws -> URL { let base = FileManager().urls(for: .cachesDirectory, in: .userDomainMask).first guard let base else { - throw NSError(domain: "MoltbotNodeStorage", code: 2, userInfo: [ + throw NSError(domain: "OpenClawNodeStorage", code: 2, userInfo: [ NSLocalizedDescriptionKey: "Caches directory unavailable", ]) } - return base.appendingPathComponent("Moltbot", isDirectory: true) + return base.appendingPathComponent("OpenClaw", isDirectory: true) } public static func canvasSnapshotsRoot(sessionKey: String) throws -> URL { diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/SystemCommands.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift similarity index 74% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/SystemCommands.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift index 5e42734c5..a2c834905 100644 --- a/apps/shared/MoltbotKit/Sources/MoltbotKit/SystemCommands.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift @@ -1,6 +1,6 @@ import Foundation -public enum MoltbotSystemCommand: String, Codable, Sendable { +public enum OpenClawSystemCommand: String, Codable, Sendable { case run = "system.run" case which = "system.which" case notify = "system.notify" @@ -8,19 +8,19 @@ public enum MoltbotSystemCommand: String, Codable, Sendable { case execApprovalsSet = "system.execApprovals.set" } -public enum MoltbotNotificationPriority: String, Codable, Sendable { +public enum OpenClawNotificationPriority: String, Codable, Sendable { case passive case active case timeSensitive } -public enum MoltbotNotificationDelivery: String, Codable, Sendable { +public enum OpenClawNotificationDelivery: String, Codable, Sendable { case system case overlay case auto } -public struct MoltbotSystemRunParams: Codable, Sendable, Equatable { +public struct OpenClawSystemRunParams: Codable, Sendable, Equatable { public var command: [String] public var rawCommand: String? public var cwd: String? @@ -57,7 +57,7 @@ public struct MoltbotSystemRunParams: Codable, Sendable, Equatable { } } -public struct MoltbotSystemWhichParams: Codable, Sendable, Equatable { +public struct OpenClawSystemWhichParams: Codable, Sendable, Equatable { public var bins: [String] public init(bins: [String]) { @@ -65,19 +65,19 @@ public struct MoltbotSystemWhichParams: Codable, Sendable, Equatable { } } -public struct MoltbotSystemNotifyParams: Codable, Sendable, Equatable { +public struct OpenClawSystemNotifyParams: Codable, Sendable, Equatable { public var title: String public var body: String public var sound: String? - public var priority: MoltbotNotificationPriority? - public var delivery: MoltbotNotificationDelivery? + public var priority: OpenClawNotificationPriority? + public var delivery: OpenClawNotificationDelivery? public init( title: String, body: String, sound: String? = nil, - priority: MoltbotNotificationPriority? = nil, - delivery: MoltbotNotificationDelivery? = nil) + priority: OpenClawNotificationPriority? = nil, + delivery: OpenClawNotificationDelivery? = nil) { self.title = title self.body = body diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/TalkDirective.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkDirective.swift similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/TalkDirective.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/TalkDirective.swift diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/TalkHistoryTimestamp.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkHistoryTimestamp.swift similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/TalkHistoryTimestamp.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/TalkHistoryTimestamp.swift diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/TalkPromptBuilder.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/TalkPromptBuilder.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/TalkSystemSpeechSynthesizer.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/TalkSystemSpeechSynthesizer.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift diff --git a/apps/shared/MoltbotKit/Sources/MoltbotKit/ToolDisplay.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift similarity index 98% rename from apps/shared/MoltbotKit/Sources/MoltbotKit/ToolDisplay.swift rename to apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift index 9016d158e..d52e24ca8 100644 --- a/apps/shared/MoltbotKit/Sources/MoltbotKit/ToolDisplay.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift @@ -90,7 +90,7 @@ public enum ToolDisplayRegistry { } private static func loadConfig() -> ToolDisplayConfig { - guard let url = MoltbotKitResources.bundle.url(forResource: "tool-display", withExtension: "json") else { + guard let url = OpenClawKitResources.bundle.url(forResource: "tool-display", withExtension: "json") else { return self.defaultConfig() } do { diff --git a/apps/shared/MoltbotKit/Sources/MoltbotProtocol/AnyCodable.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotProtocol/AnyCodable.swift rename to apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift diff --git a/apps/shared/MoltbotKit/Sources/MoltbotProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotProtocol/GatewayModels.swift rename to apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift diff --git a/apps/shared/MoltbotKit/Sources/MoltbotProtocol/WizardHelpers.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WizardHelpers.swift similarity index 100% rename from apps/shared/MoltbotKit/Sources/MoltbotProtocol/WizardHelpers.swift rename to apps/shared/OpenClawKit/Sources/OpenClawProtocol/WizardHelpers.swift diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/AssistantTextParserTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/AssistantTextParserTests.swift similarity index 97% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/AssistantTextParserTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/AssistantTextParserTests.swift index 5bb2517a4..5f36bb9c2 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/AssistantTextParserTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/AssistantTextParserTests.swift @@ -1,5 +1,5 @@ import Testing -@testable import MoltbotChatUI +@testable import OpenClawChatUI @Suite struct AssistantTextParserTests { @Test func splitsThinkAndFinalSegments() { diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/BonjourEscapesTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/BonjourEscapesTests.swift similarity index 87% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/BonjourEscapesTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/BonjourEscapesTests.swift index 0ccd6a61d..a7fa1438d 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/BonjourEscapesTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/BonjourEscapesTests.swift @@ -1,4 +1,4 @@ -import MoltbotKit +import OpenClawKit import Testing @Suite struct BonjourEscapesTests { @@ -8,7 +8,7 @@ import Testing } @Test func decodeSpaces() { - #expect(BonjourEscapes.decode("Moltbot\\032Gateway") == "Moltbot Gateway") + #expect(BonjourEscapes.decode("OpenClaw\\032Gateway") == "OpenClaw Gateway") } @Test func decodeMultipleEscapes() { diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasA2UIActionTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasA2UIActionTests.swift similarity index 53% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasA2UIActionTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasA2UIActionTests.swift index fd7e07911..f6070f6de 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasA2UIActionTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasA2UIActionTests.swift @@ -1,28 +1,28 @@ -import MoltbotKit +import OpenClawKit import Foundation import Testing @Suite struct CanvasA2UIActionTests { @Test func sanitizeTagValueIsStable() { - #expect(MoltbotCanvasA2UIAction.sanitizeTagValue("Hello World!") == "Hello_World_") - #expect(MoltbotCanvasA2UIAction.sanitizeTagValue(" ") == "-") - #expect(MoltbotCanvasA2UIAction.sanitizeTagValue("macOS 26.2") == "macOS_26.2") + #expect(OpenClawCanvasA2UIAction.sanitizeTagValue("Hello World!") == "Hello_World_") + #expect(OpenClawCanvasA2UIAction.sanitizeTagValue(" ") == "-") + #expect(OpenClawCanvasA2UIAction.sanitizeTagValue("macOS 26.2") == "macOS_26.2") } @Test func extractActionNameAcceptsNameOrAction() { - #expect(MoltbotCanvasA2UIAction.extractActionName(["name": "Hello"]) == "Hello") - #expect(MoltbotCanvasA2UIAction.extractActionName(["action": "Wave"]) == "Wave") - #expect(MoltbotCanvasA2UIAction.extractActionName(["name": " ", "action": "Fallback"]) == "Fallback") - #expect(MoltbotCanvasA2UIAction.extractActionName(["action": " "]) == nil) + #expect(OpenClawCanvasA2UIAction.extractActionName(["name": "Hello"]) == "Hello") + #expect(OpenClawCanvasA2UIAction.extractActionName(["action": "Wave"]) == "Wave") + #expect(OpenClawCanvasA2UIAction.extractActionName(["name": " ", "action": "Fallback"]) == "Fallback") + #expect(OpenClawCanvasA2UIAction.extractActionName(["action": " "]) == nil) } @Test func formatAgentMessageIsTokenEfficientAndUnambiguous() { - let messageContext = MoltbotCanvasA2UIAction.AgentMessageContext( + let messageContext = OpenClawCanvasA2UIAction.AgentMessageContext( actionName: "Get Weather", session: .init(key: "main", surfaceId: "main"), component: .init(id: "btnWeather", host: "Peter’s iPad", instanceId: "ipad16,6"), contextJSON: "{\"city\":\"Vienna\"}") - let msg = MoltbotCanvasA2UIAction.formatAgentMessage(messageContext) + let msg = OpenClawCanvasA2UIAction.formatAgentMessage(messageContext) #expect(msg.contains("CANVAS_A2UI ")) #expect(msg.contains("action=Get_Weather")) diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasA2UITests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasA2UITests.swift similarity index 61% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasA2UITests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasA2UITests.swift index c063f80e0..4c420cc94 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasA2UITests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasA2UITests.swift @@ -1,11 +1,11 @@ -import MoltbotKit +import OpenClawKit import Testing @Suite struct CanvasA2UITests { @Test func commandStringsAreStable() { - #expect(MoltbotCanvasA2UICommand.push.rawValue == "canvas.a2ui.push") - #expect(MoltbotCanvasA2UICommand.pushJSONL.rawValue == "canvas.a2ui.pushJSONL") - #expect(MoltbotCanvasA2UICommand.reset.rawValue == "canvas.a2ui.reset") + #expect(OpenClawCanvasA2UICommand.push.rawValue == "canvas.a2ui.push") + #expect(OpenClawCanvasA2UICommand.pushJSONL.rawValue == "canvas.a2ui.pushJSONL") + #expect(OpenClawCanvasA2UICommand.reset.rawValue == "canvas.a2ui.reset") } @Test func jsonlDecodesAndValidatesV0_8() throws { @@ -16,7 +16,7 @@ import Testing {"deleteSurface":{"surfaceId":"main"}} """ - let messages = try MoltbotCanvasA2UIJSONL.decodeMessagesFromJSONL(jsonl) + let messages = try OpenClawCanvasA2UIJSONL.decodeMessagesFromJSONL(jsonl) #expect(messages.count == 4) } @@ -26,7 +26,7 @@ import Testing """ #expect(throws: Error.self) { - _ = try MoltbotCanvasA2UIJSONL.decodeMessagesFromJSONL(jsonl) + _ = try OpenClawCanvasA2UIJSONL.decodeMessagesFromJSONL(jsonl) } } @@ -36,7 +36,7 @@ import Testing """ #expect(throws: Error.self) { - _ = try MoltbotCanvasA2UIJSONL.decodeMessagesFromJSONL(jsonl) + _ = try OpenClawCanvasA2UIJSONL.decodeMessagesFromJSONL(jsonl) } } } diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasSnapshotFormatTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasSnapshotFormatTests.swift similarity index 83% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasSnapshotFormatTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasSnapshotFormatTests.swift index d83a91952..ab49a4f46 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/CanvasSnapshotFormatTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/CanvasSnapshotFormatTests.swift @@ -1,11 +1,11 @@ -import MoltbotKit +import OpenClawKit import Foundation import Testing @Suite struct CanvasSnapshotFormatTests { @Test func acceptsJpgAlias() throws { struct Wrapper: Codable { - var format: MoltbotCanvasSnapshotFormat + var format: OpenClawCanvasSnapshotFormat } let data = try #require("{\"format\":\"jpg\"}".data(using: .utf8)) diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatMarkdownPreprocessorTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift similarity index 94% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatMarkdownPreprocessorTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift index ba855977b..808f74af6 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatMarkdownPreprocessorTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift @@ -1,5 +1,5 @@ import Testing -@testable import MoltbotChatUI +@testable import OpenClawChatUI @Suite("ChatMarkdownPreprocessor") struct ChatMarkdownPreprocessorTests { diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatThemeTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatThemeTests.swift similarity index 76% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatThemeTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatThemeTests.swift index 97a1238a7..2c7a5fff1 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatThemeTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatThemeTests.swift @@ -1,6 +1,6 @@ import Foundation import Testing -@testable import MoltbotChatUI +@testable import OpenClawChatUI #if os(macOS) import AppKit @@ -19,8 +19,8 @@ private func luminance(_ color: NSColor) throws -> CGFloat { let lightAppearance = try #require(NSAppearance(named: .aqua)) let darkAppearance = try #require(NSAppearance(named: .darkAqua)) - let lightResolved = MoltbotChatTheme.resolvedAssistantBubbleColor(for: lightAppearance) - let darkResolved = MoltbotChatTheme.resolvedAssistantBubbleColor(for: darkAppearance) + let lightResolved = OpenClawChatTheme.resolvedAssistantBubbleColor(for: lightAppearance) + let darkResolved = OpenClawChatTheme.resolvedAssistantBubbleColor(for: darkAppearance) #expect(try luminance(lightResolved) > luminance(darkResolved)) #else #expect(Bool(true)) diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift similarity index 84% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatViewModelTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index 9c197ec3e..3babe8b9a 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -1,7 +1,7 @@ -import MoltbotKit +import OpenClawKit import Foundation import Testing -@testable import MoltbotChatUI +@testable import OpenClawChatUI private struct TimeoutError: Error, CustomStringConvertible { let label: String @@ -31,40 +31,40 @@ private actor TestChatTransportState { var abortedRunIds: [String] = [] } -private final class TestChatTransport: @unchecked Sendable, MoltbotChatTransport { +private final class TestChatTransport: @unchecked Sendable, OpenClawChatTransport { private let state = TestChatTransportState() - private let historyResponses: [MoltbotChatHistoryPayload] - private let sessionsResponses: [MoltbotChatSessionsListResponse] + private let historyResponses: [OpenClawChatHistoryPayload] + private let sessionsResponses: [OpenClawChatSessionsListResponse] - private let stream: AsyncStream - private let continuation: AsyncStream.Continuation + private let stream: AsyncStream + private let continuation: AsyncStream.Continuation init( - historyResponses: [MoltbotChatHistoryPayload], - sessionsResponses: [MoltbotChatSessionsListResponse] = []) + historyResponses: [OpenClawChatHistoryPayload], + sessionsResponses: [OpenClawChatSessionsListResponse] = []) { self.historyResponses = historyResponses self.sessionsResponses = sessionsResponses - var cont: AsyncStream.Continuation! + var cont: AsyncStream.Continuation! self.stream = AsyncStream { c in cont = c } self.continuation = cont } - func events() -> AsyncStream { + func events() -> AsyncStream { self.stream } func setActiveSessionKey(_: String) async throws {} - func requestHistory(sessionKey: String) async throws -> MoltbotChatHistoryPayload { + func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload { let idx = await self.state.historyCallCount await self.state.setHistoryCallCount(idx + 1) if idx < self.historyResponses.count { return self.historyResponses[idx] } - return self.historyResponses.last ?? MoltbotChatHistoryPayload( + return self.historyResponses.last ?? OpenClawChatHistoryPayload( sessionKey: sessionKey, sessionId: nil, messages: [], @@ -76,23 +76,23 @@ private final class TestChatTransport: @unchecked Sendable, MoltbotChatTransport message _: String, thinking _: String, idempotencyKey: String, - attachments _: [MoltbotChatAttachmentPayload]) async throws -> MoltbotChatSendResponse + attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse { await self.state.sentRunIdsAppend(idempotencyKey) - return MoltbotChatSendResponse(runId: idempotencyKey, status: "ok") + return OpenClawChatSendResponse(runId: idempotencyKey, status: "ok") } func abortRun(sessionKey _: String, runId: String) async throws { await self.state.abortedRunIdsAppend(runId) } - func listSessions(limit _: Int?) async throws -> MoltbotChatSessionsListResponse { + func listSessions(limit _: Int?) async throws -> OpenClawChatSessionsListResponse { let idx = await self.state.sessionsCallCount await self.state.setSessionsCallCount(idx + 1) if idx < self.sessionsResponses.count { return self.sessionsResponses[idx] } - return self.sessionsResponses.last ?? MoltbotChatSessionsListResponse( + return self.sessionsResponses.last ?? OpenClawChatSessionsListResponse( ts: nil, path: nil, count: 0, @@ -104,7 +104,7 @@ private final class TestChatTransport: @unchecked Sendable, MoltbotChatTransport true } - func emit(_ evt: MoltbotChatTransportEvent) { + func emit(_ evt: OpenClawChatTransportEvent) { self.continuation.yield(evt) } @@ -139,12 +139,12 @@ extension TestChatTransportState { @Suite struct ChatViewModelTests { @Test func streamsAssistantAndClearsOnFinal() async throws { let sessionId = "sess-main" - let history1 = MoltbotChatHistoryPayload( + let history1 = OpenClawChatHistoryPayload( sessionKey: "main", sessionId: sessionId, messages: [], thinkingLevel: "off") - let history2 = MoltbotChatHistoryPayload( + let history2 = OpenClawChatHistoryPayload( sessionKey: "main", sessionId: sessionId, messages: [ @@ -157,7 +157,7 @@ extension TestChatTransportState { thinkingLevel: "off") let transport = TestChatTransport(historyResponses: [history1, history2]) - let vm = await MainActor.run { MoltbotChatViewModel(sessionKey: "main", transport: transport) } + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } await MainActor.run { vm.load() } try await waitUntil("bootstrap") { await MainActor.run { vm.healthOK && vm.sessionId == sessionId } } @@ -170,7 +170,7 @@ extension TestChatTransportState { transport.emit( .agent( - MoltbotAgentEventPayload( + OpenClawAgentEventPayload( runId: sessionId, seq: 1, stream: "assistant", @@ -183,7 +183,7 @@ extension TestChatTransportState { transport.emit( .agent( - MoltbotAgentEventPayload( + OpenClawAgentEventPayload( runId: sessionId, seq: 2, stream: "tool", @@ -200,7 +200,7 @@ extension TestChatTransportState { let runId = try #require(await transport.lastSentRunId()) transport.emit( .chat( - MoltbotChatEventPayload( + OpenClawChatEventPayload( runId: runId, sessionKey: "main", state: "final", @@ -217,20 +217,20 @@ extension TestChatTransportState { @Test func clearsStreamingOnExternalFinalEvent() async throws { let sessionId = "sess-main" - let history = MoltbotChatHistoryPayload( + let history = OpenClawChatHistoryPayload( sessionKey: "main", sessionId: sessionId, messages: [], thinkingLevel: "off") let transport = TestChatTransport(historyResponses: [history, history]) - let vm = await MainActor.run { MoltbotChatViewModel(sessionKey: "main", transport: transport) } + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } await MainActor.run { vm.load() } try await waitUntil("bootstrap") { await MainActor.run { vm.healthOK && vm.sessionId == sessionId } } transport.emit( .agent( - MoltbotAgentEventPayload( + OpenClawAgentEventPayload( runId: sessionId, seq: 1, stream: "assistant", @@ -239,7 +239,7 @@ extension TestChatTransportState { transport.emit( .agent( - MoltbotAgentEventPayload( + OpenClawAgentEventPayload( runId: sessionId, seq: 2, stream: "tool", @@ -258,7 +258,7 @@ extension TestChatTransportState { transport.emit( .chat( - MoltbotChatEventPayload( + OpenClawChatEventPayload( runId: "other-run", sessionKey: "main", state: "final", @@ -274,18 +274,18 @@ extension TestChatTransportState { let recent = now - (2 * 60 * 60 * 1000) let recentOlder = now - (5 * 60 * 60 * 1000) let stale = now - (26 * 60 * 60 * 1000) - let history = MoltbotChatHistoryPayload( + let history = OpenClawChatHistoryPayload( sessionKey: "main", sessionId: "sess-main", messages: [], thinkingLevel: "off") - let sessions = MoltbotChatSessionsListResponse( + let sessions = OpenClawChatSessionsListResponse( ts: now, path: nil, count: 4, defaults: nil, sessions: [ - MoltbotChatSessionEntry( + OpenClawChatSessionEntry( key: "recent-1", kind: nil, displayName: nil, @@ -304,7 +304,7 @@ extension TestChatTransportState { totalTokens: nil, model: nil, contextTokens: nil), - MoltbotChatSessionEntry( + OpenClawChatSessionEntry( key: "main", kind: nil, displayName: nil, @@ -323,7 +323,7 @@ extension TestChatTransportState { totalTokens: nil, model: nil, contextTokens: nil), - MoltbotChatSessionEntry( + OpenClawChatSessionEntry( key: "recent-2", kind: nil, displayName: nil, @@ -342,7 +342,7 @@ extension TestChatTransportState { totalTokens: nil, model: nil, contextTokens: nil), - MoltbotChatSessionEntry( + OpenClawChatSessionEntry( key: "old-1", kind: nil, displayName: nil, @@ -366,7 +366,7 @@ extension TestChatTransportState { let transport = TestChatTransport( historyResponses: [history], sessionsResponses: [sessions]) - let vm = await MainActor.run { MoltbotChatViewModel(sessionKey: "main", transport: transport) } + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } await MainActor.run { vm.load() } try await waitUntil("sessions loaded") { await MainActor.run { !vm.sessions.isEmpty } } @@ -377,18 +377,18 @@ extension TestChatTransportState { @Test func sessionChoicesIncludeCurrentWhenMissing() async throws { let now = Date().timeIntervalSince1970 * 1000 let recent = now - (30 * 60 * 1000) - let history = MoltbotChatHistoryPayload( + let history = OpenClawChatHistoryPayload( sessionKey: "custom", sessionId: "sess-custom", messages: [], thinkingLevel: "off") - let sessions = MoltbotChatSessionsListResponse( + let sessions = OpenClawChatSessionsListResponse( ts: now, path: nil, count: 1, defaults: nil, sessions: [ - MoltbotChatSessionEntry( + OpenClawChatSessionEntry( key: "main", kind: nil, displayName: nil, @@ -412,7 +412,7 @@ extension TestChatTransportState { let transport = TestChatTransport( historyResponses: [history], sessionsResponses: [sessions]) - let vm = await MainActor.run { MoltbotChatViewModel(sessionKey: "custom", transport: transport) } + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "custom", transport: transport) } await MainActor.run { vm.load() } try await waitUntil("sessions loaded") { await MainActor.run { !vm.sessions.isEmpty } } @@ -422,20 +422,20 @@ extension TestChatTransportState { @Test func clearsStreamingOnExternalErrorEvent() async throws { let sessionId = "sess-main" - let history = MoltbotChatHistoryPayload( + let history = OpenClawChatHistoryPayload( sessionKey: "main", sessionId: sessionId, messages: [], thinkingLevel: "off") let transport = TestChatTransport(historyResponses: [history, history]) - let vm = await MainActor.run { MoltbotChatViewModel(sessionKey: "main", transport: transport) } + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } await MainActor.run { vm.load() } try await waitUntil("bootstrap") { await MainActor.run { vm.healthOK && vm.sessionId == sessionId } } transport.emit( .agent( - MoltbotAgentEventPayload( + OpenClawAgentEventPayload( runId: sessionId, seq: 1, stream: "assistant", @@ -448,7 +448,7 @@ extension TestChatTransportState { transport.emit( .chat( - MoltbotChatEventPayload( + OpenClawChatEventPayload( runId: "other-run", sessionKey: "main", state: "error", @@ -460,13 +460,13 @@ extension TestChatTransportState { @Test func abortRequestsDoNotClearPendingUntilAbortedEvent() async throws { let sessionId = "sess-main" - let history = MoltbotChatHistoryPayload( + let history = OpenClawChatHistoryPayload( sessionKey: "main", sessionId: sessionId, messages: [], thinkingLevel: "off") let transport = TestChatTransport(historyResponses: [history, history]) - let vm = await MainActor.run { MoltbotChatViewModel(sessionKey: "main", transport: transport) } + let vm = await MainActor.run { OpenClawChatViewModel(sessionKey: "main", transport: transport) } await MainActor.run { vm.load() } try await waitUntil("bootstrap") { await MainActor.run { vm.healthOK && vm.sessionId == sessionId } } @@ -490,7 +490,7 @@ extension TestChatTransportState { transport.emit( .chat( - MoltbotChatEventPayload( + OpenClawChatEventPayload( runId: runId, sessionKey: "main", state: "aborted", diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ElevenLabsTTSValidationTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ElevenLabsTTSValidationTests.swift similarity index 96% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/ElevenLabsTTSValidationTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/ElevenLabsTTSValidationTests.swift index 0803d6b22..1d672db35 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ElevenLabsTTSValidationTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ElevenLabsTTSValidationTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import MoltbotKit +@testable import OpenClawKit final class ElevenLabsTTSValidationTests: XCTestCase { func testValidatedOutputFormatAllowsOnlyMp3Presets() { diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/GatewayNodeSessionTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift similarity index 97% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/GatewayNodeSessionTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift index 8a3a120cf..91e309615 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/GatewayNodeSessionTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift @@ -1,7 +1,7 @@ import Foundation import Testing -@testable import MoltbotKit -import MoltbotProtocol +@testable import OpenClawKit +import OpenClawProtocol struct GatewayNodeSessionTests { @Test diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/JPEGTranscoderTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/JPEGTranscoderTests.swift similarity index 99% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/JPEGTranscoderTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/JPEGTranscoderTests.swift index b49bdbf41..5070a8b14 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/JPEGTranscoderTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/JPEGTranscoderTests.swift @@ -1,4 +1,4 @@ -import MoltbotKit +import OpenClawKit import CoreGraphics import ImageIO import Testing diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkDirectiveTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkDirectiveTests.swift similarity index 98% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkDirectiveTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkDirectiveTests.swift index 2b7e637c9..11565ac74 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkDirectiveTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkDirectiveTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import MoltbotKit +@testable import OpenClawKit final class TalkDirectiveTests: XCTestCase { func testParsesDirectiveAndStripsLine() { diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkHistoryTimestampTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkHistoryTimestampTests.swift similarity index 95% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkHistoryTimestampTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkHistoryTimestampTests.swift index eeb373143..e66c4e1e9 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkHistoryTimestampTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkHistoryTimestampTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import MoltbotKit +@testable import OpenClawKit final class TalkHistoryTimestampTests: XCTestCase { func testSecondsTimestampsAreAcceptedWithSmallTolerance() { diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkPromptBuilderTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkPromptBuilderTests.swift similarity index 95% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkPromptBuilderTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkPromptBuilderTests.swift index 193ad49fa..1ca18fdf3 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/TalkPromptBuilderTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkPromptBuilderTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import MoltbotKit +@testable import OpenClawKit final class TalkPromptBuilderTests: XCTestCase { func testBuildIncludesTranscript() { diff --git a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ToolDisplayRegistryTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ToolDisplayRegistryTests.swift similarity index 75% rename from apps/shared/MoltbotKit/Tests/MoltbotKitTests/ToolDisplayRegistryTests.swift rename to apps/shared/OpenClawKit/Tests/OpenClawKitTests/ToolDisplayRegistryTests.swift index 61c529437..dbf38138a 100644 --- a/apps/shared/MoltbotKit/Tests/MoltbotKitTests/ToolDisplayRegistryTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ToolDisplayRegistryTests.swift @@ -1,10 +1,10 @@ -import MoltbotKit +import OpenClawKit import Foundation import Testing @Suite struct ToolDisplayRegistryTests { @Test func loadsToolDisplayConfigFromBundle() { - let url = MoltbotKitResources.bundle.url(forResource: "tool-display", withExtension: "json") + let url = OpenClawKitResources.bundle.url(forResource: "tool-display", withExtension: "json") #expect(url != nil) } diff --git a/apps/shared/MoltbotKit/Tools/CanvasA2UI/bootstrap.js b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js similarity index 90% rename from apps/shared/MoltbotKit/Tools/CanvasA2UI/bootstrap.js rename to apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js index 7548ace80..563adcc3b 100644 --- a/apps/shared/MoltbotKit/Tools/CanvasA2UI/bootstrap.js +++ b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js @@ -4,7 +4,7 @@ import { ContextProvider } from "@lit/context"; import { v0_8 } from "@a2ui/lit"; import "@a2ui/lit/ui"; -import { themeContext } from "@moltbot/a2ui-theme-context"; +import { themeContext } from "@openclaw/a2ui-theme-context"; const modalStyles = css` dialog { @@ -32,7 +32,6 @@ if (modalElement && Array.isArray(modalElement.styles)) { modalElement.styles = [...modalElement.styles, modalStyles]; } -const empty = Object.freeze({}); const emptyClasses = () => ({}); const textHintStyles = () => ({ h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, body: {}, caption: {} }); @@ -42,7 +41,7 @@ const buttonShadow = isAndroid ? "0 2px 10px rgba(6, 182, 212, 0.14)" : "0 10px const statusShadow = isAndroid ? "0 2px 10px rgba(0, 0, 0, 0.18)" : "0 10px 24px rgba(0, 0, 0, 0.25)"; const statusBlur = isAndroid ? "10px" : "14px"; -const moltbotTheme = { +const openclawTheme = { components: { AudioPlayer: emptyClasses(), Button: emptyClasses(), @@ -152,7 +151,7 @@ const moltbotTheme = { }, }; -class MoltbotA2UIHost extends LitElement { +class OpenClawA2UIHost extends LitElement { static properties = { surfaces: { state: true }, pendingAction: { state: true }, @@ -160,9 +159,9 @@ class MoltbotA2UIHost extends LitElement { }; #processor = v0_8.Data.createSignalA2uiMessageProcessor(); - #themeProvider = new ContextProvider(this, { + themeProvider = new ContextProvider(this, { context: themeContext, - initialValue: moltbotTheme, + initialValue: openclawTheme, }); surfaces = []; @@ -177,10 +176,10 @@ class MoltbotA2UIHost extends LitElement { position: relative; box-sizing: border-box; padding: - var(--moltbot-a2ui-inset-top, 0px) - var(--moltbot-a2ui-inset-right, 0px) - var(--moltbot-a2ui-inset-bottom, 0px) - var(--moltbot-a2ui-inset-left, 0px); + var(--openclaw-a2ui-inset-top, 0px) + var(--openclaw-a2ui-inset-right, 0px) + var(--openclaw-a2ui-inset-bottom, 0px) + var(--openclaw-a2ui-inset-left, 0px); } #surfaces { @@ -189,14 +188,14 @@ class MoltbotA2UIHost extends LitElement { gap: 12px; height: 100%; overflow: auto; - padding-bottom: var(--moltbot-a2ui-scroll-pad-bottom, 0px); + padding-bottom: var(--openclaw-a2ui-scroll-pad-bottom, 0px); } .status { position: absolute; left: 50%; transform: translateX(-50%); - top: var(--moltbot-a2ui-status-top, 12px); + top: var(--openclaw-a2ui-status-top, 12px); display: inline-flex; align-items: center; gap: 8px; @@ -217,7 +216,7 @@ class MoltbotA2UIHost extends LitElement { position: absolute; left: 50%; transform: translateX(-50%); - bottom: var(--moltbot-a2ui-toast-bottom, 12px); + bottom: var(--openclaw-a2ui-toast-bottom, 12px); display: inline-flex; align-items: center; gap: 8px; @@ -243,7 +242,7 @@ class MoltbotA2UIHost extends LitElement { position: absolute; left: 50%; transform: translateX(-50%); - top: var(--moltbot-a2ui-empty-top, var(--moltbot-a2ui-status-top, 12px)); + top: var(--openclaw-a2ui-empty-top, var(--openclaw-a2ui-status-top, 12px)); text-align: center; opacity: 0.8; padding: 10px 12px; @@ -281,18 +280,21 @@ class MoltbotA2UIHost extends LitElement { reset: () => this.reset(), getSurfaces: () => Array.from(this.#processor.getSurfaces().keys()), }; - globalThis.moltbotA2UI = api; - globalThis.clawdbotA2UI = api; + globalThis.openclawA2UI = api; this.addEventListener("a2uiaction", (evt) => this.#handleA2UIAction(evt)); this.#statusListener = (evt) => this.#handleActionStatus(evt); - globalThis.addEventListener("moltbot:a2ui-action-status", this.#statusListener); + for (const eventName of ["openclaw:a2ui-action-status"]) { + globalThis.addEventListener(eventName, this.#statusListener); + } this.#syncSurfaces(); } disconnectedCallback() { super.disconnectedCallback(); if (this.#statusListener) { - globalThis.removeEventListener("moltbot:a2ui-action-status", this.#statusListener); + for (const eventName of ["openclaw:a2ui-action-status"]) { + globalThis.removeEventListener(eventName, this.#statusListener); + } this.#statusListener = null; } } @@ -315,8 +317,8 @@ class MoltbotA2UIHost extends LitElement { #handleActionStatus(evt) { const detail = evt?.detail ?? null; - if (!detail || typeof detail.id !== "string") return; - if (!this.pendingAction || this.pendingAction.id !== detail.id) return; + if (!detail || typeof detail.id !== "string") {return;} + if (!this.pendingAction || this.pendingAction.id !== detail.id) {return;} if (detail.ok) { this.pendingAction = { ...this.pendingAction, phase: "sent", sentAt: Date.now() }; @@ -359,7 +361,7 @@ class MoltbotA2UIHost extends LitElement { for (const item of ctxItems) { const key = item?.key; const value = item?.value ?? null; - if (!key || !value) continue; + if (!key || !value) {continue;} if (typeof value.path === "string") { const resolved = sourceNode @@ -395,20 +397,15 @@ class MoltbotA2UIHost extends LitElement { ...(Object.keys(context).length ? { context } : {}), }; - globalThis.__moltbotLastA2UIAction = userAction; + globalThis.__openclawLastA2UIAction = userAction; const handler = - globalThis.webkit?.messageHandlers?.moltbotCanvasA2UIAction ?? - globalThis.webkit?.messageHandlers?.clawdbotCanvasA2UIAction ?? - globalThis.moltbotCanvasA2UIAction ?? - globalThis.clawdbotCanvasA2UIAction; + globalThis.webkit?.messageHandlers?.openclawCanvasA2UIAction ?? + globalThis.openclawCanvasA2UIAction; if (handler?.postMessage) { try { // WebKit message handlers support structured objects; Android's JS interface expects strings. - if ( - handler === globalThis.moltbotCanvasA2UIAction || - handler === globalThis.clawdbotCanvasA2UIAction - ) { + if (handler === globalThis.openclawCanvasA2UIAction) { handler.postMessage(JSON.stringify({ userAction })); } else { handler.postMessage({ userAction }); @@ -488,4 +485,6 @@ class MoltbotA2UIHost extends LitElement { } } -customElements.define("moltbot-a2ui-host", MoltbotA2UIHost); +if (!customElements.get("openclaw-a2ui-host")) { + customElements.define("openclaw-a2ui-host", OpenClawA2UIHost); +} diff --git a/apps/shared/MoltbotKit/Tools/CanvasA2UI/rolldown.config.mjs b/apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs similarity index 96% rename from apps/shared/MoltbotKit/Tools/CanvasA2UI/rolldown.config.mjs rename to apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs index 1b7a9c04e..dbd4b86ff 100644 --- a/apps/shared/MoltbotKit/Tools/CanvasA2UI/rolldown.config.mjs +++ b/apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs @@ -27,7 +27,7 @@ export default defineConfig({ alias: { "@a2ui/lit": path.resolve(a2uiLitDist, "index.js"), "@a2ui/lit/ui": path.resolve(a2uiLitDist, "0.8/ui/ui.js"), - "@moltbot/a2ui-theme-context": a2uiThemeContext, + "@openclaw/a2ui-theme-context": a2uiThemeContext, "@lit/context": path.resolve(repoRoot, "node_modules/@lit/context/index.js"), "@lit/context/": path.resolve(repoRoot, "node_modules/@lit/context/"), "@lit-labs/signals": path.resolve(repoRoot, "node_modules/@lit-labs/signals/index.js"), diff --git a/assets/chrome-extension/README.md b/assets/chrome-extension/README.md index 670089321..2a2a11a3b 100644 --- a/assets/chrome-extension/README.md +++ b/assets/chrome-extension/README.md @@ -1,16 +1,16 @@ -# Clawdbot Chrome Extension (Browser Relay) +# OpenClaw Chrome Extension (Browser Relay) -Purpose: attach Clawdbot to an existing Chrome tab so the Gateway can automate it (via the local CDP relay server). +Purpose: attach OpenClaw to an existing Chrome tab so the Gateway can automate it (via the local CDP relay server). ## Dev / load unpacked -1. Build/run Clawdbot Gateway with browser control enabled. +1. Build/run OpenClaw Gateway with browser control enabled. 2. Ensure the relay server is reachable at `http://127.0.0.1:18792/` (default). 3. Install the extension to a stable path: ```bash - clawdbot browser extension install - clawdbot browser extension path + openclaw browser extension install + openclaw browser extension path ``` 4. Chrome → `chrome://extensions` → enable “Developer mode”. diff --git a/assets/chrome-extension/background.js b/assets/chrome-extension/background.js index ab1a891e7..31ba401bd 100644 --- a/assets/chrome-extension/background.js +++ b/assets/chrome-extension/background.js @@ -114,7 +114,7 @@ function onRelayClosed(reason) { setBadge(tabId, 'connecting') void chrome.action.setTitle({ tabId, - title: 'Moltbot Browser Relay: disconnected (click to re-attach)', + title: 'OpenClaw Browser Relay: disconnected (click to re-attach)', }) } tabs.clear() @@ -225,7 +225,7 @@ async function attachTab(tabId, opts = {}) { tabBySession.set(sessionId, tabId) void chrome.action.setTitle({ tabId, - title: 'Moltbot Browser Relay: attached (click to detach)', + title: 'OpenClaw Browser Relay: attached (click to detach)', }) if (!opts.skipAttachedEvent) { @@ -278,7 +278,7 @@ async function detachTab(tabId, reason) { setBadge(tabId, 'off') void chrome.action.setTitle({ tabId, - title: 'Moltbot Browser Relay (click to attach/detach)', + title: 'OpenClaw Browser Relay (click to attach/detach)', }) } @@ -297,7 +297,7 @@ async function connectOrToggleForActiveTab() { setBadge(tabId, 'connecting') void chrome.action.setTitle({ tabId, - title: 'Moltbot Browser Relay: connecting to local relay…', + title: 'OpenClaw Browser Relay: connecting to local relay…', }) try { @@ -308,7 +308,7 @@ async function connectOrToggleForActiveTab() { setBadge(tabId, 'error') void chrome.action.setTitle({ tabId, - title: 'Moltbot Browser Relay: relay not running (open options for setup)', + title: 'OpenClaw Browser Relay: relay not running (open options for setup)', }) void maybeOpenHelpOnce() // Extra breadcrumbs in chrome://extensions service worker logs. diff --git a/assets/chrome-extension/manifest.json b/assets/chrome-extension/manifest.json index 09926ccc5..d6b593990 100644 --- a/assets/chrome-extension/manifest.json +++ b/assets/chrome-extension/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "Moltbot Browser Relay", + "name": "OpenClaw Browser Relay", "version": "0.1.0", - "description": "Attach Moltbot to your existing Chrome tab via a local CDP relay server.", + "description": "Attach OpenClaw to your existing Chrome tab via a local CDP relay server.", "icons": { "16": "icons/icon16.png", "32": "icons/icon32.png", @@ -13,7 +13,7 @@ "host_permissions": ["http://127.0.0.1/*", "http://localhost/*"], "background": { "service_worker": "background.js", "type": "module" }, "action": { - "default_title": "Moltbot Browser Relay (click to attach/detach)", + "default_title": "OpenClaw Browser Relay (click to attach/detach)", "default_icon": { "16": "icons/icon16.png", "32": "icons/icon32.png", diff --git a/assets/chrome-extension/options.html b/assets/chrome-extension/options.html index fc4aa368d..14704d65c 100644 --- a/assets/chrome-extension/options.html +++ b/assets/chrome-extension/options.html @@ -3,7 +3,7 @@ - Moltbot Browser Relay + OpenClaw Browser Relay - -
+ +
-
Ready
-
Waiting for agent
+
Ready
+
Waiting for agent
- + `; // Check if already injected - if (html.includes("__CLAWDBOT_ASSISTANT_NAME__")) return html; + if (html.includes("__OPENCLAW_ASSISTANT_NAME__")) { + return html; + } const headClose = html.indexOf(""); if (headClose !== -1) { return `${html.slice(0, headClose)}${script}${html.slice(headClose)}`; @@ -195,7 +204,7 @@ function injectControlUiConfig(html: string, opts: ControlUiInjectionOpts): stri interface ServeIndexHtmlOpts { basePath: string; - config?: MoltbotConfig; + config?: OpenClawConfig; agentId?: string; } @@ -227,10 +236,16 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex } function isSafeRelativePath(relPath: string) { - if (!relPath) return false; + if (!relPath) { + return false; + } const normalized = path.posix.normalize(relPath); - if (normalized.startsWith("../") || normalized === "..") return false; - if (normalized.includes("\0")) return false; + if (normalized.startsWith("../") || normalized === "..") { + return false; + } + if (normalized.includes("\0")) { + return false; + } return true; } @@ -240,7 +255,9 @@ export function handleControlUiHttpRequest( opts?: ControlUiRequestOptions, ): boolean { const urlRaw = req.url; - if (!urlRaw) return false; + if (!urlRaw) { + return false; + } if (req.method !== "GET" && req.method !== "HEAD") { res.statusCode = 405; res.setHeader("Content-Type", "text/plain; charset=utf-8"); @@ -266,7 +283,9 @@ export function handleControlUiHttpRequest( res.end(); return true; } - if (!pathname.startsWith(`${basePath}/`)) return false; + if (!pathname.startsWith(`${basePath}/`)) { + return false; + } } const root = resolveControlUiRoot(); @@ -282,9 +301,13 @@ export function handleControlUiHttpRequest( const uiPath = basePath && pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : pathname; const rel = (() => { - if (uiPath === ROOT_PREFIX) return ""; + if (uiPath === ROOT_PREFIX) { + return ""; + } const assetsIndex = uiPath.indexOf("/assets/"); - if (assetsIndex >= 0) return uiPath.slice(assetsIndex + 1); + if (assetsIndex >= 0) { + return uiPath.slice(assetsIndex + 1); + } return uiPath.slice(1); })(); const requested = rel && !rel.endsWith("/") ? rel : `${rel}index.html`; diff --git a/src/gateway/exec-approval-manager.ts b/src/gateway/exec-approval-manager.ts index 5e51f2abc..3c33aac4d 100644 --- a/src/gateway/exec-approval-manager.ts +++ b/src/gateway/exec-approval-manager.ts @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto"; - import type { ExecApprovalDecision } from "../infra/exec-approvals.js"; export type ExecApprovalRequestPayload = { @@ -64,7 +63,9 @@ export class ExecApprovalManager { resolve(recordId: string, decision: ExecApprovalDecision, resolvedBy?: string | null): boolean { const pending = this.pending.get(recordId); - if (!pending) return false; + if (!pending) { + return false; + } clearTimeout(pending.timer); pending.record.resolvedAtMs = Date.now(); pending.record.decision = decision; diff --git a/src/gateway/gateway-cli-backend.live.test.ts b/src/gateway/gateway-cli-backend.live.test.ts index c39b34b91..431658a8a 100644 --- a/src/gateway/gateway-cli-backend.live.test.ts +++ b/src/gateway/gateway-cli-backend.live.test.ts @@ -3,7 +3,6 @@ import fs from "node:fs/promises"; import { createServer } from "node:net"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; import { parseModelRef } from "../agents/model-selection.js"; import { loadConfig } from "../config/config.js"; @@ -12,10 +11,10 @@ import { GatewayClient } from "./client.js"; import { renderCatNoncePngBase64 } from "./live-image-probe.js"; import { startGatewayServer } from "./server.js"; -const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST); -const CLI_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND); -const CLI_IMAGE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_PROBE); -const CLI_RESUME = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_RESUME_PROBE); +const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST); +const CLI_LIVE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND); +const CLI_IMAGE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_IMAGE_PROBE); +const CLI_RESUME = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE); const describeLive = LIVE && CLI_LIVE ? describe : describe.skip; const DEFAULT_MODEL = "claude-cli/claude-sonnet-4-5"; @@ -45,11 +44,17 @@ function randomImageProbeCode(len = 6): string { } function editDistance(a: string, b: string): number { - if (a === b) return 0; + if (a === b) { + return 0; + } const aLen = a.length; const bLen = b.length; - if (aLen === 0) return bLen; - if (bLen === 0) return aLen; + if (aLen === 0) { + return bLen; + } + if (bLen === 0) { + return aLen; + } let prev = Array.from({ length: bLen + 1 }, (_v, idx) => idx); let curr = Array.from({ length: bLen + 1 }, () => 0); @@ -82,7 +87,9 @@ function extractPayloadText(result: unknown): string { function parseJsonStringArray(name: string, raw?: string): string[] | undefined { const trimmed = raw?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const parsed = JSON.parse(trimmed); if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) { throw new Error(`${name} must be a JSON array of strings.`); @@ -92,9 +99,13 @@ function parseJsonStringArray(name: string, raw?: string): string[] | undefined function parseImageMode(raw?: string): "list" | "repeat" | undefined { const trimmed = raw?.trim(); - if (!trimmed) return undefined; - if (trimmed === "list" || trimmed === "repeat") return trimmed; - throw new Error("CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_MODE must be 'list' or 'repeat'."); + if (!trimmed) { + return undefined; + } + if (trimmed === "list" || trimmed === "repeat") { + return trimmed; + } + throw new Error("OPENCLAW_LIVE_CLI_BACKEND_IMAGE_MODE must be 'list' or 'repeat'."); } function withMcpConfigOverrides(args: string[], mcpConfigPath: string): string[] { @@ -121,15 +132,20 @@ async function getFreePort(): Promise { } const port = addr.port; srv.close((err) => { - if (err) reject(err); - else resolve(port); + if (err) { + reject(err); + } else { + resolve(port); + } }); }); }); } async function isPortFree(port: number): Promise { - if (!Number.isFinite(port) || port <= 0 || port > 65535) return false; + if (!Number.isFinite(port) || port <= 0 || port > 65535) { + return false; + } return await new Promise((resolve) => { const srv = createServer(); srv.once("error", () => resolve(false)); @@ -146,7 +162,9 @@ async function getFreeGatewayPort(): Promise { const ok = (await Promise.all(candidates.map((candidate) => isPortFree(candidate)))).every( Boolean, ); - if (ok) return port; + if (ok) { + return port; + } } throw new Error("failed to acquire a free gateway port block"); } @@ -155,11 +173,16 @@ async function connectClient(params: { url: string; token: string }) { return await new Promise((resolve, reject) => { let settled = false; const stop = (err?: Error, client?: GatewayClient) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); - if (err) reject(err); - else resolve(client as GatewayClient); + if (err) { + reject(err); + } else { + resolve(client as GatewayClient); + } }; const client = new GatewayClient({ url: params.url, @@ -181,31 +204,31 @@ async function connectClient(params: { url: string; token: string }) { describeLive("gateway live (cli backend)", () => { it("runs the agent pipeline against the local CLI backend", async () => { const previous = { - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + configPath: process.env.OPENCLAW_CONFIG_PATH, + token: process.env.OPENCLAW_GATEWAY_TOKEN, + skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, + skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, + skipCron: process.env.OPENCLAW_SKIP_CRON, + skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, anthropicApiKey: process.env.ANTHROPIC_API_KEY, anthropicApiKeyOld: process.env.ANTHROPIC_API_KEY_OLD, }; - process.env.CLAWDBOT_SKIP_CHANNELS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CRON = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; delete process.env.ANTHROPIC_API_KEY; delete process.env.ANTHROPIC_API_KEY_OLD; const token = `test-${randomUUID()}`; - process.env.CLAWDBOT_GATEWAY_TOKEN = token; + process.env.OPENCLAW_GATEWAY_TOKEN = token; - const rawModel = process.env.CLAWDBOT_LIVE_CLI_BACKEND_MODEL ?? DEFAULT_MODEL; + const rawModel = process.env.OPENCLAW_LIVE_CLI_BACKEND_MODEL ?? DEFAULT_MODEL; const parsed = parseModelRef(rawModel, "claude-cli"); if (!parsed) { throw new Error( - `CLAWDBOT_LIVE_CLI_BACKEND_MODEL must resolve to a CLI backend model. Got: ${rawModel}`, + `OPENCLAW_LIVE_CLI_BACKEND_MODEL must resolve to a CLI backend model. Got: ${rawModel}`, ); } const providerId = parsed.provider; @@ -218,36 +241,36 @@ describeLive("gateway live (cli backend)", () => { ? { command: "codex", args: DEFAULT_CODEX_ARGS } : null; - const cliCommand = process.env.CLAWDBOT_LIVE_CLI_BACKEND_COMMAND ?? providerDefaults?.command; + const cliCommand = process.env.OPENCLAW_LIVE_CLI_BACKEND_COMMAND ?? providerDefaults?.command; if (!cliCommand) { throw new Error( - `CLAWDBOT_LIVE_CLI_BACKEND_COMMAND is required for provider "${providerId}".`, + `OPENCLAW_LIVE_CLI_BACKEND_COMMAND is required for provider "${providerId}".`, ); } const baseCliArgs = parseJsonStringArray( - "CLAWDBOT_LIVE_CLI_BACKEND_ARGS", - process.env.CLAWDBOT_LIVE_CLI_BACKEND_ARGS, + "OPENCLAW_LIVE_CLI_BACKEND_ARGS", + process.env.OPENCLAW_LIVE_CLI_BACKEND_ARGS, ) ?? providerDefaults?.args; if (!baseCliArgs || baseCliArgs.length === 0) { - throw new Error(`CLAWDBOT_LIVE_CLI_BACKEND_ARGS is required for provider "${providerId}".`); + throw new Error(`OPENCLAW_LIVE_CLI_BACKEND_ARGS is required for provider "${providerId}".`); } const cliClearEnv = parseJsonStringArray( - "CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV", - process.env.CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV, + "OPENCLAW_LIVE_CLI_BACKEND_CLEAR_ENV", + process.env.OPENCLAW_LIVE_CLI_BACKEND_CLEAR_ENV, ) ?? (providerId === "claude-cli" ? DEFAULT_CLEAR_ENV : []); - const cliImageArg = process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_ARG?.trim() || undefined; - const cliImageMode = parseImageMode(process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_MODE); + const cliImageArg = process.env.OPENCLAW_LIVE_CLI_BACKEND_IMAGE_ARG?.trim() || undefined; + const cliImageMode = parseImageMode(process.env.OPENCLAW_LIVE_CLI_BACKEND_IMAGE_MODE); if (cliImageMode && !cliImageArg) { throw new Error( - "CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_MODE requires CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_ARG.", + "OPENCLAW_LIVE_CLI_BACKEND_IMAGE_MODE requires OPENCLAW_LIVE_CLI_BACKEND_IMAGE_ARG.", ); } - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-live-cli-")); - const disableMcpConfig = process.env.CLAWDBOT_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG !== "0"; + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-live-cli-")); + const disableMcpConfig = process.env.OPENCLAW_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG !== "0"; let cliArgs = baseCliArgs; if (providerId === "claude-cli" && disableMcpConfig) { const mcpConfigPath = path.join(tempDir, "claude-mcp.json"); @@ -281,9 +304,9 @@ describeLive("gateway live (cli backend)", () => { }, }, }; - const tempConfigPath = path.join(tempDir, "moltbot.json"); + const tempConfigPath = path.join(tempDir, "openclaw.json"); await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`); - process.env.CLAWDBOT_CONFIG_PATH = tempConfigPath; + process.env.OPENCLAW_CONFIG_PATH = tempConfigPath; const port = await getFreeGatewayPort(); const server = await startGatewayServer(port, { @@ -305,7 +328,7 @@ describeLive("gateway live (cli backend)", () => { providerId === "codex-cli" ? `Please include the token CLI-BACKEND-${nonce} in your reply.` : `Reply with exactly: CLI backend OK ${nonce}.`; - const payload = await client.request>( + const payload = await client.request( "agent", { sessionKey, @@ -332,7 +355,7 @@ describeLive("gateway live (cli backend)", () => { providerId === "codex-cli" ? `Please include the token CLI-RESUME-${resumeNonce} in your reply.` : `Reply with exactly: CLI backend RESUME OK ${resumeNonce}.`; - const resumePayload = await client.request>( + const resumePayload = await client.request( "agent", { sessionKey, @@ -359,7 +382,7 @@ describeLive("gateway live (cli backend)", () => { const imageBase64 = renderCatNoncePngBase64(imageCode); const runIdImage = randomUUID(); - const imageProbe = await client.request>( + const imageProbe = await client.request( "agent", { sessionKey, @@ -388,7 +411,9 @@ describeLive("gateway live (cli backend)", () => { } const candidates = imageText.toUpperCase().match(/[A-Z0-9]{6,20}/g) ?? []; const bestDistance = candidates.reduce((best, cand) => { - if (Math.abs(cand.length - imageCode.length) > 2) return best; + if (Math.abs(cand.length - imageCode.length) > 2) { + return best; + } return Math.min(best, editDistance(cand, imageCode)); }, Number.POSITIVE_INFINITY); if (!(bestDistance <= 5)) { @@ -399,22 +424,46 @@ describeLive("gateway live (cli backend)", () => { client.stop(); await server.close(); await fs.rm(tempDir, { recursive: true, force: true }); - if (previous.configPath === undefined) delete process.env.CLAWDBOT_CONFIG_PATH; - else process.env.CLAWDBOT_CONFIG_PATH = previous.configPath; - if (previous.token === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; - else process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token; - if (previous.skipChannels === undefined) delete process.env.CLAWDBOT_SKIP_CHANNELS; - else process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels; - if (previous.skipGmail === undefined) delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; - else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail; - if (previous.skipCron === undefined) delete process.env.CLAWDBOT_SKIP_CRON; - else process.env.CLAWDBOT_SKIP_CRON = previous.skipCron; - if (previous.skipCanvas === undefined) delete process.env.CLAWDBOT_SKIP_CANVAS_HOST; - else process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas; - if (previous.anthropicApiKey === undefined) delete process.env.ANTHROPIC_API_KEY; - else process.env.ANTHROPIC_API_KEY = previous.anthropicApiKey; - if (previous.anthropicApiKeyOld === undefined) delete process.env.ANTHROPIC_API_KEY_OLD; - else process.env.ANTHROPIC_API_KEY_OLD = previous.anthropicApiKeyOld; + if (previous.configPath === undefined) { + delete process.env.OPENCLAW_CONFIG_PATH; + } else { + process.env.OPENCLAW_CONFIG_PATH = previous.configPath; + } + if (previous.token === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previous.token; + } + if (previous.skipChannels === undefined) { + delete process.env.OPENCLAW_SKIP_CHANNELS; + } else { + process.env.OPENCLAW_SKIP_CHANNELS = previous.skipChannels; + } + if (previous.skipGmail === undefined) { + delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER; + } else { + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previous.skipGmail; + } + if (previous.skipCron === undefined) { + delete process.env.OPENCLAW_SKIP_CRON; + } else { + process.env.OPENCLAW_SKIP_CRON = previous.skipCron; + } + if (previous.skipCanvas === undefined) { + delete process.env.OPENCLAW_SKIP_CANVAS_HOST; + } else { + process.env.OPENCLAW_SKIP_CANVAS_HOST = previous.skipCanvas; + } + if (previous.anthropicApiKey === undefined) { + delete process.env.ANTHROPIC_API_KEY; + } else { + process.env.ANTHROPIC_API_KEY = previous.anthropicApiKey; + } + if (previous.anthropicApiKeyOld === undefined) { + delete process.env.ANTHROPIC_API_KEY_OLD; + } else { + process.env.ANTHROPIC_API_KEY_OLD = previous.anthropicApiKeyOld; + } } }, 60_000); }); diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index d3cede10b..d941c1d26 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -1,13 +1,12 @@ +import type { Api, Model } from "@mariozechner/pi-ai"; import { randomBytes, randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import { createServer } from "node:net"; import os from "node:os"; import path from "node:path"; - -import type { Api, Model } from "@mariozechner/pi-ai"; -import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent"; import { describe, it } from "vitest"; -import { resolveMoltbotAgentDir } from "../agents/agent-paths.js"; +import type { OpenClawConfig, ModelProviderConfig } from "../config/types.js"; +import { resolveOpenClawAgentDir } from "../agents/agent-paths.js"; import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; import { type AuthProfileStore, @@ -21,9 +20,9 @@ import { } from "../agents/live-auth-keys.js"; import { isModernModelRef } from "../agents/live-model-filter.js"; import { getApiKeyForModel } from "../agents/model-auth.js"; -import { ensureMoltbotModelsJson } from "../agents/models-config.js"; +import { ensureOpenClawModelsJson } from "../agents/models-config.js"; +import { discoverAuthStorage, discoverModels } from "../agents/pi-model-discovery.js"; import { loadConfig } from "../config/config.js"; -import type { MoltbotConfig, ModelProviderConfig } from "../config/types.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { DEFAULT_AGENT_ID } from "../routing/session-key.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; @@ -31,10 +30,10 @@ import { GatewayClient } from "./client.js"; import { renderCatNoncePngBase64 } from "./live-image-probe.js"; import { startGatewayServer } from "./server.js"; -const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST); -const GATEWAY_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY); -const ZAI_FALLBACK = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY_ZAI_FALLBACK); -const PROVIDERS = parseFilter(process.env.CLAWDBOT_LIVE_GATEWAY_PROVIDERS); +const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST); +const GATEWAY_LIVE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_GATEWAY); +const ZAI_FALLBACK = isTruthyEnvValue(process.env.OPENCLAW_LIVE_GATEWAY_ZAI_FALLBACK); +const PROVIDERS = parseFilter(process.env.OPENCLAW_LIVE_GATEWAY_PROVIDERS); const THINKING_LEVEL = "high"; const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\s*>/i; const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/i; @@ -44,7 +43,9 @@ const describeLive = LIVE || GATEWAY_LIVE ? describe : describe.skip; function parseFilter(raw?: string): Set | null { const trimmed = raw?.trim(); - if (!trimmed || trimmed === "all") return null; + if (!trimmed || trimmed === "all") { + return null; + } const ids = trimmed .split(",") .map((s) => s.trim()) @@ -62,7 +63,9 @@ function assertNoReasoningTags(params: { phase: string; label: string; }): void { - if (!params.text) return; + if (!params.text) { + return; + } if (THINKING_TAG_RE.test(params.text) || FINAL_TAG_RE.test(params.text)) { const snippet = params.text.length > 200 ? `${params.text.slice(0, 200)}…` : params.text; throw new Error( @@ -81,22 +84,40 @@ function extractPayloadText(result: unknown): string { } function isMeaningful(text: string): boolean { - if (!text) return false; + if (!text) { + return false; + } const trimmed = text.trim(); - if (trimmed.toLowerCase() === "ok") return false; - if (trimmed.length < 60) return false; + if (trimmed.toLowerCase() === "ok") { + return false; + } + if (trimmed.length < 60) { + return false; + } const words = trimmed.split(/\s+/g).filter(Boolean); - if (words.length < 12) return false; + if (words.length < 12) { + return false; + } return true; } function isGoogleModelNotFoundText(text: string): boolean { const trimmed = text.trim(); - if (!trimmed) return false; - if (!/not found/i.test(trimmed)) return false; - if (/models\/.+ is not found for api version/i.test(trimmed)) return true; - if (/"status"\s*:\s*"NOT_FOUND"/.test(trimmed)) return true; - if (/"code"\s*:\s*404/.test(trimmed)) return true; + if (!trimmed) { + return false; + } + if (!/not found/i.test(trimmed)) { + return false; + } + if (/models\/.+ is not found for api version/i.test(trimmed)) { + return true; + } + if (/"status"\s*:\s*"NOT_FOUND"/.test(trimmed)) { + return true; + } + if (/"code"\s*:\s*404/.test(trimmed)) { + return true; + } return false; } @@ -124,7 +145,9 @@ function isOpenAIReasoningSequenceError(error: string): boolean { function isToolNonceRefusal(error: string): boolean { const msg = error.toLowerCase(); - if (!msg.includes("nonce")) return false; + if (!msg.includes("nonce")) { + return false; + } return ( msg.includes("token") || msg.includes("secret") || @@ -226,11 +249,17 @@ function randomImageProbeCode(len = 6): string { } function editDistance(a: string, b: string): number { - if (a === b) return 0; + if (a === b) { + return 0; + } const aLen = a.length; const bLen = b.length; - if (aLen === 0) return bLen; - if (bLen === 0) return aLen; + if (aLen === 0) { + return bLen; + } + if (bLen === 0) { + return aLen; + } let prev = Array.from({ length: bLen + 1 }, (_v, idx) => idx); let curr = Array.from({ length: bLen + 1 }, () => 0); @@ -264,15 +293,20 @@ async function getFreePort(): Promise { } const port = addr.port; srv.close((err) => { - if (err) reject(err); - else resolve(port); + if (err) { + reject(err); + } else { + resolve(port); + } }); }); }); } async function isPortFree(port: number): Promise { - if (!Number.isFinite(port) || port <= 0 || port > 65535) return false; + if (!Number.isFinite(port) || port <= 0 || port > 65535) { + return false; + } return await new Promise((resolve) => { const srv = createServer(); srv.once("error", () => resolve(false)); @@ -291,7 +325,9 @@ async function getFreeGatewayPort(): Promise { const ok = (await Promise.all(candidates.map((candidate) => isPortFree(candidate)))).every( Boolean, ); - if (ok) return port; + if (ok) { + return port; + } } throw new Error("failed to acquire a free gateway port block"); } @@ -305,11 +341,16 @@ async function connectClient(params: { url: string; token: string }) { return await new Promise((resolve, reject) => { let settled = false; const stop = (err?: Error, client?: GatewayClient) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); - if (err) reject(err); - else resolve(client as GatewayClient); + if (err) { + reject(err); + } else { + resolve(client as GatewayClient); + } }; const client = new GatewayClient({ url: params.url, @@ -331,7 +372,7 @@ async function connectClient(params: { url: string; token: string }) { type GatewayModelSuiteParams = { label: string; - cfg: MoltbotConfig; + cfg: OpenClawConfig; candidates: Array>; extraToolProbes: boolean; extraImageProbes: boolean; @@ -340,10 +381,10 @@ type GatewayModelSuiteParams = { }; function buildLiveGatewayConfig(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; candidates: Array>; providerOverrides?: Record; -}): MoltbotConfig { +}): OpenClawConfig { const providerOverrides = params.providerOverrides ?? {}; const lmstudioProvider = params.cfg.models?.providers?.lmstudio; const baseProviders = params.cfg.models?.providers ?? {}; @@ -382,23 +423,29 @@ function buildLiveGatewayConfig(params: { } function sanitizeAuthConfig(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentDir: string; -}): MoltbotConfig["auth"] | undefined { +}): OpenClawConfig["auth"] | undefined { const auth = params.cfg.auth; - if (!auth) return auth; + if (!auth) { + return auth; + } const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }); - let profiles: NonNullable["profiles"] | undefined; + let profiles: NonNullable["profiles"] | undefined; if (auth.profiles) { profiles = {}; for (const [profileId, profile] of Object.entries(auth.profiles)) { - if (!store.profiles[profileId]) continue; + if (!store.profiles[profileId]) { + continue; + } profiles[profileId] = profile; } - if (Object.keys(profiles).length === 0) profiles = undefined; + if (Object.keys(profiles).length === 0) { + profiles = undefined; + } } let order: Record | undefined; @@ -406,13 +453,19 @@ function sanitizeAuthConfig(params: { order = {}; for (const [provider, ids] of Object.entries(auth.order)) { const filtered = ids.filter((id) => Boolean(store.profiles[id])); - if (filtered.length === 0) continue; + if (filtered.length === 0) { + continue; + } order[provider] = filtered; } - if (Object.keys(order).length === 0) order = undefined; + if (Object.keys(order).length === 0) { + order = undefined; + } } - if (!profiles && !order && !auth.cooldowns) return undefined; + if (!profiles && !order && !auth.cooldowns) { + return undefined; + } return { ...auth, profiles, @@ -421,12 +474,14 @@ function sanitizeAuthConfig(params: { } function buildMinimaxProviderOverride(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; api: "openai-completions" | "anthropic-messages"; baseUrl: string; }): ModelProviderConfig | null { const existing = params.cfg.models?.providers?.minimax; - if (!existing || !Array.isArray(existing.models) || existing.models.length === 0) return null; + if (!existing || !Array.isArray(existing.models) || existing.models.length === 0) { + return null; + } return { ...existing, api: params.api, @@ -436,29 +491,29 @@ function buildMinimaxProviderOverride(params: { async function runGatewayModelSuite(params: GatewayModelSuiteParams) { const previous = { - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - agentDir: process.env.CLAWDBOT_AGENT_DIR, + configPath: process.env.OPENCLAW_CONFIG_PATH, + token: process.env.OPENCLAW_GATEWAY_TOKEN, + skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, + skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, + skipCron: process.env.OPENCLAW_SKIP_CRON, + skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, + agentDir: process.env.OPENCLAW_AGENT_DIR, piAgentDir: process.env.PI_CODING_AGENT_DIR, - stateDir: process.env.CLAWDBOT_STATE_DIR, + stateDir: process.env.OPENCLAW_STATE_DIR, }; let tempAgentDir: string | undefined; let tempStateDir: string | undefined; - process.env.CLAWDBOT_SKIP_CHANNELS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CRON = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; const token = `test-${randomUUID()}`; - process.env.CLAWDBOT_GATEWAY_TOKEN = token; + process.env.OPENCLAW_GATEWAY_TOKEN = token; const agentId = "dev"; - const hostAgentDir = resolveMoltbotAgentDir(); + const hostAgentDir = resolveOpenClawAgentDir(); const hostStore = ensureAuthProfileStore(hostAgentDir, { allowKeychainPrompt: false, }); @@ -471,26 +526,26 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { lastGood: hostStore.lastGood ? { ...hostStore.lastGood } : undefined, usageStats: hostStore.usageStats ? { ...hostStore.usageStats } : undefined, }; - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-live-state-")); - process.env.CLAWDBOT_STATE_DIR = tempStateDir; + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-live-state-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; tempAgentDir = path.join(tempStateDir, "agents", DEFAULT_AGENT_ID, "agent"); saveAuthProfileStore(sanitizedStore, tempAgentDir); const tempSessionAgentDir = path.join(tempStateDir, "agents", agentId, "agent"); if (tempSessionAgentDir !== tempAgentDir) { saveAuthProfileStore(sanitizedStore, tempSessionAgentDir); } - process.env.CLAWDBOT_AGENT_DIR = tempAgentDir; + process.env.OPENCLAW_AGENT_DIR = tempAgentDir; process.env.PI_CODING_AGENT_DIR = tempAgentDir; const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId); await fs.mkdir(workspaceDir, { recursive: true }); const nonceA = randomUUID(); const nonceB = randomUUID(); - const toolProbePath = path.join(workspaceDir, `.clawdbot-live-tool-probe.${nonceA}.txt`); + const toolProbePath = path.join(workspaceDir, `.openclaw-live-tool-probe.${nonceA}.txt`); await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`); - const agentDir = resolveMoltbotAgentDir(); - const sanitizedCfg: MoltbotConfig = { + const agentDir = resolveOpenClawAgentDir(); + const sanitizedCfg: OpenClawConfig = { ...params.cfg, auth: sanitizeAuthConfig({ cfg: params.cfg, agentDir }), }; @@ -499,12 +554,12 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { candidates: params.candidates, providerOverrides: params.providerOverrides, }); - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-live-")); - const tempConfigPath = path.join(tempDir, "moltbot.json"); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-live-")); + const tempConfigPath = path.join(tempDir, "openclaw.json"); await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`); - process.env.CLAWDBOT_CONFIG_PATH = tempConfigPath; + process.env.OPENCLAW_CONFIG_PATH = tempConfigPath; - await ensureMoltbotModelsJson(nextCfg); + await ensureOpenClawModelsJson(nextCfg); const port = await getFreeGatewayPort(); const server = await startGatewayServer(port, { @@ -547,10 +602,10 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { // Ensure session exists + override model for this run. // Reset between models: avoids cross-provider transcript incompatibilities // (notably OpenAI Responses requiring reasoning replay for function_call items). - await client.request>("sessions.reset", { + await client.request("sessions.reset", { key: sessionKey, }); - await client.request>("sessions.patch", { + await client.request("sessions.patch", { key: sessionKey, model: modelKey, }); @@ -636,7 +691,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { sessionKey, idempotencyKey: `idem-${runIdTool}-tool`, message: - "Moltbot live tool probe (local, safe): " + + "OpenClaw live tool probe (local, safe): " + `use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolProbePath}"}. ` + "Then reply with the two nonce values you read (include both).", thinking: params.thinkingLevel, @@ -676,7 +731,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { sessionKey, idempotencyKey: `idem-${runIdTool}-exec-read`, message: - "Moltbot live tool probe (local, safe): " + + "OpenClaw live tool probe (local, safe): " + "use the tool named `exec` (or `Exec`) to run this command: " + `mkdir -p "${tempDir}" && printf '%s' '${nonceC}' > "${toolWritePath}". ` + `Then use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolWritePath}"}. ` + @@ -761,7 +816,9 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { } else { const candidates = imageText.toUpperCase().match(/[A-Z0-9]{6,20}/g) ?? []; const bestDistance = candidates.reduce((best, cand) => { - if (Math.abs(cand.length - imageCode.length) > 2) return best; + if (Math.abs(cand.length - imageCode.length) > 2) { + return best; + } return Math.min(best, editDistance(cand, imageCode)); }, Number.POSITIVE_INFINITY); // OCR / image-read flake: allow a small edit distance, but still require the "cat" token above. @@ -940,15 +997,15 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { await fs.rm(tempStateDir, { recursive: true, force: true }); } - process.env.CLAWDBOT_CONFIG_PATH = previous.configPath; - process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token; - process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail; - process.env.CLAWDBOT_SKIP_CRON = previous.skipCron; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas; - process.env.CLAWDBOT_AGENT_DIR = previous.agentDir; + process.env.OPENCLAW_CONFIG_PATH = previous.configPath; + process.env.OPENCLAW_GATEWAY_TOKEN = previous.token; + process.env.OPENCLAW_SKIP_CHANNELS = previous.skipChannels; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previous.skipGmail; + process.env.OPENCLAW_SKIP_CRON = previous.skipCron; + process.env.OPENCLAW_SKIP_CANVAS_HOST = previous.skipCanvas; + process.env.OPENCLAW_AGENT_DIR = previous.agentDir; process.env.PI_CODING_AGENT_DIR = previous.piAgentDir; - process.env.CLAWDBOT_STATE_DIR = previous.stateDir; + process.env.OPENCLAW_STATE_DIR = previous.stateDir; } } @@ -957,17 +1014,17 @@ describeLive("gateway live (dev agent, profile keys)", () => { "runs meaningful prompts across models with available keys", async () => { const cfg = loadConfig(); - await ensureMoltbotModelsJson(cfg); + await ensureOpenClawModelsJson(cfg); - const agentDir = resolveMoltbotAgentDir(); + const agentDir = resolveOpenClawAgentDir(); const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false, }); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); - const all = modelRegistry.getAll() as Array>; + const all = modelRegistry.getAll(); - const rawModels = process.env.CLAWDBOT_LIVE_GATEWAY_MODELS?.trim(); + const rawModels = process.env.OPENCLAW_LIVE_GATEWAY_MODELS?.trim(); const useModern = !rawModels || rawModels === "modern" || rawModels === "all"; const useExplicit = Boolean(rawModels) && !useModern; const filter = useExplicit ? parseFilter(rawModels) : null; @@ -977,7 +1034,9 @@ describeLive("gateway live (dev agent, profile keys)", () => { const candidates: Array> = []; for (const model of wanted) { - if (PROVIDERS && !PROVIDERS.has(model.provider)) continue; + if (PROVIDERS && !PROVIDERS.has(model.provider)) { + continue; + } try { // eslint-disable-next-line no-await-in-loop const apiKeyInfo = await getApiKeyForModel({ @@ -1042,34 +1101,38 @@ describeLive("gateway live (dev agent, profile keys)", () => { ); it("z.ai fallback handles anthropic tool history", async () => { - if (!ZAI_FALLBACK) return; + if (!ZAI_FALLBACK) { + return; + } const previous = { - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + configPath: process.env.OPENCLAW_CONFIG_PATH, + token: process.env.OPENCLAW_GATEWAY_TOKEN, + skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, + skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, + skipCron: process.env.OPENCLAW_SKIP_CRON, + skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, }; - process.env.CLAWDBOT_SKIP_CHANNELS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CRON = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; const token = `test-${randomUUID()}`; - process.env.CLAWDBOT_GATEWAY_TOKEN = token; + process.env.OPENCLAW_GATEWAY_TOKEN = token; const cfg = loadConfig(); - await ensureMoltbotModelsJson(cfg); + await ensureOpenClawModelsJson(cfg); - const agentDir = resolveMoltbotAgentDir(); + const agentDir = resolveOpenClawAgentDir(); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); const anthropic = modelRegistry.find("anthropic", "claude-opus-4-5") as Model | null; const zai = modelRegistry.find("zai", "glm-4.7") as Model | null; - if (!anthropic || !zai) return; + if (!anthropic || !zai) { + return; + } try { await getApiKeyForModel({ model: anthropic, cfg }); await getApiKeyForModel({ model: zai, cfg }); @@ -1082,7 +1145,7 @@ describeLive("gateway live (dev agent, profile keys)", () => { await fs.mkdir(workspaceDir, { recursive: true }); const nonceA = randomUUID(); const nonceB = randomUUID(); - const toolProbePath = path.join(workspaceDir, `.clawdbot-live-zai-fallback.${nonceA}.txt`); + const toolProbePath = path.join(workspaceDir, `.openclaw-live-zai-fallback.${nonceA}.txt`); await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`); const port = await getFreeGatewayPort(); @@ -1100,11 +1163,11 @@ describeLive("gateway live (dev agent, profile keys)", () => { try { const sessionKey = `agent:${agentId}:live-zai-fallback`; - await client.request>("sessions.patch", { + await client.request("sessions.patch", { key: sessionKey, model: "anthropic/claude-opus-4-5", }); - await client.request>("sessions.reset", { + await client.request("sessions.reset", { key: sessionKey, }); @@ -1136,7 +1199,7 @@ describeLive("gateway live (dev agent, profile keys)", () => { throw new Error(`anthropic tool probe missing nonce: ${toolText}`); } - await client.request>("sessions.patch", { + await client.request("sessions.patch", { key: sessionKey, model: "zai/glm-4.7", }); @@ -1173,12 +1236,12 @@ describeLive("gateway live (dev agent, profile keys)", () => { await server.close({ reason: "live test complete" }); await fs.rm(toolProbePath, { force: true }); - process.env.CLAWDBOT_CONFIG_PATH = previous.configPath; - process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token; - process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail; - process.env.CLAWDBOT_SKIP_CRON = previous.skipCron; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas; + process.env.OPENCLAW_CONFIG_PATH = previous.configPath; + process.env.OPENCLAW_GATEWAY_TOKEN = previous.token; + process.env.OPENCLAW_SKIP_CHANNELS = previous.skipChannels; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previous.skipGmail; + process.env.OPENCLAW_SKIP_CRON = previous.skipCron; + process.env.OPENCLAW_SKIP_CANVAS_HOST = previous.skipCanvas; } }, 180_000); }); diff --git a/src/gateway/gateway.e2e.test.ts b/src/gateway/gateway.e2e.test.ts index 372432983..bb9e152d5 100644 --- a/src/gateway/gateway.e2e.test.ts +++ b/src/gateway/gateway.e2e.test.ts @@ -2,16 +2,14 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - +import { startGatewayServer } from "./server.js"; import { connectDeviceAuthReq, connectGatewayClient, getFreeGatewayPort, } from "./test-helpers.e2e.js"; import { installOpenAiResponsesMock } from "./test-helpers.openai-mock.js"; -import { startGatewayServer } from "./server.js"; function extractPayloadText(result: unknown): string { const record = result as Record; @@ -29,39 +27,39 @@ describe("gateway e2e", () => { async () => { const prev = { home: process.env.HOME, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - skipBrowser: process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER, + configPath: process.env.OPENCLAW_CONFIG_PATH, + token: process.env.OPENCLAW_GATEWAY_TOKEN, + skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, + skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, + skipCron: process.env.OPENCLAW_SKIP_CRON, + skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, + skipBrowser: process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER, }; const { baseUrl: openaiBaseUrl, restore } = installOpenAiResponsesMock(); - const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-mock-home-")); + const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-mock-home-")); process.env.HOME = tempHome; - process.env.CLAWDBOT_SKIP_CHANNELS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; - process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = "1"; + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CRON = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; + process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1"; const token = `test-${randomUUID()}`; - process.env.CLAWDBOT_GATEWAY_TOKEN = token; + process.env.OPENCLAW_GATEWAY_TOKEN = token; - const workspaceDir = path.join(tempHome, "clawd"); + const workspaceDir = path.join(tempHome, "openclaw"); await fs.mkdir(workspaceDir, { recursive: true }); const nonceA = randomUUID(); const nonceB = randomUUID(); - const toolProbePath = path.join(workspaceDir, `.clawdbot-tool-probe.${nonceA}.txt`); + const toolProbePath = path.join(workspaceDir, `.openclaw-tool-probe.${nonceA}.txt`); await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`); - const configDir = path.join(tempHome, ".clawdbot"); + const configDir = path.join(tempHome, ".openclaw"); await fs.mkdir(configDir, { recursive: true }); - const configPath = path.join(configDir, "moltbot.json"); + const configPath = path.join(configDir, "openclaw.json"); const cfg = { agents: { defaults: { workspace: workspaceDir } }, @@ -91,7 +89,7 @@ describe("gateway e2e", () => { }; await fs.writeFile(configPath, `${JSON.stringify(cfg, null, 2)}\n`); - process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_CONFIG_PATH = configPath; const port = await getFreeGatewayPort(); const server = await startGatewayServer(port, { @@ -109,7 +107,7 @@ describe("gateway e2e", () => { try { const sessionKey = "agent:dev:mock-openai"; - await client.request>("sessions.patch", { + await client.request("sessions.patch", { key: sessionKey, model: "openai/gpt-5.2", }); @@ -141,13 +139,13 @@ describe("gateway e2e", () => { await fs.rm(tempHome, { recursive: true, force: true }); restore(); process.env.HOME = prev.home; - process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; - process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; - process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; - process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; - process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = prev.skipBrowser; + process.env.OPENCLAW_CONFIG_PATH = prev.configPath; + process.env.OPENCLAW_GATEWAY_TOKEN = prev.token; + process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail; + process.env.OPENCLAW_SKIP_CRON = prev.skipCron; + process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas; + process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = prev.skipBrowser; } }, ); @@ -155,27 +153,27 @@ describe("gateway e2e", () => { it("runs wizard over ws and writes auth token config", { timeout: 90_000 }, async () => { const prev = { home: process.env.HOME, - stateDir: process.env.CLAWDBOT_STATE_DIR, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - skipBrowser: process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER, + stateDir: process.env.OPENCLAW_STATE_DIR, + configPath: process.env.OPENCLAW_CONFIG_PATH, + token: process.env.OPENCLAW_GATEWAY_TOKEN, + skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, + skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, + skipCron: process.env.OPENCLAW_SKIP_CRON, + skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, + skipBrowser: process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER, }; - process.env.CLAWDBOT_SKIP_CHANNELS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; - process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = "1"; - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CRON = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; + process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1"; + delete process.env.OPENCLAW_GATEWAY_TOKEN; - const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-wizard-home-")); + const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wizard-home-")); process.env.HOME = tempHome; - delete process.env.CLAWDBOT_STATE_DIR; - delete process.env.CLAWDBOT_CONFIG_PATH; + delete process.env.OPENCLAW_STATE_DIR; + delete process.env.OPENCLAW_CONFIG_PATH; const wizardToken = `wiz-${randomUUID()}`; const port = await getFreeGatewayPort(); @@ -219,9 +217,13 @@ describe("gateway e2e", () => { let didSendToken = false; while (!next.done) { const step = next.step; - if (!step) throw new Error("wizard missing step"); + if (!step) { + throw new Error("wizard missing step"); + } const value = step.type === "text" ? wizardToken : null; - if (step.type === "text") didSendToken = true; + if (step.type === "text") { + didSendToken = true; + } next = await client.request("wizard.next", { sessionId, answer: { stepId: step.id, value }, @@ -263,14 +265,14 @@ describe("gateway e2e", () => { await server2.close({ reason: "wizard auth verify" }); await fs.rm(tempHome, { recursive: true, force: true }); process.env.HOME = prev.home; - process.env.CLAWDBOT_STATE_DIR = prev.stateDir; - process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; - process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; - process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; - process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; - process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = prev.skipBrowser; + process.env.OPENCLAW_STATE_DIR = prev.stateDir; + process.env.OPENCLAW_CONFIG_PATH = prev.configPath; + process.env.OPENCLAW_GATEWAY_TOKEN = prev.token; + process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail; + process.env.OPENCLAW_SKIP_CRON = prev.skipCron; + process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas; + process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = prev.skipBrowser; } }); }); diff --git a/src/gateway/hooks-mapping.test.ts b/src/gateway/hooks-mapping.test.ts index 8900ffd07..d7b9924ed 100644 --- a/src/gateway/hooks-mapping.test.ts +++ b/src/gateway/hooks-mapping.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; - import { applyHookMappings, resolveHookMappings } from "./hooks-mapping.js"; const baseUrl = new URL("http://127.0.0.1:18789/hooks/gmail"); @@ -63,9 +62,9 @@ describe("hooks mapping", () => { }); it("runs transform module", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-hooks-")); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-hooks-")); const modPath = path.join(dir, "transform.mjs"); - const placeholder = "${" + "payload.name}"; + const placeholder = "${payload.name}"; fs.writeFileSync( modPath, `export default ({ payload }) => ({ kind: "wake", text: \`Ping ${placeholder}\` });`, @@ -99,7 +98,7 @@ describe("hooks mapping", () => { }); it("treats null transform as a handled skip", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-hooks-skip-")); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-hooks-skip-")); const modPath = path.join(dir, "transform.mjs"); fs.writeFileSync(modPath, "export default () => null;"); diff --git a/src/gateway/hooks-mapping.ts b/src/gateway/hooks-mapping.ts index 2ebf9b136..abcea54f6 100644 --- a/src/gateway/hooks-mapping.ts +++ b/src/gateway/hooks-mapping.ts @@ -1,8 +1,7 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; - -import { CONFIG_PATH, type HookMappingConfig, type HooksConfig } from "../config/config.js"; import type { HookMessageChannel } from "./hooks.js"; +import { CONFIG_PATH, type HookMappingConfig, type HooksConfig } from "../config/config.js"; export type HookMappingResolved = { id: string; @@ -104,10 +103,14 @@ export function resolveHookMappings(hooks?: HooksConfig): HookMappingResolved[] const presets = hooks?.presets ?? []; const gmailAllowUnsafe = hooks?.gmail?.allowUnsafeExternalContent; const mappings: HookMappingConfig[] = []; - if (hooks?.mappings) mappings.push(...hooks.mappings); + if (hooks?.mappings) { + mappings.push(...hooks.mappings); + } for (const preset of presets) { const presetMappings = hookPresetMappings[preset]; - if (!presetMappings) continue; + if (!presetMappings) { + continue; + } if (preset === "gmail" && typeof gmailAllowUnsafe === "boolean") { mappings.push( ...presetMappings.map((mapping) => ({ @@ -119,7 +122,9 @@ export function resolveHookMappings(hooks?: HooksConfig): HookMappingResolved[] } mappings.push(...presetMappings); } - if (mappings.length === 0) return []; + if (mappings.length === 0) { + return []; + } const configDir = path.dirname(CONFIG_PATH); const transformsDir = hooks?.transformsDir @@ -133,12 +138,18 @@ export async function applyHookMappings( mappings: HookMappingResolved[], ctx: HookMappingContext, ): Promise { - if (mappings.length === 0) return null; + if (mappings.length === 0) { + return null; + } for (const mapping of mappings) { - if (!mappingMatches(mapping, ctx)) continue; + if (!mappingMatches(mapping, ctx)) { + continue; + } const base = buildActionFromMapping(mapping, ctx); - if (!base.ok) return base; + if (!base.ok) { + return base; + } let override: HookTransformResult = null; if (mapping.transform) { @@ -149,9 +160,13 @@ export async function applyHookMappings( } } - if (!base.action) return { ok: true, action: null, skipped: true }; + if (!base.action) { + return { ok: true, action: null, skipped: true }; + } const merged = mergeAction(base.action, override, mapping.action); - if (!merged.ok) return merged; + if (!merged.ok) { + return merged; + } return merged; } return null; @@ -197,11 +212,15 @@ function normalizeHookMapping( function mappingMatches(mapping: HookMappingResolved, ctx: HookMappingContext) { if (mapping.matchPath) { - if (mapping.matchPath !== normalizeMatchPath(ctx.path)) return false; + if (mapping.matchPath !== normalizeMatchPath(ctx.path)) { + return false; + } } if (mapping.matchSource) { const source = typeof ctx.payload.source === "string" ? ctx.payload.source : undefined; - if (!source || source !== mapping.matchSource) return false; + if (!source || source !== mapping.matchSource) { + return false; + } } return true; } @@ -249,7 +268,7 @@ function mergeAction( if (!override) { return validateAction(base); } - const kind = (override.kind ?? base.kind ?? defaultAction) as "wake" | "agent"; + const kind = override.kind ?? base.kind ?? defaultAction; if (kind === "wake") { const baseWake = base.kind === "wake" ? base : undefined; const text = typeof override.text === "string" ? override.text : (baseWake?.text ?? ""); @@ -295,7 +314,9 @@ function validateAction(action: HookAction): HookMappingResult { async function loadTransform(transform: HookMappingTransformResolved): Promise { const cached = transformCache.get(transform.modulePath); - if (cached) return cached; + if (cached) { + return cached; + } const url = pathToFileURL(transform.modulePath).href; const mod = (await import(url)) as Record; const fn = resolveTransformFn(mod, transform.exportName); @@ -312,38 +333,60 @@ function resolveTransformFn(mod: Record, exportName?: string): } function resolvePath(baseDir: string, target: string): string { - if (!target) return baseDir; - if (path.isAbsolute(target)) return target; + if (!target) { + return baseDir; + } + if (path.isAbsolute(target)) { + return target; + } return path.join(baseDir, target); } function normalizeMatchPath(raw?: string): string | undefined { - if (!raw) return undefined; + if (!raw) { + return undefined; + } const trimmed = raw.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } return trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); } function renderOptional(value: string | undefined, ctx: HookMappingContext) { - if (!value) return undefined; + if (!value) { + return undefined; + } const rendered = renderTemplate(value, ctx).trim(); return rendered ? rendered : undefined; } function renderTemplate(template: string, ctx: HookMappingContext) { - if (!template) return ""; + if (!template) { + return ""; + } return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, expr: string) => { const value = resolveTemplateExpr(expr.trim(), ctx); - if (value === undefined || value === null) return ""; - if (typeof value === "string") return value; - if (typeof value === "number" || typeof value === "boolean") return String(value); + if (value === undefined || value === null) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } return JSON.stringify(value); }); } function resolveTemplateExpr(expr: string, ctx: HookMappingContext) { - if (expr === "path") return ctx.path; - if (expr === "now") return new Date().toISOString(); + if (expr === "path") { + return ctx.path; + } + if (expr === "now") { + return new Date().toISOString(); + } if (expr.startsWith("headers.")) { return getByPath(ctx.headers, expr.slice("headers.".length)); } @@ -360,7 +403,9 @@ function resolveTemplateExpr(expr: string, ctx: HookMappingContext) { } function getByPath(input: Record, pathExpr: string): unknown { - if (!pathExpr) return undefined; + if (!pathExpr) { + return undefined; + } const parts: Array = []; const re = /([^.[\]]+)|(\[(\d+)\])/g; let match = re.exec(pathExpr); @@ -374,13 +419,19 @@ function getByPath(input: Record, pathExpr: string): unknown { } let current: unknown = input; for (const part of parts) { - if (current === null || current === undefined) return undefined; + if (current === null || current === undefined) { + return undefined; + } if (typeof part === "number") { - if (!Array.isArray(current)) return undefined; + if (!Array.isArray(current)) { + return undefined; + } current = current[part] as unknown; continue; } - if (typeof current !== "object") return undefined; + if (typeof current !== "object") { + return undefined; + } current = (current as Record)[part]; } return current; diff --git a/src/gateway/hooks.test.ts b/src/gateway/hooks.test.ts index a943f00ab..550ba9caf 100644 --- a/src/gateway/hooks.test.ts +++ b/src/gateway/hooks.test.ts @@ -1,7 +1,7 @@ import type { IncomingMessage } from "node:http"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import type { MoltbotConfig } from "../config/config.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; +import type { OpenClawConfig } from "../config/config.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createIMessageTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; import { @@ -26,7 +26,7 @@ describe("gateway hooks helpers", () => { token: "secret", path: "hooks///", }, - } as MoltbotConfig; + } as OpenClawConfig; const resolved = resolveHooksConfig(base); expect(resolved?.basePath).toBe("/hooks"); expect(resolved?.token).toBe("secret"); @@ -35,7 +35,7 @@ describe("gateway hooks helpers", () => { test("resolveHooksConfig rejects root path", () => { const cfg = { hooks: { enabled: true, token: "x", path: "/" }, - } as MoltbotConfig; + } as OpenClawConfig; expect(() => resolveHooksConfig(cfg)).toThrow("hooks.path may not be '/'"); }); @@ -43,7 +43,7 @@ describe("gateway hooks helpers", () => { const req = { headers: { authorization: "Bearer top", - "x-moltbot-token": "header", + "x-openclaw-token": "header", }, } as unknown as IncomingMessage; const url = new URL("http://localhost/hooks/wake?token=query"); @@ -52,7 +52,7 @@ describe("gateway hooks helpers", () => { expect(result1.fromQuery).toBe(false); const req2 = { - headers: { "x-moltbot-token": "header" }, + headers: { "x-openclaw-token": "header" }, } as unknown as IncomingMessage; const result2 = extractHookToken(req2, url); expect(result2.token).toBe("header"); diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index 1fc6d52f4..543faf747 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -1,8 +1,8 @@ -import { randomUUID } from "node:crypto"; import type { IncomingMessage } from "node:http"; -import { listChannelPlugins } from "../channels/plugins/index.js"; +import { randomUUID } from "node:crypto"; import type { ChannelId } from "../channels/plugins/types.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { listChannelPlugins } from "../channels/plugins/index.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { type HookMappingResolved, resolveHookMappings } from "./hooks-mapping.js"; @@ -16,8 +16,10 @@ export type HooksConfigResolved = { mappings: HookMappingResolved[]; }; -export function resolveHooksConfig(cfg: MoltbotConfig): HooksConfigResolved | null { - if (cfg.hooks?.enabled !== true) return null; +export function resolveHooksConfig(cfg: OpenClawConfig): HooksConfigResolved | null { + if (cfg.hooks?.enabled !== true) { + return null; + } const token = cfg.hooks?.token?.trim(); if (!token) { throw new Error("hooks.enabled requires hooks.token"); @@ -51,13 +53,21 @@ export function extractHookToken(req: IncomingMessage, url: URL): HookTokenResul typeof req.headers.authorization === "string" ? req.headers.authorization.trim() : ""; if (auth.toLowerCase().startsWith("bearer ")) { const token = auth.slice(7).trim(); - if (token) return { token, fromQuery: false }; + if (token) { + return { token, fromQuery: false }; + } } const headerToken = - typeof req.headers["x-moltbot-token"] === "string" ? req.headers["x-moltbot-token"].trim() : ""; - if (headerToken) return { token: headerToken, fromQuery: false }; + typeof req.headers["x-openclaw-token"] === "string" + ? req.headers["x-openclaw-token"].trim() + : ""; + if (headerToken) { + return { token: headerToken, fromQuery: false }; + } const queryToken = url.searchParams.get("token"); - if (queryToken) return { token: queryToken.trim(), fromQuery: true }; + if (queryToken) { + return { token: queryToken.trim(), fromQuery: true }; + } return { token: undefined, fromQuery: false }; } @@ -70,7 +80,9 @@ export async function readJsonBody( let total = 0; const chunks: Buffer[] = []; req.on("data", (chunk: Buffer) => { - if (done) return; + if (done) { + return; + } total += chunk.length; if (total > maxBytes) { done = true; @@ -81,7 +93,9 @@ export async function readJsonBody( chunks.push(chunk); }); req.on("end", () => { - if (done) return; + if (done) { + return; + } done = true; const raw = Buffer.concat(chunks).toString("utf-8").trim(); if (!raw) { @@ -96,7 +110,9 @@ export async function readJsonBody( } }); req.on("error", (err) => { - if (done) return; + if (done) { + return; + } done = true; resolve({ ok: false, error: String(err) }); }); @@ -121,7 +137,9 @@ export function normalizeWakePayload( | { ok: true; value: { text: string; mode: "now" | "next-heartbeat" } } | { ok: false; error: string } { const text = typeof payload.text === "string" ? payload.text.trim() : ""; - if (!text) return { ok: false, error: "text required" }; + if (!text) { + return { ok: false, error: "text required" }; + } const mode = payload.mode === "next-heartbeat" ? "next-heartbeat" : "now"; return { ok: true, value: { text, mode } }; } @@ -147,10 +165,16 @@ const getHookChannelSet = () => new Set(listHookChannelValues()); export const getHookChannelError = () => `channel must be ${listHookChannelValues().join("|")}`; export function resolveHookChannel(raw: unknown): HookMessageChannel | null { - if (raw === undefined) return "last"; - if (typeof raw !== "string") return null; + if (raw === undefined) { + return "last"; + } + if (typeof raw !== "string") { + return null; + } const normalized = normalizeMessageChannel(raw); - if (!normalized || !getHookChannelSet().has(normalized)) return null; + if (!normalized || !getHookChannelSet().has(normalized)) { + return null; + } return normalized as HookMessageChannel; } @@ -168,7 +192,9 @@ export function normalizeAgentPayload( } | { ok: false; error: string } { const message = typeof payload.message === "string" ? payload.message.trim() : ""; - if (!message) return { ok: false, error: "message required" }; + if (!message) { + return { ok: false, error: "message required" }; + } const nameRaw = payload.name; const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "Hook"; const wakeMode = payload.wakeMode === "next-heartbeat" ? "next-heartbeat" : "now"; @@ -179,7 +205,9 @@ export function normalizeAgentPayload( ? sessionKeyRaw.trim() : `hook:${idFactory()}`; const channel = resolveHookChannel(payload.channel); - if (!channel) return { ok: false, error: getHookChannelError() }; + if (!channel) { + return { ok: false, error: getHookChannelError() }; + } const toRaw = payload.to; const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined; const modelRaw = payload.model; diff --git a/src/gateway/http-common.ts b/src/gateway/http-common.ts index 993a3ac36..c7abc8286 100644 --- a/src/gateway/http-common.ts +++ b/src/gateway/http-common.ts @@ -1,5 +1,4 @@ import type { IncomingMessage, ServerResponse } from "node:http"; - import { readJsonBody } from "./hooks.js"; export function sendJson(res: ServerResponse, status: number, body: unknown) { diff --git a/src/gateway/http-utils.ts b/src/gateway/http-utils.ts index b6c9de38d..95be8d221 100644 --- a/src/gateway/http-utils.ts +++ b/src/gateway/http-utils.ts @@ -1,38 +1,51 @@ -import { randomUUID } from "node:crypto"; import type { IncomingMessage } from "node:http"; - +import { randomUUID } from "node:crypto"; import { buildAgentMainSessionKey, normalizeAgentId } from "../routing/session-key.js"; export function getHeader(req: IncomingMessage, name: string): string | undefined { const raw = req.headers[name.toLowerCase()]; - if (typeof raw === "string") return raw; - if (Array.isArray(raw)) return raw[0]; + if (typeof raw === "string") { + return raw; + } + if (Array.isArray(raw)) { + return raw[0]; + } return undefined; } export function getBearerToken(req: IncomingMessage): string | undefined { const raw = getHeader(req, "authorization")?.trim() ?? ""; - if (!raw.toLowerCase().startsWith("bearer ")) return undefined; + if (!raw.toLowerCase().startsWith("bearer ")) { + return undefined; + } const token = raw.slice(7).trim(); return token || undefined; } export function resolveAgentIdFromHeader(req: IncomingMessage): string | undefined { const raw = - getHeader(req, "x-moltbot-agent-id")?.trim() || getHeader(req, "x-moltbot-agent")?.trim() || ""; - if (!raw) return undefined; + getHeader(req, "x-openclaw-agent-id")?.trim() || + getHeader(req, "x-openclaw-agent")?.trim() || + ""; + if (!raw) { + return undefined; + } return normalizeAgentId(raw); } export function resolveAgentIdFromModel(model: string | undefined): string | undefined { const raw = model?.trim(); - if (!raw) return undefined; + if (!raw) { + return undefined; + } const m = - raw.match(/^moltbot[:/](?[a-z0-9][a-z0-9_-]{0,63})$/i) ?? + raw.match(/^openclaw[:/](?[a-z0-9][a-z0-9_-]{0,63})$/i) ?? raw.match(/^agent:(?[a-z0-9][a-z0-9_-]{0,63})$/i); const agentId = m?.groups?.agentId; - if (!agentId) return undefined; + if (!agentId) { + return undefined; + } return normalizeAgentId(agentId); } @@ -41,7 +54,9 @@ export function resolveAgentIdForRequest(params: { model: string | undefined; }): string { const fromHeader = resolveAgentIdFromHeader(params.req); - if (fromHeader) return fromHeader; + if (fromHeader) { + return fromHeader; + } const fromModel = resolveAgentIdFromModel(params.model); return fromModel ?? "main"; @@ -53,8 +68,10 @@ export function resolveSessionKey(params: { user?: string | undefined; prefix: string; }): string { - const explicit = getHeader(params.req, "x-moltbot-session-key")?.trim(); - if (explicit) return explicit; + const explicit = getHeader(params.req, "x-openclaw-session-key")?.trim(); + if (explicit) { + return explicit; + } const user = params.user?.trim(); const mainKey = user ? `${params.prefix}-user:${user}` : `${params.prefix}:${randomUUID()}`; diff --git a/src/gateway/live-image-probe.ts b/src/gateway/live-image-probe.ts index 77e33e5f2..883d0ac41 100644 --- a/src/gateway/live-image-probe.ts +++ b/src/gateway/live-image-probe.ts @@ -68,10 +68,16 @@ function fillPixel( b: number, a = 255, ) { - if (x < 0 || y < 0) return; - if (x >= width) return; + if (x < 0 || y < 0) { + return; + } + if (x >= width) { + return; + } const idx = (y * width + x) * 4; - if (idx < 0 || idx + 3 >= buf.length) return; + if (idx < 0 || idx + 3 >= buf.length) { + return; + } buf[idx] = r; buf[idx + 1] = g; buf[idx + 2] = b; @@ -109,12 +115,16 @@ function drawGlyph5x7(params: { color: { r: number; g: number; b: number; a?: number }; }) { const rows = GLYPH_ROWS_5X7[params.char]; - if (!rows) return; + if (!rows) { + return; + } for (let row = 0; row < 7; row += 1) { const bits = rows[row] ?? 0; for (let col = 0; col < 5; col += 1) { const on = (bits & (1 << (4 - col))) !== 0; - if (!on) continue; + if (!on) { + continue; + } for (let dy = 0; dy < params.scale; dy += 1) { for (let dx = 0; dx < params.scale; dx += 1) { fillPixel( diff --git a/src/gateway/net.test.ts b/src/gateway/net.test.ts index 46c426d63..2cbd75a2e 100644 --- a/src/gateway/net.test.ts +++ b/src/gateway/net.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { resolveGatewayListenHosts } from "./net.js"; describe("resolveGatewayListenHosts", () => { diff --git a/src/gateway/net.ts b/src/gateway/net.ts index 6702e0e8b..e7730428b 100644 --- a/src/gateway/net.ts +++ b/src/gateway/net.ts @@ -1,56 +1,81 @@ import net from "node:net"; - import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js"; export function isLoopbackAddress(ip: string | undefined): boolean { - if (!ip) return false; - if (ip === "127.0.0.1") return true; - if (ip.startsWith("127.")) return true; - if (ip === "::1") return true; - if (ip.startsWith("::ffff:127.")) return true; + if (!ip) { + return false; + } + if (ip === "127.0.0.1") { + return true; + } + if (ip.startsWith("127.")) { + return true; + } + if (ip === "::1") { + return true; + } + if (ip.startsWith("::ffff:127.")) { + return true; + } return false; } function normalizeIPv4MappedAddress(ip: string): string { - if (ip.startsWith("::ffff:")) return ip.slice("::ffff:".length); + if (ip.startsWith("::ffff:")) { + return ip.slice("::ffff:".length); + } return ip; } function normalizeIp(ip: string | undefined): string | undefined { const trimmed = ip?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } return normalizeIPv4MappedAddress(trimmed.toLowerCase()); } function stripOptionalPort(ip: string): string { if (ip.startsWith("[")) { const end = ip.indexOf("]"); - if (end !== -1) return ip.slice(1, end); + if (end !== -1) { + return ip.slice(1, end); + } + } + if (net.isIP(ip)) { + return ip; } - if (net.isIP(ip)) return ip; const lastColon = ip.lastIndexOf(":"); if (lastColon > -1 && ip.includes(".") && ip.indexOf(":") === lastColon) { const candidate = ip.slice(0, lastColon); - if (net.isIP(candidate) === 4) return candidate; + if (net.isIP(candidate) === 4) { + return candidate; + } } return ip; } export function parseForwardedForClientIp(forwardedFor?: string): string | undefined { const raw = forwardedFor?.split(",")[0]?.trim(); - if (!raw) return undefined; + if (!raw) { + return undefined; + } return normalizeIp(stripOptionalPort(raw)); } function parseRealIp(realIp?: string): string | undefined { const raw = realIp?.trim(); - if (!raw) return undefined; + if (!raw) { + return undefined; + } return normalizeIp(stripOptionalPort(raw)); } export function isTrustedProxyAddress(ip: string | undefined, trustedProxies?: string[]): boolean { const normalized = normalizeIp(ip); - if (!normalized || !trustedProxies || trustedProxies.length === 0) return false; + if (!normalized || !trustedProxies || trustedProxies.length === 0) { + return false; + } return trustedProxies.some((proxy) => normalizeIp(proxy) === normalized); } @@ -61,19 +86,31 @@ export function resolveGatewayClientIp(params: { trustedProxies?: string[]; }): string | undefined { const remote = normalizeIp(params.remoteAddr); - if (!remote) return undefined; - if (!isTrustedProxyAddress(remote, params.trustedProxies)) return remote; + if (!remote) { + return undefined; + } + if (!isTrustedProxyAddress(remote, params.trustedProxies)) { + return remote; + } return parseForwardedForClientIp(params.forwardedFor) ?? parseRealIp(params.realIp) ?? remote; } export function isLocalGatewayAddress(ip: string | undefined): boolean { - if (isLoopbackAddress(ip)) return true; - if (!ip) return false; + if (isLoopbackAddress(ip)) { + return true; + } + if (!ip) { + return false; + } const normalized = normalizeIPv4MappedAddress(ip.trim().toLowerCase()); const tailnetIPv4 = pickPrimaryTailnetIPv4(); - if (tailnetIPv4 && normalized === tailnetIPv4.toLowerCase()) return true; + if (tailnetIPv4 && normalized === tailnetIPv4.toLowerCase()) { + return true; + } const tailnetIPv6 = pickPrimaryTailnetIPv6(); - if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase()) return true; + if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase()) { + return true; + } return false; } @@ -97,14 +134,20 @@ export async function resolveGatewayBindHost( if (mode === "loopback") { // 127.0.0.1 rarely fails, but handle gracefully - if (await canBindToHost("127.0.0.1")) return "127.0.0.1"; + if (await canBindToHost("127.0.0.1")) { + return "127.0.0.1"; + } return "0.0.0.0"; // extreme fallback } if (mode === "tailnet") { const tailnetIP = pickPrimaryTailnetIPv4(); - if (tailnetIP && (await canBindToHost(tailnetIP))) return tailnetIP; - if (await canBindToHost("127.0.0.1")) return "127.0.0.1"; + if (tailnetIP && (await canBindToHost(tailnetIP))) { + return tailnetIP; + } + if (await canBindToHost("127.0.0.1")) { + return "127.0.0.1"; + } return "0.0.0.0"; } @@ -114,15 +157,21 @@ export async function resolveGatewayBindHost( if (mode === "custom") { const host = customHost?.trim(); - if (!host) return "0.0.0.0"; // invalid config → fall back to all + if (!host) { + return "0.0.0.0"; + } // invalid config → fall back to all - if (isValidIPv4(host) && (await canBindToHost(host))) return host; + if (isValidIPv4(host) && (await canBindToHost(host))) { + return host; + } // Custom IP failed → fall back to LAN return "0.0.0.0"; } if (mode === "auto") { - if (await canBindToHost("127.0.0.1")) return "127.0.0.1"; + if (await canBindToHost("127.0.0.1")) { + return "127.0.0.1"; + } return "0.0.0.0"; } @@ -155,9 +204,13 @@ export async function resolveGatewayListenHosts( bindHost: string, opts?: { canBindToHost?: (host: string) => Promise }, ): Promise { - if (bindHost !== "127.0.0.1") return [bindHost]; + if (bindHost !== "127.0.0.1") { + return [bindHost]; + } const canBind = opts?.canBindToHost ?? canBindToHost; - if (await canBind("::1")) return [bindHost, "::1"]; + if (await canBind("::1")) { + return [bindHost, "::1"]; + } return [bindHost]; } @@ -169,7 +222,9 @@ export async function resolveGatewayListenHosts( */ function isValidIPv4(host: string): boolean { const parts = host.split("."); - if (parts.length !== 4) return false; + if (parts.length !== 4) { + return false; + } return parts.every((part) => { const n = parseInt(part, 10); return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n); diff --git a/src/gateway/node-command-policy.ts b/src/gateway/node-command-policy.ts index 0edb5db2a..f22611404 100644 --- a/src/gateway/node-command-policy.ts +++ b/src/gateway/node-command-policy.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { NodeSession } from "./node-registry.js"; const CANVAS_COMMANDS = [ @@ -59,23 +59,45 @@ const PLATFORM_DEFAULTS: Record = { function normalizePlatformId(platform?: string, deviceFamily?: string): string { const raw = (platform ?? "").trim().toLowerCase(); - if (raw.startsWith("ios")) return "ios"; - if (raw.startsWith("android")) return "android"; - if (raw.startsWith("mac")) return "macos"; - if (raw.startsWith("darwin")) return "macos"; - if (raw.startsWith("win")) return "windows"; - if (raw.startsWith("linux")) return "linux"; + if (raw.startsWith("ios")) { + return "ios"; + } + if (raw.startsWith("android")) { + return "android"; + } + if (raw.startsWith("mac")) { + return "macos"; + } + if (raw.startsWith("darwin")) { + return "macos"; + } + if (raw.startsWith("win")) { + return "windows"; + } + if (raw.startsWith("linux")) { + return "linux"; + } const family = (deviceFamily ?? "").trim().toLowerCase(); - if (family.includes("iphone") || family.includes("ipad") || family.includes("ios")) return "ios"; - if (family.includes("android")) return "android"; - if (family.includes("mac")) return "macos"; - if (family.includes("windows")) return "windows"; - if (family.includes("linux")) return "linux"; + if (family.includes("iphone") || family.includes("ipad") || family.includes("ios")) { + return "ios"; + } + if (family.includes("android")) { + return "android"; + } + if (family.includes("mac")) { + return "macos"; + } + if (family.includes("windows")) { + return "windows"; + } + if (family.includes("linux")) { + return "linux"; + } return "unknown"; } export function resolveNodeCommandAllowlist( - cfg: MoltbotConfig, + cfg: OpenClawConfig, node?: Pick, ): Set { const platformId = normalizePlatformId(node?.platform, node?.deviceFamily); @@ -85,7 +107,9 @@ export function resolveNodeCommandAllowlist( const allow = new Set([...base, ...extra].map((cmd) => cmd.trim()).filter(Boolean)); for (const blocked of deny) { const trimmed = blocked.trim(); - if (trimmed) allow.delete(trimmed); + if (trimmed) { + allow.delete(trimmed); + } } return allow; } @@ -96,7 +120,9 @@ export function isNodeCommandAllowed(params: { allowlist: Set; }): { ok: true } | { ok: false; reason: string } { const command = params.command.trim(); - if (!command) return { ok: false, reason: "command required" }; + if (!command) { + return { ok: false, reason: "command required" }; + } if (!params.allowlist.has(command)) { return { ok: false, reason: "command not allowlisted" }; } diff --git a/src/gateway/node-registry.ts b/src/gateway/node-registry.ts index af881364f..c29737ebb 100644 --- a/src/gateway/node-registry.ts +++ b/src/gateway/node-registry.ts @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto"; - import type { GatewayWsClient } from "./server/ws-types.js"; export type NodeSession = { @@ -81,11 +80,15 @@ export class NodeRegistry { unregister(connId: string): string | null { const nodeId = this.nodesByConn.get(connId); - if (!nodeId) return null; + if (!nodeId) { + return null; + } this.nodesByConn.delete(connId); this.nodesById.delete(nodeId); for (const [id, pending] of this.pendingInvokes.entries()) { - if (pending.nodeId !== nodeId) continue; + if (pending.nodeId !== nodeId) { + continue; + } clearTimeout(pending.timer); pending.reject(new Error(`node disconnected (${pending.command})`)); this.pendingInvokes.delete(id); @@ -160,8 +163,12 @@ export class NodeRegistry { error?: { code?: string; message?: string } | null; }): boolean { const pending = this.pendingInvokes.get(params.id); - if (!pending) return false; - if (pending.nodeId !== params.nodeId) return false; + if (!pending) { + return false; + } + if (pending.nodeId !== params.nodeId) { + return false; + } clearTimeout(pending.timer); this.pendingInvokes.delete(params.id); pending.resolve({ @@ -175,7 +182,9 @@ export class NodeRegistry { sendEvent(nodeId: string, event: string, payload?: unknown): boolean { const node = this.nodesById.get(nodeId); - if (!node) return false; + if (!node) { + return false; + } return this.sendEventToSession(node, event, payload); } diff --git a/src/gateway/openai-http.e2e.test.ts b/src/gateway/openai-http.e2e.test.ts index 7068f6623..713ab5e7f 100644 --- a/src/gateway/openai-http.e2e.test.ts +++ b/src/gateway/openai-http.e2e.test.ts @@ -1,5 +1,4 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; - import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; @@ -67,7 +66,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { const server = await startServerWithDefaultConfig(port); try { const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(404); @@ -83,7 +82,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { }); try { const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(404); @@ -124,8 +123,8 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions( port, - { model: "moltbot", messages: [{ role: "user", content: "hi" }] }, - { "x-moltbot-agent-id": "beta" }, + { model: "openclaw", messages: [{ role: "user", content: "hi" }] }, + { "x-openclaw-agent-id": "beta" }, ); expect(res.status).toBe(200); @@ -140,7 +139,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { - model: "moltbot:beta", + model: "openclaw:beta", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); @@ -158,10 +157,10 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { const res = await postChatCompletions( port, { - model: "moltbot:beta", + model: "openclaw:beta", messages: [{ role: "user", content: "hi" }], }, - { "x-moltbot-agent-id": "alpha" }, + { "x-openclaw-agent-id": "alpha" }, ); expect(res.status).toBe(200); @@ -177,10 +176,10 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions( port, - { model: "moltbot", messages: [{ role: "user", content: "hi" }] }, + { model: "openclaw", messages: [{ role: "user", content: "hi" }] }, { - "x-moltbot-agent-id": "beta", - "x-moltbot-session-key": "agent:beta:openai:custom", + "x-openclaw-agent-id": "beta", + "x-openclaw-session-key": "agent:beta:openai:custom", }, ); expect(res.status).toBe(200); @@ -196,7 +195,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { user: "alice", - model: "moltbot", + model: "openclaw", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); @@ -211,7 +210,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [ { role: "user", @@ -232,7 +231,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { { mockAgentOnce([{ text: "I am Claude" }]); const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Hello, who are you?" }, @@ -255,7 +254,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Hello" }, @@ -274,7 +273,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [ { role: "developer", content: "You are a helpful assistant." }, { role: "user", content: "Hello" }, @@ -292,7 +291,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { { mockAgentOnce([{ text: "ok" }]); const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "What's the weather?" }, @@ -316,7 +315,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { stream: false, - model: "moltbot", + model: "openclaw", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); @@ -331,7 +330,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { { const res = await postChatCompletions(port, { - model: "moltbot", + model: "openclaw", messages: [{ role: "system", content: "yo" }], }); expect(res.status).toBe(400); @@ -359,7 +358,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { const res = await postChatCompletions(port, { stream: true, - model: "moltbot", + model: "openclaw", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); @@ -392,7 +391,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { const repeatedRes = await postChatCompletions(port, { stream: true, - model: "moltbot", + model: "openclaw", messages: [{ role: "user", content: "hi" }], }); expect(repeatedRes.status).toBe(200); @@ -417,7 +416,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { const fallbackRes = await postChatCompletions(port, { stream: true, - model: "moltbot", + model: "openclaw", messages: [{ role: "user", content: "hi" }], }); expect(fallbackRes.status).toBe(200); diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index 5a05f08d5..9a623d75e 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -1,6 +1,5 @@ -import { randomUUID } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; - +import { randomUUID } from "node:crypto"; import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js"; import { createDefaultDeps } from "../cli/deps.js"; import { agentCommand } from "../commands/agent.js"; @@ -45,17 +44,27 @@ function asMessages(val: unknown): OpenAiChatMessage[] { } function extractTextContent(content: unknown): string { - if (typeof content === "string") return content; + if (typeof content === "string") { + return content; + } if (Array.isArray(content)) { return content .map((part) => { - if (!part || typeof part !== "object") return ""; + if (!part || typeof part !== "object") { + return ""; + } const type = (part as { type?: unknown }).type; const text = (part as { text?: unknown }).text; const inputText = (part as { input_text?: unknown }).input_text; - if (type === "text" && typeof text === "string") return text; - if (type === "input_text" && typeof text === "string") return text; - if (typeof inputText === "string") return inputText; + if (type === "text" && typeof text === "string") { + return text; + } + if (type === "input_text" && typeof text === "string") { + return text; + } + if (typeof inputText === "string") { + return inputText; + } return ""; }) .filter(Boolean) @@ -75,10 +84,14 @@ function buildAgentPrompt(messagesUnknown: unknown): { []; for (const msg of messages) { - if (!msg || typeof msg !== "object") continue; + if (!msg || typeof msg !== "object") { + continue; + } const role = typeof msg.role === "string" ? msg.role.trim() : ""; const content = extractTextContent(msg.content).trim(); - if (!role || !content) continue; + if (!role || !content) { + continue; + } if (role === "system" || role === "developer") { systemParts.push(content); continue; @@ -115,7 +128,9 @@ function buildAgentPrompt(messagesUnknown: unknown): { break; } } - if (currentIndex < 0) currentIndex = conversationEntries.length - 1; + if (currentIndex < 0) { + currentIndex = conversationEntries.length - 1; + } const currentEntry = conversationEntries[currentIndex]?.entry; if (currentEntry) { const historyEntries = conversationEntries.slice(0, currentIndex).map((entry) => entry.entry); @@ -147,7 +162,9 @@ function resolveOpenAiSessionKey(params: { } function coerceRequest(val: unknown): OpenAiChatCompletionRequest { - if (!val || typeof val !== "object") return {}; + if (!val || typeof val !== "object") { + return {}; + } return val as OpenAiChatCompletionRequest; } @@ -157,7 +174,9 @@ export async function handleOpenAiHttpRequest( opts: OpenAiHttpOptions, ): Promise { const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`); - if (url.pathname !== "/v1/chat/completions") return false; + if (url.pathname !== "/v1/chat/completions") { + return false; + } if (req.method !== "POST") { sendMethodNotAllowed(res); @@ -177,11 +196,13 @@ export async function handleOpenAiHttpRequest( } const body = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? 1024 * 1024); - if (body === undefined) return true; + if (body === undefined) { + return true; + } const payload = coerceRequest(body); const stream = Boolean(payload.stream); - const model = typeof payload.model === "string" ? payload.model : "moltbot"; + const model = typeof payload.model === "string" ? payload.model : "openclaw"; const user = typeof payload.user === "string" ? payload.user : undefined; const agentId = resolveAgentIdForRequest({ req, model }); @@ -223,7 +244,7 @@ export async function handleOpenAiHttpRequest( .map((p) => (typeof p.text === "string" ? p.text : "")) .filter(Boolean) .join("\n\n") - : "No response from Moltbot."; + : "No response from OpenClaw."; sendJson(res, 200, { id: runId, @@ -254,14 +275,20 @@ export async function handleOpenAiHttpRequest( let closed = false; const unsubscribe = onAgentEvent((evt) => { - if (evt.runId !== runId) return; - if (closed) return; + if (evt.runId !== runId) { + return; + } + if (closed) { + return; + } if (evt.stream === "assistant") { const delta = evt.data?.delta; const text = evt.data?.text; const content = typeof delta === "string" ? delta : typeof text === "string" ? text : ""; - if (!content) return; + if (!content) { + return; + } if (!wroteRole) { wroteRole = true; @@ -323,7 +350,9 @@ export async function handleOpenAiHttpRequest( deps, ); - if (closed) return; + if (closed) { + return; + } if (!sawAssistantDelta) { if (!wroteRole) { @@ -344,7 +373,7 @@ export async function handleOpenAiHttpRequest( .map((p) => (typeof p.text === "string" ? p.text : "")) .filter(Boolean) .join("\n\n") - : "No response from Moltbot."; + : "No response from OpenClaw."; sawAssistantDelta = true; writeSse(res, { @@ -362,7 +391,9 @@ export async function handleOpenAiHttpRequest( }); } } catch (err) { - if (closed) return; + if (closed) { + return; + } writeSse(res, { id: runId, object: "chat.completion.chunk", diff --git a/src/gateway/openresponses-http.e2e.test.ts b/src/gateway/openresponses-http.e2e.test.ts index ce7df2a1d..b79aa55a8 100644 --- a/src/gateway/openresponses-http.e2e.test.ts +++ b/src/gateway/openresponses-http.e2e.test.ts @@ -1,5 +1,4 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; - import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; @@ -73,7 +72,9 @@ function parseSseEvents(text: string): Array<{ event?: string; data: string }> { } async function ensureResponseConsumed(res: Response) { - if (res.bodyUsed) return; + if (res.bodyUsed) { + return; + } try { await res.text(); } catch { @@ -87,7 +88,7 @@ describe("OpenResponses HTTP API (e2e)", () => { const _server = await startServerWithDefaultConfig(port); try { const res = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: "hi", }); expect(res.status).toBe(404); @@ -102,7 +103,7 @@ describe("OpenResponses HTTP API (e2e)", () => { }); try { const res = await postResponses(disabledPort, { - model: "moltbot", + model: "openclaw", input: "hi", }); expect(res.status).toBe(404); @@ -130,7 +131,7 @@ describe("OpenResponses HTTP API (e2e)", () => { const resMissingAuth = await fetch(`http://127.0.0.1:${port}/v1/responses`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: "moltbot", input: "hi" }), + body: JSON.stringify({ model: "openclaw", input: "hi" }), }); expect(resMissingAuth.status).toBe(401); await ensureResponseConsumed(resMissingAuth); @@ -146,8 +147,8 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const resHeader = await postResponses( port, - { model: "moltbot", input: "hi" }, - { "x-moltbot-agent-id": "beta" }, + { model: "openclaw", input: "hi" }, + { "x-openclaw-agent-id": "beta" }, ); expect(resHeader.status).toBe(200); const [optsHeader] = agentCommand.mock.calls[0] ?? []; @@ -157,7 +158,7 @@ describe("OpenResponses HTTP API (e2e)", () => { await ensureResponseConsumed(resHeader); mockAgentOnce([{ text: "hello" }]); - const resModel = await postResponses(port, { model: "moltbot:beta", input: "hi" }); + const resModel = await postResponses(port, { model: "openclaw:beta", input: "hi" }); expect(resModel.status).toBe(200); const [optsModel] = agentCommand.mock.calls[0] ?? []; expect((optsModel as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch( @@ -168,7 +169,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const resUser = await postResponses(port, { user: "alice", - model: "moltbot", + model: "openclaw", input: "hi", }); expect(resUser.status).toBe(200); @@ -180,7 +181,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const resString = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: "hello world", }); expect(resString.status).toBe(200); @@ -190,7 +191,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const resArray = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: [{ type: "message", role: "user", content: "hello there" }], }); expect(resArray.status).toBe(200); @@ -200,7 +201,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const resSystemDeveloper = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: [ { type: "message", role: "system", content: "You are a helpful assistant." }, { type: "message", role: "developer", content: "Be concise." }, @@ -218,7 +219,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const resInstructions = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: "hi", instructions: "Always respond in French.", }); @@ -231,7 +232,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "I am Claude" }]); const resHistory = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: [ { type: "message", role: "system", content: "You are a helpful assistant." }, { type: "message", role: "user", content: "Hello, who are you?" }, @@ -251,7 +252,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "ok" }]); const resFunctionOutput = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: [ { type: "message", role: "user", content: "What's the weather?" }, { type: "function_call_output", call_id: "call_1", output: "Sunny, 70F." }, @@ -266,7 +267,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "ok" }]); const resInputFile = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: [ { type: "message", @@ -297,7 +298,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "ok" }]); const resToolNone = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: "hi", tools: [ { @@ -316,7 +317,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "ok" }]); const resToolChoice = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: "hi", tools: [ { @@ -340,7 +341,7 @@ describe("OpenResponses HTTP API (e2e)", () => { await ensureResponseConsumed(resToolChoice); const resUnknownTool = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: "hi", tools: [ { @@ -355,7 +356,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "ok" }]); const resMaxTokens = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: "hi", max_output_tokens: 123, }); @@ -374,7 +375,7 @@ describe("OpenResponses HTTP API (e2e)", () => { }); const resUsage = await postResponses(port, { stream: false, - model: "moltbot", + model: "openclaw", input: "hi", }); expect(resUsage.status).toBe(200); @@ -385,7 +386,7 @@ describe("OpenResponses HTTP API (e2e)", () => { mockAgentOnce([{ text: "hello" }]); const resShape = await postResponses(port, { stream: false, - model: "moltbot", + model: "openclaw", input: "hi", }); expect(resShape.status).toBe(200); @@ -407,7 +408,7 @@ describe("OpenResponses HTTP API (e2e)", () => { await ensureResponseConsumed(resShape); const resNoUser = await postResponses(port, { - model: "moltbot", + model: "openclaw", input: [{ type: "message", role: "system", content: "yo" }], }); expect(resNoUser.status).toBe(400); @@ -434,7 +435,7 @@ describe("OpenResponses HTTP API (e2e)", () => { const resDelta = await postResponses(port, { stream: true, - model: "moltbot", + model: "openclaw", input: "hi", }); expect(resDelta.status).toBe(200); @@ -470,7 +471,7 @@ describe("OpenResponses HTTP API (e2e)", () => { const resFallback = await postResponses(port, { stream: true, - model: "moltbot", + model: "openclaw", input: "hi", }); expect(resFallback.status).toBe(200); @@ -485,7 +486,7 @@ describe("OpenResponses HTTP API (e2e)", () => { const resTypeMatch = await postResponses(port, { stream: true, - model: "moltbot", + model: "openclaw", input: "hi", }); expect(resTypeMatch.status).toBe(200); @@ -493,7 +494,9 @@ describe("OpenResponses HTTP API (e2e)", () => { const typeText = await resTypeMatch.text(); const typeEvents = parseSseEvents(typeText); for (const event of typeEvents) { - if (event.data === "[DONE]") continue; + if (event.data === "[DONE]") { + continue; + } const parsed = JSON.parse(event.data) as { type?: string }; expect(event.event).toBe(parsed.type); } diff --git a/src/gateway/openresponses-http.ts b/src/gateway/openresponses-http.ts index 147ca5fb9..adbc49e6b 100644 --- a/src/gateway/openresponses-http.ts +++ b/src/gateway/openresponses-http.ts @@ -1,42 +1,20 @@ /** * OpenResponses HTTP Handler * - * Implements the OpenResponses `/v1/responses` endpoint for Moltbot Gateway. + * Implements the OpenResponses `/v1/responses` endpoint for OpenClaw Gateway. * * @see https://www.open-responses.com/ */ -import { randomUUID } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; - +import { randomUUID } from "node:crypto"; +import type { ClientToolDefinition } from "../agents/pi-embedded-runner/run/params.js"; +import type { ImageContent } from "../commands/agent/types.js"; +import type { GatewayHttpResponsesConfig } from "../config/types.gateway.js"; import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js"; import { createDefaultDeps } from "../cli/deps.js"; import { agentCommand } from "../commands/agent.js"; import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js"; -import { defaultRuntime } from "../runtime.js"; -import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js"; -import { getBearerToken, resolveAgentIdForRequest, resolveSessionKey } from "./http-utils.js"; -import { - readJsonBodyOrError, - sendJson, - sendMethodNotAllowed, - sendUnauthorized, - setSseHeaders, - writeDone, -} from "./http-common.js"; -import { - CreateResponseBodySchema, - type ContentPart, - type CreateResponseBody, - type ItemParam, - type OutputItem, - type ResponseResource, - type StreamingEvent, - type Usage, -} from "./open-responses.schema.js"; -import type { GatewayHttpResponsesConfig } from "../config/types.gateway.js"; -import type { ClientToolDefinition } from "../agents/pi-embedded-runner/run/params.js"; -import type { ImageContent } from "../commands/agent/types.js"; import { DEFAULT_INPUT_FILE_MAX_BYTES, DEFAULT_INPUT_FILE_MAX_CHARS, @@ -55,6 +33,27 @@ import { type InputImageLimits, type InputImageSource, } from "../media/input-files.js"; +import { defaultRuntime } from "../runtime.js"; +import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js"; +import { + readJsonBodyOrError, + sendJson, + sendMethodNotAllowed, + sendUnauthorized, + setSseHeaders, + writeDone, +} from "./http-common.js"; +import { getBearerToken, resolveAgentIdForRequest, resolveSessionKey } from "./http-utils.js"; +import { + CreateResponseBodySchema, + type ContentPart, + type CreateResponseBody, + type ItemParam, + type OutputItem, + type ResponseResource, + type StreamingEvent, + type Usage, +} from "./open-responses.schema.js"; type OpenResponsesHttpOptions = { auth: ResolvedGatewayAuth; @@ -71,11 +70,17 @@ function writeSseEvent(res: ServerResponse, event: StreamingEvent) { } function extractTextContent(content: string | ContentPart[]): string { - if (typeof content === "string") return content; + if (typeof content === "string") { + return content; + } return content .map((part) => { - if (part.type === "input_text") return part.text; - if (part.type === "output_text") return part.text; + if (part.type === "input_text") { + return part.text; + } + if (part.type === "output_text") { + return part.text; + } return ""; }) .filter(Boolean) @@ -127,7 +132,9 @@ function applyToolChoice(params: { toolChoice: CreateResponseBody["tool_choice"]; }): { tools: ClientToolDefinition[]; extraSystemPrompt?: string } { const { tools, toolChoice } = params; - if (!toolChoice) return { tools }; + if (!toolChoice) { + return { tools }; + } if (toolChoice === "none") { return { tools: [] }; @@ -176,7 +183,9 @@ export function buildAgentPrompt(input: string | ItemParam[]): { for (const item of input) { if (item.type === "message") { const content = extractTextContent(item.content).trim(); - if (!content) continue; + if (!content) { + continue; + } if (item.role === "system" || item.role === "developer") { systemParts.push(content); @@ -210,7 +219,9 @@ export function buildAgentPrompt(input: string | ItemParam[]): { break; } } - if (currentIndex < 0) currentIndex = conversationEntries.length - 1; + if (currentIndex < 0) { + currentIndex = conversationEntries.length - 1; + } const currentEntry = conversationEntries[currentIndex]?.entry; if (currentEntry) { @@ -257,7 +268,9 @@ function toUsage( } | undefined, ): Usage { - if (!value) return createEmptyUsage(); + if (!value) { + return createEmptyUsage(); + } const input = value.input ?? 0; const output = value.output ?? 0; const cacheRead = value.cacheRead ?? 0; @@ -320,7 +333,9 @@ export async function handleOpenResponsesHttpRequest( opts: OpenResponsesHttpOptions, ): Promise { const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`); - if (url.pathname !== "/v1/responses") return false; + if (url.pathname !== "/v1/responses") { + return false; + } if (req.method !== "POST") { sendMethodNotAllowed(res); @@ -346,7 +361,9 @@ export async function handleOpenResponsesHttpRequest( ? limits.maxBodyBytes : Math.max(limits.maxBodyBytes, limits.files.maxBytes * 2, limits.images.maxBytes * 2)); const body = await readJsonBodyOrError(req, res, maxBodyBytes); - if (body === undefined) return true; + if (body === undefined) { + return true; + } // Validate request body with Zod const parseResult = CreateResponseBodySchema.safeParse(body); @@ -552,7 +569,7 @@ export async function handleOpenResponsesHttpRequest( .map((p) => (typeof p.text === "string" ? p.text : "")) .filter(Boolean) .join("\n\n") - : "No response from Moltbot."; + : "No response from OpenClaw."; const response = createResponseResource({ id: responseId, @@ -592,9 +609,15 @@ export async function handleOpenResponsesHttpRequest( let finalizeRequested: { status: ResponseResource["status"]; text: string } | null = null; const maybeFinalize = () => { - if (closed) return; - if (!finalizeRequested) return; - if (!finalUsage) return; + if (closed) { + return; + } + if (!finalizeRequested) { + return; + } + if (!finalUsage) { + return; + } const usage = finalUsage; closed = true; @@ -642,7 +665,9 @@ export async function handleOpenResponsesHttpRequest( }; const requestFinalize = (status: ResponseResource["status"], text: string) => { - if (finalizeRequested) return; + if (finalizeRequested) { + return; + } finalizeRequested = { status, text }; maybeFinalize(); }; @@ -681,14 +706,20 @@ export async function handleOpenResponsesHttpRequest( }); unsubscribe = onAgentEvent((evt) => { - if (evt.runId !== responseId) return; - if (closed) return; + if (evt.runId !== responseId) { + return; + } + if (closed) { + return; + } if (evt.stream === "assistant") { const delta = evt.data?.delta; const text = evt.data?.text; const content = typeof delta === "string" ? delta : typeof text === "string" ? text : ""; - if (!content) return; + if (!content) { + return; + } sawAssistantDelta = true; accumulatedText += content; @@ -706,7 +737,7 @@ export async function handleOpenResponsesHttpRequest( if (evt.stream === "lifecycle") { const phase = evt.data?.phase; if (phase === "end" || phase === "error") { - const finalText = accumulatedText || "No response from Moltbot."; + const finalText = accumulatedText || "No response from OpenClaw."; const finalStatus = phase === "error" ? "failed" : "completed"; requestFinalize(finalStatus, finalText); } @@ -740,7 +771,9 @@ export async function handleOpenResponsesHttpRequest( finalUsage = extractUsageFromResult(result); maybeFinalize(); - if (closed) return; + if (closed) { + return; + } // Fallback: if no streaming deltas were received, send the full response if (!sawAssistantDelta) { @@ -831,7 +864,7 @@ export async function handleOpenResponsesHttpRequest( .map((p) => (typeof p.text === "string" ? p.text : "")) .filter(Boolean) .join("\n\n") - : "No response from Moltbot."; + : "No response from OpenClaw."; accumulatedText = content; sawAssistantDelta = true; @@ -845,7 +878,9 @@ export async function handleOpenResponsesHttpRequest( }); } } catch (err) { - if (closed) return; + if (closed) { + return; + } finalUsage = finalUsage ?? createEmptyUsage(); const errorResponse = createResponseResource({ diff --git a/src/gateway/probe.ts b/src/gateway/probe.ts index 39be5f7ea..c2593e041 100644 --- a/src/gateway/probe.ts +++ b/src/gateway/probe.ts @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto"; - import type { SystemPresence } from "../infra/system-presence.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { GatewayClient } from "./client.js"; @@ -28,7 +27,9 @@ export type GatewayProbeResult = { }; function formatError(err: unknown): string { - if (err instanceof Error) return err.message; + if (err instanceof Error) { + return err.message; + } return String(err); } @@ -46,7 +47,9 @@ export async function probeGateway(opts: { return await new Promise((resolve) => { let settled = false; const settle = (result: Omit) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); client.stop(); diff --git a/src/gateway/protocol/client-info.ts b/src/gateway/protocol/client-info.ts index bab32ea9c..7bb30f917 100644 --- a/src/gateway/protocol/client-info.ts +++ b/src/gateway/protocol/client-info.ts @@ -1,16 +1,16 @@ export const GATEWAY_CLIENT_IDS = { WEBCHAT_UI: "webchat-ui", - CONTROL_UI: "moltbot-control-ui", + CONTROL_UI: "openclaw-control-ui", WEBCHAT: "webchat", CLI: "cli", GATEWAY_CLIENT: "gateway-client", - MACOS_APP: "moltbot-macos", - IOS_APP: "moltbot-ios", - ANDROID_APP: "moltbot-android", + MACOS_APP: "openclaw-macos", + IOS_APP: "openclaw-ios", + ANDROID_APP: "openclaw-android", NODE_HOST: "node-host", TEST: "test", FINGERPRINT: "fingerprint", - PROBE: "moltbot-probe", + PROBE: "openclaw-probe", } as const; export type GatewayClientId = (typeof GATEWAY_CLIENT_IDS)[keyof typeof GATEWAY_CLIENT_IDS]; @@ -47,7 +47,9 @@ const GATEWAY_CLIENT_MODE_SET = new Set(Object.values(GATEWAY export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined { const normalized = raw?.trim().toLowerCase(); - if (!normalized) return undefined; + if (!normalized) { + return undefined; + } return GATEWAY_CLIENT_ID_SET.has(normalized as GatewayClientId) ? (normalized as GatewayClientId) : undefined; @@ -59,7 +61,9 @@ export function normalizeGatewayClientName(raw?: string | null): GatewayClientNa export function normalizeGatewayClientMode(raw?: string | null): GatewayClientMode | undefined { const normalized = raw?.trim().toLowerCase(); - if (!normalized) return undefined; + if (!normalized) { + return undefined; + } return GATEWAY_CLIENT_MODE_SET.has(normalized as GatewayClientMode) ? (normalized as GatewayClientMode) : undefined; diff --git a/src/gateway/protocol/index.test.ts b/src/gateway/protocol/index.test.ts index 828951366..c74e7361d 100644 --- a/src/gateway/protocol/index.test.ts +++ b/src/gateway/protocol/index.test.ts @@ -1,6 +1,5 @@ -import { describe, expect, it } from "vitest"; import type { ErrorObject } from "ajv"; - +import { describe, expect, it } from "vitest"; import { formatValidationErrors } from "./index.js"; const makeError = (overrides: Partial): ErrorObject => ({ diff --git a/src/gateway/protocol/index.ts b/src/gateway/protocol/index.ts index 6e5a862d1..9bd0b4054 100644 --- a/src/gateway/protocol/index.ts +++ b/src/gateway/protocol/index.ts @@ -321,7 +321,9 @@ export const validateWebLoginStartParams = export const validateWebLoginWaitParams = ajv.compile(WebLoginWaitParamsSchema); export function formatValidationErrors(errors: ErrorObject[] | null | undefined) { - if (!errors?.length) return "unknown validation error"; + if (!errors?.length) { + return "unknown validation error"; + } const parts: string[] = []; diff --git a/src/gateway/protocol/schema/agent.ts b/src/gateway/protocol/schema/agent.ts index 3f1a5b5a8..9e11a6341 100644 --- a/src/gateway/protocol/schema/agent.ts +++ b/src/gateway/protocol/schema/agent.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString, SessionLabelString } from "./primitives.js"; export const AgentEventSchema = Type.Object( diff --git a/src/gateway/protocol/schema/agents-models-skills.ts b/src/gateway/protocol/schema/agents-models-skills.ts index c3b39257f..564fa68a2 100644 --- a/src/gateway/protocol/schema/agents-models-skills.ts +++ b/src/gateway/protocol/schema/agents-models-skills.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const ModelChoiceSchema = Type.Object( diff --git a/src/gateway/protocol/schema/channels.ts b/src/gateway/protocol/schema/channels.ts index 76c6ac4d8..cbbaaa192 100644 --- a/src/gateway/protocol/schema/channels.ts +++ b/src/gateway/protocol/schema/channels.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const TalkModeParamsSchema = Type.Object( diff --git a/src/gateway/protocol/schema/config.ts b/src/gateway/protocol/schema/config.ts index beeaac5d5..eb7389a4d 100644 --- a/src/gateway/protocol/schema/config.ts +++ b/src/gateway/protocol/schema/config.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const ConfigGetParamsSchema = Type.Object({}, { additionalProperties: false }); diff --git a/src/gateway/protocol/schema/cron.ts b/src/gateway/protocol/schema/cron.ts index 63ed0c209..47c26ec91 100644 --- a/src/gateway/protocol/schema/cron.ts +++ b/src/gateway/protocol/schema/cron.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const CronScheduleSchema = Type.Union([ diff --git a/src/gateway/protocol/schema/devices.ts b/src/gateway/protocol/schema/devices.ts index ec32f381b..8163be27a 100644 --- a/src/gateway/protocol/schema/devices.ts +++ b/src/gateway/protocol/schema/devices.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const DevicePairListParamsSchema = Type.Object({}, { additionalProperties: false }); diff --git a/src/gateway/protocol/schema/exec-approvals.ts b/src/gateway/protocol/schema/exec-approvals.ts index e6f7ce906..a88cdffcd 100644 --- a/src/gateway/protocol/schema/exec-approvals.ts +++ b/src/gateway/protocol/schema/exec-approvals.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const ExecApprovalsAllowlistEntrySchema = Type.Object( diff --git a/src/gateway/protocol/schema/logs-chat.ts b/src/gateway/protocol/schema/logs-chat.ts index dc04a29d5..b8d0fe1ba 100644 --- a/src/gateway/protocol/schema/logs-chat.ts +++ b/src/gateway/protocol/schema/logs-chat.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const LogsTailParamsSchema = Type.Object( diff --git a/src/gateway/protocol/schema/nodes.ts b/src/gateway/protocol/schema/nodes.ts index 7762f30b8..4eaccb8d7 100644 --- a/src/gateway/protocol/schema/nodes.ts +++ b/src/gateway/protocol/schema/nodes.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const NodePairRequestParamsSchema = Type.Object( diff --git a/src/gateway/protocol/schema/protocol-schemas.ts b/src/gateway/protocol/schema/protocol-schemas.ts index e92f114e2..11eb6e2ba 100644 --- a/src/gateway/protocol/schema/protocol-schemas.ts +++ b/src/gateway/protocol/schema/protocol-schemas.ts @@ -1,5 +1,4 @@ import type { TSchema } from "@sinclair/typebox"; - import { AgentEventSchema, AgentIdentityParamsSchema, @@ -51,15 +50,6 @@ import { CronStatusParamsSchema, CronUpdateParamsSchema, } from "./cron.js"; -import { - ExecApprovalsGetParamsSchema, - ExecApprovalsNodeGetParamsSchema, - ExecApprovalsNodeSetParamsSchema, - ExecApprovalsSetParamsSchema, - ExecApprovalsSnapshotSchema, - ExecApprovalRequestParamsSchema, - ExecApprovalResolveParamsSchema, -} from "./exec-approvals.js"; import { DevicePairApproveParamsSchema, DevicePairListParamsSchema, @@ -69,6 +59,15 @@ import { DeviceTokenRevokeParamsSchema, DeviceTokenRotateParamsSchema, } from "./devices.js"; +import { + ExecApprovalsGetParamsSchema, + ExecApprovalsNodeGetParamsSchema, + ExecApprovalsNodeSetParamsSchema, + ExecApprovalsSetParamsSchema, + ExecApprovalsSnapshotSchema, + ExecApprovalRequestParamsSchema, + ExecApprovalResolveParamsSchema, +} from "./exec-approvals.js"; import { ConnectParamsSchema, ErrorShapeSchema, diff --git a/src/gateway/protocol/schema/sessions.ts b/src/gateway/protocol/schema/sessions.ts index 67156a5de..ab6bbb12a 100644 --- a/src/gateway/protocol/schema/sessions.ts +++ b/src/gateway/protocol/schema/sessions.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString, SessionLabelString } from "./primitives.js"; export const SessionsListParamsSchema = Type.Object( diff --git a/src/gateway/protocol/schema/types.ts b/src/gateway/protocol/schema/types.ts index 696503721..193784bc8 100644 --- a/src/gateway/protocol/schema/types.ts +++ b/src/gateway/protocol/schema/types.ts @@ -1,5 +1,4 @@ import type { Static } from "@sinclair/typebox"; - import type { AgentEventSchema, AgentIdentityParamsSchema, @@ -49,6 +48,13 @@ import type { CronStatusParamsSchema, CronUpdateParamsSchema, } from "./cron.js"; +import type { + DevicePairApproveParamsSchema, + DevicePairListParamsSchema, + DevicePairRejectParamsSchema, + DeviceTokenRevokeParamsSchema, + DeviceTokenRotateParamsSchema, +} from "./devices.js"; import type { ExecApprovalsGetParamsSchema, ExecApprovalsNodeGetParamsSchema, @@ -58,13 +64,6 @@ import type { ExecApprovalRequestParamsSchema, ExecApprovalResolveParamsSchema, } from "./exec-approvals.js"; -import type { - DevicePairApproveParamsSchema, - DevicePairListParamsSchema, - DevicePairRejectParamsSchema, - DeviceTokenRevokeParamsSchema, - DeviceTokenRotateParamsSchema, -} from "./devices.js"; import type { ConnectParamsSchema, ErrorShapeSchema, diff --git a/src/gateway/protocol/schema/wizard.ts b/src/gateway/protocol/schema/wizard.ts index 1ceaa0b9a..2a5f75e2e 100644 --- a/src/gateway/protocol/schema/wizard.ts +++ b/src/gateway/protocol/schema/wizard.ts @@ -1,5 +1,4 @@ import { Type } from "@sinclair/typebox"; - import { NonEmptyString } from "./primitives.js"; export const WizardStartParamsSchema = Type.Object( diff --git a/src/gateway/server-broadcast.test.ts b/src/gateway/server-broadcast.test.ts index 44d164cf0..0dcec9e09 100644 --- a/src/gateway/server-broadcast.test.ts +++ b/src/gateway/server-broadcast.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from "vitest"; - -import { createGatewayBroadcaster } from "./server-broadcast.js"; import type { GatewayWsClient } from "./server/ws-types.js"; +import { createGatewayBroadcaster } from "./server-broadcast.js"; type TestSocket = { bufferedAmount: number; diff --git a/src/gateway/server-broadcast.ts b/src/gateway/server-broadcast.ts index 61df31097..abbc3d87e 100644 --- a/src/gateway/server-broadcast.ts +++ b/src/gateway/server-broadcast.ts @@ -17,11 +17,17 @@ const EVENT_SCOPE_GUARDS: Record = { function hasEventScope(client: GatewayWsClient, event: string): boolean { const required = EVENT_SCOPE_GUARDS[event]; - if (!required) return true; + if (!required) { + return true; + } const role = client.connect.role ?? "operator"; - if (role !== "operator") return false; + if (role !== "operator") { + return false; + } const scopes = Array.isArray(client.connect.scopes) ? client.connect.scopes : []; - if (scopes.includes(ADMIN_SCOPE)) return true; + if (scopes.includes(ADMIN_SCOPE)) { + return true; + } return required.some((scope) => scopes.includes(scope)); } @@ -56,9 +62,13 @@ export function createGatewayBroadcaster(params: { clients: Set } logWs("out", "event", logMeta); for (const c of params.clients) { - if (!hasEventScope(c, event)) continue; + if (!hasEventScope(c, event)) { + continue; + } const slow = c.socket.bufferedAmount > MAX_BUFFERED_BYTES; - if (slow && opts?.dropIfSlow) continue; + if (slow && opts?.dropIfSlow) { + continue; + } if (slow) { try { c.socket.close(1008, "slow consumer"); diff --git a/src/gateway/server-browser.ts b/src/gateway/server-browser.ts index f525348bb..02f3659de 100644 --- a/src/gateway/server-browser.ts +++ b/src/gateway/server-browser.ts @@ -5,10 +5,12 @@ export type BrowserControlServer = { }; export async function startBrowserControlServerIfEnabled(): Promise { - if (isTruthyEnvValue(process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER)) return null; + if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER)) { + return null; + } // Lazy import: keeps startup fast, but still bundles for the embedded // gateway (bun --compile) via the static specifier path. - const override = process.env.CLAWDBOT_BROWSER_CONTROL_MODULE?.trim(); + const override = process.env.OPENCLAW_BROWSER_CONTROL_MODULE?.trim(); const mod = override ? await import(override) : await import("../browser/control-service.js"); const start = typeof (mod as { startBrowserControlServiceFromConfig?: unknown }) @@ -21,7 +23,9 @@ export async function startBrowserControlServerIfEnabled(): Promise Promise }).stopBrowserControlService : (mod as { stopBrowserControlServer?: () => Promise }).stopBrowserControlServer; - if (!start) return null; + if (!start) { + return null; + } await start(); return { stop: stop ?? (async () => {}) }; } diff --git a/src/gateway/server-channels.ts b/src/gateway/server-channels.ts index 6f8348430..73a6a11cd 100644 --- a/src/gateway/server-channels.ts +++ b/src/gateway/server-channels.ts @@ -1,12 +1,12 @@ +import type { ChannelAccountSnapshot } from "../channels/plugins/types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { createSubsystemLogger } from "../logging/subsystem.js"; +import type { RuntimeEnv } from "../runtime.js"; import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; import { type ChannelId, getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js"; -import type { ChannelAccountSnapshot } from "../channels/plugins/types.js"; -import type { MoltbotConfig } from "../config/config.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resetDirectoryCache } from "../infra/outbound/target-resolver.js"; -import type { createSubsystemLogger } from "../logging/subsystem.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; -import type { RuntimeEnv } from "../runtime.js"; export type ChannelRuntimeSnapshot = { channels: Partial>; @@ -30,7 +30,9 @@ function createRuntimeStore(): ChannelRuntimeStore { } function isAccountEnabled(account: unknown): boolean { - if (!account || typeof account !== "object") return true; + if (!account || typeof account !== "object") { + return true; + } const enabled = (account as { enabled?: boolean }).enabled; return enabled !== false; } @@ -45,7 +47,7 @@ function cloneDefaultRuntime(channelId: ChannelId, accountId: string): ChannelAc } type ChannelManagerOptions = { - loadConfig: () => MoltbotConfig; + loadConfig: () => OpenClawConfig; channelLogs: Record; channelRuntimeEnvs: Record; }; @@ -66,7 +68,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage const getStore = (channelId: ChannelId): ChannelRuntimeStore => { const existing = channelStores.get(channelId); - if (existing) return existing; + if (existing) { + return existing; + } const next = createRuntimeStore(); channelStores.set(channelId, next); return next; @@ -92,16 +96,22 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage const startChannel = async (channelId: ChannelId, accountId?: string) => { const plugin = getChannelPlugin(channelId); const startAccount = plugin?.gateway?.startAccount; - if (!startAccount) return; + if (!startAccount) { + return; + } const cfg = loadConfig(); resetDirectoryCache({ channel: channelId, accountId }); const store = getStore(channelId); const accountIds = accountId ? [accountId] : plugin.config.listAccountIds(cfg); - if (accountIds.length === 0) return; + if (accountIds.length === 0) { + return; + } await Promise.all( accountIds.map(async (id) => { - if (store.tasks.has(id)) return; + if (store.tasks.has(id)) { + return; + } const account = plugin.config.resolveAccount(cfg, id); const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) @@ -186,7 +196,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage Array.from(knownIds.values()).map(async (id) => { const abort = store.aborts.get(id); const task = store.tasks.get(id); - if (!abort && !task && !plugin?.gateway?.stopAccount) return; + if (!abort && !task && !plugin?.gateway?.stopAccount) { + return; + } abort?.abort(); if (plugin?.gateway?.stopAccount) { const account = plugin.config.resolveAccount(cfg, id); @@ -225,7 +237,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage const markChannelLoggedOut = (channelId: ChannelId, cleared: boolean, accountId?: string) => { const plugin = getChannelPlugin(channelId); - if (!plugin) return; + if (!plugin) { + return; + } const cfg = loadConfig(); const resolvedId = accountId ?? diff --git a/src/gateway/server-chat.agent-events.test.ts b/src/gateway/server-chat.agent-events.test.ts index 14657464a..43761af11 100644 --- a/src/gateway/server-chat.agent-events.test.ts +++ b/src/gateway/server-chat.agent-events.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { createAgentEventHandler, createChatRunState } from "./server-chat.js"; describe("agent event handler", () => { diff --git a/src/gateway/server-chat.ts b/src/gateway/server-chat.ts index 8c67767a6..953b26268 100644 --- a/src/gateway/server-chat.ts +++ b/src/gateway/server-chat.ts @@ -11,7 +11,9 @@ import { formatForLog } from "./ws-log.js"; */ function shouldSuppressHeartbeatBroadcast(runId: string): boolean { const runContext = getAgentRunContext(runId); - if (!runContext?.isHeartbeat) return false; + if (!runContext?.isHeartbeat) { + return false; + } try { const cfg = loadConfig(); @@ -52,22 +54,32 @@ export function createChatRunRegistry(): ChatRunRegistry { const shift = (sessionId: string) => { const queue = chatRunSessions.get(sessionId); - if (!queue || queue.length === 0) return undefined; + if (!queue || queue.length === 0) { + return undefined; + } const entry = queue.shift(); - if (!queue.length) chatRunSessions.delete(sessionId); + if (!queue.length) { + chatRunSessions.delete(sessionId); + } return entry; }; const remove = (sessionId: string, clientRunId: string, sessionKey?: string) => { const queue = chatRunSessions.get(sessionId); - if (!queue || queue.length === 0) return undefined; + if (!queue || queue.length === 0) { + return undefined; + } const idx = queue.findIndex( (entry) => entry.clientRunId === clientRunId && (sessionKey ? entry.sessionKey === sessionKey : true), ); - if (idx < 0) return undefined; + if (idx < 0) { + return undefined; + } const [entry] = queue.splice(idx, 1); - if (!queue.length) chatRunSessions.delete(sessionId); + if (!queue.length) { + chatRunSessions.delete(sessionId); + } return entry; }; @@ -137,7 +149,9 @@ export function createAgentEventHandler({ chatRunState.buffers.set(clientRunId, text); const now = Date.now(); const last = chatRunState.deltaSentAt.get(clientRunId) ?? 0; - if (now - last < 150) return; + if (now - last < 150) { + return; + } chatRunState.deltaSentAt.set(clientRunId, now); const payload = { runId: clientRunId, @@ -202,12 +216,18 @@ export function createAgentEventHandler({ const shouldEmitToolEvents = (runId: string, sessionKey?: string) => { const runContext = getAgentRunContext(runId); const runVerbose = normalizeVerboseLevel(runContext?.verboseLevel); - if (runVerbose) return runVerbose === "on"; - if (!sessionKey) return false; + if (runVerbose) { + return runVerbose === "on"; + } + if (!sessionKey) { + return false; + } try { const { cfg, entry } = loadSessionEntry(sessionKey); const sessionVerbose = normalizeVerboseLevel(entry?.verboseLevel); - if (sessionVerbose) return sessionVerbose === "on"; + if (sessionVerbose) { + return sessionVerbose === "on"; + } const defaultVerbose = normalizeVerboseLevel(cfg.agents?.defaults?.verboseDefault); return defaultVerbose === "on"; } catch { diff --git a/src/gateway/server-close.ts b/src/gateway/server-close.ts index da9f5a39e..ea0323587 100644 --- a/src/gateway/server-close.ts +++ b/src/gateway/server-close.ts @@ -1,10 +1,10 @@ import type { Server as HttpServer } from "node:http"; import type { WebSocketServer } from "ws"; import type { CanvasHostHandler, CanvasHostServer } from "../canvas-host/server.js"; -import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js"; -import { stopGmailWatcher } from "../hooks/gmail-watcher.js"; import type { HeartbeatRunner } from "../infra/heartbeat-runner.js"; import type { PluginServicesHandle } from "../plugins/services.js"; +import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js"; +import { stopGmailWatcher } from "../hooks/gmail-watcher.js"; export function createGatewayCloseHandler(params: { bonjourStop: (() => Promise) | null; diff --git a/src/gateway/server-constants.ts b/src/gateway/server-constants.ts index 015164475..996ea6358 100644 --- a/src/gateway/server-constants.ts +++ b/src/gateway/server-constants.ts @@ -7,7 +7,9 @@ let maxChatHistoryMessagesBytes = DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES; export const getMaxChatHistoryMessagesBytes = () => maxChatHistoryMessagesBytes; export const __setMaxChatHistoryMessagesBytesForTest = (value?: number) => { - if (!process.env.VITEST && process.env.NODE_ENV !== "test") return; + if (!process.env.VITEST && process.env.NODE_ENV !== "test") { + return; + } if (value === undefined) { maxChatHistoryMessagesBytes = DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES; return; @@ -18,9 +20,11 @@ export const __setMaxChatHistoryMessagesBytesForTest = (value?: number) => { }; export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000; export const getHandshakeTimeoutMs = () => { - if (process.env.VITEST && process.env.CLAWDBOT_TEST_HANDSHAKE_TIMEOUT_MS) { - const parsed = Number(process.env.CLAWDBOT_TEST_HANDSHAKE_TIMEOUT_MS); - if (Number.isFinite(parsed) && parsed > 0) return parsed; + if (process.env.VITEST && process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS) { + const parsed = Number(process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS); + if (Number.isFinite(parsed) && parsed > 0) { + return parsed; + } } return DEFAULT_HANDSHAKE_TIMEOUT_MS; }; diff --git a/src/gateway/server-cron.ts b/src/gateway/server-cron.ts index 9a0c0ca98..68b0bc095 100644 --- a/src/gateway/server-cron.ts +++ b/src/gateway/server-cron.ts @@ -1,5 +1,5 @@ -import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import type { CliDeps } from "../cli/deps.js"; +import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { loadConfig } from "../config/config.js"; import { resolveAgentMainSessionKey } from "../config/sessions.js"; import { runCronIsolatedAgentTurn } from "../cron/isolated-agent.js"; @@ -26,7 +26,7 @@ export function buildGatewayCronService(params: { }): GatewayCronState { const cronLogger = getChildLogger({ module: "cron" }); const storePath = resolveCronStorePath(params.cfg.cron?.store); - const cronEnabled = process.env.CLAWDBOT_SKIP_CRON !== "1" && params.cfg.cron?.enabled !== false; + const cronEnabled = process.env.OPENCLAW_SKIP_CRON !== "1" && params.cfg.cron?.enabled !== false; const resolveCronAgent = (requested?: string | null) => { const runtimeConfig = loadConfig(); diff --git a/src/gateway/server-discovery-runtime.ts b/src/gateway/server-discovery-runtime.ts index 2dec5883e..eaf7c964f 100644 --- a/src/gateway/server-discovery-runtime.ts +++ b/src/gateway/server-discovery-runtime.ts @@ -1,6 +1,6 @@ import { startGatewayBonjourAdvertiser } from "../infra/bonjour.js"; import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js"; -import { WIDE_AREA_DISCOVERY_DOMAIN, writeWideAreaGatewayZone } from "../infra/widearea-dns.js"; +import { resolveWideAreaDiscoveryDomain, writeWideAreaGatewayZone } from "../infra/widearea-dns.js"; import { formatBonjourInstanceName, resolveBonjourCliPath, @@ -13,6 +13,7 @@ export async function startGatewayDiscovery(params: { gatewayTls?: { enabled: boolean; fingerprintSha256?: string }; canvasPort?: number; wideAreaDiscoveryEnabled: boolean; + wideAreaDiscoveryDomain?: string | null; tailscaleMode: "off" | "serve" | "funnel"; /** mDNS/Bonjour discovery mode (default: minimal). */ mdnsMode?: "off" | "minimal" | "full"; @@ -23,7 +24,7 @@ export async function startGatewayDiscovery(params: { // mDNS can be disabled via config (mdnsMode: off) or env var. const bonjourEnabled = mdnsMode !== "off" && - process.env.CLAWDBOT_DISABLE_BONJOUR !== "1" && + process.env.OPENCLAW_DISABLE_BONJOUR !== "1" && process.env.NODE_ENV !== "test" && !process.env.VITEST; const mdnsMinimal = mdnsMode !== "full"; @@ -32,7 +33,7 @@ export async function startGatewayDiscovery(params: { const tailnetDns = needsTailnetDns ? await resolveTailnetDnsHint({ enabled: tailscaleEnabled }) : undefined; - const sshPortEnv = mdnsMinimal ? undefined : process.env.CLAWDBOT_SSH_PORT?.trim(); + const sshPortEnv = mdnsMinimal ? undefined : process.env.OPENCLAW_SSH_PORT?.trim(); const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN; const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0 ? sshPortParsed : undefined; const cliPath = mdnsMinimal ? undefined : resolveBonjourCliPath(); @@ -57,6 +58,15 @@ export async function startGatewayDiscovery(params: { } if (params.wideAreaDiscoveryEnabled) { + const wideAreaDomain = resolveWideAreaDiscoveryDomain({ + configDomain: params.wideAreaDiscoveryDomain ?? undefined, + }); + if (!wideAreaDomain) { + params.logDiscovery.warn( + "discovery.wideArea.enabled is true, but no domain was configured; set discovery.wideArea.domain to enable unicast DNS-SD", + ); + return { bonjourStop }; + } const tailnetIPv4 = pickPrimaryTailnetIPv4(); if (!tailnetIPv4) { params.logDiscovery.warn( @@ -66,6 +76,7 @@ export async function startGatewayDiscovery(params: { try { const tailnetIPv6 = pickPrimaryTailnetIPv6(); const result = await writeWideAreaGatewayZone({ + domain: wideAreaDomain, gatewayPort: params.port, displayName: formatBonjourInstanceName(params.machineDisplayName), tailnetIPv4, @@ -77,7 +88,7 @@ export async function startGatewayDiscovery(params: { cliPath: resolveBonjourCliPath(), }); params.logDiscovery.info( - `wide-area DNS-SD ${result.changed ? "updated" : "unchanged"} (${WIDE_AREA_DISCOVERY_DOMAIN} → ${result.zonePath})`, + `wide-area DNS-SD ${result.changed ? "updated" : "unchanged"} (${wideAreaDomain} → ${result.zonePath})`, ); } catch (err) { params.logDiscovery.warn(`wide-area discovery update failed: ${String(err)}`); diff --git a/src/gateway/server-discovery.test.ts b/src/gateway/server-discovery.test.ts index 3b7a62b90..7f0ce113e 100644 --- a/src/gateway/server-discovery.test.ts +++ b/src/gateway/server-discovery.test.ts @@ -10,21 +10,21 @@ describe("resolveTailnetDnsHint", () => { const prevTailnetDns = { value: undefined as string | undefined }; beforeEach(() => { - prevTailnetDns.value = process.env.CLAWDBOT_TAILNET_DNS; - delete process.env.CLAWDBOT_TAILNET_DNS; + prevTailnetDns.value = process.env.OPENCLAW_TAILNET_DNS; + delete process.env.OPENCLAW_TAILNET_DNS; getTailnetHostname.mockReset(); }); afterEach(() => { if (prevTailnetDns.value === undefined) { - delete process.env.CLAWDBOT_TAILNET_DNS; + delete process.env.OPENCLAW_TAILNET_DNS; } else { - process.env.CLAWDBOT_TAILNET_DNS = prevTailnetDns.value; + process.env.OPENCLAW_TAILNET_DNS = prevTailnetDns.value; } }); test("returns env hint when disabled", async () => { - process.env.CLAWDBOT_TAILNET_DNS = "studio.tailnet.ts.net."; + process.env.OPENCLAW_TAILNET_DNS = "studio.tailnet.ts.net."; const value = await resolveTailnetDnsHint({ enabled: false }); expect(value).toBe("studio.tailnet.ts.net"); expect(getTailnetHostname).not.toHaveBeenCalled(); diff --git a/src/gateway/server-discovery.ts b/src/gateway/server-discovery.ts index 94fd3f2d3..98554d81d 100644 --- a/src/gateway/server-discovery.ts +++ b/src/gateway/server-discovery.ts @@ -13,15 +13,21 @@ export type ResolveBonjourCliPathOptions = { export function formatBonjourInstanceName(displayName: string) { const trimmed = displayName.trim(); - if (!trimmed) return "Moltbot"; - if (/moltbot/i.test(trimmed)) return trimmed; - return `${trimmed} (Moltbot)`; + if (!trimmed) { + return "OpenClaw"; + } + if (/openclaw/i.test(trimmed)) { + return trimmed; + } + return `${trimmed} (OpenClaw)`; } export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}): string | undefined { const env = opts.env ?? process.env; - const envPath = env.CLAWDBOT_CLI_PATH?.trim(); - if (envPath) return envPath; + const envPath = env.OPENCLAW_CLI_PATH?.trim(); + if (envPath) { + return envPath; + } const statSync = opts.statSync ?? fs.statSync; const isFile = (candidate: string) => { @@ -34,8 +40,10 @@ export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}): const execPath = opts.execPath ?? process.execPath; const execDir = path.dirname(execPath); - const siblingCli = path.join(execDir, "moltbot"); - if (isFile(siblingCli)) return siblingCli; + const siblingCli = path.join(execDir, "openclaw"); + if (isFile(siblingCli)) { + return siblingCli; + } const argv = opts.argv ?? process.argv; const argvPath = argv[1]; @@ -45,9 +53,13 @@ export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}): const cwd = opts.cwd ?? process.cwd(); const distCli = path.join(cwd, "dist", "index.js"); - if (isFile(distCli)) return distCli; - const binCli = path.join(cwd, "bin", "moltbot.js"); - if (isFile(binCli)) return binCli; + if (isFile(distCli)) { + return distCli; + } + const binCli = path.join(cwd, "bin", "openclaw"); + if (isFile(binCli)) { + return binCli; + } return undefined; } @@ -58,10 +70,14 @@ export async function resolveTailnetDnsHint(opts?: { enabled?: boolean; }): Promise { const env = opts?.env ?? process.env; - const envRaw = env.CLAWDBOT_TAILNET_DNS?.trim(); + const envRaw = env.OPENCLAW_TAILNET_DNS?.trim(); const envValue = envRaw && envRaw.length > 0 ? envRaw.replace(/\.$/, "") : ""; - if (envValue) return envValue; - if (opts?.enabled === false) return undefined; + if (envValue) { + return envValue; + } + if (opts?.enabled === false) { + return undefined; + } const exec = opts?.exec ?? diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index f08dc811c..d2dcdef95 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -1,3 +1,5 @@ +import type { TlsOptions } from "node:tls"; +import type { WebSocketServer } from "ws"; import { createServer as createHttpServer, type Server as HttpServer, @@ -5,15 +7,14 @@ import { type ServerResponse, } from "node:http"; import { createServer as createHttpsServer } from "node:https"; -import type { TlsOptions } from "node:tls"; -import type { WebSocketServer } from "ws"; -import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js"; import type { CanvasHostHandler } from "../canvas-host/server.js"; -import { loadConfig } from "../config/config.js"; import type { createSubsystemLogger } from "../logging/subsystem.js"; -import { handleSlackHttpRequest } from "../slack/http/index.js"; import { resolveAgentAvatar } from "../agents/identity-avatar.js"; +import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js"; +import { loadConfig } from "../config/config.js"; +import { handleSlackHttpRequest } from "../slack/http/index.js"; import { handleControlUiAvatarRequest, handleControlUiHttpRequest } from "./control-ui.js"; +import { applyHookMappings } from "./hooks-mapping.js"; import { extractHookToken, getHookChannelError, @@ -26,7 +27,6 @@ import { resolveHookChannel, resolveHookDeliver, } from "./hooks.js"; -import { applyHookMappings } from "./hooks-mapping.js"; import { handleOpenAiHttpRequest } from "./openai-http.js"; import { handleOpenResponsesHttpRequest } from "./openresponses-http.js"; import { handleToolsInvokeHttpRequest } from "./tools-invoke-http.js"; @@ -69,7 +69,9 @@ export function createHooksRequestHandler( const { getHooksConfig, bindHost, port, logHooks, dispatchAgentHook, dispatchWakeHook } = opts; return async (req, res) => { const hooksConfig = getHooksConfig(); - if (!hooksConfig) return false; + if (!hooksConfig) { + return false; + } const url = new URL(req.url ?? "/", `http://${bindHost}:${port}`); const basePath = hooksConfig.basePath; if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) { @@ -87,7 +89,7 @@ export function createHooksRequestHandler( logHooks.warn( "Hook token provided via query parameter is deprecated for security reasons. " + "Tokens in URLs appear in logs, browser history, and referrer headers. " + - "Use Authorization: Bearer or X-Moltbot-Token header instead.", + "Use Authorization: Bearer or X-OpenClaw-Token header instead.", ); } @@ -233,21 +235,30 @@ export function createGatewayHttpServer(opts: { async function handleRequest(req: IncomingMessage, res: ServerResponse) { // Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event. - if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") return; + if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") { + return; + } try { const configSnapshot = loadConfig(); const trustedProxies = configSnapshot.gateway?.trustedProxies ?? []; - if (await handleHooksRequest(req, res)) return; + if (await handleHooksRequest(req, res)) { + return; + } if ( await handleToolsInvokeHttpRequest(req, res, { auth: resolvedAuth, trustedProxies, }) - ) + ) { return; - if (await handleSlackHttpRequest(req, res)) return; - if (handlePluginRequest && (await handlePluginRequest(req, res))) return; + } + if (await handleSlackHttpRequest(req, res)) { + return; + } + if (handlePluginRequest && (await handlePluginRequest(req, res))) { + return; + } if (openResponsesEnabled) { if ( await handleOpenResponsesHttpRequest(req, res, { @@ -255,8 +266,9 @@ export function createGatewayHttpServer(opts: { config: openResponsesConfig, trustedProxies, }) - ) + ) { return; + } } if (openAiChatCompletionsEnabled) { if ( @@ -264,12 +276,17 @@ export function createGatewayHttpServer(opts: { auth: resolvedAuth, trustedProxies, }) - ) + ) { return; + } } if (canvasHost) { - if (await handleA2uiHttpRequest(req, res)) return; - if (await canvasHost.handleHttpRequest(req, res)) return; + if (await handleA2uiHttpRequest(req, res)) { + return; + } + if (await canvasHost.handleHttpRequest(req, res)) { + return; + } } if (controlUiEnabled) { if ( @@ -277,15 +294,17 @@ export function createGatewayHttpServer(opts: { basePath: controlUiBasePath, resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId), }) - ) + ) { return; + } if ( handleControlUiHttpRequest(req, res, { basePath: controlUiBasePath, config: configSnapshot, }) - ) + ) { return; + } } res.statusCode = 404; @@ -308,7 +327,9 @@ export function attachGatewayUpgradeHandler(opts: { }) { const { httpServer, wss, canvasHost } = opts; httpServer.on("upgrade", (req, socket, head) => { - if (canvasHost?.handleUpgrade(req, socket, head)) return; + if (canvasHost?.handleUpgrade(req, socket, head)) { + return; + } wss.handleUpgrade(req, socket, head, (ws) => { wss.emit("connection", ws, req); }); diff --git a/src/gateway/server-maintenance.ts b/src/gateway/server-maintenance.ts index 499521b84..898e8ef74 100644 --- a/src/gateway/server-maintenance.ts +++ b/src/gateway/server-maintenance.ts @@ -1,15 +1,15 @@ import type { HealthSummary } from "../commands/health.js"; -import { abortChatRunById, type ChatAbortControllerEntry } from "./chat-abort.js"; -import { setBroadcastHealthUpdate } from "./server/health-state.js"; import type { ChatRunEntry } from "./server-chat.js"; +import type { DedupeEntry } from "./server-shared.js"; +import { abortChatRunById, type ChatAbortControllerEntry } from "./chat-abort.js"; import { DEDUPE_MAX, DEDUPE_TTL_MS, HEALTH_REFRESH_INTERVAL_MS, TICK_INTERVAL_MS, } from "./server-constants.js"; -import type { DedupeEntry } from "./server-shared.js"; import { formatError } from "./server-utils.js"; +import { setBroadcastHealthUpdate } from "./server/health-state.js"; export function startGatewayMaintenanceTimers(params: { broadcast: ( @@ -75,17 +75,21 @@ export function startGatewayMaintenanceTimers(params: { const dedupeCleanup = setInterval(() => { const now = Date.now(); for (const [k, v] of params.dedupe) { - if (now - v.ts > DEDUPE_TTL_MS) params.dedupe.delete(k); + if (now - v.ts > DEDUPE_TTL_MS) { + params.dedupe.delete(k); + } } if (params.dedupe.size > DEDUPE_MAX) { - const entries = [...params.dedupe.entries()].sort((a, b) => a[1].ts - b[1].ts); + const entries = [...params.dedupe.entries()].toSorted((a, b) => a[1].ts - b[1].ts); for (let i = 0; i < params.dedupe.size - DEDUPE_MAX; i++) { params.dedupe.delete(entries[i][0]); } } for (const [runId, entry] of params.chatAbortControllers) { - if (now <= entry.expiresAtMs) continue; + if (now <= entry.expiresAtMs) { + continue; + } abortChatRunById( { chatAbortControllers: params.chatAbortControllers, @@ -103,7 +107,9 @@ export function startGatewayMaintenanceTimers(params: { const ABORTED_RUN_TTL_MS = 60 * 60_000; for (const [runId, abortedAt] of params.chatRunState.abortedRuns) { - if (now - abortedAt <= ABORTED_RUN_TTL_MS) continue; + if (now - abortedAt <= ABORTED_RUN_TTL_MS) { + continue; + } params.chatRunState.abortedRuns.delete(runId); params.chatRunBuffers.delete(runId); params.chatDeltaSentAt.delete(runId); diff --git a/src/gateway/server-methods.ts b/src/gateway/server-methods.ts index 66492b976..f76a637fa 100644 --- a/src/gateway/server-methods.ts +++ b/src/gateway/server-methods.ts @@ -1,3 +1,4 @@ +import type { GatewayRequestHandlers, GatewayRequestOptions } from "./server-methods/types.js"; import { ErrorCodes, errorShape } from "./protocol/index.js"; import { agentHandlers } from "./server-methods/agent.js"; import { agentsHandlers } from "./server-methods/agents.js"; @@ -19,7 +20,6 @@ import { skillsHandlers } from "./server-methods/skills.js"; import { systemHandlers } from "./server-methods/system.js"; import { talkHandlers } from "./server-methods/talk.js"; import { ttsHandlers } from "./server-methods/tts.js"; -import type { GatewayRequestHandlers, GatewayRequestOptions } from "./server-methods/types.js"; import { updateHandlers } from "./server-methods/update.js"; import { usageHandlers } from "./server-methods/usage.js"; import { voicewakeHandlers } from "./server-methods/voicewake.js"; @@ -91,11 +91,15 @@ const WRITE_METHODS = new Set([ ]); function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["client"]) { - if (!client?.connect) return null; + if (!client?.connect) { + return null; + } const role = client.connect.role ?? "operator"; const scopes = client.connect.scopes ?? []; if (NODE_ROLE_METHODS.has(method)) { - if (role === "node") return null; + if (role === "node") { + return null; + } return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`); } if (role === "node") { @@ -104,7 +108,9 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c if (role !== "operator") { return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`); } - if (scopes.includes(ADMIN_SCOPE)) return null; + if (scopes.includes(ADMIN_SCOPE)) { + return null; + } if (APPROVAL_METHODS.has(method) && !scopes.includes(APPROVALS_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.approvals"); } @@ -117,10 +123,18 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c if (WRITE_METHODS.has(method) && !scopes.includes(WRITE_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.write"); } - if (APPROVAL_METHODS.has(method)) return null; - if (PAIRING_METHODS.has(method)) return null; - if (READ_METHODS.has(method)) return null; - if (WRITE_METHODS.has(method)) return null; + if (APPROVAL_METHODS.has(method)) { + return null; + } + if (PAIRING_METHODS.has(method)) { + return null; + } + if (READ_METHODS.has(method)) { + return null; + } + if (WRITE_METHODS.has(method)) { + return null; + } if (ADMIN_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix))) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin"); } diff --git a/src/gateway/server-methods/agent-job.ts b/src/gateway/server-methods/agent-job.ts index 2d819c944..872d88722 100644 --- a/src/gateway/server-methods/agent-job.ts +++ b/src/gateway/server-methods/agent-job.ts @@ -28,26 +28,30 @@ function recordAgentRunSnapshot(entry: AgentRunSnapshot) { } function ensureAgentRunListener() { - if (agentRunListenerStarted) return; + if (agentRunListenerStarted) { + return; + } agentRunListenerStarted = true; onAgentEvent((evt) => { - if (!evt) return; - if (evt.stream !== "lifecycle") return; + if (!evt) { + return; + } + if (evt.stream !== "lifecycle") { + return; + } const phase = evt.data?.phase; if (phase === "start") { - const startedAt = - typeof evt.data?.startedAt === "number" ? (evt.data.startedAt as number) : undefined; + const startedAt = typeof evt.data?.startedAt === "number" ? evt.data.startedAt : undefined; agentRunStarts.set(evt.runId, startedAt ?? Date.now()); return; } - if (phase !== "end" && phase !== "error") return; + if (phase !== "end" && phase !== "error") { + return; + } const startedAt = - typeof evt.data?.startedAt === "number" - ? (evt.data.startedAt as number) - : agentRunStarts.get(evt.runId); - const endedAt = - typeof evt.data?.endedAt === "number" ? (evt.data.endedAt as number) : undefined; - const error = typeof evt.data?.error === "string" ? (evt.data.error as string) : undefined; + typeof evt.data?.startedAt === "number" ? evt.data.startedAt : agentRunStarts.get(evt.runId); + const endedAt = typeof evt.data?.endedAt === "number" ? evt.data.endedAt : undefined; + const error = typeof evt.data?.error === "string" ? evt.data.error : undefined; agentRunStarts.delete(evt.runId); recordAgentRunSnapshot({ runId: evt.runId, @@ -72,23 +76,35 @@ export async function waitForAgentJob(params: { const { runId, timeoutMs } = params; ensureAgentRunListener(); const cached = getCachedAgentRun(runId); - if (cached) return cached; - if (timeoutMs <= 0) return null; + if (cached) { + return cached; + } + if (timeoutMs <= 0) { + return null; + } return await new Promise((resolve) => { let settled = false; const finish = (entry: AgentRunSnapshot | null) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); unsubscribe(); resolve(entry); }; const unsubscribe = onAgentEvent((evt) => { - if (!evt || evt.stream !== "lifecycle") return; - if (evt.runId !== runId) return; + if (!evt || evt.stream !== "lifecycle") { + return; + } + if (evt.runId !== runId) { + return; + } const phase = evt.data?.phase; - if (phase !== "end" && phase !== "error") return; + if (phase !== "end" && phase !== "error") { + return; + } const cached = getCachedAgentRun(runId); if (cached) { finish(cached); @@ -96,11 +112,10 @@ export async function waitForAgentJob(params: { } const startedAt = typeof evt.data?.startedAt === "number" - ? (evt.data.startedAt as number) + ? evt.data.startedAt : agentRunStarts.get(evt.runId); - const endedAt = - typeof evt.data?.endedAt === "number" ? (evt.data.endedAt as number) : undefined; - const error = typeof evt.data?.error === "string" ? (evt.data.error as string) : undefined; + const endedAt = typeof evt.data?.endedAt === "number" ? evt.data.endedAt : undefined; + const error = typeof evt.data?.error === "string" ? evt.data.error : undefined; const snapshot: AgentRunSnapshot = { runId: evt.runId, status: phase === "error" ? "error" : "ok", diff --git a/src/gateway/server-methods/agent-timestamp.test.ts b/src/gateway/server-methods/agent-timestamp.test.ts new file mode 100644 index 000000000..1b291ca8f --- /dev/null +++ b/src/gateway/server-methods/agent-timestamp.test.ts @@ -0,0 +1,141 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { formatZonedTimestamp } from "../../auto-reply/envelope.js"; +import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; + +describe("injectTimestamp", () => { + beforeEach(() => { + vi.useFakeTimers(); + // Wednesday, January 28, 2026 at 8:30 PM EST (01:30 UTC Jan 29) + vi.setSystemTime(new Date("2026-01-29T01:30:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("prepends a compact timestamp matching formatZonedTimestamp", () => { + const result = injectTimestamp("Is it the weekend?", { + timezone: "America/New_York", + }); + + expect(result).toMatch(/^\[Wed 2026-01-28 20:30 EST\] Is it the weekend\?$/); + }); + + it("uses channel envelope format with DOW prefix", () => { + const now = new Date(); + const expected = formatZonedTimestamp(now, "America/New_York"); + + const result = injectTimestamp("hello", { timezone: "America/New_York" }); + + // DOW prefix + formatZonedTimestamp format + expect(result).toBe(`[Wed ${expected}] hello`); + }); + + it("always uses 24-hour format", () => { + const result = injectTimestamp("hello", { timezone: "America/New_York" }); + + expect(result).toContain("20:30"); + expect(result).not.toContain("PM"); + expect(result).not.toContain("AM"); + }); + + it("uses the configured timezone", () => { + const result = injectTimestamp("hello", { timezone: "America/Chicago" }); + + // 8:30 PM EST = 7:30 PM CST = 19:30 + expect(result).toMatch(/^\[Wed 2026-01-28 19:30 CST\]/); + }); + + it("defaults to UTC when no timezone specified", () => { + const result = injectTimestamp("hello", {}); + + // 2026-01-29T01:30:00Z + expect(result).toMatch(/^\[Thu 2026-01-29 01:30/); + }); + + it("returns empty/whitespace messages unchanged", () => { + expect(injectTimestamp("", { timezone: "UTC" })).toBe(""); + expect(injectTimestamp(" ", { timezone: "UTC" })).toBe(" "); + }); + + it("does NOT double-stamp messages with channel envelope timestamps", () => { + const enveloped = "[Discord user1 2026-01-28 20:30 EST] hello there"; + const result = injectTimestamp(enveloped, { timezone: "America/New_York" }); + + expect(result).toBe(enveloped); + }); + + it("does NOT double-stamp messages already injected by us", () => { + const alreadyStamped = "[Wed 2026-01-28 20:30 EST] hello there"; + const result = injectTimestamp(alreadyStamped, { timezone: "America/New_York" }); + + expect(result).toBe(alreadyStamped); + }); + + it("does NOT double-stamp messages with cron-injected timestamps", () => { + const cronMessage = + "[cron:abc123 my-job] do the thing\nCurrent time: Wednesday, January 28th, 2026 — 8:30 PM (America/New_York)"; + const result = injectTimestamp(cronMessage, { timezone: "America/New_York" }); + + expect(result).toBe(cronMessage); + }); + + it("handles midnight correctly", () => { + vi.setSystemTime(new Date("2026-02-01T05:00:00.000Z")); // midnight EST + + const result = injectTimestamp("hello", { timezone: "America/New_York" }); + + expect(result).toMatch(/^\[Sun 2026-02-01 00:00 EST\]/); + }); + + it("handles date boundaries (just before midnight)", () => { + vi.setSystemTime(new Date("2026-02-01T04:59:00.000Z")); // 23:59 Jan 31 EST + + const result = injectTimestamp("hello", { timezone: "America/New_York" }); + + expect(result).toMatch(/^\[Sat 2026-01-31 23:59 EST\]/); + }); + + it("handles DST correctly (same UTC hour, different local time)", () => { + // EST (winter): UTC-5 → 2026-01-15T05:00Z = midnight Jan 15 + vi.setSystemTime(new Date("2026-01-15T05:00:00.000Z")); + const winter = injectTimestamp("winter", { timezone: "America/New_York" }); + expect(winter).toMatch(/^\[Thu 2026-01-15 00:00 EST\]/); + + // EDT (summer): UTC-4 → 2026-07-15T04:00Z = midnight Jul 15 + vi.setSystemTime(new Date("2026-07-15T04:00:00.000Z")); + const summer = injectTimestamp("summer", { timezone: "America/New_York" }); + expect(summer).toMatch(/^\[Wed 2026-07-15 00:00 EDT\]/); + }); + + it("accepts a custom now date", () => { + const customDate = new Date("2025-07-04T16:00:00.000Z"); // July 4, noon ET + + const result = injectTimestamp("fireworks?", { + timezone: "America/New_York", + now: customDate, + }); + + expect(result).toMatch(/^\[Fri 2025-07-04 12:00 EDT\]/); + }); +}); + +describe("timestampOptsFromConfig", () => { + it("extracts timezone from config", () => { + const opts = timestampOptsFromConfig({ + agents: { + defaults: { + userTimezone: "America/Chicago", + }, + }, + } as any); + + expect(opts.timezone).toBe("America/Chicago"); + }); + + it("falls back gracefully with empty config", () => { + const opts = timestampOptsFromConfig({} as any); + + expect(opts.timezone).toBeDefined(); // resolveUserTimezone provides a default + }); +}); diff --git a/src/gateway/server-methods/agent-timestamp.ts b/src/gateway/server-methods/agent-timestamp.ts new file mode 100644 index 000000000..715262de2 --- /dev/null +++ b/src/gateway/server-methods/agent-timestamp.ts @@ -0,0 +1,80 @@ +import type { OpenClawConfig } from "../../config/types.js"; +import { resolveUserTimezone } from "../../agents/date-time.js"; +import { formatZonedTimestamp } from "../../auto-reply/envelope.js"; + +/** + * Cron jobs inject "Current time: ..." into their messages. + * Skip injection for those. + */ +const CRON_TIME_PATTERN = /Current time: /; + +/** + * Matches a leading `[... YYYY-MM-DD HH:MM ...]` envelope — either from + * channel plugins or from a previous injection. Uses the same YYYY-MM-DD + * HH:MM format as {@link formatZonedTimestamp}, so detection stays in sync + * with the formatting. + */ +const TIMESTAMP_ENVELOPE_PATTERN = /^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/; + +export interface TimestampInjectionOptions { + timezone?: string; + now?: Date; +} + +/** + * Injects a compact timestamp prefix into a message if one isn't already + * present. Uses the same `YYYY-MM-DD HH:MM TZ` format as channel envelope + * timestamps ({@link formatZonedTimestamp}), keeping token cost low (~7 + * tokens) and format consistent across all agent contexts. + * + * Used by the gateway `agent` and `chat.send` handlers to give TUI, web, + * spawned subagents, `sessions_send`, and heartbeat wake events date/time + * awareness — without modifying the system prompt (which is cached). + * + * Channel messages (Discord, Telegram, etc.) already have timestamps via + * envelope formatting and take a separate code path — they never reach + * these handlers, so there is no double-stamping risk. The detection + * pattern is a safety net for edge cases. + * + * @see https://github.com/moltbot/moltbot/issues/3658 + */ +export function injectTimestamp(message: string, opts?: TimestampInjectionOptions): string { + if (!message.trim()) { + return message; + } + + // Already has an envelope or injected timestamp + if (TIMESTAMP_ENVELOPE_PATTERN.test(message)) { + return message; + } + + // Already has a cron-injected timestamp + if (CRON_TIME_PATTERN.test(message)) { + return message; + } + + const now = opts?.now ?? new Date(); + const timezone = opts?.timezone ?? "UTC"; + + const formatted = formatZonedTimestamp(now, timezone); + if (!formatted) { + return message; + } + + // 3-letter DOW: small models (8B) can't reliably derive day-of-week from + // a date, and may treat a bare "Wed" as a typo. Costs ~1 token. + const dow = new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "short" }).format( + now, + ); + + return `[${dow} ${formatted}] ${message}`; +} + +/** + * Build TimestampInjectionOptions from an OpenClawConfig. + */ +export function timestampOptsFromConfig(cfg: OpenClawConfig): TimestampInjectionOptions { + return { + timezone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone), + }; +} diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 149ab4a67..797309d21 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import type { GatewayRequestContext } from "./types.js"; import { agentHandlers } from "./agent.js"; @@ -8,6 +7,7 @@ const mocks = vi.hoisted(() => ({ updateSessionStore: vi.fn(), agentCommand: vi.fn(), registerAgentRunContext: vi.fn(), + loadConfigReturn: {} as Record, })); vi.mock("../session-utils.js", () => ({ @@ -32,7 +32,7 @@ vi.mock("../../commands/agent.js", () => ({ })); vi.mock("../../config/config.js", () => ({ - loadConfig: () => ({}), + loadConfig: () => mocks.loadConfigReturn, })); vi.mock("../../agents/agent-scope.js", () => ({ @@ -115,6 +115,59 @@ describe("gateway agent handler", () => { expect(capturedEntry?.claudeCliSessionId).toBe(existingClaudeCliSessionId); }); + it("injects a timestamp into the message passed to agentCommand", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-29T01:30:00.000Z")); // Wed Jan 28, 8:30 PM EST + mocks.agentCommand.mockReset(); + + mocks.loadConfigReturn = { + agents: { + defaults: { + userTimezone: "America/New_York", + }, + }, + }; + + mocks.loadSessionEntry.mockReturnValue({ + cfg: mocks.loadConfigReturn, + storePath: "/tmp/sessions.json", + entry: { + sessionId: "existing-session-id", + updatedAt: Date.now(), + }, + canonicalKey: "agent:main:main", + }); + mocks.updateSessionStore.mockResolvedValue(undefined); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + const respond = vi.fn(); + await agentHandlers.agent({ + params: { + message: "Is it the weekend?", + agentId: "main", + sessionKey: "agent:main:main", + idempotencyKey: "test-timestamp-inject", + }, + respond, + context: makeContext(), + req: { type: "req", id: "ts-1", method: "agent" }, + client: null, + isWebchatConnect: () => false, + }); + + // Wait for the async agentCommand call + await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); + + const callArgs = mocks.agentCommand.mock.calls[0][0]; + expect(callArgs.message).toBe("[Wed 2026-01-28 20:30 EST] Is it the weekend?"); + + mocks.loadConfigReturn = {}; + vi.useRealTimers(); + }); + it("handles missing cliSessionIds gracefully", async () => { mocks.loadSessionEntry.mockReturnValue({ cfg: {}, diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index d159d1f78..8377699cc 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; -import { agentCommand } from "../../commands/agent.js"; +import type { GatewayRequestHandlers } from "./types.js"; import { listAgentIds } from "../../agents/agent-scope.js"; +import { agentCommand } from "../../commands/agent.js"; import { loadConfig } from "../../config/config.js"; import { resolveAgentIdFromSessionKey, @@ -14,6 +15,7 @@ import { resolveAgentDeliveryPlan, resolveAgentOutboundTarget, } from "../../infra/outbound/agent-delivery.js"; +import { normalizeAgentId } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js"; @@ -23,11 +25,10 @@ import { isGatewayMessageChannel, normalizeMessageChannel, } from "../../utils/message-channel.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; +import { resolveAssistantIdentity } from "../assistant-identity.js"; import { parseMessageWithAttachments } from "../chat-attachments.js"; +import { resolveAssistantAvatarUrl } from "../control-ui-shared.js"; import { - type AgentIdentityParams, - type AgentWaitParams, ErrorCodes, errorShape, formatValidationErrors, @@ -37,14 +38,12 @@ import { } from "../protocol/index.js"; import { loadSessionEntry } from "../session-utils.js"; import { formatForLog } from "../ws-log.js"; -import { resolveAssistantIdentity } from "../assistant-identity.js"; -import { resolveAssistantAvatarUrl } from "../control-ui-shared.js"; import { waitForAgentJob } from "./agent-job.js"; -import type { GatewayRequestHandlers } from "./types.js"; +import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; export const agentHandlers: GatewayRequestHandlers = { agent: async ({ params, respond, context }) => { - const p = params as Record; + const p = params; if (!validateAgentParams(p)) { respond( false, @@ -138,6 +137,13 @@ export const agentHandlers: GatewayRequestHandlers = { return; } } + + // Inject timestamp into messages that don't already have one. + // Channel messages (Discord, Telegram, etc.) get timestamps via envelope + // formatting in a separate code path — they never reach this handler. + // See: https://github.com/moltbot/moltbot/issues/3658 + message = injectTimestamp(message, timestampOptsFromConfig(cfg)); + const isKnownGatewayChannel = (value: string): boolean => isGatewayMessageChannel(value); const channelHints = [request.channel, request.replyChannel] .filter((value): value is string => typeof value === "string") @@ -430,7 +436,7 @@ export const agentHandlers: GatewayRequestHandlers = { ); return; } - const p = params as AgentIdentityParams; + const p = params; const agentIdRaw = typeof p.agentId === "string" ? p.agentId.trim() : ""; const sessionKeyRaw = typeof p.sessionKey === "string" ? p.sessionKey.trim() : ""; let agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : undefined; @@ -471,7 +477,7 @@ export const agentHandlers: GatewayRequestHandlers = { ); return; } - const p = params as AgentWaitParams; + const p = params; const runId = p.runId.trim(); const timeoutMs = typeof p.timeoutMs === "number" && Number.isFinite(p.timeoutMs) diff --git a/src/gateway/server-methods/agents.ts b/src/gateway/server-methods/agents.ts index 444efbe9d..a700e495e 100644 --- a/src/gateway/server-methods/agents.ts +++ b/src/gateway/server-methods/agents.ts @@ -1,3 +1,4 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { loadConfig } from "../../config/config.js"; import { ErrorCodes, @@ -6,7 +7,6 @@ import { validateAgentsListParams, } from "../protocol/index.js"; import { listAgentsForGateway } from "../session-utils.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const agentsHandlers: GatewayRequestHandlers = { "agents.list": ({ params, respond }) => { diff --git a/src/gateway/server-methods/browser.ts b/src/gateway/server-methods/browser.ts index 16811fbcf..42e53e859 100644 --- a/src/gateway/server-methods/browser.ts +++ b/src/gateway/server-methods/browser.ts @@ -1,4 +1,6 @@ import crypto from "node:crypto"; +import type { NodeSession } from "../node-registry.js"; +import type { GatewayRequestHandlers } from "./types.js"; import { createBrowserControlContext, startBrowserControlServiceFromConfig, @@ -7,10 +9,8 @@ import { createBrowserRouteDispatcher } from "../../browser/routes/dispatcher.js import { loadConfig } from "../../config/config.js"; import { saveMediaBuffer } from "../../media/store.js"; import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js"; -import type { NodeSession } from "../node-registry.js"; import { ErrorCodes, errorShape } from "../protocol/index.js"; import { safeParseJson } from "./nodes.helpers.js"; -import type { GatewayRequestHandlers } from "./types.js"; type BrowserRequestParams = { method?: string; @@ -46,18 +46,32 @@ function normalizeNodeKey(value: string) { function resolveBrowserNode(nodes: NodeSession[], query: string): NodeSession | null { const q = query.trim(); - if (!q) return null; + if (!q) { + return null; + } const qNorm = normalizeNodeKey(q); const matches = nodes.filter((node) => { - if (node.nodeId === q) return true; - if (typeof node.remoteIp === "string" && node.remoteIp === q) return true; + if (node.nodeId === q) { + return true; + } + if (typeof node.remoteIp === "string" && node.remoteIp === q) { + return true; + } const name = typeof node.displayName === "string" ? node.displayName : ""; - if (name && normalizeNodeKey(name) === qNorm) return true; - if (q.length >= 6 && node.nodeId.startsWith(q)) return true; + if (name && normalizeNodeKey(name) === qNorm) { + return true; + } + if (q.length >= 6 && node.nodeId.startsWith(q)) { + return true; + } return false; }); - if (matches.length === 1) return matches[0] ?? null; - if (matches.length === 0) return null; + if (matches.length === 1) { + return matches[0] ?? null; + } + if (matches.length === 0) { + return null; + } throw new Error( `ambiguous node: ${q} (matches: ${matches .map((node) => node.displayName || node.remoteIp || node.nodeId) @@ -71,7 +85,9 @@ function resolveBrowserNodeTarget(params: { }): NodeSession | null { const policy = params.cfg.gateway?.nodes?.browser; const mode = policy?.mode ?? "auto"; - if (mode === "off") return null; + if (mode === "off") { + return null; + } const browserNodes = params.nodes.filter((node) => isBrowserNode(node)); if (browserNodes.length === 0) { if (policy?.node?.trim()) { @@ -87,13 +103,19 @@ function resolveBrowserNodeTarget(params: { } return resolved; } - if (mode === "manual") return null; - if (browserNodes.length === 1) return browserNodes[0] ?? null; + if (mode === "manual") { + return null; + } + if (browserNodes.length === 1) { + return browserNodes[0] ?? null; + } return null; } async function persistProxyFiles(files: BrowserProxyFile[] | undefined) { - if (!files || files.length === 0) return new Map(); + if (!files || files.length === 0) { + return new Map(); + } const mapping = new Map(); for (const file of files) { const buffer = Buffer.from(file.base64, "base64"); @@ -104,7 +126,9 @@ async function persistProxyFiles(files: BrowserProxyFile[] | undefined) { } function applyProxyPaths(result: unknown, mapping: Map) { - if (!result || typeof result !== "object") return; + if (!result || typeof result !== "object") { + return; + } const obj = result as Record; if (typeof obj.path === "string" && mapping.has(obj.path)) { obj.path = mapping.get(obj.path); diff --git a/src/gateway/server-methods/channels.ts b/src/gateway/server-methods/channels.ts index 75b1d9777..529fba686 100644 --- a/src/gateway/server-methods/channels.ts +++ b/src/gateway/server-methods/channels.ts @@ -1,3 +1,7 @@ +import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; +import { buildChannelUiCatalog } from "../../channels/plugins/catalog.js"; import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js"; import { type ChannelId, @@ -5,10 +9,7 @@ import { listChannelPlugins, normalizeChannelId, } from "../../channels/plugins/index.js"; -import { buildChannelUiCatalog } from "../../channels/plugins/catalog.js"; import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js"; -import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; import { loadConfig, readConfigFileSnapshot } from "../../config/config.js"; import { getChannelActivity } from "../../infra/channel-activity.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; @@ -21,7 +22,6 @@ import { validateChannelsStatusParams, } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; type ChannelLogoutPayload = { channel: ChannelId; @@ -33,7 +33,7 @@ type ChannelLogoutPayload = { export async function logoutChannelAccount(params: { channelId: ChannelId; accountId?: string | null; - cfg: MoltbotConfig; + cfg: OpenClawConfig; context: GatewayRequestContext; plugin: ChannelPlugin; }): Promise { @@ -98,7 +98,9 @@ export const channelsHandlers: GatewayRequestHandlers = { const defaultRuntime = runtime.channels[channelId]; const raw = accounts?.[accountId] ?? (accountId === defaultAccountId ? defaultRuntime : undefined); - if (!raw) return undefined; + if (!raw) { + return undefined; + } return raw; }; @@ -171,7 +173,9 @@ export const channelsHandlers: GatewayRequestHandlers = { probe: probeResult, audit: auditResult, }); - if (lastProbeAt) snapshot.lastProbeAt = lastProbeAt; + if (lastProbeAt) { + snapshot.lastProbeAt = lastProbeAt; + } const activity = getChannelActivity({ channel: channelId as never, accountId, diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 9010a6f21..ba5347dc3 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -1,8 +1,9 @@ +import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; import { randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; - -import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; +import type { MsgContext } from "../../auto-reply/templating.js"; +import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../agents/identity.js"; import { resolveThinkingDefault } from "../../agents/model-selection.js"; @@ -13,7 +14,6 @@ import { extractShortModelName, type ResponsePrefixContext, } from "../../auto-reply/reply/response-prefix-template.js"; -import type { MsgContext } from "../../auto-reply/templating.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; import { @@ -23,6 +23,7 @@ import { resolveChatRunExpiresAtMs, } from "../chat-abort.js"; import { type ChatImageContent, parseMessageWithAttachments } from "../chat-attachments.js"; +import { stripEnvelopeFromMessages } from "../chat-sanitize.js"; import { ErrorCodes, errorShape, @@ -39,9 +40,8 @@ import { readSessionMessages, resolveSessionModelRef, } from "../session-utils.js"; -import { stripEnvelopeFromMessages } from "../chat-sanitize.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; +import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; type TranscriptAppendResult = { ok: boolean; @@ -56,8 +56,12 @@ function resolveTranscriptPath(params: { sessionFile?: string; }): string | null { const { sessionId, storePath, sessionFile } = params; - if (sessionFile) return sessionFile; - if (!storePath) return null; + if (sessionFile) { + return sessionFile; + } + if (!storePath) { + return null; + } return path.join(path.dirname(storePath), `${sessionId}.jsonl`); } @@ -65,7 +69,9 @@ function ensureTranscriptFile(params: { transcriptPath: string; sessionId: strin ok: boolean; error?: string; } { - if (fs.existsSync(params.transcriptPath)) return { ok: true }; + if (fs.existsSync(params.transcriptPath)) { + return { ok: true }; + } try { fs.mkdirSync(path.dirname(params.transcriptPath), { recursive: true }); const header = { @@ -443,9 +449,14 @@ export const chatHandlers: GatewayRequestHandlers = { ); const commandBody = injectThinking ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage; const clientInfo = client?.connect?.client; + // Inject timestamp so agents know the current date/time. + // Only BodyForAgent gets the timestamp — Body stays raw for UI display. + // See: https://github.com/moltbot/moltbot/issues/3658 + const stampedMessage = injectTimestamp(parsedMessage, timestampOptsFromConfig(cfg)); + const ctx: MsgContext = { Body: parsedMessage, - BodyForAgent: parsedMessage, + BodyForAgent: stampedMessage, BodyForCommands: commandBody, RawBody: parsedMessage, CommandBody: commandBody, @@ -476,9 +487,13 @@ export const chatHandlers: GatewayRequestHandlers = { context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`); }, deliver: async (payload, info) => { - if (info.kind !== "final") return; + if (info.kind !== "final") { + return; + } const text = payload.text?.trim() ?? ""; - if (!text) return; + if (!text) { + return; + } finalReplyParts.push(text); }, }); diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 4f5056295..0ac9bf9ee 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -1,4 +1,6 @@ +import type { GatewayRequestHandlers, RespondFn } from "./types.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; +import { listChannelPlugins } from "../../channels/plugins/index.js"; import { CONFIG_PATH, loadConfig, @@ -11,14 +13,13 @@ import { import { applyLegacyMigrations } from "../../config/legacy.js"; import { applyMergePatch } from "../../config/merge-patch.js"; import { buildConfigSchema } from "../../config/schema.js"; -import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { formatDoctorNonInteractiveHint, type RestartSentinelPayload, writeRestartSentinel, } from "../../infra/restart-sentinel.js"; -import { listChannelPlugins } from "../../channels/plugins/index.js"; -import { loadMoltbotPlugins } from "../../plugins/loader.js"; +import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; +import { loadOpenClawPlugins } from "../../plugins/loader.js"; import { ErrorCodes, errorShape, @@ -29,11 +30,12 @@ import { validateConfigSchemaParams, validateConfigSetParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers, RespondFn } from "./types.js"; function resolveBaseHash(params: unknown): string | null { const raw = (params as { baseHash?: unknown })?.baseHash; - if (typeof raw !== "string") return null; + if (typeof raw !== "string") { + return null; + } const trimmed = raw.trim(); return trimmed ? trimmed : null; } @@ -43,7 +45,9 @@ function requireConfigBaseHash( snapshot: Awaited>, respond: RespondFn, ): boolean { - if (!snapshot.exists) return true; + if (!snapshot.exists) { + return true; + } const snapshotHash = resolveConfigSnapshotHash(snapshot); if (!snapshotHash) { respond( @@ -112,7 +116,7 @@ export const configHandlers: GatewayRequestHandlers = { } const cfg = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); - const pluginRegistry = loadMoltbotPlugins({ + const pluginRegistry = loadOpenClawPlugins({ config: cfg, workspaceDir, logger: { diff --git a/src/gateway/server-methods/connect.ts b/src/gateway/server-methods/connect.ts index 309693782..bd7d70072 100644 --- a/src/gateway/server-methods/connect.ts +++ b/src/gateway/server-methods/connect.ts @@ -1,5 +1,5 @@ -import { ErrorCodes, errorShape } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; +import { ErrorCodes, errorShape } from "../protocol/index.js"; export const connectHandlers: GatewayRequestHandlers = { connect: ({ respond }) => { diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index 4420358b8..82591dd35 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -1,6 +1,7 @@ +import type { CronJobCreate, CronJobPatch } from "../../cron/types.js"; +import type { GatewayRequestHandlers } from "./types.js"; import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js"; import { readCronRunLogEntries, resolveCronRunLogPath } from "../../cron/run-log.js"; -import type { CronJobCreate, CronJobPatch } from "../../cron/types.js"; import { ErrorCodes, errorShape, @@ -14,7 +15,6 @@ import { validateCronUpdateParams, validateWakeParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const cronHandlers: GatewayRequestHandlers = { wake: ({ params, respond, context }) => { @@ -89,7 +89,7 @@ export const cronHandlers: GatewayRequestHandlers = { const normalizedPatch = normalizeCronJobPatch((params as { patch?: unknown } | null)?.patch); const candidate = normalizedPatch && typeof params === "object" && params !== null - ? { ...(params as Record), patch: normalizedPatch } + ? { ...params, patch: normalizedPatch } : params; if (!validateCronUpdateParams(candidate)) { respond( diff --git a/src/gateway/server-methods/devices.ts b/src/gateway/server-methods/devices.ts index ebf7d7f94..b57cfd6d9 100644 --- a/src/gateway/server-methods/devices.ts +++ b/src/gateway/server-methods/devices.ts @@ -1,3 +1,4 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { approveDevicePairing, listDevicePairing, @@ -17,7 +18,6 @@ import { validateDeviceTokenRevokeParams, validateDeviceTokenRotateParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; function redactPairedDevice( device: { tokens?: Record } & Record, diff --git a/src/gateway/server-methods/exec-approval.test.ts b/src/gateway/server-methods/exec-approval.test.ts index 71a63e5a3..0a80b9e9d 100644 --- a/src/gateway/server-methods/exec-approval.test.ts +++ b/src/gateway/server-methods/exec-approval.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { ExecApprovalManager } from "../exec-approval-manager.js"; -import { createExecApprovalHandlers } from "./exec-approval.js"; import { validateExecApprovalRequestParams } from "../protocol/index.js"; +import { createExecApprovalHandlers } from "./exec-approval.js"; const noop = () => {}; @@ -117,7 +117,9 @@ describe("exec approval handlers", () => { const context = { broadcast: (event: string, payload: unknown) => { - if (event !== "exec.approval.requested") return; + if (event !== "exec.approval.requested") { + return; + } const id = (payload as { id?: string })?.id ?? ""; void handlers["exec.approval.resolve"]({ params: { id, decision: "allow-once" }, diff --git a/src/gateway/server-methods/exec-approval.ts b/src/gateway/server-methods/exec-approval.ts index 572afc58f..beb3f0372 100644 --- a/src/gateway/server-methods/exec-approval.ts +++ b/src/gateway/server-methods/exec-approval.ts @@ -1,6 +1,7 @@ -import type { ExecApprovalDecision } from "../../infra/exec-approvals.js"; import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js"; +import type { ExecApprovalDecision } from "../../infra/exec-approvals.js"; import type { ExecApprovalManager } from "../exec-approval-manager.js"; +import type { GatewayRequestHandlers } from "./types.js"; import { ErrorCodes, errorShape, @@ -8,7 +9,6 @@ import { validateExecApprovalRequestParams, validateExecApprovalResolveParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; export function createExecApprovalHandlers( manager: ExecApprovalManager, diff --git a/src/gateway/server-methods/exec-approvals.ts b/src/gateway/server-methods/exec-approvals.ts index 71821b7d0..df0157459 100644 --- a/src/gateway/server-methods/exec-approvals.ts +++ b/src/gateway/server-methods/exec-approvals.ts @@ -1,3 +1,4 @@ +import type { GatewayRequestHandlers, RespondFn } from "./types.js"; import { ensureExecApprovals, normalizeExecApprovals, @@ -17,11 +18,12 @@ import { validateExecApprovalsSetParams, } from "../protocol/index.js"; import { respondUnavailableOnThrow, safeParseJson } from "./nodes.helpers.js"; -import type { GatewayRequestHandlers, RespondFn } from "./types.js"; function resolveBaseHash(params: unknown): string | null { const raw = (params as { baseHash?: unknown })?.baseHash; - if (typeof raw !== "string") return null; + if (typeof raw !== "string") { + return null; + } const trimmed = raw.trim(); return trimmed ? trimmed : null; } @@ -31,7 +33,9 @@ function requireApprovalsBaseHash( snapshot: ExecApprovalsSnapshot, respond: RespondFn, ): boolean { - if (!snapshot.exists) return true; + if (!snapshot.exists) { + return true; + } if (!snapshot.hash) { respond( false, diff --git a/src/gateway/server-methods/health.ts b/src/gateway/server-methods/health.ts index d03a468ee..b4e0ae8ae 100644 --- a/src/gateway/server-methods/health.ts +++ b/src/gateway/server-methods/health.ts @@ -1,9 +1,9 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { getStatusSummary } from "../../commands/status.js"; import { ErrorCodes, errorShape } from "../protocol/index.js"; import { HEALTH_REFRESH_INTERVAL_MS } from "../server-constants.js"; import { formatError } from "../server-utils.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const healthHandlers: GatewayRequestHandlers = { health: async ({ respond, context, params }) => { diff --git a/src/gateway/server-methods/logs.test.ts b/src/gateway/server-methods/logs.test.ts index 6a28a23d4..fd9a46f92 100644 --- a/src/gateway/server-methods/logs.test.ts +++ b/src/gateway/server-methods/logs.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, describe, expect, it, vi } from "vitest"; - import { resetLogger, setLoggerOverride } from "../../logging.js"; import { logsHandlers } from "./logs.js"; @@ -16,16 +14,16 @@ describe("logs.tail", () => { }); it("falls back to latest rolling log file when today is missing", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-logs-")); - const older = path.join(tempDir, "moltbot-2026-01-20.log"); - const newer = path.join(tempDir, "moltbot-2026-01-21.log"); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-logs-")); + const older = path.join(tempDir, "openclaw-2026-01-20.log"); + const newer = path.join(tempDir, "openclaw-2026-01-21.log"); await fs.writeFile(older, '{"msg":"old"}\n'); await fs.writeFile(newer, '{"msg":"new"}\n'); await fs.utimes(older, new Date(0), new Date(0)); await fs.utimes(newer, new Date(), new Date()); - setLoggerOverride({ file: path.join(tempDir, "moltbot-2026-01-22.log") }); + setLoggerOverride({ file: path.join(tempDir, "openclaw-2026-01-22.log") }); const respond = vi.fn(); await logsHandlers["logs.tail"]({ diff --git a/src/gateway/server-methods/logs.ts b/src/gateway/server-methods/logs.ts index cda5eed38..e3c1af75f 100644 --- a/src/gateway/server-methods/logs.ts +++ b/src/gateway/server-methods/logs.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import type { GatewayRequestHandlers } from "./types.js"; import { getResolvedLoggerSettings } from "../../logging.js"; import { ErrorCodes, @@ -7,13 +8,12 @@ import { formatValidationErrors, validateLogsTailParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; const DEFAULT_LIMIT = 500; const DEFAULT_MAX_BYTES = 250_000; const MAX_LIMIT = 5000; const MAX_BYTES = 1_000_000; -const ROLLING_LOG_RE = /^moltbot-\d{4}-\d{2}-\d{2}\.log$/; +const ROLLING_LOG_RE = /^openclaw-\d{4}-\d{2}-\d{2}\.log$/; function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); @@ -25,12 +25,18 @@ function isRollingLogFile(file: string): boolean { async function resolveLogFile(file: string): Promise { const stat = await fs.stat(file).catch(() => null); - if (stat) return file; - if (!isRollingLogFile(file)) return file; + if (stat) { + return file; + } + if (!isRollingLogFile(file)) { + return file; + } const dir = path.dirname(file); const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null); - if (!entries) return file; + if (!entries) { + return file; + } const candidates = await Promise.all( entries @@ -43,7 +49,7 @@ async function resolveLogFile(file: string): Promise { ); const sorted = candidates .filter((entry): entry is NonNullable => Boolean(entry)) - .sort((a, b) => b.mtimeMs - a.mtimeMs); + .toSorted((a, b) => b.mtimeMs - a.mtimeMs); return sorted[0]?.path ?? file; } diff --git a/src/gateway/server-methods/models.ts b/src/gateway/server-methods/models.ts index ec2f5a0aa..68eca48a1 100644 --- a/src/gateway/server-methods/models.ts +++ b/src/gateway/server-methods/models.ts @@ -1,10 +1,10 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { ErrorCodes, errorShape, formatValidationErrors, validateModelsListParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const modelsHandlers: GatewayRequestHandlers = { "models.list": async ({ params, respond, context }) => { diff --git a/src/gateway/server-methods/nodes.helpers.ts b/src/gateway/server-methods/nodes.helpers.ts index fc7964e30..5f77112e1 100644 --- a/src/gateway/server-methods/nodes.helpers.ts +++ b/src/gateway/server-methods/nodes.helpers.ts @@ -1,7 +1,7 @@ import type { ErrorObject } from "ajv"; +import type { RespondFn } from "./types.js"; import { ErrorCodes, errorShape, formatValidationErrors } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; -import type { RespondFn } from "./types.js"; type ValidatorFn = ((value: unknown) => boolean) & { errors?: ErrorObject[] | null; @@ -34,13 +34,17 @@ export function uniqueSortedStrings(values: unknown[]) { return [...new Set(values.filter((v) => typeof v === "string"))] .map((v) => v.trim()) .filter(Boolean) - .sort(); + .toSorted(); } export function safeParseJson(value: string | null | undefined): unknown { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const trimmed = value.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } try { return JSON.parse(trimmed) as unknown; } catch { diff --git a/src/gateway/server-methods/nodes.ts b/src/gateway/server-methods/nodes.ts index 0ae80dd9c..b4ad29ba4 100644 --- a/src/gateway/server-methods/nodes.ts +++ b/src/gateway/server-methods/nodes.ts @@ -1,3 +1,6 @@ +import type { GatewayRequestHandlers } from "./types.js"; +import { loadConfig } from "../../config/config.js"; +import { listDevicePairing } from "../../infra/device-pairing.js"; import { approveNodePairing, listNodePairing, @@ -6,7 +9,7 @@ import { requestNodePairing, verifyNodeToken, } from "../../infra/node-pairing.js"; -import { listDevicePairing } from "../../infra/device-pairing.js"; +import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js"; import { ErrorCodes, errorShape, @@ -28,18 +31,21 @@ import { safeParseJson, uniqueSortedStrings, } from "./nodes.helpers.js"; -import { loadConfig } from "../../config/config.js"; -import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js"; -import type { GatewayRequestHandlers } from "./types.js"; function isNodeEntry(entry: { role?: string; roles?: string[] }) { - if (entry.role === "node") return true; - if (Array.isArray(entry.roles) && entry.roles.includes("node")) return true; + if (entry.role === "node") { + return true; + } + if (Array.isArray(entry.roles) && entry.roles.includes("node")) { + return true; + } return false; } function normalizeNodeInvokeResultParams(params: unknown): unknown { - if (!params || typeof params !== "object") return params; + if (!params || typeof params !== "object") { + return params; + } const raw = params as Record; const normalized: Record = { ...raw }; if (normalized.payloadJSON === null) { @@ -284,11 +290,17 @@ export const nodeHandlers: GatewayRequestHandlers = { }); nodes.sort((a, b) => { - if (a.connected !== b.connected) return a.connected ? -1 : 1; + if (a.connected !== b.connected) { + return a.connected ? -1 : 1; + } const an = (a.displayName ?? a.nodeId).toLowerCase(); const bn = (b.displayName ?? b.nodeId).toLowerCase(); - if (an < bn) return -1; - if (an > bn) return 1; + if (an < bn) { + return -1; + } + if (an > bn) { + return 1; + } return a.nodeId.localeCompare(b.nodeId); }); diff --git a/src/gateway/server-methods/send.test.ts b/src/gateway/server-methods/send.test.ts index 8c50da881..e581aed2c 100644 --- a/src/gateway/server-methods/send.test.ts +++ b/src/gateway/server-methods/send.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import type { GatewayRequestContext } from "./types.js"; import { sendHandlers } from "./send.js"; diff --git a/src/gateway/server-methods/send.ts b/src/gateway/server-methods/send.ts index 1d3adbb6f..246ee27e2 100644 --- a/src/gateway/server-methods/send.ts +++ b/src/gateway/server-methods/send.ts @@ -1,16 +1,15 @@ +import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; +import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; -import type { ChannelId } from "../../channels/plugins/types.js"; import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js"; -import { loadConfig } from "../../config/config.js"; import { createOutboundSendDeps } from "../../cli/deps.js"; +import { loadConfig } from "../../config/config.js"; import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js"; -import { normalizeReplyPayloadsForDelivery } from "../../infra/outbound/payloads.js"; import { ensureOutboundSessionEntry, resolveOutboundSessionRoute, } from "../../infra/outbound/outbound-session.js"; -import { resolveSessionAgentId } from "../../agents/agent-scope.js"; -import type { OutboundChannel } from "../../infra/outbound/targets.js"; +import { normalizeReplyPayloadsForDelivery } from "../../infra/outbound/payloads.js"; import { resolveOutboundTarget } from "../../infra/outbound/targets.js"; import { normalizePollInput } from "../../polls.js"; import { @@ -21,7 +20,6 @@ import { validateSendParams, } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; type InflightResult = { ok: boolean; @@ -46,7 +44,7 @@ const getInflightMap = (context: GatewayRequestContext) => { export const sendHandlers: GatewayRequestHandlers = { send: async ({ params, respond, context }) => { - const p = params as Record; + const p = params; if (!validateSendParams(p)) { respond( false, @@ -104,8 +102,8 @@ export const sendHandlers: GatewayRequestHandlers = { typeof request.accountId === "string" && request.accountId.trim().length ? request.accountId.trim() : undefined; - const outboundChannel = channel as Exclude; - const plugin = getChannelPlugin(channel as ChannelId); + const outboundChannel = channel; + const plugin = getChannelPlugin(channel); if (!plugin) { respond( false, @@ -201,9 +199,15 @@ export const sendHandlers: GatewayRequestHandlers = { messageId: result.messageId, channel, }; - if ("chatId" in result) payload.chatId = result.chatId; - if ("channelId" in result) payload.channelId = result.channelId; - if ("toJid" in result) payload.toJid = result.toJid; + if ("chatId" in result) { + payload.chatId = result.chatId; + } + if ("channelId" in result) { + payload.channelId = result.channelId; + } + if ("toJid" in result) { + payload.toJid = result.toJid; + } if ("conversationId" in result) { payload.conversationId = result.conversationId; } @@ -237,7 +241,7 @@ export const sendHandlers: GatewayRequestHandlers = { } }, poll: async ({ params, respond, context }) => { - const p = params as Record; + const p = params; if (!validatePollParams(p)) { respond( false, @@ -290,7 +294,7 @@ export const sendHandlers: GatewayRequestHandlers = { ? request.accountId.trim() : undefined; try { - const plugin = getChannelPlugin(channel as ChannelId); + const plugin = getChannelPlugin(channel); const outbound = plugin?.outbound; if (!outbound?.sendPoll) { respond( @@ -302,7 +306,7 @@ export const sendHandlers: GatewayRequestHandlers = { } const cfg = loadConfig(); const resolved = resolveOutboundTarget({ - channel: channel as Exclude, + channel: channel, to, cfg, accountId, @@ -326,10 +330,18 @@ export const sendHandlers: GatewayRequestHandlers = { messageId: result.messageId, channel, }; - if (result.toJid) payload.toJid = result.toJid; - if (result.channelId) payload.channelId = result.channelId; - if (result.conversationId) payload.conversationId = result.conversationId; - if (result.pollId) payload.pollId = result.pollId; + if (result.toJid) { + payload.toJid = result.toJid; + } + if (result.channelId) { + payload.channelId = result.channelId; + } + if (result.conversationId) { + payload.conversationId = result.conversationId; + } + if (result.pollId) { + payload.pollId = result.pollId; + } context.dedupe.set(`poll:${idem}`, { ts: Date.now(), ok: true, diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index df59a3f31..3fdb4ee75 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; - +import type { GatewayRequestHandlers } from "./types.js"; import { abortEmbeddedPiRun, waitForEmbeddedPiRunEnd } from "../../agents/pi-embedded.js"; import { stopSubagentsForRequester } from "../../auto-reply/reply/abort.js"; import { clearSessionQueues } from "../../auto-reply/reply/queue.js"; @@ -38,7 +38,6 @@ import { } from "../session-utils.js"; import { applySessionsPatchToStore } from "../sessions-patch.js"; import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const sessionsHandlers: GatewayRequestHandlers = { "sessions.list": ({ params, respond }) => { @@ -53,7 +52,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const p = params as import("../protocol/index.js").SessionsListParams; + const p = params; const cfg = loadConfig(); const { storePath, store } = loadCombinedSessionStoreForGateway(cfg); const result = listSessionsFromStore({ @@ -78,7 +77,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const p = params as import("../protocol/index.js").SessionsPreviewParams; + const p = params; const keysRaw = Array.isArray(p.keys) ? p.keys : []; const keys = keysRaw .map((key) => String(key ?? "").trim()) @@ -144,7 +143,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const p = params as import("../protocol/index.js").SessionsResolveParams; + const p = params; const cfg = loadConfig(); const resolved = resolveSessionKeyFromResolveParams({ cfg, p }); @@ -166,7 +165,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const p = params as import("../protocol/index.js").SessionsPatchParams; + const p = params; const key = String(p.key ?? "").trim(); if (!key) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); @@ -215,7 +214,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const p = params as import("../protocol/index.js").SessionsResetParams; + const p = params; const key = String(p.key ?? "").trim(); if (!key) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); @@ -273,7 +272,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const p = params as import("../protocol/index.js").SessionsDeleteParams; + const p = params; const key = String(p.key ?? "").trim(); if (!key) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); @@ -300,7 +299,9 @@ export const sessionsHandlers: GatewayRequestHandlers = { const existed = Boolean(entry); const queueKeys = new Set(target.storeKeys); queueKeys.add(target.canonicalKey); - if (sessionId) queueKeys.add(sessionId); + if (sessionId) { + queueKeys.add(sessionId); + } clearSessionQueues([...queueKeys]); stopSubagentsForRequester({ cfg, requesterSessionKey: target.canonicalKey }); if (sessionId) { @@ -325,7 +326,9 @@ export const sessionsHandlers: GatewayRequestHandlers = { store[primaryKey] = store[existingKey]; delete store[existingKey]; } - if (store[primaryKey]) delete store[primaryKey]; + if (store[primaryKey]) { + delete store[primaryKey]; + } }); const archived: string[] = []; @@ -336,7 +339,9 @@ export const sessionsHandlers: GatewayRequestHandlers = { entry?.sessionFile, target.agentId, )) { - if (!fs.existsSync(candidate)) continue; + if (!fs.existsSync(candidate)) { + continue; + } try { archived.push(archiveFileOnDisk(candidate, "deleted")); } catch { @@ -359,7 +364,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const p = params as import("../protocol/index.js").SessionsCompactParams; + const p = params; const key = String(p.key ?? "").trim(); if (!key) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); @@ -443,7 +448,9 @@ export const sessionsHandlers: GatewayRequestHandlers = { await updateSessionStore(storePath, (store) => { const entryKey = compactTarget.primaryKey; const entryToUpdate = store[entryKey]; - if (!entryToUpdate) return; + if (!entryToUpdate) { + return; + } delete entryToUpdate.inputTokens; delete entryToUpdate.outputTokens; delete entryToUpdate.totalTokens; diff --git a/src/gateway/server-methods/skills.ts b/src/gateway/server-methods/skills.ts index ceb102092..0bdda27f8 100644 --- a/src/gateway/server-methods/skills.ts +++ b/src/gateway/server-methods/skills.ts @@ -1,8 +1,9 @@ +import type { OpenClawConfig } from "../../config/config.js"; +import type { GatewayRequestHandlers } from "./types.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { installSkill } from "../../agents/skills-install.js"; import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js"; import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js"; -import type { MoltbotConfig } from "../../config/config.js"; import { loadConfig, writeConfigFile } from "../../config/config.js"; import { getRemoteSkillEligibility } from "../../infra/skills-remote.js"; import { @@ -14,9 +15,8 @@ import { validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; -function listWorkspaceDirs(cfg: MoltbotConfig): string[] { +function listWorkspaceDirs(cfg: OpenClawConfig): string[] { const dirs = new Set(); const list = cfg.agents?.list; if (Array.isArray(list)) { @@ -38,21 +38,27 @@ function collectSkillBins(entries: SkillEntry[]): string[] { const install = entry.metadata?.install ?? []; for (const bin of required) { const trimmed = bin.trim(); - if (trimmed) bins.add(trimmed); + if (trimmed) { + bins.add(trimmed); + } } for (const bin of anyBins) { const trimmed = bin.trim(); - if (trimmed) bins.add(trimmed); + if (trimmed) { + bins.add(trimmed); + } } for (const spec of install) { const specBins = spec?.bins ?? []; for (const bin of specBins) { const trimmed = String(bin).trim(); - if (trimmed) bins.add(trimmed); + if (trimmed) { + bins.add(trimmed); + } } } } - return [...bins].sort(); + return [...bins].toSorted(); } export const skillsHandlers: GatewayRequestHandlers = { @@ -93,9 +99,11 @@ export const skillsHandlers: GatewayRequestHandlers = { const bins = new Set(); for (const workspaceDir of workspaceDirs) { const entries = loadWorkspaceSkillEntries(workspaceDir, { config: cfg }); - for (const bin of collectSkillBins(entries)) bins.add(bin); + for (const bin of collectSkillBins(entries)) { + bins.add(bin); + } } - respond(true, { bins: [...bins].sort() }, undefined); + respond(true, { bins: [...bins].toSorted() }, undefined); }, "skills.install": async ({ params, respond }) => { if (!validateSkillsInstallParams(params)) { @@ -156,23 +164,31 @@ export const skillsHandlers: GatewayRequestHandlers = { } if (typeof p.apiKey === "string") { const trimmed = p.apiKey.trim(); - if (trimmed) current.apiKey = trimmed; - else delete current.apiKey; + if (trimmed) { + current.apiKey = trimmed; + } else { + delete current.apiKey; + } } if (p.env && typeof p.env === "object") { const nextEnv = current.env ? { ...current.env } : {}; for (const [key, value] of Object.entries(p.env)) { const trimmedKey = key.trim(); - if (!trimmedKey) continue; + if (!trimmedKey) { + continue; + } const trimmedVal = value.trim(); - if (!trimmedVal) delete nextEnv[trimmedKey]; - else nextEnv[trimmedKey] = trimmedVal; + if (!trimmedVal) { + delete nextEnv[trimmedKey]; + } else { + nextEnv[trimmedKey] = trimmedVal; + } } current.env = nextEnv; } entries[p.skillKey] = current; skills.entries = entries; - const nextConfig: MoltbotConfig = { + const nextConfig: OpenClawConfig = { ...cfg, skills, }; diff --git a/src/gateway/server-methods/system.ts b/src/gateway/server-methods/system.ts index 66d6eb3de..fa440a29a 100644 --- a/src/gateway/server-methods/system.ts +++ b/src/gateway/server-methods/system.ts @@ -1,10 +1,10 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { resolveMainSessionKeyFromConfig } from "../../config/sessions.js"; import { getLastHeartbeatEvent } from "../../infra/heartbeat-events.js"; import { setHeartbeatsEnabled } from "../../infra/heartbeat-runner.js"; import { enqueueSystemEvent, isSystemEventContextChanged } from "../../infra/system-events.js"; import { listSystemPresence, updateSystemPresence } from "../../infra/system-presence.js"; import { ErrorCodes, errorShape } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const systemHandlers: GatewayRequestHandlers = { "last-heartbeat": ({ respond }) => { @@ -54,15 +54,15 @@ export const systemHandlers: GatewayRequestHandlers = { const reason = typeof params.reason === "string" ? params.reason : undefined; const roles = Array.isArray(params.roles) && params.roles.every((t) => typeof t === "string") - ? (params.roles as string[]) + ? params.roles : undefined; const scopes = Array.isArray(params.scopes) && params.scopes.every((t) => typeof t === "string") - ? (params.scopes as string[]) + ? params.scopes : undefined; const tags = Array.isArray(params.tags) && params.tags.every((t) => typeof t === "string") - ? (params.tags as string[]) + ? params.tags : undefined; const presenceUpdate = updateSystemPresence({ text, diff --git a/src/gateway/server-methods/talk.ts b/src/gateway/server-methods/talk.ts index 491ed1401..78f354b94 100644 --- a/src/gateway/server-methods/talk.ts +++ b/src/gateway/server-methods/talk.ts @@ -1,10 +1,10 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { ErrorCodes, errorShape, formatValidationErrors, validateTalkModeParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const talkHandlers: GatewayRequestHandlers = { "talk.mode": ({ params, respond, context, client, isWebchatConnect }) => { diff --git a/src/gateway/server-methods/tts.ts b/src/gateway/server-methods/tts.ts index 5e4e8254e..4535149bb 100644 --- a/src/gateway/server-methods/tts.ts +++ b/src/gateway/server-methods/tts.ts @@ -1,3 +1,4 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { loadConfig } from "../../config/config.js"; import { OPENAI_TTS_MODELS, @@ -16,7 +17,6 @@ import { } from "../../tts/tts.js"; import { ErrorCodes, errorShape } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const ttsHandlers: GatewayRequestHandlers = { "tts.status": async ({ respond }) => { diff --git a/src/gateway/server-methods/types.ts b/src/gateway/server-methods/types.ts index c23459a2d..e6b944f75 100644 --- a/src/gateway/server-methods/types.ts +++ b/src/gateway/server-methods/types.ts @@ -2,13 +2,13 @@ import type { ModelCatalogEntry } from "../../agents/model-catalog.js"; import type { createDefaultDeps } from "../../cli/deps.js"; import type { HealthSummary } from "../../commands/health.js"; import type { CronService } from "../../cron/service.js"; +import type { createSubsystemLogger } from "../../logging/subsystem.js"; import type { WizardSession } from "../../wizard/session.js"; import type { ChatAbortControllerEntry } from "../chat-abort.js"; import type { NodeRegistry } from "../node-registry.js"; import type { ConnectParams, ErrorShape, RequestFrame } from "../protocol/index.js"; import type { ChannelRuntimeSnapshot } from "../server-channels.js"; import type { DedupeEntry } from "../server-shared.js"; -import type { createSubsystemLogger } from "../../logging/subsystem.js"; type SubsystemLogger = ReturnType; diff --git a/src/gateway/server-methods/update.ts b/src/gateway/server-methods/update.ts index db8cf2c53..5f7bdcee3 100644 --- a/src/gateway/server-methods/update.ts +++ b/src/gateway/server-methods/update.ts @@ -1,10 +1,11 @@ -import { resolveMoltbotPackageRoot } from "../../infra/moltbot-root.js"; -import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; +import type { GatewayRequestHandlers } from "./types.js"; +import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js"; import { formatDoctorNonInteractiveHint, type RestartSentinelPayload, writeRestartSentinel, } from "../../infra/restart-sentinel.js"; +import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { runGatewayUpdate } from "../../infra/update-runner.js"; import { ErrorCodes, @@ -12,7 +13,6 @@ import { formatValidationErrors, validateUpdateRunParams, } from "../protocol/index.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const updateHandlers: GatewayRequestHandlers = { "update.run": async ({ params, respond }) => { @@ -49,7 +49,7 @@ export const updateHandlers: GatewayRequestHandlers = { let result: Awaited>; try { const root = - (await resolveMoltbotPackageRoot({ + (await resolveOpenClawPackageRoot({ moduleUrl: import.meta.url, argv1: process.argv[1], cwd: process.cwd(), diff --git a/src/gateway/server-methods/usage.ts b/src/gateway/server-methods/usage.ts index dcdd89742..550217a5d 100644 --- a/src/gateway/server-methods/usage.ts +++ b/src/gateway/server-methods/usage.ts @@ -1,8 +1,8 @@ -import { loadConfig } from "../../config/config.js"; import type { CostUsageSummary } from "../../infra/session-cost-usage.js"; -import { loadCostUsageSummary } from "../../infra/session-cost-usage.js"; -import { loadProviderUsageSummary } from "../../infra/provider-usage.js"; import type { GatewayRequestHandlers } from "./types.js"; +import { loadConfig } from "../../config/config.js"; +import { loadProviderUsageSummary } from "../../infra/provider-usage.js"; +import { loadCostUsageSummary } from "../../infra/session-cost-usage.js"; const COST_USAGE_CACHE_TTL_MS = 30_000; @@ -15,10 +15,14 @@ type CostUsageCacheEntry = { const costUsageCache = new Map(); const parseDays = (raw: unknown): number => { - if (typeof raw === "number" && Number.isFinite(raw)) return Math.floor(raw); + if (typeof raw === "number" && Number.isFinite(raw)) { + return Math.floor(raw); + } if (typeof raw === "string" && raw.trim() !== "") { const parsed = Number(raw); - if (Number.isFinite(parsed)) return Math.floor(parsed); + if (Number.isFinite(parsed)) { + return Math.floor(parsed); + } } return 30; }; @@ -35,7 +39,9 @@ async function loadCostUsageSummaryCached(params: { } if (cached?.inFlight) { - if (cached.summary) return cached.summary; + if (cached.summary) { + return cached.summary; + } return await cached.inFlight; } @@ -46,7 +52,9 @@ async function loadCostUsageSummaryCached(params: { return summary; }) .catch((err) => { - if (entry.summary) return entry.summary; + if (entry.summary) { + return entry.summary; + } throw err; }) .finally(() => { @@ -60,7 +68,9 @@ async function loadCostUsageSummaryCached(params: { entry.inFlight = inFlight; costUsageCache.set(days, entry); - if (entry.summary) return entry.summary; + if (entry.summary) { + return entry.summary; + } return await inFlight; } diff --git a/src/gateway/server-methods/voicewake.ts b/src/gateway/server-methods/voicewake.ts index 3f43488aa..aa1355dc7 100644 --- a/src/gateway/server-methods/voicewake.ts +++ b/src/gateway/server-methods/voicewake.ts @@ -1,8 +1,8 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { loadVoiceWakeConfig, setVoiceWakeTriggers } from "../../infra/voicewake.js"; import { ErrorCodes, errorShape } from "../protocol/index.js"; import { normalizeVoiceWakeTriggers } from "../server-utils.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const voicewakeHandlers: GatewayRequestHandlers = { "voicewake.get": async ({ respond }) => { diff --git a/src/gateway/server-methods/web.ts b/src/gateway/server-methods/web.ts index e55f5f2fb..18cf2e2fd 100644 --- a/src/gateway/server-methods/web.ts +++ b/src/gateway/server-methods/web.ts @@ -1,3 +1,4 @@ +import type { GatewayRequestHandlers } from "./types.js"; import { listChannelPlugins } from "../../channels/plugins/index.js"; import { ErrorCodes, @@ -7,7 +8,6 @@ import { validateWebLoginWaitParams, } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestHandlers } from "./types.js"; const WEB_LOGIN_METHODS = new Set(["web.login.start", "web.login.wait"]); diff --git a/src/gateway/server-methods/wizard.ts b/src/gateway/server-methods/wizard.ts index 7a1e42324..8585a066c 100644 --- a/src/gateway/server-methods/wizard.ts +++ b/src/gateway/server-methods/wizard.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import type { GatewayRequestHandlers } from "./types.js"; import { defaultRuntime } from "../../runtime.js"; import { WizardSession } from "../../wizard/session.js"; import { @@ -11,7 +12,6 @@ import { validateWizardStatusParams, } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; -import type { GatewayRequestHandlers } from "./types.js"; export const wizardHandlers: GatewayRequestHandlers = { "wizard.start": async ({ params, respond, context }) => { @@ -33,7 +33,7 @@ export const wizardHandlers: GatewayRequestHandlers = { } const sessionId = randomUUID(); const opts = { - mode: params.mode as "local" | "remote" | undefined, + mode: params.mode, workspace: typeof params.workspace === "string" ? params.workspace : undefined, }; const session = new WizardSession((prompter) => @@ -58,7 +58,7 @@ export const wizardHandlers: GatewayRequestHandlers = { ); return; } - const sessionId = params.sessionId as string; + const sessionId = params.sessionId; const session = context.wizardSessions.get(sessionId); if (!session) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "wizard not found")); @@ -95,7 +95,7 @@ export const wizardHandlers: GatewayRequestHandlers = { ); return; } - const sessionId = params.sessionId as string; + const sessionId = params.sessionId; const session = context.wizardSessions.get(sessionId); if (!session) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "wizard not found")); @@ -121,7 +121,7 @@ export const wizardHandlers: GatewayRequestHandlers = { ); return; } - const sessionId = params.sessionId as string; + const sessionId = params.sessionId; const session = context.wizardSessions.get(sessionId); if (!session) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "wizard not found")); diff --git a/src/gateway/server-mobile-nodes.ts b/src/gateway/server-mobile-nodes.ts index c9271f15c..c4baeaf14 100644 --- a/src/gateway/server-mobile-nodes.ts +++ b/src/gateway/server-mobile-nodes.ts @@ -2,7 +2,9 @@ import type { NodeRegistry } from "./node-registry.js"; const isMobilePlatform = (platform: unknown): boolean => { const p = typeof platform === "string" ? platform.trim().toLowerCase() : ""; - if (!p) return false; + if (!p) { + return false; + } return p.startsWith("ios") || p.startsWith("ipados") || p.startsWith("android"); }; diff --git a/src/gateway/server-node-events.test.ts b/src/gateway/server-node-events.test.ts index 25372dabb..f21bb2fe2 100644 --- a/src/gateway/server-node-events.test.ts +++ b/src/gateway/server-node-events.test.ts @@ -7,12 +7,12 @@ vi.mock("../infra/heartbeat-wake.js", () => ({ requestHeartbeatNow: vi.fn(), })); -import { enqueueSystemEvent } from "../infra/system-events.js"; -import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; -import { handleNodeEvent } from "./server-node-events.js"; -import type { NodeEventContext } from "./server-node-events-types.js"; -import type { HealthSummary } from "../commands/health.js"; import type { CliDeps } from "../cli/deps.js"; +import type { HealthSummary } from "../commands/health.js"; +import type { NodeEventContext } from "./server-node-events-types.js"; +import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; +import { enqueueSystemEvent } from "../infra/system-events.js"; +import { handleNodeEvent } from "./server-node-events.js"; const enqueueSystemEventMock = vi.mocked(enqueueSystemEvent); const requestHeartbeatNowMock = vi.mocked(requestHeartbeatNow); diff --git a/src/gateway/server-node-events.ts b/src/gateway/server-node-events.ts index 6870f8a0f..10933485b 100644 --- a/src/gateway/server-node-events.ts +++ b/src/gateway/server-node-events.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import type { NodeEvent, NodeEventContext } from "./server-node-events-types.js"; import { normalizeChannelId } from "../channels/plugins/index.js"; import { agentCommand } from "../commands/agent.js"; import { loadConfig } from "../config/config.js"; @@ -7,14 +8,15 @@ import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; import { normalizeMainKey } from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; -import type { NodeEvent, NodeEventContext } from "./server-node-events-types.js"; import { loadSessionEntry } from "./session-utils.js"; import { formatForLog } from "./ws-log.js"; export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt: NodeEvent) => { switch (evt.event) { case "voice.transcript": { - if (!evt.payloadJSON) return; + if (!evt.payloadJSON) { + return; + } let payload: unknown; try { payload = JSON.parse(evt.payloadJSON) as unknown; @@ -24,8 +26,12 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt const obj = typeof payload === "object" && payload !== null ? (payload as Record) : {}; const text = typeof obj.text === "string" ? obj.text.trim() : ""; - if (!text) return; - if (text.length > 20_000) return; + if (!text) { + return; + } + if (text.length > 20_000) { + return; + } const sessionKeyRaw = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : ""; const cfg = loadConfig(); const rawMainKey = normalizeMainKey(cfg.session?.mainKey); @@ -73,7 +79,9 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt return; } case "agent.request": { - if (!evt.payloadJSON) return; + if (!evt.payloadJSON) { + return; + } type AgentDeepLink = { message?: string; sessionKey?: string | null; @@ -91,8 +99,12 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt return; } const message = (link?.message ?? "").trim(); - if (!message) return; - if (message.length > 20_000) return; + if (!message) { + return; + } + if (message.length > 20_000) { + return; + } const channelRaw = typeof link?.channel === "string" ? link.channel.trim() : ""; const channel = normalizeChannelId(channelRaw) ?? undefined; @@ -141,7 +153,9 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt return; } case "chat.subscribe": { - if (!evt.payloadJSON) return; + if (!evt.payloadJSON) { + return; + } let payload: unknown; try { payload = JSON.parse(evt.payloadJSON) as unknown; @@ -151,12 +165,16 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt const obj = typeof payload === "object" && payload !== null ? (payload as Record) : {}; const sessionKey = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : ""; - if (!sessionKey) return; + if (!sessionKey) { + return; + } ctx.nodeSubscribe(nodeId, sessionKey); return; } case "chat.unsubscribe": { - if (!evt.payloadJSON) return; + if (!evt.payloadJSON) { + return; + } let payload: unknown; try { payload = JSON.parse(evt.payloadJSON) as unknown; @@ -166,14 +184,18 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt const obj = typeof payload === "object" && payload !== null ? (payload as Record) : {}; const sessionKey = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : ""; - if (!sessionKey) return; + if (!sessionKey) { + return; + } ctx.nodeUnsubscribe(nodeId, sessionKey); return; } case "exec.started": case "exec.finished": case "exec.denied": { - if (!evt.payloadJSON) return; + if (!evt.payloadJSON) { + return; + } let payload: unknown; try { payload = JSON.parse(evt.payloadJSON) as unknown; @@ -184,7 +206,9 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt typeof payload === "object" && payload !== null ? (payload as Record) : {}; const sessionKey = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : `node-${nodeId}`; - if (!sessionKey) return; + if (!sessionKey) { + return; + } const runId = typeof obj.runId === "string" ? obj.runId.trim() : ""; const command = typeof obj.command === "string" ? obj.command.trim() : ""; const exitCode = @@ -198,14 +222,20 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt let text = ""; if (evt.event === "exec.started") { text = `Exec started (node=${nodeId}${runId ? ` id=${runId}` : ""})`; - if (command) text += `: ${command}`; + if (command) { + text += `: ${command}`; + } } else if (evt.event === "exec.finished") { const exitLabel = timedOut ? "timeout" : `code ${exitCode ?? "?"}`; text = `Exec finished (node=${nodeId}${runId ? ` id=${runId}` : ""}, ${exitLabel})`; - if (output) text += `\n${output}`; + if (output) { + text += `\n${output}`; + } } else { text = `Exec denied (node=${nodeId}${runId ? ` id=${runId}` : ""}${reason ? `, ${reason}` : ""})`; - if (command) text += `: ${command}`; + if (command) { + text += `: ${command}`; + } } enqueueSystemEvent(text, { sessionKey, contextKey: runId ? `exec:${runId}` : "exec" }); diff --git a/src/gateway/server-node-subscriptions.test.ts b/src/gateway/server-node-subscriptions.test.ts index 9af7a9630..776e5a048 100644 --- a/src/gateway/server-node-subscriptions.test.ts +++ b/src/gateway/server-node-subscriptions.test.ts @@ -17,7 +17,7 @@ describe("node subscription manager", () => { manager.sendToSession("main", "chat", { ok: true }, sendEvent); expect(sent).toHaveLength(2); - expect(sent.map((s) => s.nodeId).sort()).toEqual(["node-a", "node-b"]); + expect(sent.map((s) => s.nodeId).toSorted()).toEqual(["node-a", "node-b"]); expect(sent[0].event).toBe("chat"); }); diff --git a/src/gateway/server-node-subscriptions.ts b/src/gateway/server-node-subscriptions.ts index 33eecb0be..e341ce30f 100644 --- a/src/gateway/server-node-subscriptions.ts +++ b/src/gateway/server-node-subscriptions.ts @@ -39,14 +39,18 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager { const subscribe = (nodeId: string, sessionKey: string) => { const normalizedNodeId = nodeId.trim(); const normalizedSessionKey = sessionKey.trim(); - if (!normalizedNodeId || !normalizedSessionKey) return; + if (!normalizedNodeId || !normalizedSessionKey) { + return; + } let nodeSet = nodeSubscriptions.get(normalizedNodeId); if (!nodeSet) { nodeSet = new Set(); nodeSubscriptions.set(normalizedNodeId, nodeSet); } - if (nodeSet.has(normalizedSessionKey)) return; + if (nodeSet.has(normalizedSessionKey)) { + return; + } nodeSet.add(normalizedSessionKey); let sessionSet = sessionSubscribers.get(normalizedSessionKey); @@ -60,25 +64,35 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager { const unsubscribe = (nodeId: string, sessionKey: string) => { const normalizedNodeId = nodeId.trim(); const normalizedSessionKey = sessionKey.trim(); - if (!normalizedNodeId || !normalizedSessionKey) return; + if (!normalizedNodeId || !normalizedSessionKey) { + return; + } const nodeSet = nodeSubscriptions.get(normalizedNodeId); nodeSet?.delete(normalizedSessionKey); - if (nodeSet?.size === 0) nodeSubscriptions.delete(normalizedNodeId); + if (nodeSet?.size === 0) { + nodeSubscriptions.delete(normalizedNodeId); + } const sessionSet = sessionSubscribers.get(normalizedSessionKey); sessionSet?.delete(normalizedNodeId); - if (sessionSet?.size === 0) sessionSubscribers.delete(normalizedSessionKey); + if (sessionSet?.size === 0) { + sessionSubscribers.delete(normalizedSessionKey); + } }; const unsubscribeAll = (nodeId: string) => { const normalizedNodeId = nodeId.trim(); const nodeSet = nodeSubscriptions.get(normalizedNodeId); - if (!nodeSet) return; + if (!nodeSet) { + return; + } for (const sessionKey of nodeSet) { const sessionSet = sessionSubscribers.get(sessionKey); sessionSet?.delete(normalizedNodeId); - if (sessionSet?.size === 0) sessionSubscribers.delete(sessionKey); + if (sessionSet?.size === 0) { + sessionSubscribers.delete(sessionKey); + } } nodeSubscriptions.delete(normalizedNodeId); }; @@ -90,9 +104,13 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager { sendEvent?: NodeSendEventFn | null, ) => { const normalizedSessionKey = sessionKey.trim(); - if (!normalizedSessionKey || !sendEvent) return; + if (!normalizedSessionKey || !sendEvent) { + return; + } const subs = sessionSubscribers.get(normalizedSessionKey); - if (!subs || subs.size === 0) return; + if (!subs || subs.size === 0) { + return; + } const payloadJSON = toPayloadJSON(payload); for (const nodeId of subs) { @@ -105,7 +123,9 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager { payload: unknown, sendEvent?: NodeSendEventFn | null, ) => { - if (!sendEvent) return; + if (!sendEvent) { + return; + } const payloadJSON = toPayloadJSON(payload); for (const nodeId of nodeSubscriptions.keys()) { sendEvent({ nodeId, event, payloadJSON }); @@ -118,7 +138,9 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager { listConnected?: NodeListConnectedFn | null, sendEvent?: NodeSendEventFn | null, ) => { - if (!sendEvent || !listConnected) return; + if (!sendEvent || !listConnected) { + return; + } const payloadJSON = toPayloadJSON(payload); for (const node of listConnected()) { sendEvent({ nodeId: node.nodeId, event, payloadJSON }); diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index 58f47c7de..0901a5116 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -3,10 +3,10 @@ import type { PluginRegistry } from "../plugins/registry.js"; import type { PluginDiagnostic } from "../plugins/types.js"; import { loadGatewayPlugins } from "./server-plugins.js"; -const loadMoltbotPlugins = vi.hoisted(() => vi.fn()); +const loadOpenClawPlugins = vi.hoisted(() => vi.fn()); vi.mock("../plugins/loader.js", () => ({ - loadMoltbotPlugins, + loadOpenClawPlugins, })); const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({ @@ -34,7 +34,7 @@ describe("loadGatewayPlugins", () => { message: "failed to load plugin: boom", }, ]; - loadMoltbotPlugins.mockReturnValue(createRegistry(diagnostics)); + loadOpenClawPlugins.mockReturnValue(createRegistry(diagnostics)); const log = { info: vi.fn(), diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index fca914249..39d1d4773 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -1,6 +1,6 @@ import type { loadConfig } from "../config/config.js"; -import { loadMoltbotPlugins } from "../plugins/loader.js"; import type { GatewayRequestHandler } from "./server-methods/types.js"; +import { loadOpenClawPlugins } from "../plugins/loader.js"; export function loadGatewayPlugins(params: { cfg: ReturnType; @@ -14,7 +14,7 @@ export function loadGatewayPlugins(params: { coreGatewayHandlers: Record; baseMethods: string[]; }) { - const pluginRegistry = loadMoltbotPlugins({ + const pluginRegistry = loadOpenClawPlugins({ config: params.cfg, workspaceDir: params.workspaceDir, logger: { diff --git a/src/gateway/server-reload-handlers.ts b/src/gateway/server-reload-handlers.ts index 6cea5ac02..393a38cf7 100644 --- a/src/gateway/server-reload-handlers.ts +++ b/src/gateway/server-reload-handlers.ts @@ -1,17 +1,17 @@ import type { CliDeps } from "../cli/deps.js"; import type { loadConfig } from "../config/config.js"; -import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js"; import type { HeartbeatRunner } from "../infra/heartbeat-runner.js"; +import type { ChannelKind, GatewayReloadPlan } from "./config-reload.js"; +import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js"; +import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js"; +import { isTruthyEnvValue } from "../infra/env.js"; import { resetDirectoryCache } from "../infra/outbound/target-resolver.js"; import { authorizeGatewaySigusr1Restart, setGatewaySigusr1RestartPolicy, } from "../infra/restart.js"; import { setCommandLaneConcurrency } from "../process/command-queue.js"; -import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js"; import { CommandLane } from "../process/lanes.js"; -import { isTruthyEnvValue } from "../infra/env.js"; -import type { ChannelKind, GatewayReloadPlan } from "./config-reload.js"; import { resolveHooksConfig } from "./hooks.js"; import { startBrowserControlServerIfEnabled } from "./server-browser.js"; import { buildGatewayCronService, type GatewayCronState } from "./server-cron.js"; @@ -87,7 +87,7 @@ export function createGatewayReloadHandlers(params: { if (plan.restartGmailWatcher) { await stopGmailWatcher().catch(() => {}); - if (!isTruthyEnvValue(process.env.CLAWDBOT_SKIP_GMAIL_WATCHER)) { + if (!isTruthyEnvValue(process.env.OPENCLAW_SKIP_GMAIL_WATCHER)) { try { const gmailResult = await startGmailWatcher(nextConfig); if (gmailResult.started) { @@ -103,17 +103,17 @@ export function createGatewayReloadHandlers(params: { params.logHooks.error(`gmail watcher failed to start: ${String(err)}`); } } else { - params.logHooks.info("skipping gmail watcher restart (CLAWDBOT_SKIP_GMAIL_WATCHER=1)"); + params.logHooks.info("skipping gmail watcher restart (OPENCLAW_SKIP_GMAIL_WATCHER=1)"); } } if (plan.restartChannels.size > 0) { if ( - isTruthyEnvValue(process.env.CLAWDBOT_SKIP_CHANNELS) || - isTruthyEnvValue(process.env.CLAWDBOT_SKIP_PROVIDERS) + isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) || + isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS) ) { params.logChannels.info( - "skipping channel reload (CLAWDBOT_SKIP_CHANNELS=1 or CLAWDBOT_SKIP_PROVIDERS=1)", + "skipping channel reload (OPENCLAW_SKIP_CHANNELS=1 or OPENCLAW_SKIP_PROVIDERS=1)", ); } else { const restartChannel = async (name: ChannelKind) => { diff --git a/src/gateway/server-restart-sentinel.ts b/src/gateway/server-restart-sentinel.ts index 28719290e..2600a0b63 100644 --- a/src/gateway/server-restart-sentinel.ts +++ b/src/gateway/server-restart-sentinel.ts @@ -1,6 +1,6 @@ +import type { CliDeps } from "../cli/deps.js"; import { resolveAnnounceTargetFromKey } from "../agents/tools/sessions-send-helpers.js"; import { normalizeChannelId } from "../channels/plugins/index.js"; -import type { CliDeps } from "../cli/deps.js"; import { agentCommand } from "../commands/agent.js"; import { resolveMainSessionKeyFromConfig } from "../config/sessions.js"; import { resolveOutboundTarget } from "../infra/outbound/targets.js"; @@ -16,7 +16,9 @@ import { loadSessionEntry } from "./session-utils.js"; export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { const sentinel = await consumeRestartSentinel(); - if (!sentinel) return; + if (!sentinel) { + return; + } const payload = sentinel.payload; const sessionKey = payload.sessionKey?.trim(); const message = formatRestartSentinelMessage(payload); diff --git a/src/gateway/server-runtime-config.ts b/src/gateway/server-runtime-config.ts index 2d699988a..b8ae4ee1b 100644 --- a/src/gateway/server-runtime-config.ts +++ b/src/gateway/server-runtime-config.ts @@ -77,12 +77,12 @@ export async function resolveGatewayRuntimeConfig(params: { (authMode === "token" && hasToken) || (authMode === "password" && hasPassword); const hooksConfig = resolveHooksConfig(params.cfg); const canvasHostEnabled = - process.env.CLAWDBOT_SKIP_CANVAS_HOST !== "1" && params.cfg.canvasHost?.enabled !== false; + process.env.OPENCLAW_SKIP_CANVAS_HOST !== "1" && params.cfg.canvasHost?.enabled !== false; assertGatewayAuthConfigured(resolvedAuth); if (tailscaleMode === "funnel" && authMode !== "password") { throw new Error( - "tailscale funnel requires gateway auth mode=password (set gateway.auth.password or CLAWDBOT_GATEWAY_PASSWORD)", + "tailscale funnel requires gateway auth mode=password (set gateway.auth.password or OPENCLAW_GATEWAY_PASSWORD)", ); } if (tailscaleMode !== "off" && !isLoopbackHost(bindHost)) { @@ -90,7 +90,7 @@ export async function resolveGatewayRuntimeConfig(params: { } if (!isLoopbackHost(bindHost) && !hasSharedSecret) { throw new Error( - `refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token/password, or set CLAWDBOT_GATEWAY_TOKEN/CLAWDBOT_GATEWAY_PASSWORD)`, + `refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token/password, or set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD)`, ); } diff --git a/src/gateway/server-runtime-state.ts b/src/gateway/server-runtime-state.ts index 02b060a3b..5e5d2d78c 100644 --- a/src/gateway/server-runtime-state.ts +++ b/src/gateway/server-runtime-state.ts @@ -1,28 +1,28 @@ import type { Server as HttpServer } from "node:http"; import { WebSocketServer } from "ws"; -import { CANVAS_HOST_PATH } from "../canvas-host/a2ui.js"; -import { type CanvasHostHandler, createCanvasHostHandler } from "../canvas-host/server.js"; import type { CliDeps } from "../cli/deps.js"; import type { createSubsystemLogger } from "../logging/subsystem.js"; +import type { PluginRegistry } from "../plugins/registry.js"; import type { RuntimeEnv } from "../runtime.js"; import type { ResolvedGatewayAuth } from "./auth.js"; import type { ChatAbortControllerEntry } from "./chat-abort.js"; import type { HooksConfigResolved } from "./hooks.js"; -import { createGatewayHooksRequestHandler } from "./server/hooks.js"; -import { listenGatewayHttpServer } from "./server/http-listen.js"; -import { resolveGatewayListenHosts } from "./net.js"; -import { createGatewayPluginRequestHandler } from "./server/plugins-http.js"; +import type { DedupeEntry } from "./server-shared.js"; +import type { GatewayTlsRuntime } from "./server/tls.js"; import type { GatewayWsClient } from "./server/ws-types.js"; +import { CANVAS_HOST_PATH } from "../canvas-host/a2ui.js"; +import { type CanvasHostHandler, createCanvasHostHandler } from "../canvas-host/server.js"; +import { resolveGatewayListenHosts } from "./net.js"; import { createGatewayBroadcaster } from "./server-broadcast.js"; import { type ChatRunEntry, createChatRunState } from "./server-chat.js"; import { MAX_PAYLOAD_BYTES } from "./server-constants.js"; import { attachGatewayUpgradeHandler, createGatewayHttpServer } from "./server-http.js"; -import type { DedupeEntry } from "./server-shared.js"; -import type { PluginRegistry } from "../plugins/registry.js"; -import type { GatewayTlsRuntime } from "./server/tls.js"; +import { createGatewayHooksRequestHandler } from "./server/hooks.js"; +import { listenGatewayHttpServer } from "./server/http-listen.js"; +import { createGatewayPluginRequestHandler } from "./server/plugins-http.js"; export async function createGatewayRuntimeState(params: { - cfg: import("../config/config.js").MoltbotConfig; + cfg: import("../config/config.js").OpenClawConfig; bindHost: string; port: number; controlUiEnabled: boolean; @@ -129,7 +129,9 @@ export async function createGatewayRuntimeState(params: { httpServers.push(httpServer); httpBindHosts.push(host); } catch (err) { - if (host === bindHosts[0]) throw err; + if (host === bindHosts[0]) { + throw err; + } params.log.warn( `gateway: failed to bind loopback alias ${host}:${params.port} (${String(err)})`, ); diff --git a/src/gateway/server-session-key.ts b/src/gateway/server-session-key.ts index b1931c4bb..4a9694f66 100644 --- a/src/gateway/server-session-key.ts +++ b/src/gateway/server-session-key.ts @@ -5,7 +5,9 @@ import { toAgentRequestSessionKey } from "../routing/session-key.js"; export function resolveSessionKeyForRun(runId: string) { const cached = getAgentRunContext(runId)?.sessionKey; - if (cached) return cached; + if (cached) { + return cached; + } const cfg = loadConfig(); const storePath = resolveStorePath(cfg.session?.store); const store = loadSessionStore(storePath); diff --git a/src/gateway/server-startup-log.ts b/src/gateway/server-startup-log.ts index cf6d2575c..a62adaf88 100644 --- a/src/gateway/server-startup-log.ts +++ b/src/gateway/server-startup-log.ts @@ -1,7 +1,7 @@ import chalk from "chalk"; +import type { loadConfig } from "../config/config.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveConfiguredModelRef } from "../agents/model-selection.js"; -import type { loadConfig } from "../config/config.js"; import { getResolvedLoggerSettings } from "../logging.js"; export function logGatewayStartup(params: { diff --git a/src/gateway/server-startup.ts b/src/gateway/server-startup.ts index e6bdbb0dc..1971ef8a2 100644 --- a/src/gateway/server-startup.ts +++ b/src/gateway/server-startup.ts @@ -1,3 +1,6 @@ +import type { CliDeps } from "../cli/deps.js"; +import type { loadConfig } from "../config/config.js"; +import type { loadOpenClawPlugins } from "../plugins/loader.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { @@ -5,9 +8,6 @@ import { resolveConfiguredModelRef, resolveHooksGmailModel, } from "../agents/model-selection.js"; -import type { CliDeps } from "../cli/deps.js"; -import type { loadConfig } from "../config/config.js"; -import { isTruthyEnvValue } from "../infra/env.js"; import { startGmailWatcher } from "../hooks/gmail-watcher.js"; import { clearInternalHooks, @@ -15,7 +15,7 @@ import { triggerInternalHook, } from "../hooks/internal-hooks.js"; import { loadInternalHooks } from "../hooks/loader.js"; -import type { loadMoltbotPlugins } from "../plugins/loader.js"; +import { isTruthyEnvValue } from "../infra/env.js"; import { type PluginServicesHandle, startPluginServices } from "../plugins/services.js"; import { startBrowserControlServerIfEnabled } from "./server-browser.js"; import { @@ -25,7 +25,7 @@ import { export async function startGatewaySidecars(params: { cfg: ReturnType; - pluginRegistry: ReturnType; + pluginRegistry: ReturnType; defaultWorkspaceDir: string; deps: CliDeps; startChannels: () => Promise; @@ -38,7 +38,7 @@ export async function startGatewaySidecars(params: { logChannels: { info: (msg: string) => void; error: (msg: string) => void }; logBrowser: { error: (msg: string) => void }; }) { - // Start clawd browser control server (unless disabled via config). + // Start OpenClaw browser control server (unless disabled via config). let browserControl: Awaited> = null; try { browserControl = await startBrowserControlServerIfEnabled(); @@ -47,7 +47,7 @@ export async function startGatewaySidecars(params: { } // Start Gmail watcher if configured (hooks.gmail.account). - if (!isTruthyEnvValue(process.env.CLAWDBOT_SKIP_GMAIL_WATCHER)) { + if (!isTruthyEnvValue(process.env.OPENCLAW_SKIP_GMAIL_WATCHER)) { try { const gmailResult = await startGmailWatcher(params.cfg); if (gmailResult.started) { @@ -112,10 +112,10 @@ export async function startGatewaySidecars(params: { } // Launch configured channels so gateway replies via the surface the message came from. - // Tests can opt out via CLAWDBOT_SKIP_CHANNELS (or legacy CLAWDBOT_SKIP_PROVIDERS). + // Tests can opt out via OPENCLAW_SKIP_CHANNELS (or legacy OPENCLAW_SKIP_PROVIDERS). const skipChannels = - isTruthyEnvValue(process.env.CLAWDBOT_SKIP_CHANNELS) || - isTruthyEnvValue(process.env.CLAWDBOT_SKIP_PROVIDERS); + isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) || + isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS); if (!skipChannels) { try { await params.startChannels(); @@ -124,7 +124,7 @@ export async function startGatewaySidecars(params: { } } else { params.logChannels.info( - "skipping channel start (CLAWDBOT_SKIP_CHANNELS=1 or CLAWDBOT_SKIP_PROVIDERS=1)", + "skipping channel start (OPENCLAW_SKIP_CHANNELS=1 or OPENCLAW_SKIP_PROVIDERS=1)", ); } diff --git a/src/gateway/server-utils.ts b/src/gateway/server-utils.ts index 43cbfa5a5..ed8127567 100644 --- a/src/gateway/server-utils.ts +++ b/src/gateway/server-utils.ts @@ -11,8 +11,12 @@ export function normalizeVoiceWakeTriggers(input: unknown): string[] { } export function formatError(err: unknown): string { - if (err instanceof Error) return err.message; - if (typeof err === "string") return err; + if (err instanceof Error) { + return err.message; + } + if (typeof err === "string") { + return err; + } const statusValue = (err as { status?: unknown })?.status; const codeValue = (err as { code?: unknown })?.code; const hasStatus = statusValue !== undefined; diff --git a/src/gateway/server-wizard-sessions.ts b/src/gateway/server-wizard-sessions.ts index bfa858874..9b2c3de45 100644 --- a/src/gateway/server-wizard-sessions.ts +++ b/src/gateway/server-wizard-sessions.ts @@ -5,15 +5,21 @@ export function createWizardSessionTracker() { const findRunningWizard = (): string | null => { for (const [id, session] of wizardSessions) { - if (session.getStatus() === "running") return id; + if (session.getStatus() === "running") { + return id; + } } return null; }; const purgeWizardSession = (id: string) => { const session = wizardSessions.get(id); - if (!session) return; - if (session.getStatus() === "running") return; + if (!session) { + return; + } + if (session.getStatus() === "running") { + return; + } wizardSessions.delete(id); }; diff --git a/src/gateway/server-ws-runtime.ts b/src/gateway/server-ws-runtime.ts index 6f12748e7..563caf89f 100644 --- a/src/gateway/server-ws-runtime.ts +++ b/src/gateway/server-ws-runtime.ts @@ -1,9 +1,9 @@ import type { WebSocketServer } from "ws"; import type { createSubsystemLogger } from "../logging/subsystem.js"; import type { ResolvedGatewayAuth } from "./auth.js"; -import { attachGatewayWsConnectionHandler } from "./server/ws-connection.js"; -import type { GatewayWsClient } from "./server/ws-types.js"; import type { GatewayRequestContext, GatewayRequestHandlers } from "./server-methods/types.js"; +import type { GatewayWsClient } from "./server/ws-types.js"; +import { attachGatewayWsConnectionHandler } from "./server/ws-connection.js"; export function attachGatewayWsHandlers(params: { wss: WebSocketServer; diff --git a/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts b/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts index ff8f34e67..b12093959 100644 --- a/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts @@ -113,9 +113,13 @@ const createStubChannelPlugin = (params: { deliveryMode: "direct", resolveTarget: ({ to, allowFrom }) => { const trimmed = to?.trim() ?? ""; - if (trimmed) return { ok: true, to: trimmed }; + if (trimmed) { + return { ok: true, to: trimmed }; + } const first = allowFrom?.[0]; - if (first) return { ok: true, to: String(first) }; + if (first) { + return { ok: true, to: String(first) }; + } return { ok: false, error: new Error(`missing target for ${params.id}`), @@ -167,7 +171,7 @@ describe("gateway server agent", () => { test("agent marks implicit delivery when lastTo is stale", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+436769770569"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -199,7 +203,7 @@ describe("gateway server agent", () => { test("agent forwards sessionKey to agentCommand", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -227,7 +231,7 @@ describe("gateway server agent", () => { test("agent derives sessionKey from agentId", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); testState.agentsConfig = { list: [{ id: "ops" }] }; await writeSessionStore({ @@ -285,7 +289,7 @@ describe("gateway server agent", () => { test("agent forwards accountId to agentCommand", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -320,7 +324,7 @@ describe("gateway server agent", () => { test("agent avoids lastAccountId when explicit to is provided", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -353,7 +357,7 @@ describe("gateway server agent", () => { test("agent keeps explicit accountId when explicit to is provided", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -387,7 +391,7 @@ describe("gateway server agent", () => { test("agent falls back to lastAccountId for implicit delivery", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -418,7 +422,7 @@ describe("gateway server agent", () => { test("agent forwards image attachments as images[]", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -459,7 +463,7 @@ describe("gateway server agent", () => { test("agent falls back to whatsapp when delivery requested and no last channel exists", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -488,7 +492,7 @@ describe("gateway server agent", () => { test("agent routes main last-channel whatsapp", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -521,7 +525,7 @@ describe("gateway server agent", () => { test("agent routes main last-channel telegram", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -553,7 +557,7 @@ describe("gateway server agent", () => { test("agent routes main last-channel discord", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -585,7 +589,7 @@ describe("gateway server agent", () => { test("agent routes main last-channel slack", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -617,7 +621,7 @@ describe("gateway server agent", () => { test("agent routes main last-channel signal", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { diff --git a/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts b/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts index 7c62b7d83..ceb01d498 100644 --- a/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts @@ -4,11 +4,11 @@ import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; import type { ChannelPlugin } from "../channels/plugins/types.js"; -import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js"; import type { PluginRegistry } from "../plugins/registry.js"; +import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; +import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; -import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; import { agentCommand, connectOk, @@ -137,7 +137,7 @@ describe("gateway server agent", () => { ]); registryState.registry = registry; setActivePluginRegistry(registry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -177,7 +177,7 @@ describe("gateway server agent", () => { ]); registryState.registry = registry; setActivePluginRegistry(registry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -231,7 +231,7 @@ describe("gateway server agent", () => { test("agent ignores webchat last-channel for routing", async () => { testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -262,7 +262,7 @@ describe("gateway server agent", () => { }); test("agent uses webchat for internal runs when last provider is webchat", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -395,7 +395,7 @@ describe("gateway server agent", () => { }); test("agent events stream to webchat clients when run context is registered", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { @@ -422,7 +422,9 @@ describe("gateway server agent", () => { const finalChatP = onceMessage( webchatWs, (o) => { - if (o.type !== "event" || o.event !== "chat") return false; + if (o.type !== "event" || o.event !== "chat") { + return false; + } const payload = o.payload as { state?: unknown; runId?: unknown } | undefined; return payload?.state === "final" && payload.runId === "run-auto-1"; }, diff --git a/src/gateway/server.auth.e2e.test.ts b/src/gateway/server.auth.e2e.test.ts index ee700ead6..cc8533f43 100644 --- a/src/gateway/server.auth.e2e.test.ts +++ b/src/gateway/server.auth.e2e.test.ts @@ -1,8 +1,9 @@ import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; +import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; +import { buildDeviceAuthPayload } from "./device-auth.js"; import { PROTOCOL_VERSION } from "./protocol/index.js"; import { getHandshakeTimeoutMs } from "./server-constants.js"; -import { buildDeviceAuthPayload } from "./device-auth.js"; import { connectReq, getFreePort, @@ -12,12 +13,13 @@ import { startServerWithClient, testState, } from "./test-helpers.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; installGatewayTestHooks({ scope: "suite" }); async function waitForWsClose(ws: WebSocket, timeoutMs: number): Promise { - if (ws.readyState === WebSocket.CLOSED) return true; + if (ws.readyState === WebSocket.CLOSED) { + return true; + } return await new Promise((resolve) => { const timer = setTimeout(() => resolve(ws.readyState === WebSocket.CLOSED), timeoutMs); ws.once("close", () => { @@ -49,8 +51,8 @@ describe("gateway server auth/connect", () => { test("closes silent handshakes after timeout", { timeout: 60_000 }, async () => { vi.useRealTimers(); - const prevHandshakeTimeout = process.env.CLAWDBOT_TEST_HANDSHAKE_TIMEOUT_MS; - process.env.CLAWDBOT_TEST_HANDSHAKE_TIMEOUT_MS = "50"; + const prevHandshakeTimeout = process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS; + process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS = "50"; try { const ws = await openWs(port); const handshakeTimeoutMs = getHandshakeTimeoutMs(); @@ -58,9 +60,9 @@ describe("gateway server auth/connect", () => { expect(closed).toBe(true); } finally { if (prevHandshakeTimeout === undefined) { - delete process.env.CLAWDBOT_TEST_HANDSHAKE_TIMEOUT_MS; + delete process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS; } else { - process.env.CLAWDBOT_TEST_HANDSHAKE_TIMEOUT_MS = prevHandshakeTimeout; + process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS = prevHandshakeTimeout; } } }); @@ -220,8 +222,8 @@ describe("gateway server auth/connect", () => { let prevToken: string | undefined; beforeAll(async () => { - prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - process.env.CLAWDBOT_GATEWAY_TOKEN = "secret"; + prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_GATEWAY_TOKEN = "secret"; port = await getFreePort(); server = await startGatewayServer(port); }); @@ -229,9 +231,9 @@ describe("gateway server auth/connect", () => { afterAll(async () => { await server.close(); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } }); @@ -294,9 +296,9 @@ describe("gateway server auth/connect", () => { ws.close(); await server.close(); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } }); @@ -309,8 +311,8 @@ describe("gateway server auth/connect", () => { trustedProxies: ["127.0.0.1"], }, } as any); - const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - process.env.CLAWDBOT_GATEWAY_TOKEN = "secret"; + const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_GATEWAY_TOKEN = "secret"; const port = await getFreePort(); const server = await startGatewayServer(port); const ws = new WebSocket(`ws://127.0.0.1:${port}`, { @@ -359,17 +361,17 @@ describe("gateway server auth/connect", () => { ws.close(); await server.close(); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } }); test("allows control ui with stale device identity when device auth is disabled", async () => { testState.gatewayControlUi = { dangerouslyDisableDeviceAuth: true }; testState.gatewayAuth = { mode: "token", token: "secret" }; - const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - process.env.CLAWDBOT_GATEWAY_TOKEN = "secret"; + const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_GATEWAY_TOKEN = "secret"; const port = await getFreePort(); const server = await startGatewayServer(port); const ws = await openWs(port); @@ -407,9 +409,9 @@ describe("gateway server auth/connect", () => { ws.close(); await server.close(); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } }); @@ -443,9 +445,9 @@ describe("gateway server auth/connect", () => { ws2.close(); await server.close(); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } }); @@ -461,7 +463,7 @@ describe("gateway server auth/connect", () => { const { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } = await import("../utils/message-channel.js"); const { server, ws, port, prevToken } = await startServerWithClient("secret"); - const identityDir = await mkdtemp(join(tmpdir(), "moltbot-device-scope-")); + const identityDir = await mkdtemp(join(tmpdir(), "openclaw-device-scope-")); const identity = loadOrCreateDeviceIdentity(join(identityDir, "device.json")); const client = { id: GATEWAY_CLIENT_NAMES.TEST, @@ -522,9 +524,9 @@ describe("gateway server auth/connect", () => { ws2.close(); await server.close(); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } }); @@ -560,9 +562,9 @@ describe("gateway server auth/connect", () => { ws2.close(); await server.close(); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } }); diff --git a/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts b/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts index 52e7b714e..6caefbe00 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, test, vi } from "vitest"; import { emitAgentEvent } from "../infra/agent-events.js"; +import { __setMaxChatHistoryMessagesBytesForTest } from "./server-constants.js"; import { connectOk, getReplyFromConfig, @@ -14,12 +15,13 @@ import { testState, writeSessionStore, } from "./test-helpers.js"; -import { __setMaxChatHistoryMessagesBytesForTest } from "./server-constants.js"; installGatewayTestHooks({ scope: "suite" }); async function waitFor(condition: () => boolean, timeoutMs = 1500) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - if (condition()) return; + if (condition()) { + return; + } await new Promise((r) => setTimeout(r, 5)); } throw new Error("timeout waiting for condition"); @@ -56,7 +58,7 @@ describe("gateway server chat", () => { const historyMaxBytes = 192 * 1024; __setMaxChatHistoryMessagesBytesForTest(historyMaxBytes); await connectOk(ws); - const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); tempDirs.push(sessionDir); testState.sessionStorePath = path.join(sessionDir, "sessions.json"); const writeStore = async ( @@ -111,9 +113,10 @@ describe("gateway server chat", () => { idempotencyKey: "idem-route", }); expect(routeRes.ok).toBe(true); - const stored = JSON.parse( - await fs.readFile(testState.sessionStorePath as string, "utf-8"), - ) as Record; + const stored = JSON.parse(await fs.readFile(testState.sessionStorePath, "utf-8")) as Record< + string, + { lastChannel?: string; lastTo?: string } | undefined + >; expect(stored["agent:main:main"]?.lastChannel).toBe("whatsapp"); expect(stored["agent:main:main"]?.lastTo).toBe("+1555"); @@ -126,8 +129,12 @@ describe("gateway server chat", () => { opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-1"); const signal = opts?.abortSignal; await new Promise((resolve) => { - if (!signal) return resolve(); - if (signal.aborted) return resolve(); + if (!signal) { + return resolve(); + } + if (signal.aborted) { + return resolve(); + } signal.addEventListener("abort", () => resolve(), { once: true }); }); }); @@ -154,9 +161,12 @@ describe("gateway server chat", () => { await new Promise((resolve, reject) => { const deadline = Date.now() + 1000; const tick = () => { - if (spy.mock.calls.length > callsBefore) return resolve(); - if (Date.now() > deadline) + if (spy.mock.calls.length > callsBefore) { + return resolve(); + } + if (Date.now() > deadline) { return reject(new Error("timeout waiting for getReplyFromConfig")); + } setTimeout(tick, 5); }; tick(); @@ -182,8 +192,12 @@ describe("gateway server chat", () => { opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-save-1"); const signal = opts?.abortSignal; await new Promise((resolve) => { - if (!signal) return resolve(); - if (signal.aborted) return resolve(); + if (!signal) { + return resolve(); + } + if (signal.aborted) { + return resolve(); + } signal.addEventListener("abort", () => resolve(), { once: true }); }); }); @@ -221,8 +235,12 @@ describe("gateway server chat", () => { opts?.onAgentRunStart?.(opts.runId ?? "idem-stop-1"); const signal = opts?.abortSignal; await new Promise((resolve) => { - if (!signal) return resolve(); - if (signal.aborted) return resolve(); + if (!signal) { + return resolve(); + } + if (signal.aborted) { + return resolve(); + } signal.addEventListener("abort", () => resolve(), { once: true }); }); }); @@ -302,8 +320,12 @@ describe("gateway server chat", () => { opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-all-1"); const signal = opts?.abortSignal; await new Promise((resolve) => { - if (!signal) return resolve(); - if (signal.aborted) return resolve(); + if (!signal) { + return resolve(); + } + if (signal.aborted) { + return resolve(); + } signal.addEventListener("abort", () => resolve(), { once: true }); }); }); @@ -368,8 +390,12 @@ describe("gateway server chat", () => { agentStartedResolve?.(); const signal = opts?.abortSignal; await new Promise((resolve) => { - if (!signal) return resolve(); - if (signal.aborted) return resolve(); + if (!signal) { + return resolve(); + } + if (signal.aborted) { + return resolve(); + } signal.addEventListener("abort", () => resolve(), { once: true }); }); }); diff --git a/src/gateway/server.chat.gateway-server-chat.e2e.test.ts b/src/gateway/server.chat.gateway-server-chat.e2e.test.ts index f550dc588..59c0bec18 100644 --- a/src/gateway/server.chat.gateway-server-chat.e2e.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.e2e.test.ts @@ -3,8 +3,8 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js"; +import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { connectOk, getReplyFromConfig, @@ -38,7 +38,9 @@ afterAll(async () => { async function waitFor(condition: () => boolean, timeoutMs = 1500) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - if (condition()) return; + if (condition()) { + return; + } await new Promise((r) => setTimeout(r, 5)); } throw new Error("timeout waiting for condition"); @@ -100,7 +102,7 @@ describe("gateway server chat", () => { const sessionCall = spy.mock.calls.at(-1)?.[0] as { SessionKey?: string } | undefined; expect(sessionCall?.SessionKey).toBe("agent:main:subagent:abc"); - const sendPolicyDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const sendPolicyDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); tempDirs.push(sendPolicyDir); testState.sessionStorePath = path.join(sendPolicyDir, "sessions.json"); testState.sessionConfig = { @@ -139,7 +141,7 @@ describe("gateway server chat", () => { testState.sessionStorePath = undefined; testState.sessionConfig = undefined; - const agentBlockedDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const agentBlockedDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); tempDirs.push(agentBlockedDir); testState.sessionStorePath = path.join(agentBlockedDir, "sessions.json"); testState.sessionConfig = { @@ -241,7 +243,7 @@ describe("gateway server chat", () => { | undefined; expect(imgOnlyOpts?.images).toEqual([{ type: "image", data: pngB64, mimeType: "image/png" }]); - const historyDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const historyDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); tempDirs.push(historyDir); testState.sessionStorePath = path.join(historyDir, "sessions.json"); await writeSessionStore({ @@ -273,11 +275,17 @@ describe("gateway server chat", () => { expect(defaultRes.ok).toBe(true); const defaultMsgs = defaultRes.payload?.messages ?? []; const firstContentText = (msg: unknown): string | undefined => { - if (!msg || typeof msg !== "object") return undefined; + if (!msg || typeof msg !== "object") { + return undefined; + } const content = (msg as { content?: unknown }).content; - if (!Array.isArray(content) || content.length === 0) return undefined; + if (!Array.isArray(content) || content.length === 0) { + return undefined; + } const first = content[0]; - if (!first || typeof first !== "object") return undefined; + if (!first || typeof first !== "object") { + return undefined; + } const text = (first as { text?: unknown }).text; return typeof text === "string" ? text : undefined; }; @@ -287,13 +295,15 @@ describe("gateway server chat", () => { testState.agentConfig = undefined; testState.sessionStorePath = undefined; testState.sessionConfig = undefined; - if (webchatWs) webchatWs.close(); + if (webchatWs) { + webchatWs.close(); + } await Promise.all(tempDirs.map((dir) => fs.rm(dir, { recursive: true, force: true }))); } }); test("routes chat.send slash commands without agent runs", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); try { testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ @@ -332,7 +342,7 @@ describe("gateway server chat", () => { }); test("agent events include sessionKey and agent.wait covers lifecycle flows", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await writeSessionStore({ entries: { diff --git a/src/gateway/server.config-apply.e2e.test.ts b/src/gateway/server.config-apply.e2e.test.ts index 9547c3664..2172555fb 100644 --- a/src/gateway/server.config-apply.e2e.test.ts +++ b/src/gateway/server.config-apply.e2e.test.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { WebSocket } from "ws"; - import { connectOk, getFreePort, @@ -19,16 +18,19 @@ let port = 0; let previousToken: string | undefined; beforeAll(async () => { - previousToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + previousToken = process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; port = await getFreePort(); server = await startGatewayServer(port); }); afterAll(async () => { await server.close(); - if (previousToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; - else process.env.CLAWDBOT_GATEWAY_TOKEN = previousToken; + if (previousToken === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previousToken; + } }); const openClient = async () => { @@ -49,7 +51,7 @@ describe("gateway config.apply", () => { id, method: "config.apply", params: { - raw: '{ "agents": { "list": [{ "id": "main", "workspace": "~/clawd" }] } }', + raw: '{ "agents": { "list": [{ "id": "main", "workspace": "~/openclaw" }] } }', sessionKey: "agent:main:whatsapp:dm:+15555550123", restartDelayMs: 0, }, @@ -62,7 +64,7 @@ describe("gateway config.apply", () => { expect(res.ok).toBe(true); // Verify sentinel file was created (restart was scheduled) - const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json"); + const sentinelPath = path.join(os.homedir(), ".openclaw", "restart-sentinel.json"); // Wait for file to be written await new Promise((resolve) => setTimeout(resolve, 100)); diff --git a/src/gateway/server.config-patch.e2e.test.ts b/src/gateway/server.config-patch.e2e.test.ts index 193bb4f06..0ce19ebe3 100644 --- a/src/gateway/server.config-patch.e2e.test.ts +++ b/src/gateway/server.config-patch.e2e.test.ts @@ -2,9 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; - import { resolveConfigSnapshotHash } from "../config/config.js"; - import { connectOk, installGatewayTestHooks, @@ -190,7 +188,7 @@ describe("gateway config.patch", () => { ); expect(patchRes.ok).toBe(true); - const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json"); + const sentinelPath = path.join(os.homedir(), ".openclaw", "restart-sentinel.json"); await new Promise((resolve) => setTimeout(resolve, 100)); try { @@ -288,7 +286,7 @@ describe("gateway config.patch", () => { describe("gateway server sessions", () => { it("filters sessions by agentId", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-sessions-agents-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-agents-")); testState.sessionConfig = { store: path.join(dir, "{agentId}", "sessions.json"), }; @@ -332,7 +330,7 @@ describe("gateway server sessions", () => { agentId: "home", }); expect(homeSessions.ok).toBe(true); - expect(homeSessions.payload?.sessions.map((s) => s.key).sort()).toEqual([ + expect(homeSessions.payload?.sessions.map((s) => s.key).toSorted()).toEqual([ "agent:home:discord:group:dev", "agent:home:main", ]); @@ -349,7 +347,7 @@ describe("gateway server sessions", () => { }); it("resolves and patches main alias to default agent main key", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-sessions-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-")); const storePath = path.join(dir, "sessions.json"); testState.sessionStorePath = storePath; testState.agentsConfig = { list: [{ id: "ops", default: true }] }; diff --git a/src/gateway/server.cron.e2e.test.ts b/src/gateway/server.cron.e2e.test.ts index f63efdd94..f7d898299 100644 --- a/src/gateway/server.cron.e2e.test.ts +++ b/src/gateway/server.cron.e2e.test.ts @@ -39,7 +39,9 @@ async function waitForNonEmptyFile(pathname: string, timeoutMs = 2000) { const startedAt = process.hrtime.bigint(); for (;;) { const raw = await fs.readFile(pathname, "utf-8").catch(() => ""); - if (raw.trim().length > 0) return raw; + if (raw.trim().length > 0) { + return raw; + } const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6; if (elapsedMs >= timeoutMs) { throw new Error(`timeout waiting for file ${pathname}`); @@ -50,9 +52,9 @@ async function waitForNonEmptyFile(pathname: string, timeoutMs = 2000) { describe("gateway server cron", () => { test("handles cron CRUD, normalization, and patch semantics", { timeout: 120_000 }, async () => { - const prevSkipCron = process.env.CLAWDBOT_SKIP_CRON; - process.env.CLAWDBOT_SKIP_CRON = "0"; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-cron-")); + const prevSkipCron = process.env.OPENCLAW_SKIP_CRON; + process.env.OPENCLAW_SKIP_CRON = "0"; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-cron-")); testState.cronStorePath = path.join(dir, "cron", "jobs.json"); testState.sessionConfig = { mainKey: "primary" }; testState.cronEnabled = false; @@ -256,17 +258,17 @@ describe("gateway server cron", () => { testState.sessionConfig = undefined; testState.cronEnabled = undefined; if (prevSkipCron === undefined) { - delete process.env.CLAWDBOT_SKIP_CRON; + delete process.env.OPENCLAW_SKIP_CRON; } else { - process.env.CLAWDBOT_SKIP_CRON = prevSkipCron; + process.env.OPENCLAW_SKIP_CRON = prevSkipCron; } } }); test("writes cron run history and auto-runs due jobs", async () => { - const prevSkipCron = process.env.CLAWDBOT_SKIP_CRON; - process.env.CLAWDBOT_SKIP_CRON = "0"; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gw-cron-log-")); + const prevSkipCron = process.env.OPENCLAW_SKIP_CRON; + process.env.OPENCLAW_SKIP_CRON = "0"; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-cron-log-")); testState.cronStorePath = path.join(dir, "cron", "jobs.json"); testState.cronEnabled = undefined; await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true }); @@ -353,9 +355,9 @@ describe("gateway server cron", () => { testState.cronStorePath = undefined; testState.cronEnabled = undefined; if (prevSkipCron === undefined) { - delete process.env.CLAWDBOT_SKIP_CRON; + delete process.env.OPENCLAW_SKIP_CRON; } else { - process.env.CLAWDBOT_SKIP_CRON = prevSkipCron; + process.env.OPENCLAW_SKIP_CRON = prevSkipCron; } } }, 45_000); diff --git a/src/gateway/server.health.e2e.test.ts b/src/gateway/server.health.e2e.test.ts index 29511372e..797e3b646 100644 --- a/src/gateway/server.health.e2e.test.ts +++ b/src/gateway/server.health.e2e.test.ts @@ -11,6 +11,7 @@ import { } from "../infra/device-identity.js"; import { emitHeartbeatEvent } from "../infra/heartbeat-events.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; +import { buildDeviceAuthPayload } from "./device-auth.js"; import { connectOk, getFreePort, @@ -19,7 +20,6 @@ import { startGatewayServer, startServerWithClient, } from "./test-helpers.js"; -import { buildDeviceAuthPayload } from "./device-auth.js"; installGatewayTestHooks({ scope: "suite" }); @@ -28,16 +28,19 @@ let port = 0; let previousToken: string | undefined; beforeAll(async () => { - previousToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + previousToken = process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; port = await getFreePort(); server = await startGatewayServer(port); }); afterAll(async () => { await server.close(); - if (previousToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; - else process.env.CLAWDBOT_GATEWAY_TOKEN = previousToken; + if (previousToken === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previousToken; + } }); const openClient = async (opts?: Parameters[1]) => { @@ -210,11 +213,13 @@ describe("gateway server health/presence", () => { expect(evt.payload?.presence?.length).toBeGreaterThan(0); expect(typeof evt.seq).toBe("number"); } - for (const c of clients) c.close(); + for (const c of clients) { + c.close(); + } }); test("presence includes client fingerprint", async () => { - const identityPath = path.join(os.tmpdir(), `moltbot-device-${randomUUID()}.json`); + const identityPath = path.join(os.tmpdir(), `openclaw-device-${randomUUID()}.json`); const identity = loadOrCreateDeviceIdentity(identityPath); const role = "operator"; const scopes: string[] = []; diff --git a/src/gateway/server.hooks.e2e.test.ts b/src/gateway/server.hooks.e2e.test.ts index 0eebeac80..97e4e37ef 100644 --- a/src/gateway/server.hooks.e2e.test.ts +++ b/src/gateway/server.hooks.e2e.test.ts @@ -108,7 +108,7 @@ describe("gateway server hooks", () => { method: "POST", headers: { "Content-Type": "application/json", - "x-moltbot-token": "hook-secret", + "x-openclaw-token": "hook-secret", }, body: JSON.stringify({ text: "Header auth" }), }); diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index f641c4076..9e5142f13 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1,10 +1,13 @@ -import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { initSubagentRegistry } from "../agents/subagent-registry.js"; -import { registerSkillsChangeListener } from "../agents/skills/refresh.js"; import type { CanvasHostServer } from "../canvas-host/server.js"; +import type { PluginServicesHandle } from "../plugins/services.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { startBrowserControlServerIfEnabled } from "./server-browser.js"; +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { registerSkillsChangeListener } from "../agents/skills/refresh.js"; +import { initSubagentRegistry } from "../agents/subagent-registry.js"; import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js"; -import { createDefaultDeps } from "../cli/deps.js"; import { formatCliCommand } from "../cli/command-format.js"; +import { createDefaultDeps } from "../cli/deps.js"; import { CONFIG_PATH, isNixMode, @@ -13,27 +16,52 @@ import { readConfigFileSnapshot, writeConfigFile, } from "../config/config.js"; -import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js"; -import { logAcceptedEnvOption } from "../infra/env.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js"; +import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js"; +import { logAcceptedEnvOption } from "../infra/env.js"; +import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js"; import { onHeartbeatEvent } from "../infra/heartbeat-events.js"; import { startHeartbeatRunner } from "../infra/heartbeat-runner.js"; import { getMachineDisplayName } from "../infra/machine-name.js"; -import { ensureMoltbotCliOnPath } from "../infra/path-env.js"; +import { ensureOpenClawCliOnPath } from "../infra/path-env.js"; +import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js"; import { primeRemoteSkillsCache, refreshRemoteBinsForConnectedNodes, setSkillsRemoteRegistry, } from "../infra/skills-remote.js"; import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js"; -import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js"; import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js"; import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js"; -import type { PluginServicesHandle } from "../plugins/services.js"; -import type { RuntimeEnv } from "../runtime.js"; import { runOnboardingWizard } from "../wizard/onboarding.js"; import { startGatewayConfigReloader } from "./config-reload.js"; +import { ExecApprovalManager } from "./exec-approval-manager.js"; +import { NodeRegistry } from "./node-registry.js"; +import { createChannelManager } from "./server-channels.js"; +import { createAgentEventHandler } from "./server-chat.js"; +import { createGatewayCloseHandler } from "./server-close.js"; +import { buildGatewayCronService } from "./server-cron.js"; +import { startGatewayDiscovery } from "./server-discovery-runtime.js"; +import { applyGatewayLaneConcurrency } from "./server-lanes.js"; +import { startGatewayMaintenanceTimers } from "./server-maintenance.js"; +import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js"; +import { coreGatewayHandlers } from "./server-methods.js"; +import { createExecApprovalHandlers } from "./server-methods/exec-approval.js"; +import { safeParseJson } from "./server-methods/nodes.helpers.js"; +import { hasConnectedMobileNode } from "./server-mobile-nodes.js"; +import { loadGatewayModelCatalog } from "./server-model-catalog.js"; +import { createNodeSubscriptionManager } from "./server-node-subscriptions.js"; +import { loadGatewayPlugins } from "./server-plugins.js"; +import { createGatewayReloadHandlers } from "./server-reload-handlers.js"; +import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js"; +import { createGatewayRuntimeState } from "./server-runtime-state.js"; +import { resolveSessionKeyForRun } from "./server-session-key.js"; +import { logGatewayStartup } from "./server-startup-log.js"; +import { startGatewaySidecars } from "./server-startup.js"; +import { startGatewayTailscaleExposure } from "./server-tailscale.js"; +import { createWizardSessionTracker } from "./server-wizard-sessions.js"; +import { attachGatewayWsHandlers } from "./server-ws-runtime.js"; import { getHealthCache, getHealthVersion, @@ -41,39 +69,11 @@ import { incrementPresenceVersion, refreshGatewayHealthSnapshot, } from "./server/health-state.js"; -import { startGatewayDiscovery } from "./server-discovery-runtime.js"; -import { ExecApprovalManager } from "./exec-approval-manager.js"; -import { createExecApprovalHandlers } from "./server-methods/exec-approval.js"; -import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js"; -import type { startBrowserControlServerIfEnabled } from "./server-browser.js"; -import { createChannelManager } from "./server-channels.js"; -import { createAgentEventHandler } from "./server-chat.js"; -import { createGatewayCloseHandler } from "./server-close.js"; -import { buildGatewayCronService } from "./server-cron.js"; -import { applyGatewayLaneConcurrency } from "./server-lanes.js"; -import { startGatewayMaintenanceTimers } from "./server-maintenance.js"; -import { coreGatewayHandlers } from "./server-methods.js"; -import { GATEWAY_EVENTS, listGatewayMethods } from "./server-methods-list.js"; -import { loadGatewayModelCatalog } from "./server-model-catalog.js"; -import { NodeRegistry } from "./node-registry.js"; -import { createNodeSubscriptionManager } from "./server-node-subscriptions.js"; -import { safeParseJson } from "./server-methods/nodes.helpers.js"; -import { loadGatewayPlugins } from "./server-plugins.js"; -import { createGatewayReloadHandlers } from "./server-reload-handlers.js"; -import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js"; -import { createGatewayRuntimeState } from "./server-runtime-state.js"; -import { hasConnectedMobileNode } from "./server-mobile-nodes.js"; -import { resolveSessionKeyForRun } from "./server-session-key.js"; -import { startGatewaySidecars } from "./server-startup.js"; -import { logGatewayStartup } from "./server-startup-log.js"; -import { startGatewayTailscaleExposure } from "./server-tailscale.js"; import { loadGatewayTlsRuntime } from "./server/tls.js"; -import { createWizardSessionTracker } from "./server-wizard-sessions.js"; -import { attachGatewayWsHandlers } from "./server-ws-runtime.js"; export { __resetModelCatalogCacheForTest } from "./server-model-catalog.js"; -ensureMoltbotCliOnPath(); +ensureOpenClawCliOnPath(); const log = createSubsystemLogger("gateway"); const logCanvas = log.child("canvas"); @@ -149,13 +149,13 @@ export async function startGatewayServer( opts: GatewayServerOptions = {}, ): Promise { // Ensure all default port derivations (browser/canvas) see the actual runtime port. - process.env.CLAWDBOT_GATEWAY_PORT = String(port); + process.env.OPENCLAW_GATEWAY_PORT = String(port); logAcceptedEnvOption({ - key: "CLAWDBOT_RAW_STREAM", + key: "OPENCLAW_RAW_STREAM", description: "raw stream logging enabled", }); logAcceptedEnvOption({ - key: "CLAWDBOT_RAW_STREAM_PATH", + key: "OPENCLAW_RAW_STREAM_PATH", description: "raw stream log path override", }); @@ -169,7 +169,7 @@ export async function startGatewayServer( const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed); if (!migrated) { throw new Error( - `Legacy config entries detected but auto-migration failed. Run "${formatCliCommand("moltbot doctor")}" to migrate.`, + `Legacy config entries detected but auto-migration failed. Run "${formatCliCommand("openclaw doctor")}" to migrate.`, ); } await writeConfigFile(migrated); @@ -191,7 +191,7 @@ export async function startGatewayServer( .join("\n") : "Unknown validation issue."; throw new Error( - `Invalid config at ${configSnapshot.path}.\n${issues}\nRun "${formatCliCommand("moltbot doctor")}" to repair, then retry.`, + `Invalid config at ${configSnapshot.path}.\n${issues}\nRun "${formatCliCommand("openclaw doctor")}" to repair, then retry.`, ); } @@ -351,6 +351,7 @@ export async function startGatewayServer( ? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 } : undefined, wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true, + wideAreaDiscoveryDomain: cfgAtStart.discovery?.wideArea?.domain, tailscaleMode, mdnsMode: cfgAtStart.discovery?.mdns?.mode, logDiscovery, @@ -365,8 +366,12 @@ export async function startGatewayServer( let skillsRefreshTimer: ReturnType | null = null; const skillsRefreshDelayMs = 30_000; const skillsChangeUnsub = registerSkillsChangeListener((event) => { - if (event.reason === "remote-node") return; - if (skillsRefreshTimer) clearTimeout(skillsRefreshTimer); + if (event.reason === "remote-node") { + return; + } + if (skillsRefreshTimer) { + clearTimeout(skillsRefreshTimer); + } skillsRefreshTimer = setTimeout(() => { skillsRefreshTimer = null; const latest = loadConfig(); diff --git a/src/gateway/server.ios-client-id.e2e.test.ts b/src/gateway/server.ios-client-id.e2e.test.ts index 086d14d27..f612bdcf0 100644 --- a/src/gateway/server.ios-client-id.e2e.test.ts +++ b/src/gateway/server.ios-client-id.e2e.test.ts @@ -1,6 +1,5 @@ import { afterAll, beforeAll, test } from "vitest"; import WebSocket from "ws"; - import { PROTOCOL_VERSION } from "./protocol/index.js"; import { getFreePort, onceMessage, startGatewayServer } from "./test-helpers.server.js"; @@ -54,11 +53,11 @@ function connectReq( ); } -test("accepts moltbot-ios as a valid gateway client id", async () => { +test("accepts openclaw-ios as a valid gateway client id", async () => { const ws = new WebSocket(`ws://127.0.0.1:${port}`); await new Promise((resolve) => ws.once("open", resolve)); - const res = await connectReq(ws, { clientId: "moltbot-ios", platform: "ios" }); + const res = await connectReq(ws, { clientId: "openclaw-ios", platform: "ios" }); // We don't care if auth fails here; we only care that schema validation accepts the client id. // A schema rejection would close the socket before sending a response. if (!res.ok) { @@ -73,11 +72,11 @@ test("accepts moltbot-ios as a valid gateway client id", async () => { ws.close(); }); -test("accepts moltbot-android as a valid gateway client id", async () => { +test("accepts openclaw-android as a valid gateway client id", async () => { const ws = new WebSocket(`ws://127.0.0.1:${port}`); await new Promise((resolve) => ws.once("open", resolve)); - const res = await connectReq(ws, { clientId: "moltbot-android", platform: "android" }); + const res = await connectReq(ws, { clientId: "openclaw-android", platform: "android" }); // We don't care if auth fails here; we only care that schema validation accepts the client id. // A schema rejection would close the socket before sending a response. if (!res.ok) { diff --git a/src/gateway/server.models-voicewake-misc.e2e.test.ts b/src/gateway/server.models-voicewake-misc.e2e.test.ts index 775013e04..27ae4237a 100644 --- a/src/gateway/server.models-voicewake-misc.e2e.test.ts +++ b/src/gateway/server.models-voicewake-misc.e2e.test.ts @@ -4,12 +4,11 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { WebSocket } from "ws"; - -import { getChannelPlugin } from "../channels/plugins/index.js"; import type { ChannelOutboundAdapter } from "../channels/plugins/types.js"; +import type { PluginRegistry } from "../plugins/registry.js"; +import { getChannelPlugin } from "../channels/plugins/index.js"; import { resolveCanvasHostUrl } from "../infra/canvas-host-url.js"; import { GatewayLockError } from "../infra/gateway-lock.js"; -import type { PluginRegistry } from "../plugins/registry.js"; import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js"; import { createOutboundTestPlugin } from "../test-utils/channel-plugins.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; @@ -93,12 +92,12 @@ const emptyRegistry = createRegistry([]); describe("gateway server models + voicewake", () => { const setTempHome = (homeDir: string) => { const prevHome = process.env.HOME; - const prevStateDir = process.env.CLAWDBOT_STATE_DIR; + const prevStateDir = process.env.OPENCLAW_STATE_DIR; const prevUserProfile = process.env.USERPROFILE; const prevHomeDrive = process.env.HOMEDRIVE; const prevHomePath = process.env.HOMEPATH; process.env.HOME = homeDir; - process.env.CLAWDBOT_STATE_DIR = path.join(homeDir, ".clawdbot"); + process.env.OPENCLAW_STATE_DIR = path.join(homeDir, ".openclaw"); process.env.USERPROFILE = homeDir; if (process.platform === "win32") { const parsed = path.parse(homeDir); @@ -112,9 +111,9 @@ describe("gateway server models + voicewake", () => { process.env.HOME = prevHome; } if (prevStateDir === undefined) { - delete process.env.CLAWDBOT_STATE_DIR; + delete process.env.OPENCLAW_STATE_DIR; } else { - process.env.CLAWDBOT_STATE_DIR = prevStateDir; + process.env.OPENCLAW_STATE_DIR = prevStateDir; } if (prevUserProfile === undefined) { delete process.env.USERPROFILE; @@ -140,12 +139,12 @@ describe("gateway server models + voicewake", () => { "voicewake.get returns defaults and voicewake.set broadcasts", { timeout: 60_000 }, async () => { - const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-home-")); + const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-home-")); const restoreHome = setTempHome(homeDir); const initial = await rpcReq<{ triggers: string[] }>(ws, "voicewake.get"); expect(initial.ok).toBe(true); - expect(initial.payload?.triggers).toEqual(["clawd", "claude", "computer"]); + expect(initial.payload?.triggers).toEqual(["openclaw", "claude", "computer"]); const changedP = onceMessage<{ type: "event"; @@ -171,7 +170,7 @@ describe("gateway server models + voicewake", () => { expect(after.payload?.triggers).toEqual(["hi", "there"]); const onDisk = JSON.parse( - await fs.readFile(path.join(homeDir, ".clawdbot", "settings", "voicewake.json"), "utf8"), + await fs.readFile(path.join(homeDir, ".openclaw", "settings", "voicewake.json"), "utf8"), ) as { triggers?: unknown; updatedAtMs?: unknown }; expect(onDisk.triggers).toEqual(["hi", "there"]); expect(typeof onDisk.updatedAtMs).toBe("number"); @@ -181,7 +180,7 @@ describe("gateway server models + voicewake", () => { ); test("pushes voicewake.changed to nodes on connect and on updates", async () => { - const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-home-")); + const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-home-")); const restoreHome = setTempHome(homeDir); const nodeWs = new WebSocket(`ws://127.0.0.1:${port}`); @@ -203,7 +202,7 @@ describe("gateway server models + voicewake", () => { const first = await firstEventP; expect(first.event).toBe("voicewake.changed"); expect((first.payload as { triggers?: unknown } | undefined)?.triggers).toEqual([ - "clawd", + "openclaw", "claude", "computer", ]); @@ -213,14 +212,14 @@ describe("gateway server models + voicewake", () => { (o) => o.type === "event" && o.event === "voicewake.changed", ); const setRes = await rpcReq<{ triggers: string[] }>(ws, "voicewake.set", { - triggers: ["clawd", "computer"], + triggers: ["openclaw", "computer"], }); expect(setRes.ok).toBe(true); const broadcast = await broadcastP; expect(broadcast.event).toBe("voicewake.changed"); expect((broadcast.payload as { triggers?: unknown } | undefined)?.triggers).toEqual([ - "clawd", + "openclaw", "computer", ]); @@ -315,14 +314,14 @@ describe("gateway server models + voicewake", () => { describe("gateway server misc", () => { test("hello-ok advertises the gateway port for canvas host", async () => { - const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - const prevCanvasPort = process.env.CLAWDBOT_CANVAS_HOST_PORT; - process.env.CLAWDBOT_GATEWAY_TOKEN = "secret"; + const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; + const prevCanvasPort = process.env.OPENCLAW_CANVAS_HOST_PORT; + process.env.OPENCLAW_GATEWAY_TOKEN = "secret"; testTailnetIPv4.value = "100.64.0.1"; testState.gatewayBind = "lan"; const canvasPort = await getFreePort(); testState.canvasHostPort = canvasPort; - process.env.CLAWDBOT_CANVAS_HOST_PORT = String(canvasPort); + process.env.OPENCLAW_CANVAS_HOST_PORT = String(canvasPort); const testPort = await getFreePort(); const canvasHostUrl = resolveCanvasHostUrl({ @@ -332,14 +331,14 @@ describe("gateway server misc", () => { }); expect(canvasHostUrl).toBe(`http://100.64.0.1:${canvasPort}`); if (prevToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; } if (prevCanvasPort === undefined) { - delete process.env.CLAWDBOT_CANVAS_HOST_PORT; + delete process.env.OPENCLAW_CANVAS_HOST_PORT; } else { - process.env.CLAWDBOT_CANVAS_HOST_PORT = prevCanvasPort; + process.env.OPENCLAW_CANVAS_HOST_PORT = prevCanvasPort; } }); @@ -375,8 +374,10 @@ describe("gateway server misc", () => { }); test("auto-enables configured channel plugins on startup", async () => { - const configPath = process.env.CLAWDBOT_CONFIG_PATH; - if (!configPath) throw new Error("Missing CLAWDBOT_CONFIG_PATH"); + const configPath = process.env.OPENCLAW_CONFIG_PATH; + if (!configPath) { + throw new Error("Missing OPENCLAW_CONFIG_PATH"); + } await fs.mkdir(path.dirname(configPath), { recursive: true }); await fs.writeFile( configPath, diff --git a/src/gateway/server.nodes.late-invoke.test.ts b/src/gateway/server.nodes.late-invoke.test.ts index 52f73e898..36a7972e3 100644 --- a/src/gateway/server.nodes.late-invoke.test.ts +++ b/src/gateway/server.nodes.late-invoke.test.ts @@ -1,8 +1,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; - -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; +import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; vi.mock("../infra/update-runner.js", () => ({ runGatewayUpdate: vi.fn(async () => ({ diff --git a/src/gateway/server.reload.e2e.test.ts b/src/gateway/server.reload.e2e.test.ts index b9fc76d98..f991d07c9 100644 --- a/src/gateway/server.reload.e2e.test.ts +++ b/src/gateway/server.reload.e2e.test.ts @@ -115,8 +115,8 @@ const hoisted = vi.hoisted(() => { const startGatewayConfigReloader = vi.fn( (opts: { onHotReload: typeof onHotReload; onRestart: typeof onRestart }) => { - onHotReload = opts.onHotReload as typeof onHotReload; - onRestart = opts.onRestart as typeof onRestart; + onHotReload = opts.onHotReload; + onRestart = opts.onRestart; return { stop: reloaderStop }; }, ); @@ -172,22 +172,22 @@ describe("gateway hot reload", () => { let prevSkipGmail: string | undefined; beforeEach(() => { - prevSkipChannels = process.env.CLAWDBOT_SKIP_CHANNELS; - prevSkipGmail = process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; - process.env.CLAWDBOT_SKIP_CHANNELS = "0"; - delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; + prevSkipChannels = process.env.OPENCLAW_SKIP_CHANNELS; + prevSkipGmail = process.env.OPENCLAW_SKIP_GMAIL_WATCHER; + process.env.OPENCLAW_SKIP_CHANNELS = "0"; + delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER; }); afterEach(() => { if (prevSkipChannels === undefined) { - delete process.env.CLAWDBOT_SKIP_CHANNELS; + delete process.env.OPENCLAW_SKIP_CHANNELS; } else { - process.env.CLAWDBOT_SKIP_CHANNELS = prevSkipChannels; + process.env.OPENCLAW_SKIP_CHANNELS = prevSkipChannels; } if (prevSkipGmail === undefined) { - delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; + delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER; } else { - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prevSkipGmail; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prevSkipGmail; } }); diff --git a/src/gateway/server.roles-allowlist-update.e2e.test.ts b/src/gateway/server.roles-allowlist-update.e2e.test.ts index 275970c44..406ba342c 100644 --- a/src/gateway/server.roles-allowlist-update.e2e.test.ts +++ b/src/gateway/server.roles-allowlist-update.e2e.test.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; - import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { GatewayClient } from "./client.js"; @@ -73,17 +72,23 @@ const connectNodeClient = async (params: { commands: params.commands, onEvent: params.onEvent, onHelloOk: () => { - if (settled) return; + if (settled) { + return; + } settled = true; resolveReady?.(); }, onConnectError: (err) => { - if (settled) return; + if (settled) { + return; + } settled = true; rejectReady?.(err); }, onClose: (code, reason) => { - if (settled) return; + if (settled) { + return; + } settled = true; rejectReady?.(new Error(`gateway closed (${code}): ${reason}`)); }, @@ -101,7 +106,9 @@ const connectNodeClient = async (params: { async function waitForSignal(check: () => boolean, timeoutMs = 2000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { - if (check()) return; + if (check()) { + return; + } await new Promise((resolve) => setTimeout(resolve, 10)); } throw new Error("timeout"); @@ -176,7 +183,7 @@ describe("gateway update.run", () => { await waitForSignal(() => sigusr1.mock.calls.length > 0); expect(sigusr1).toHaveBeenCalled(); - const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json"); + const sentinelPath = path.join(os.homedir(), ".openclaw", "restart-sentinel.json"); const raw = await fs.readFile(sentinelPath, "utf-8"); const parsed = JSON.parse(raw) as { payload?: { kind?: string; stats?: { mode?: string } }; diff --git a/src/gateway/server.sessions-send.e2e.test.ts b/src/gateway/server.sessions-send.e2e.test.ts index 2829c531e..52a3d380e 100644 --- a/src/gateway/server.sessions-send.e2e.test.ts +++ b/src/gateway/server.sessions-send.e2e.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { createMoltbotTools } from "../agents/moltbot-tools.js"; +import { createOpenClawTools } from "../agents/openclaw-tools.js"; import { resolveSessionTranscriptPath } from "../config/sessions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; import { @@ -19,25 +19,25 @@ let prevGatewayPort: string | undefined; let prevGatewayToken: string | undefined; beforeAll(async () => { - prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; - prevGatewayToken = process.env.CLAWDBOT_GATEWAY_TOKEN; + prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; + prevGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; gatewayPort = await getFreePort(); - process.env.CLAWDBOT_GATEWAY_PORT = String(gatewayPort); - process.env.CLAWDBOT_GATEWAY_TOKEN = "test-token"; + process.env.OPENCLAW_GATEWAY_PORT = String(gatewayPort); + process.env.OPENCLAW_GATEWAY_TOKEN = "test-token"; server = await startGatewayServer(gatewayPort); }); afterAll(async () => { await server.close(); if (prevGatewayPort === undefined) { - delete process.env.CLAWDBOT_GATEWAY_PORT; + delete process.env.OPENCLAW_GATEWAY_PORT; } else { - process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; } if (prevGatewayToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = prevGatewayToken; + process.env.OPENCLAW_GATEWAY_TOKEN = prevGatewayToken; } }); @@ -86,8 +86,10 @@ describe("sessions_send gateway loopback", () => { }); }); - const tool = createMoltbotTools().find((candidate) => candidate.name === "sessions_send"); - if (!tool) throw new Error("missing sessions_send tool"); + const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send"); + if (!tool) { + throw new Error("missing sessions_send tool"); + } const result = await tool.execute("call-loopback", { sessionKey: "main", @@ -151,8 +153,10 @@ describe("sessions_send label lookup", () => { timeoutMs: 5000, }); - const tool = createMoltbotTools().find((candidate) => candidate.name === "sessions_send"); - if (!tool) throw new Error("missing sessions_send tool"); + const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send"); + if (!tool) { + throw new Error("missing sessions_send tool"); + } // Send using label instead of sessionKey const result = await tool.execute("call-by-label", { @@ -171,8 +175,10 @@ describe("sessions_send label lookup", () => { }); it("returns error when label not found", { timeout: 60_000 }, async () => { - const tool = createMoltbotTools().find((candidate) => candidate.name === "sessions_send"); - if (!tool) throw new Error("missing sessions_send tool"); + const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send"); + if (!tool) { + throw new Error("missing sessions_send tool"); + } const result = await tool.execute("call-missing-label", { label: "nonexistent-label", @@ -185,8 +191,10 @@ describe("sessions_send label lookup", () => { }); it("returns error when neither sessionKey nor label provided", { timeout: 60_000 }, async () => { - const tool = createMoltbotTools().find((candidate) => candidate.name === "sessions_send"); - if (!tool) throw new Error("missing sessions_send tool"); + const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send"); + if (!tool) { + throw new Error("missing sessions_send tool"); + } const result = await tool.execute("call-no-key", { message: "hello", diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts index 5099081c1..90cd4dcc5 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; +import { DEFAULT_PROVIDER } from "../agents/defaults.js"; import { connectOk, embeddedRunMock, @@ -14,7 +15,6 @@ import { testState, writeSessionStore, } from "./test-helpers.js"; -import { DEFAULT_PROVIDER } from "../agents/defaults.js"; const sessionCleanupMocks = vi.hoisted(() => ({ clearSessionQueues: vi.fn(() => ({ followupCleared: 0, laneCleared: 0, keys: [] })), @@ -48,16 +48,19 @@ let port = 0; let previousToken: string | undefined; beforeAll(async () => { - previousToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + previousToken = process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; port = await getFreePort(); server = await startGatewayServer(port); }); afterAll(async () => { await server.close(); - if (previousToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; - else process.env.CLAWDBOT_GATEWAY_TOKEN = previousToken; + if (previousToken === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previousToken; + } }); const openClient = async (opts?: Parameters[1]) => { @@ -74,7 +77,7 @@ describe("gateway server sessions", () => { }); test("lists and patches session store via sessions.* RPC", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-sessions-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-")); const storePath = path.join(dir, "sessions.json"); const now = Date.now(); const recent = now - 30_000; @@ -370,7 +373,7 @@ describe("gateway server sessions", () => { }); test("sessions.preview returns transcript previews", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-sessions-preview-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-preview-")); const storePath = path.join(dir, "sessions.json"); testState.sessionStorePath = storePath; const sessionId = "sess-preview"; @@ -415,7 +418,7 @@ describe("gateway server sessions", () => { }); test("sessions.delete rejects main and aborts active runs", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-sessions-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-")); const storePath = path.join(dir, "sessions.json"); testState.sessionStorePath = storePath; diff --git a/src/gateway/server/close-reason.ts b/src/gateway/server/close-reason.ts index c20ccae3d..e530c07aa 100644 --- a/src/gateway/server/close-reason.ts +++ b/src/gateway/server/close-reason.ts @@ -3,8 +3,12 @@ import { Buffer } from "node:buffer"; const CLOSE_REASON_MAX_BYTES = 120; export function truncateCloseReason(reason: string, maxBytes = CLOSE_REASON_MAX_BYTES): string { - if (!reason) return "invalid handshake"; + if (!reason) { + return "invalid handshake"; + } const buf = Buffer.from(reason); - if (buf.length <= maxBytes) return reason; + if (buf.length <= maxBytes) { + return reason; + } return buf.subarray(0, maxBytes).toString(); } diff --git a/src/gateway/server/health-state.ts b/src/gateway/server/health-state.ts index 6eee1ecd8..8bc481dfc 100644 --- a/src/gateway/server/health-state.ts +++ b/src/gateway/server/health-state.ts @@ -1,10 +1,10 @@ +import type { Snapshot } from "../protocol/index.js"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { getHealthSnapshot, type HealthSummary } from "../../commands/health.js"; import { CONFIG_PATH, STATE_DIR, loadConfig } from "../../config/config.js"; import { resolveMainSessionKey } from "../../config/sessions.js"; -import { normalizeMainKey } from "../../routing/session-key.js"; import { listSystemPresence } from "../../infra/system-presence.js"; -import type { Snapshot } from "../protocol/index.js"; +import { normalizeMainKey } from "../../routing/session-key.js"; let presenceVersion = 1; let healthVersion = 1; diff --git a/src/gateway/server/hooks.ts b/src/gateway/server/hooks.ts index 18d46368f..8b4c107b5 100644 --- a/src/gateway/server/hooks.ts +++ b/src/gateway/server/hooks.ts @@ -1,14 +1,13 @@ import { randomUUID } from "node:crypto"; - import type { CliDeps } from "../../cli/deps.js"; +import type { CronJob } from "../../cron/types.js"; +import type { createSubsystemLogger } from "../../logging/subsystem.js"; +import type { HookMessageChannel, HooksConfigResolved } from "../hooks.js"; import { loadConfig } from "../../config/config.js"; import { resolveMainSessionKeyFromConfig } from "../../config/sessions.js"; import { runCronIsolatedAgentTurn } from "../../cron/isolated-agent.js"; -import type { CronJob } from "../../cron/types.js"; import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; -import type { createSubsystemLogger } from "../../logging/subsystem.js"; -import type { HookMessageChannel, HooksConfigResolved } from "../hooks.js"; import { createHooksRequestHandler } from "../server-http.js"; type SubsystemLogger = ReturnType; diff --git a/src/gateway/server/http-listen.ts b/src/gateway/server/http-listen.ts index b890a14d2..c2ae20a87 100644 --- a/src/gateway/server/http-listen.ts +++ b/src/gateway/server/http-listen.ts @@ -1,5 +1,4 @@ import type { Server as HttpServer } from "node:http"; - import { GatewayLockError } from "../../infra/gateway-lock.js"; export async function listenGatewayHttpServer(params: { diff --git a/src/gateway/server/plugins-http.test.ts b/src/gateway/server/plugins-http.test.ts index 0308ebe31..b373a23df 100644 --- a/src/gateway/server/plugins-http.test.ts +++ b/src/gateway/server/plugins-http.test.ts @@ -1,8 +1,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { describe, expect, it, vi } from "vitest"; - -import { createGatewayPluginRequestHandler } from "./plugins-http.js"; import { createTestRegistry } from "./__tests__/test-utils.js"; +import { createGatewayPluginRequestHandler } from "./plugins-http.js"; const makeResponse = (): { res: ServerResponse; diff --git a/src/gateway/server/plugins-http.ts b/src/gateway/server/plugins-http.ts index f8a7f85fd..8140be67d 100644 --- a/src/gateway/server/plugins-http.ts +++ b/src/gateway/server/plugins-http.ts @@ -1,5 +1,4 @@ import type { IncomingMessage, ServerResponse } from "node:http"; - import type { createSubsystemLogger } from "../../logging/subsystem.js"; import type { PluginRegistry } from "../../plugins/registry.js"; @@ -18,7 +17,9 @@ export function createGatewayPluginRequestHandler(params: { return async (req, res) => { const routes = registry.httpRoutes ?? []; const handlers = registry.httpHandlers ?? []; - if (routes.length === 0 && handlers.length === 0) return false; + if (routes.length === 0 && handlers.length === 0) { + return false; + } if (routes.length > 0) { const url = new URL(req.url ?? "/", "http://localhost"); @@ -42,7 +43,9 @@ export function createGatewayPluginRequestHandler(params: { for (const entry of handlers) { try { const handled = await entry.handler(req, res); - if (handled) return true; + if (handled) { + return true; + } } catch (err) { log.warn(`plugin http handler failed (${entry.pluginId}): ${String(err)}`); if (!res.headersSent) { diff --git a/src/gateway/server/ws-connection.ts b/src/gateway/server/ws-connection.ts index c413b3cec..661ed17a2 100644 --- a/src/gateway/server/ws-connection.ts +++ b/src/gateway/server/ws-connection.ts @@ -1,20 +1,18 @@ -import { randomUUID } from "node:crypto"; - import type { WebSocket, WebSocketServer } from "ws"; +import { randomUUID } from "node:crypto"; +import type { createSubsystemLogger } from "../../logging/subsystem.js"; +import type { ResolvedGatewayAuth } from "../auth.js"; +import type { GatewayRequestContext, GatewayRequestHandlers } from "../server-methods/types.js"; +import type { GatewayWsClient } from "./ws-types.js"; import { resolveCanvasHostUrl } from "../../infra/canvas-host-url.js"; import { listSystemPresence, upsertPresence } from "../../infra/system-presence.js"; -import type { createSubsystemLogger } from "../../logging/subsystem.js"; import { isWebchatClient } from "../../utils/message-channel.js"; - -import type { ResolvedGatewayAuth } from "../auth.js"; import { isLoopbackAddress } from "../net.js"; import { getHandshakeTimeoutMs } from "../server-constants.js"; -import type { GatewayRequestContext, GatewayRequestHandlers } from "../server-methods/types.js"; import { formatError } from "../server-utils.js"; import { logWs } from "../ws-log.js"; import { getHealthVersion, getPresenceVersion, incrementPresenceVersion } from "./health-state.js"; import { attachGatewayWsMessageHandler } from "./ws-connection/message-handler.js"; -import type { GatewayWsClient } from "./ws-types.js"; type SubsystemLogger = ReturnType; @@ -95,7 +93,9 @@ export function attachGatewayWsConnectionHandler(params: { let lastFrameId: string | undefined; const setCloseCause = (cause: string, meta?: Record) => { - if (!closeCause) closeCause = cause; + if (!closeCause) { + closeCause = cause; + } if (meta && Object.keys(meta).length > 0) { closeMeta = { ...closeMeta, ...meta }; } @@ -125,10 +125,14 @@ export function attachGatewayWsConnectionHandler(params: { }); const close = (code = 1000, reason?: string) => { - if (closed) return; + if (closed) { + return; + } closed = true; clearTimeout(handshakeTimer); - if (client) clients.delete(client); + if (client) { + clients.delete(client); + } try { socket.close(code, reason); } catch { diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index d1f6ae511..3ddcb7253 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -1,7 +1,11 @@ import type { IncomingMessage } from "node:http"; -import os from "node:os"; - import type { WebSocket } from "ws"; +import os from "node:os"; +import type { createSubsystemLogger } from "../../../logging/subsystem.js"; +import type { ResolvedGatewayAuth } from "../../auth.js"; +import type { GatewayRequestContext, GatewayRequestHandlers } from "../../server-methods/types.js"; +import type { GatewayWsClient } from "../ws-types.js"; +import { loadConfig } from "../../../config/config.js"; import { deriveDeviceIdFromPublicKey, normalizeDevicePublicKeyBase64Url, @@ -17,17 +21,15 @@ import { } from "../../../infra/device-pairing.js"; import { updatePairedNodeMetadata } from "../../../infra/node-pairing.js"; import { recordRemoteNodeInfo, refreshRemoteNodeBins } from "../../../infra/skills-remote.js"; -import { loadVoiceWakeConfig } from "../../../infra/voicewake.js"; import { upsertPresence } from "../../../infra/system-presence.js"; +import { loadVoiceWakeConfig } from "../../../infra/voicewake.js"; import { rawDataToString } from "../../../infra/ws.js"; -import type { createSubsystemLogger } from "../../../logging/subsystem.js"; import { isGatewayCliClient, isWebchatClient } from "../../../utils/message-channel.js"; -import type { ResolvedGatewayAuth } from "../../auth.js"; import { authorizeGatewayConnect, isLocalDirectRequest } from "../../auth.js"; -import { loadConfig } from "../../../config/config.js"; import { buildDeviceAuthPayload } from "../../device-auth.js"; import { isLoopbackAddress, isTrustedProxyAddress, resolveGatewayClientIp } from "../../net.js"; import { resolveNodeCommandAllowlist } from "../../node-command-policy.js"; +import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js"; import { type ConnectParams, ErrorCodes, @@ -35,17 +37,13 @@ import { errorShape, formatValidationErrors, PROTOCOL_VERSION, - type RequestFrame, validateConnectParams, validateRequestFrame, } from "../../protocol/index.js"; -import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js"; import { MAX_BUFFERED_BYTES, MAX_PAYLOAD_BYTES, TICK_INTERVAL_MS } from "../../server-constants.js"; -import type { GatewayRequestContext, GatewayRequestHandlers } from "../../server-methods/types.js"; import { handleGatewayRequest } from "../../server-methods.js"; import { formatError } from "../../server-utils.js"; import { formatForLog, logWs } from "../../ws-log.js"; - import { truncateCloseReason } from "../close-reason.js"; import { buildGatewaySnapshot, @@ -54,7 +52,6 @@ import { incrementPresenceVersion, refreshGatewayHealthSnapshot, } from "../health-state.js"; -import type { GatewayWsClient } from "../ws-types.js"; type SubsystemLogger = ReturnType; @@ -62,10 +59,14 @@ const DEVICE_SIGNATURE_SKEW_MS = 10 * 60 * 1000; function resolveHostName(hostHeader?: string): string { const host = (hostHeader ?? "").trim().toLowerCase(); - if (!host) return ""; + if (!host) { + return ""; + } if (host.startsWith("[")) { const end = host.indexOf("]"); - if (end !== -1) return host.slice(1, end); + if (end !== -1) { + return host.slice(1, end); + } } const [name] = host.split(":"); return name ?? ""; @@ -230,7 +231,9 @@ export function attachGatewayWsMessageHandler(params: { const isWebchatConnect = (p: ConnectParams | null | undefined) => isWebchatClient(p?.client); socket.on("message", async (data) => { - if (isClosed()) return; + if (isClosed()) { + return; + } const text = rawDataToString(data); try { const parsed = JSON.parse(text); @@ -263,11 +266,11 @@ export function attachGatewayWsMessageHandler(params: { const isRequestFrame = validateRequestFrame(parsed); if ( !isRequestFrame || - (parsed as RequestFrame).method !== "connect" || - !validateConnectParams((parsed as RequestFrame).params) + parsed.method !== "connect" || + !validateConnectParams(parsed.params) ) { const handshakeError = isRequestFrame - ? (parsed as RequestFrame).method === "connect" + ? parsed.method === "connect" ? `invalid connect params: ${formatValidationErrors(validateConnectParams.errors)}` : "invalid handshake: first request must be connect" : "invalid request frame"; @@ -279,7 +282,7 @@ export function attachGatewayWsMessageHandler(params: { handshakeError, }); if (isRequestFrame) { - const req = parsed as RequestFrame; + const req = parsed; send({ type: "res", id: req.id, @@ -300,7 +303,7 @@ export function attachGatewayWsMessageHandler(params: { return; } - const frame = parsed as RequestFrame; + const frame = parsed; const connectParams = frame.params as ConnectParams; const clientLabel = connectParams.client.displayName ?? connectParams.client.id; @@ -682,30 +685,40 @@ export function attachGatewayWsMessageHandler(params: { const isPaired = paired?.publicKey === devicePublicKey; if (!isPaired) { const ok = await requirePairing("not-paired"); - if (!ok) return; + if (!ok) { + return; + } } else { const allowedRoles = new Set( Array.isArray(paired.roles) ? paired.roles : paired.role ? [paired.role] : [], ); if (allowedRoles.size === 0) { const ok = await requirePairing("role-upgrade", paired); - if (!ok) return; + if (!ok) { + return; + } } else if (!allowedRoles.has(role)) { const ok = await requirePairing("role-upgrade", paired); - if (!ok) return; + if (!ok) { + return; + } } const pairedScopes = Array.isArray(paired.scopes) ? paired.scopes : []; if (scopes.length > 0) { if (pairedScopes.length === 0) { const ok = await requirePairing("scope-upgrade", paired); - if (!ok) return; + if (!ok) { + return; + } } else { const allowedScopes = new Set(pairedScopes); const missingScope = scopes.find((scope) => !allowedScopes.has(scope)); if (missingScope) { const ok = await requirePairing("scope-upgrade", paired); - if (!ok) return; + if (!ok) { + return; + } } } } @@ -789,7 +802,7 @@ export function attachGatewayWsMessageHandler(params: { type: "hello-ok", protocol: PROTOCOL_VERSION, server: { - version: process.env.CLAWDBOT_VERSION ?? process.env.npm_package_version ?? "dev", + version: process.env.OPENCLAW_VERSION ?? process.env.npm_package_version ?? "dev", commit: process.env.GIT_COMMIT, host: os.hostname(), connId, @@ -829,7 +842,9 @@ export function attachGatewayWsMessageHandler(params: { const instanceIdRaw = connectParams.client.instanceId; const instanceId = typeof instanceIdRaw === "string" ? instanceIdRaw.trim() : ""; const nodeIdsForPairing = new Set([nodeSession.nodeId]); - if (instanceId) nodeIdsForPairing.add(instanceId); + if (instanceId) { + nodeIdsForPairing.add(instanceId); + } for (const nodeId of nodeIdsForPairing) { void updatePairedNodeMetadata(nodeId, { lastConnectedAtMs: nodeSession.connectedAtMs, @@ -897,7 +912,7 @@ export function attachGatewayWsMessageHandler(params: { }); return; } - const req = parsed as RequestFrame; + const req = parsed; logWs("in", "req", { connId, id: req.id, method: req.method }); const respond = ( ok: boolean, diff --git a/src/gateway/server/ws-types.ts b/src/gateway/server/ws-types.ts index f604b37df..daeda9a29 100644 --- a/src/gateway/server/ws-types.ts +++ b/src/gateway/server/ws-types.ts @@ -1,5 +1,4 @@ import type { WebSocket } from "ws"; - import type { ConnectParams } from "../protocol/index.js"; export type GatewayWsClient = { diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index 3cbbd4343..56a5a059b 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -13,7 +13,7 @@ describe("readFirstUserMessageFromTranscript", () => { let storePath: string; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-session-fs-test-")); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-fs-test-")); storePath = path.join(tmpDir, "sessions.json"); }); @@ -159,7 +159,7 @@ describe("readLastMessagePreviewFromTranscript", () => { let storePath: string; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-session-fs-test-")); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-fs-test-")); storePath = path.join(tmpDir, "sessions.json"); }); @@ -348,7 +348,7 @@ describe("readSessionPreviewItemsFromTranscript", () => { let storePath: string; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-session-preview-test-")); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-preview-test-")); storePath = path.join(tmpDir, "sessions.json"); }); diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index d6453ace6..936ad9419 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -1,10 +1,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - +import type { SessionPreviewItem } from "./session-utils.types.js"; import { resolveSessionTranscriptPath } from "../config/sessions.js"; import { stripEnvelope } from "./chat-sanitize.js"; -import type { SessionPreviewItem } from "./session-utils.types.js"; export function readSessionMessages( sessionId: string, @@ -14,12 +13,16 @@ export function readSessionMessages( const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile); const filePath = candidates.find((p) => fs.existsSync(p)); - if (!filePath) return []; + if (!filePath) { + return []; + } const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/); const messages: unknown[] = []; for (const line of lines) { - if (!line.trim()) continue; + if (!line.trim()) { + continue; + } try { const parsed = JSON.parse(line); if (parsed?.message) { @@ -39,7 +42,9 @@ export function resolveSessionTranscriptCandidates( agentId?: string, ): string[] { const candidates: string[] = []; - if (sessionFile) candidates.push(sessionFile); + if (sessionFile) { + candidates.push(sessionFile); + } if (storePath) { const dir = path.dirname(storePath); candidates.push(path.join(dir, `${sessionId}.jsonl`)); @@ -47,7 +52,8 @@ export function resolveSessionTranscriptCandidates( if (agentId) { candidates.push(resolveSessionTranscriptPath(sessionId, agentId)); } - candidates.push(path.join(os.homedir(), ".clawdbot", "sessions", `${sessionId}.jsonl`)); + const home = os.homedir(); + candidates.push(path.join(home, ".openclaw", "sessions", `${sessionId}.jsonl`)); return candidates; } @@ -70,7 +76,9 @@ export function capArrayByJsonBytes( items: T[], maxBytes: number, ): { items: T[]; bytes: number } { - if (items.length === 0) return { items, bytes: 2 }; + if (items.length === 0) { + return { items, bytes: 2 }; + } const parts = items.map((item) => jsonUtf8Bytes(item)); let bytes = 2 + parts.reduce((a, b) => a + b, 0) + (items.length - 1); let start = 0; @@ -90,13 +98,21 @@ type TranscriptMessage = { }; function extractTextFromContent(content: TranscriptMessage["content"]): string | null { - if (typeof content === "string") return content.trim() || null; - if (!Array.isArray(content)) return null; + if (typeof content === "string") { + return content.trim() || null; + } + if (!Array.isArray(content)) { + return null; + } for (const part of content) { - if (!part || typeof part.text !== "string") continue; + if (!part || typeof part.text !== "string") { + continue; + } if (part.type === "text" || part.type === "output_text" || part.type === "input_text") { const trimmed = part.text.trim(); - if (trimmed) return trimmed; + if (trimmed) { + return trimmed; + } } } return null; @@ -110,25 +126,33 @@ export function readFirstUserMessageFromTranscript( ): string | null { const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId); const filePath = candidates.find((p) => fs.existsSync(p)); - if (!filePath) return null; + if (!filePath) { + return null; + } let fd: number | null = null; try { fd = fs.openSync(filePath, "r"); const buf = Buffer.alloc(8192); const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0); - if (bytesRead === 0) return null; + if (bytesRead === 0) { + return null; + } const chunk = buf.toString("utf-8", 0, bytesRead); const lines = chunk.split(/\r?\n/).slice(0, MAX_LINES_TO_SCAN); for (const line of lines) { - if (!line.trim()) continue; + if (!line.trim()) { + continue; + } try { const parsed = JSON.parse(line); const msg = parsed?.message as TranscriptMessage | undefined; if (msg?.role === "user") { const text = extractTextFromContent(msg.content); - if (text) return text; + if (text) { + return text; + } } } catch { // skip malformed lines @@ -137,7 +161,9 @@ export function readFirstUserMessageFromTranscript( } catch { // file read error } finally { - if (fd !== null) fs.closeSync(fd); + if (fd !== null) { + fs.closeSync(fd); + } } return null; } @@ -153,14 +179,18 @@ export function readLastMessagePreviewFromTranscript( ): string | null { const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId); const filePath = candidates.find((p) => fs.existsSync(p)); - if (!filePath) return null; + if (!filePath) { + return null; + } let fd: number | null = null; try { fd = fs.openSync(filePath, "r"); const stat = fs.fstatSync(fd); const size = stat.size; - if (size === 0) return null; + if (size === 0) { + return null; + } const readStart = Math.max(0, size - LAST_MSG_MAX_BYTES); const readLen = Math.min(size, LAST_MSG_MAX_BYTES); @@ -178,7 +208,9 @@ export function readLastMessagePreviewFromTranscript( const msg = parsed?.message as TranscriptMessage | undefined; if (msg?.role === "user" || msg?.role === "assistant") { const text = extractTextFromContent(msg.content); - if (text) return text; + if (text) { + return text; + } } } catch { // skip malformed @@ -187,7 +219,9 @@ export function readLastMessagePreviewFromTranscript( } catch { // file error } finally { - if (fd !== null) fs.closeSync(fd); + if (fd !== null) { + fs.closeSync(fd); + } } return null; } @@ -210,7 +244,9 @@ type TranscriptPreviewMessage = { }; function normalizeRole(role: string | undefined, isTool: boolean): SessionPreviewItem["role"] { - if (isTool) return "tool"; + if (isTool) { + return "tool"; + } switch ((role ?? "").toLowerCase()) { case "user": return "user"; @@ -226,8 +262,12 @@ function normalizeRole(role: string | undefined, isTool: boolean): SessionPrevie } function truncatePreviewText(text: string, maxChars: number): string { - if (maxChars <= 0 || text.length <= maxChars) return text; - if (maxChars <= 3) return text.slice(0, maxChars); + if (maxChars <= 0 || text.length <= maxChars) { + return text; + } + if (maxChars <= 3) { + return text.slice(0, maxChars); + } return `${text.slice(0, maxChars - 3)}...`; } @@ -252,10 +292,16 @@ function extractPreviewText(message: TranscriptPreviewMessage): string | null { } function isToolCall(message: TranscriptPreviewMessage): boolean { - if (message.toolName || message.tool_name) return true; - if (!Array.isArray(message.content)) return false; + if (message.toolName || message.tool_name) { + return true; + } + if (!Array.isArray(message.content)) { + return false; + } return message.content.some((entry) => { - if (entry?.name) return true; + if (entry?.name) { + return true; + } const raw = typeof entry?.type === "string" ? entry.type.toLowerCase() : ""; return raw === "toolcall" || raw === "tool_call"; }); @@ -278,10 +324,14 @@ function extractToolNames(message: TranscriptPreviewMessage): string[] { } function extractMediaSummary(message: TranscriptPreviewMessage): string | null { - if (!Array.isArray(message.content)) return null; + if (!Array.isArray(message.content)) { + return null; + } for (const entry of message.content) { const raw = typeof entry?.type === "string" ? entry.type.trim().toLowerCase() : ""; - if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") continue; + if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") { + continue; + } return `[${raw}]`; } return null; @@ -303,15 +353,21 @@ function buildPreviewItems( const shown = toolNames.slice(0, 2); const overflow = toolNames.length - shown.length; text = `call ${shown.join(", ")}`; - if (overflow > 0) text += ` +${overflow}`; + if (overflow > 0) { + text += ` +${overflow}`; + } } } if (!text) { text = extractMediaSummary(message); } - if (!text) continue; + if (!text) { + continue; + } let trimmed = text.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } if (role === "user") { trimmed = stripEnvelope(trimmed); } @@ -319,7 +375,9 @@ function buildPreviewItems( items.push({ role, text: trimmed }); } - if (items.length <= maxItems) return items; + if (items.length <= maxItems) { + return items; + } return items.slice(-maxItems); } @@ -333,7 +391,9 @@ function readRecentMessagesFromTranscript( fd = fs.openSync(filePath, "r"); const stat = fs.fstatSync(fd); const size = stat.size; - if (size === 0) return []; + if (size === 0) { + return []; + } const readStart = Math.max(0, size - readBytes); const readLen = Math.min(size, readBytes); @@ -352,17 +412,21 @@ function readRecentMessagesFromTranscript( const msg = parsed?.message as TranscriptPreviewMessage | undefined; if (msg && typeof msg === "object") { collected.push(msg); - if (collected.length >= maxMessages) break; + if (collected.length >= maxMessages) { + break; + } } } catch { // skip malformed lines } } - return collected.reverse(); + return collected.toReversed(); } catch { return []; } finally { - if (fd !== null) fs.closeSync(fd); + if (fd !== null) { + fs.closeSync(fd); + } } } @@ -376,7 +440,9 @@ export function readSessionPreviewItemsFromTranscript( ): SessionPreviewItem[] { const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId); const filePath = candidates.find((p) => fs.existsSync(p)); - if (!filePath) return []; + if (!filePath) { + return []; + } const boundedItems = Math.max(1, Math.min(maxItems, 50)); const boundedChars = Math.max(20, Math.min(maxChars, 2000)); diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 317a7c91a..76798db43 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -1,7 +1,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, test } from "vitest"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; import { capArrayByJsonBytes, @@ -46,7 +46,7 @@ describe("gateway session utils", () => { const cfg = { session: { mainKey: "work" }, agents: { list: [{ id: "ops", default: true }] }, - } as MoltbotConfig; + } as OpenClawConfig; expect(resolveSessionStoreKey({ cfg, sessionKey: "main" })).toBe("agent:ops:work"); expect(resolveSessionStoreKey({ cfg, sessionKey: "work" })).toBe("agent:ops:work"); expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:ops:main" })).toBe("agent:ops:work"); @@ -56,7 +56,7 @@ describe("gateway session utils", () => { const cfg = { session: { mainKey: "main" }, agents: { list: [{ id: "ops", default: true }] }, - } as MoltbotConfig; + } as OpenClawConfig; expect(resolveSessionStoreKey({ cfg, sessionKey: "discord:group:123" })).toBe( "agent:ops:discord:group:123", ); @@ -69,7 +69,7 @@ describe("gateway session utils", () => { const cfg = { session: { scope: "global", mainKey: "work" }, agents: { list: [{ id: "ops", default: true }] }, - } as MoltbotConfig; + } as OpenClawConfig; expect(resolveSessionStoreKey({ cfg, sessionKey: "main" })).toBe("global"); const target = resolveGatewaySessionStoreTarget({ cfg, key: "main" }); expect(target.canonicalKey).toBe("global"); @@ -79,14 +79,14 @@ describe("gateway session utils", () => { test("resolveGatewaySessionStoreTarget uses canonical key for main alias", () => { const storeTemplate = path.join( os.tmpdir(), - "moltbot-session-utils", + "openclaw-session-utils", "{agentId}", "sessions.json", ); const cfg = { session: { mainKey: "main", store: storeTemplate }, agents: { list: [{ id: "ops", default: true }] }, - } as MoltbotConfig; + } as OpenClawConfig; const target = resolveGatewaySessionStoreTarget({ cfg, key: "main" }); expect(target.canonicalKey).toBe("agent:ops:main"); expect(target.storeKeys).toEqual(expect.arrayContaining(["agent:ops:main", "main"])); @@ -193,7 +193,7 @@ describe("listSessionsFromStore search", () => { const baseCfg = { session: { mainKey: "main" }, agents: { list: [{ id: "main", default: true }] }, - } as MoltbotConfig; + } as OpenClawConfig; const makeStore = (): Record => ({ "agent:main:work-project": { diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 164be999e..ec3b147f8 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1,11 +1,16 @@ import fs from "node:fs"; import path from "node:path"; - +import type { + GatewayAgentRow, + GatewaySessionRow, + GatewaySessionsDefaults, + SessionsListResult, +} from "./session-utils.types.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { lookupContextTokens } from "../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveConfiguredModelRef } from "../agents/model-selection.js"; -import { type MoltbotConfig, loadConfig } from "../config/config.js"; +import { type OpenClawConfig, loadConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import { buildGroupDisplayName, @@ -26,12 +31,6 @@ import { readFirstUserMessageFromTranscript, readLastMessagePreviewFromTranscript, } from "./session-utils.fs.js"; -import type { - GatewayAgentRow, - GatewaySessionRow, - GatewaySessionsDefaults, - SessionsListResult, -} from "./session-utils.types.js"; export { archiveFileOnDisk, @@ -78,30 +77,48 @@ function resolveAvatarMime(filePath: string): string { } function isWorkspaceRelativePath(value: string): boolean { - if (!value) return false; - if (value.startsWith("~")) return false; - if (AVATAR_SCHEME_RE.test(value) && !WINDOWS_ABS_RE.test(value)) return false; + if (!value) { + return false; + } + if (value.startsWith("~")) { + return false; + } + if (AVATAR_SCHEME_RE.test(value) && !WINDOWS_ABS_RE.test(value)) { + return false; + } return true; } function resolveIdentityAvatarUrl( - cfg: MoltbotConfig, + cfg: OpenClawConfig, agentId: string, avatar: string | undefined, ): string | undefined { - if (!avatar) return undefined; + if (!avatar) { + return undefined; + } const trimmed = avatar.trim(); - if (!trimmed) return undefined; - if (AVATAR_DATA_RE.test(trimmed) || AVATAR_HTTP_RE.test(trimmed)) return trimmed; - if (!isWorkspaceRelativePath(trimmed)) return undefined; + if (!trimmed) { + return undefined; + } + if (AVATAR_DATA_RE.test(trimmed) || AVATAR_HTTP_RE.test(trimmed)) { + return trimmed; + } + if (!isWorkspaceRelativePath(trimmed)) { + return undefined; + } const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const workspaceRoot = path.resolve(workspaceDir); const resolved = path.resolve(workspaceRoot, trimmed); const relative = path.relative(workspaceRoot, resolved); - if (relative.startsWith("..") || path.isAbsolute(relative)) return undefined; + if (relative.startsWith("..") || path.isAbsolute(relative)) { + return undefined; + } try { const stat = fs.statSync(resolved); - if (!stat.isFile() || stat.size > AVATAR_MAX_BYTES) return undefined; + if (!stat.isFile() || stat.size > AVATAR_MAX_BYTES) { + return undefined; + } const buffer = fs.readFileSync(resolved); const mime = resolveAvatarMime(resolved); return `data:${mime};base64,${buffer.toString("base64")}`; @@ -121,10 +138,14 @@ function formatSessionIdPrefix(sessionId: string, updatedAt?: number | null): st } function truncateTitle(text: string, maxLen: number): string { - if (text.length <= maxLen) return text; + if (text.length <= maxLen) { + return text; + } const cut = text.slice(0, maxLen - 1); const lastSpace = cut.lastIndexOf(" "); - if (lastSpace > maxLen * 0.6) return cut.slice(0, lastSpace) + "…"; + if (lastSpace > maxLen * 0.6) { + return cut.slice(0, lastSpace) + "…"; + } return cut + "…"; } @@ -132,7 +153,9 @@ export function deriveSessionTitle( entry: SessionEntry | undefined, firstUserMessage?: string | null, ): string | undefined { - if (!entry) return undefined; + if (!entry) { + return undefined; + } if (entry.displayName?.trim()) { return entry.displayName.trim(); @@ -166,8 +189,12 @@ export function loadSessionEntry(sessionKey: string) { } export function classifySessionKey(key: string, entry?: SessionEntry): GatewaySessionRow["kind"] { - if (key === "global") return "global"; - if (key === "unknown") return "unknown"; + if (key === "global") { + return "global"; + } + if (key === "unknown") { + return "unknown"; + } if (entry?.chatType === "group" || entry?.chatType === "channel") { return "group"; } @@ -211,12 +238,14 @@ function listExistingAgentIdsFromDisk(): string[] { } } -function listConfiguredAgentIds(cfg: MoltbotConfig): string[] { +function listConfiguredAgentIds(cfg: OpenClawConfig): string[] { const agents = cfg.agents?.list ?? []; if (agents.length > 0) { const ids = new Set(); for (const entry of agents) { - if (entry?.id) ids.add(normalizeAgentId(entry.id)); + if (entry?.id) { + ids.add(normalizeAgentId(entry.id)); + } } const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); ids.add(defaultId); @@ -230,7 +259,9 @@ function listConfiguredAgentIds(cfg: MoltbotConfig): string[] { const ids = new Set(); const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); ids.add(defaultId); - for (const id of listExistingAgentIdsFromDisk()) ids.add(id); + for (const id of listExistingAgentIdsFromDisk()) { + ids.add(id); + } const sorted = Array.from(ids).filter(Boolean); sorted.sort((a, b) => a.localeCompare(b)); if (sorted.includes(defaultId)) { @@ -239,7 +270,7 @@ function listConfiguredAgentIds(cfg: MoltbotConfig): string[] { return sorted; } -export function listAgentsForGateway(cfg: MoltbotConfig): { +export function listAgentsForGateway(cfg: OpenClawConfig): { defaultId: string; mainKey: string; scope: SessionScope; @@ -253,7 +284,9 @@ export function listAgentsForGateway(cfg: MoltbotConfig): { { name?: string; identity?: GatewayAgentRow["identity"] } >(); for (const entry of cfg.agents?.list ?? []) { - if (!entry?.id) continue; + if (!entry?.id) { + continue; + } const identity = entry.identity ? { name: entry.identity.name?.trim() || undefined, @@ -296,19 +329,30 @@ export function listAgentsForGateway(cfg: MoltbotConfig): { } function canonicalizeSessionKeyForAgent(agentId: string, key: string): string { - if (key === "global" || key === "unknown") return key; - if (key.startsWith("agent:")) return key; + if (key === "global" || key === "unknown") { + return key; + } + if (key.startsWith("agent:")) { + return key; + } return `agent:${normalizeAgentId(agentId)}:${key}`; } -function resolveDefaultStoreAgentId(cfg: MoltbotConfig): string { +function resolveDefaultStoreAgentId(cfg: OpenClawConfig): string { return normalizeAgentId(resolveDefaultAgentId(cfg)); } -export function resolveSessionStoreKey(params: { cfg: MoltbotConfig; sessionKey: string }): string { +export function resolveSessionStoreKey(params: { + cfg: OpenClawConfig; + sessionKey: string; +}): string { const raw = params.sessionKey.trim(); - if (!raw) return raw; - if (raw === "global" || raw === "unknown") return raw; + if (!raw) { + return raw; + } + if (raw === "global" || raw === "unknown") { + return raw; + } const parsed = parseAgentSessionKey(raw); if (parsed) { @@ -318,7 +362,9 @@ export function resolveSessionStoreKey(params: { cfg: MoltbotConfig; sessionKey: agentId, sessionKey: raw, }); - if (canonical !== raw) return canonical; + if (canonical !== raw) { + return canonical; + } return raw; } @@ -330,24 +376,32 @@ export function resolveSessionStoreKey(params: { cfg: MoltbotConfig; sessionKey: return canonicalizeSessionKeyForAgent(agentId, raw); } -function resolveSessionStoreAgentId(cfg: MoltbotConfig, canonicalKey: string): string { +function resolveSessionStoreAgentId(cfg: OpenClawConfig, canonicalKey: string): string { if (canonicalKey === "global" || canonicalKey === "unknown") { return resolveDefaultStoreAgentId(cfg); } const parsed = parseAgentSessionKey(canonicalKey); - if (parsed?.agentId) return normalizeAgentId(parsed.agentId); + if (parsed?.agentId) { + return normalizeAgentId(parsed.agentId); + } return resolveDefaultStoreAgentId(cfg); } function canonicalizeSpawnedByForAgent(agentId: string, spawnedBy?: string): string | undefined { const raw = spawnedBy?.trim(); - if (!raw) return undefined; - if (raw === "global" || raw === "unknown") return raw; - if (raw.startsWith("agent:")) return raw; + if (!raw) { + return undefined; + } + if (raw === "global" || raw === "unknown") { + return raw; + } + if (raw.startsWith("agent:")) { + return raw; + } return `agent:${normalizeAgentId(agentId)}:${raw}`; } -export function resolveGatewaySessionStoreTarget(params: { cfg: MoltbotConfig; key: string }): { +export function resolveGatewaySessionStoreTarget(params: { cfg: OpenClawConfig; key: string }): { agentId: string; storePath: string; canonicalKey: string; @@ -369,7 +423,9 @@ export function resolveGatewaySessionStoreTarget(params: { cfg: MoltbotConfig; k const storeKeys = new Set(); storeKeys.add(canonicalKey); - if (key && key !== canonicalKey) storeKeys.add(key); + if (key && key !== canonicalKey) { + storeKeys.add(key); + } return { agentId, storePath, @@ -403,7 +459,7 @@ function mergeSessionEntryIntoCombined(params: { } } -export function loadCombinedSessionStoreForGateway(cfg: MoltbotConfig): { +export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): { storePath: string; store: Record; } { @@ -446,7 +502,7 @@ export function loadCombinedSessionStoreForGateway(cfg: MoltbotConfig): { return { storePath, store: combined }; } -export function getSessionDefaults(cfg: MoltbotConfig): GatewaySessionsDefaults { +export function getSessionDefaults(cfg: OpenClawConfig): GatewaySessionsDefaults { const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, @@ -464,7 +520,7 @@ export function getSessionDefaults(cfg: MoltbotConfig): GatewaySessionsDefaults } export function resolveSessionModelRef( - cfg: MoltbotConfig, + cfg: OpenClawConfig, entry?: SessionEntry, ): { provider: string; model: string } { const resolved = resolveConfiguredModelRef({ @@ -483,7 +539,7 @@ export function resolveSessionModelRef( } export function listSessionsFromStore(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; storePath: string; store: Record; opts: import("./protocol/index.js").SessionsListParams; @@ -506,23 +562,37 @@ export function listSessionsFromStore(params: { let sessions = Object.entries(store) .filter(([key]) => { - if (!includeGlobal && key === "global") return false; - if (!includeUnknown && key === "unknown") return false; + if (!includeGlobal && key === "global") { + return false; + } + if (!includeUnknown && key === "unknown") { + return false; + } if (agentId) { - if (key === "global" || key === "unknown") return false; + if (key === "global" || key === "unknown") { + return false; + } const parsed = parseAgentSessionKey(key); - if (!parsed) return false; + if (!parsed) { + return false; + } return normalizeAgentId(parsed.agentId) === agentId; } return true; }) .filter(([key, entry]) => { - if (!spawnedBy) return true; - if (key === "unknown" || key === "global") return false; + if (!spawnedBy) { + return true; + } + if (key === "unknown" || key === "global") { + return false; + } return entry?.spawnedBy === spawnedBy; }) .filter(([, entry]) => { - if (!label) return true; + if (!label) { + return true; + } return entry?.label === label; }) .map(([key, entry]) => { @@ -587,7 +657,7 @@ export function listSessionsFromStore(params: { lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId, }; }) - .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)); + .toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)); if (search) { sessions = sessions.filter((s) => { @@ -625,7 +695,9 @@ export function listSessionsFromStore(params: { storePath, entry.sessionFile, ); - if (lastMsg) lastMessagePreview = lastMsg; + if (lastMsg) { + lastMessagePreview = lastMsg; + } } } return { ...rest, derivedTitle, lastMessagePreview } satisfies GatewaySessionRow; diff --git a/src/gateway/sessions-patch.test.ts b/src/gateway/sessions-patch.test.ts index 53e5d9343..eb109601a 100644 --- a/src/gateway/sessions-patch.test.ts +++ b/src/gateway/sessions-patch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; import { applySessionsPatchToStore } from "./sessions-patch.js"; @@ -7,26 +7,30 @@ describe("gateway sessions patch", () => { test("persists elevatedLevel=off (does not clear)", async () => { const store: Record = {}; const res = await applySessionsPatchToStore({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, store, storeKey: "agent:main:main", patch: { elevatedLevel: "off" }, }); expect(res.ok).toBe(true); - if (!res.ok) return; + if (!res.ok) { + return; + } expect(res.entry.elevatedLevel).toBe("off"); }); test("persists elevatedLevel=on", async () => { const store: Record = {}; const res = await applySessionsPatchToStore({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, store, storeKey: "agent:main:main", patch: { elevatedLevel: "on" }, }); expect(res.ok).toBe(true); - if (!res.ok) return; + if (!res.ok) { + return; + } expect(res.entry.elevatedLevel).toBe("on"); }); @@ -35,26 +39,30 @@ describe("gateway sessions patch", () => { "agent:main:main": { elevatedLevel: "off" } as SessionEntry, }; const res = await applySessionsPatchToStore({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, store, storeKey: "agent:main:main", patch: { elevatedLevel: null }, }); expect(res.ok).toBe(true); - if (!res.ok) return; + if (!res.ok) { + return; + } expect(res.entry.elevatedLevel).toBeUndefined(); }); test("rejects invalid elevatedLevel values", async () => { const store: Record = {}; const res = await applySessionsPatchToStore({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, store, storeKey: "agent:main:main", patch: { elevatedLevel: "maybe" }, }); expect(res.ok).toBe(false); - if (res.ok) return; + if (res.ok) { + return; + } expect(res.error.message).toContain("invalid elevatedLevel"); }); @@ -71,14 +79,16 @@ describe("gateway sessions patch", () => { } as SessionEntry, }; const res = await applySessionsPatchToStore({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, store, storeKey: "agent:main:main", patch: { model: "openai/gpt-5.2" }, loadGatewayModelCatalog: async () => [{ provider: "openai", id: "gpt-5.2" }], }); expect(res.ok).toBe(true); - if (!res.ok) return; + if (!res.ok) { + return; + } expect(res.entry.providerOverride).toBe("openai"); expect(res.entry.modelOverride).toBe("gpt-5.2"); expect(res.entry.authProfileOverride).toBeUndefined(); diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index 46b5e7c40..36fe85e3a 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -1,7 +1,8 @@ import { randomUUID } from "node:crypto"; - -import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import type { ModelCatalogEntry } from "../agents/model-catalog.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { SessionEntry } from "../config/sessions.js"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveAllowedModelRef, resolveConfiguredModelRef } from "../agents/model-selection.js"; import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; import { @@ -13,13 +14,11 @@ import { normalizeUsageDisplay, supportsXHighThinking, } from "../auto-reply/thinking.js"; -import type { MoltbotConfig } from "../config/config.js"; -import type { SessionEntry } from "../config/sessions.js"; import { isSubagentSessionKey } from "../routing/session-key.js"; import { applyVerboseOverride, parseVerboseOverride } from "../sessions/level-overrides.js"; +import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js"; import { normalizeSendPolicy } from "../sessions/send-policy.js"; import { parseSessionLabel } from "../sessions/session-label.js"; -import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js"; import { ErrorCodes, type ErrorShape, @@ -56,7 +55,7 @@ function normalizeExecAsk(raw: string): "off" | "on-miss" | "always" | undefined } export async function applySessionsPatchToStore(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; store: Record; storeKey: string; patch: SessionsPatchParams; @@ -76,10 +75,14 @@ export async function applySessionsPatchToStore(params: { if ("spawnedBy" in patch) { const raw = patch.spawnedBy; if (raw === null) { - if (existing?.spawnedBy) return invalid("spawnedBy cannot be cleared once set"); + if (existing?.spawnedBy) { + return invalid("spawnedBy cannot be cleared once set"); + } } else if (raw !== undefined) { const trimmed = String(raw).trim(); - if (!trimmed) return invalid("invalid spawnedBy: empty"); + if (!trimmed) { + return invalid("invalid spawnedBy: empty"); + } if (!isSubagentSessionKey(storeKey)) { return invalid("spawnedBy is only supported for subagent:* sessions"); } @@ -96,9 +99,13 @@ export async function applySessionsPatchToStore(params: { delete next.label; } else if (raw !== undefined) { const parsed = parseSessionLabel(raw); - if (!parsed.ok) return invalid(parsed.error); + if (!parsed.ok) { + return invalid(parsed.error); + } for (const [key, entry] of Object.entries(store)) { - if (key === storeKey) continue; + if (key === storeKey) { + continue; + } if (entry?.label === parsed.label) { return invalid(`label already in use: ${parsed.label}`); } @@ -125,15 +132,20 @@ export async function applySessionsPatchToStore(params: { `invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`, ); } - if (normalized === "off") delete next.thinkingLevel; - else next.thinkingLevel = normalized; + if (normalized === "off") { + delete next.thinkingLevel; + } else { + next.thinkingLevel = normalized; + } } } if ("verboseLevel" in patch) { const raw = patch.verboseLevel; const parsed = parseVerboseOverride(raw); - if (!parsed.ok) return invalid(parsed.error); + if (!parsed.ok) { + return invalid(parsed.error); + } applyVerboseOverride(next, parsed.value); } @@ -146,8 +158,11 @@ export async function applySessionsPatchToStore(params: { if (!normalized) { return invalid('invalid reasoningLevel (use "on"|"off"|"stream")'); } - if (normalized === "off") delete next.reasoningLevel; - else next.reasoningLevel = normalized; + if (normalized === "off") { + delete next.reasoningLevel; + } else { + next.reasoningLevel = normalized; + } } } @@ -157,9 +172,14 @@ export async function applySessionsPatchToStore(params: { delete next.responseUsage; } else if (raw !== undefined) { const normalized = normalizeUsageDisplay(String(raw)); - if (!normalized) return invalid('invalid responseUsage (use "off"|"tokens"|"full")'); - if (normalized === "off") delete next.responseUsage; - else next.responseUsage = normalized; + if (!normalized) { + return invalid('invalid responseUsage (use "off"|"tokens"|"full")'); + } + if (normalized === "off") { + delete next.responseUsage; + } else { + next.responseUsage = normalized; + } } } @@ -169,7 +189,9 @@ export async function applySessionsPatchToStore(params: { delete next.elevatedLevel; } else if (raw !== undefined) { const normalized = normalizeElevatedLevel(String(raw)); - if (!normalized) return invalid('invalid elevatedLevel (use "on"|"off"|"ask"|"full")'); + if (!normalized) { + return invalid('invalid elevatedLevel (use "on"|"off"|"ask"|"full")'); + } // Persist "off" explicitly so patches can override defaults. next.elevatedLevel = normalized; } @@ -181,7 +203,9 @@ export async function applySessionsPatchToStore(params: { delete next.execHost; } else if (raw !== undefined) { const normalized = normalizeExecHost(String(raw)); - if (!normalized) return invalid('invalid execHost (use "sandbox"|"gateway"|"node")'); + if (!normalized) { + return invalid('invalid execHost (use "sandbox"|"gateway"|"node")'); + } next.execHost = normalized; } } @@ -192,7 +216,9 @@ export async function applySessionsPatchToStore(params: { delete next.execSecurity; } else if (raw !== undefined) { const normalized = normalizeExecSecurity(String(raw)); - if (!normalized) return invalid('invalid execSecurity (use "deny"|"allowlist"|"full")'); + if (!normalized) { + return invalid('invalid execSecurity (use "deny"|"allowlist"|"full")'); + } next.execSecurity = normalized; } } @@ -203,7 +229,9 @@ export async function applySessionsPatchToStore(params: { delete next.execAsk; } else if (raw !== undefined) { const normalized = normalizeExecAsk(String(raw)); - if (!normalized) return invalid('invalid execAsk (use "off"|"on-miss"|"always")'); + if (!normalized) { + return invalid('invalid execAsk (use "off"|"on-miss"|"always")'); + } next.execAsk = normalized; } } @@ -214,7 +242,9 @@ export async function applySessionsPatchToStore(params: { delete next.execNode; } else if (raw !== undefined) { const trimmed = String(raw).trim(); - if (!trimmed) return invalid("invalid execNode: empty"); + if (!trimmed) { + return invalid("invalid execNode: empty"); + } next.execNode = trimmed; } } @@ -237,7 +267,9 @@ export async function applySessionsPatchToStore(params: { }); } else if (raw !== undefined) { const trimmed = String(raw).trim(); - if (!trimmed) return invalid("invalid model: empty"); + if (!trimmed) { + return invalid("invalid model: empty"); + } if (!params.loadGatewayModelCatalog) { return { ok: false, @@ -291,7 +323,9 @@ export async function applySessionsPatchToStore(params: { delete next.sendPolicy; } else if (raw !== undefined) { const normalized = normalizeSendPolicy(String(raw)); - if (!normalized) return invalid('invalid sendPolicy (use "allow"|"deny")'); + if (!normalized) { + return invalid('invalid sendPolicy (use "allow"|"deny")'); + } next.sendPolicy = normalized; } } diff --git a/src/gateway/sessions-resolve.ts b/src/gateway/sessions-resolve.ts index 2d64fa89f..1bf8edfd2 100644 --- a/src/gateway/sessions-resolve.ts +++ b/src/gateway/sessions-resolve.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { loadSessionStore } from "../config/sessions.js"; import { parseSessionLabel } from "../sessions/session-label.js"; import { @@ -16,7 +16,7 @@ import { export type SessionsResolveResult = { ok: true; key: string } | { ok: false; error: ErrorShape }; export function resolveSessionKeyFromResolveParams(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; p: SessionsResolveParams; }): SessionsResolveResult { const { cfg, p } = params; diff --git a/src/gateway/test-helpers.e2e.ts b/src/gateway/test-helpers.e2e.ts index ab124a83b..3a5fe38ff 100644 --- a/src/gateway/test-helpers.e2e.ts +++ b/src/gateway/test-helpers.e2e.ts @@ -1,5 +1,4 @@ import { WebSocket } from "ws"; - import { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, @@ -13,7 +12,6 @@ import { type GatewayClientMode, type GatewayClientName, } from "../utils/message-channel.js"; - import { GatewayClient } from "./client.js"; import { buildDeviceAuthPayload } from "./device-auth.js"; import { PROTOCOL_VERSION } from "./protocol/index.js"; @@ -33,11 +31,16 @@ export async function connectGatewayClient(params: { return await new Promise>((resolve, reject) => { let settled = false; const stop = (err?: Error, client?: InstanceType) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); - if (err) reject(err); - else resolve(client as InstanceType); + if (err) { + reject(err); + } else { + resolve(client as InstanceType); + } }; const client = new GatewayClient({ url: params.url, @@ -112,7 +115,9 @@ export async function connectDeviceAuthReq(params: { url: string; token?: string }; const handler = (data: WebSocket.RawData) => { const obj = JSON.parse(rawDataToString(data)) as { type?: unknown; id?: unknown }; - if (obj?.type !== "res" || obj?.id !== "c1") return; + if (obj?.type !== "res" || obj?.id !== "c1") { + return; + } clearTimeout(timer); ws.off("message", handler); ws.off("close", closeHandler); diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 298e52618..792a644c9 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -1,15 +1,14 @@ import crypto from "node:crypto"; -import fs from "node:fs/promises"; import fsSync from "node:fs"; +import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { vi } from "vitest"; - +import { Mock, vi } from "vitest"; import type { ChannelPlugin, ChannelOutboundAdapter } from "../channels/plugins/types.js"; -import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import type { AgentBinding } from "../config/types.agents.js"; import type { HooksConfig } from "../config/types.hooks.js"; import type { PluginRegistry } from "../plugins/registry.js"; +import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; @@ -188,19 +187,19 @@ export const resetTestPluginRegistry = () => { }; const testConfigRoot = { - value: path.join(os.tmpdir(), `moltbot-gateway-test-${process.pid}-${crypto.randomUUID()}`), + value: path.join(os.tmpdir(), `openclaw-gateway-test-${process.pid}-${crypto.randomUUID()}`), }; export const setTestConfigRoot = (root: string) => { testConfigRoot.value = root; - process.env.CLAWDBOT_CONFIG_PATH = path.join(root, "moltbot.json"); + process.env.OPENCLAW_CONFIG_PATH = path.join(root, "openclaw.json"); }; export const testTailnetIPv4 = hoisted.testTailnetIPv4; export const piSdkMock = hoisted.piSdkMock; export const cronIsolatedRun = hoisted.cronIsolatedRun; -export const agentCommand = hoisted.agentCommand; -export const getReplyFromConfig = hoisted.getReplyFromConfig; +export const agentCommand: Mock<() => void> = hoisted.agentCommand; +export const getReplyFromConfig: Mock<() => void> = hoisted.getReplyFromConfig; export const testState = { agentConfig: undefined as Record | undefined, @@ -227,20 +226,25 @@ export const testIsNixMode = hoisted.testIsNixMode; export const sessionStoreSaveDelayMs = hoisted.sessionStoreSaveDelayMs; export const embeddedRunMock = hoisted.embeddedRunMock; -vi.mock("@mariozechner/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-coding-agent", +vi.mock("../agents/pi-model-discovery.js", async () => { + const actual = await vi.importActual( + "../agents/pi-model-discovery.js", ); + class MockModelRegistry extends actual.ModelRegistry { + override getAll(): ReturnType { + if (!piSdkMock.enabled) { + return super.getAll(); + } + piSdkMock.discoverCalls += 1; + // Cast to expected type for testing purposes + return piSdkMock.models as ReturnType; + } + } + return { ...actual, - discoverModels: (...args: unknown[]) => { - if (!piSdkMock.enabled) { - return (actual.discoverModels as (...args: unknown[]) => unknown)(...args); - } - piSdkMock.discoverCalls += 1; - return piSdkMock.models; - }, + ModelRegistry: MockModelRegistry, }; }); @@ -271,7 +275,7 @@ vi.mock("../config/sessions.js", async () => { vi.mock("../config/config.js", async () => { const actual = await vi.importActual("../config/config.js"); - const resolveConfigPath = () => path.join(testConfigRoot.value, "moltbot.json"); + const resolveConfigPath = () => path.join(testConfigRoot.value, "openclaw.json"); const hashConfigRaw = (raw: string | null) => crypto .createHash("sha256") @@ -389,7 +393,7 @@ vi.mock("../config/config.js", async () => { : {}; const defaults = { model: { primary: "anthropic/claude-opus-4-5" }, - workspace: path.join(os.tmpdir(), "clawd-gateway-test"), + workspace: path.join(os.tmpdir(), "openclaw-gateway-test"), ...fileDefaults, ...testState.agentConfig, }; @@ -409,7 +413,7 @@ vi.mock("../config/config.js", async () => { : {}; const overrideChannels = testState.channelsConfig && typeof testState.channelsConfig === "object" - ? { ...(testState.channelsConfig as Record) } + ? { ...testState.channelsConfig } : {}; const mergedChannels = { ...fileChannels, ...overrideChannels }; if (testState.allowFrom !== undefined) { @@ -436,9 +440,12 @@ vi.mock("../config/config.js", async () => { ...fileSession, mainKey: fileSession.mainKey ?? "main", }; - if (typeof testState.sessionStorePath === "string") + if (typeof testState.sessionStorePath === "string") { session.store = testState.sessionStorePath; - if (testState.sessionConfig) Object.assign(session, testState.sessionConfig); + } + if (testState.sessionConfig) { + Object.assign(session, testState.sessionConfig); + } const fileGateway = fileConfig.gateway && @@ -446,9 +453,15 @@ vi.mock("../config/config.js", async () => { !Array.isArray(fileConfig.gateway) ? ({ ...(fileConfig.gateway as Record) } as Record) : {}; - if (testState.gatewayBind) fileGateway.bind = testState.gatewayBind; - if (testState.gatewayAuth) fileGateway.auth = testState.gatewayAuth; - if (testState.gatewayControlUi) fileGateway.controlUi = testState.gatewayControlUi; + if (testState.gatewayBind) { + fileGateway.bind = testState.gatewayBind; + } + if (testState.gatewayAuth) { + fileGateway.auth = testState.gatewayAuth; + } + if (testState.gatewayControlUi) { + fileGateway.controlUi = testState.gatewayControlUi; + } const gateway = Object.keys(fileGateway).length > 0 ? fileGateway : undefined; const fileCanvasHost = @@ -457,8 +470,9 @@ vi.mock("../config/config.js", async () => { !Array.isArray(fileConfig.canvasHost) ? ({ ...(fileConfig.canvasHost as Record) } as Record) : {}; - if (typeof testState.canvasHostPort === "number") + if (typeof testState.canvasHostPort === "number") { fileCanvasHost.port = testState.canvasHostPort; + } const canvasHost = Object.keys(fileCanvasHost).length > 0 ? fileCanvasHost : undefined; const hooks = testState.hooksConfig ?? (fileConfig.hooks as HooksConfig | undefined); @@ -467,8 +481,12 @@ vi.mock("../config/config.js", async () => { fileConfig.cron && typeof fileConfig.cron === "object" && !Array.isArray(fileConfig.cron) ? ({ ...(fileConfig.cron as Record) } as Record) : {}; - if (typeof testState.cronEnabled === "boolean") fileCron.enabled = testState.cronEnabled; - if (typeof testState.cronStorePath === "string") fileCron.store = testState.cronStorePath; + if (typeof testState.cronEnabled === "boolean") { + fileCron.enabled = testState.cronEnabled; + } + if (typeof testState.cronStorePath === "string") { + fileCron.store = testState.cronStorePath; + } const cron = Object.keys(fileCron).length > 0 ? fileCron : undefined; const config = { @@ -560,5 +578,7 @@ vi.mock("../cli/deps.js", async () => { }; }); -process.env.CLAWDBOT_SKIP_CHANNELS = "1"; -process.env.CLAWDBOT_SKIP_CRON = "1"; +process.env.OPENCLAW_SKIP_CHANNELS = "1"; +process.env.OPENCLAW_SKIP_CRON = "1"; +process.env.OPENCLAW_SKIP_CHANNELS = "1"; +process.env.OPENCLAW_SKIP_CRON = "1"; diff --git a/src/gateway/test-helpers.openai-mock.ts b/src/gateway/test-helpers.openai-mock.ts index ea5977c04..77e7abb1f 100644 --- a/src/gateway/test-helpers.openai-mock.ts +++ b/src/gateway/test-helpers.openai-mock.ts @@ -22,7 +22,9 @@ type OpenAIResponseStreamEvent = function extractLastUserText(input: unknown[]): string { for (let i = input.length - 1; i >= 0; i -= 1) { const item = input[i] as Record | undefined; - if (!item || item.role !== "user") continue; + if (!item || item.role !== "user") { + continue; + } const content = item.content; if (Array.isArray(content)) { const text = content @@ -36,7 +38,9 @@ function extractLastUserText(input: unknown[]): string { .map((c) => c.text) .join("\n") .trim(); - if (text) return text; + if (text) { + return text; + } } } return ""; @@ -45,7 +49,9 @@ function extractLastUserText(input: unknown[]): string { function extractToolOutput(input: unknown[]): string { for (const itemRaw of input) { const item = itemRaw as Record | undefined; - if (!item || item.type !== "function_call_output") continue; + if (!item || item.type !== "function_call_output") { + continue; + } return typeof item.output === "string" ? item.output : ""; } return ""; @@ -128,10 +134,18 @@ async function* fakeOpenAIResponsesStream( } function decodeBodyText(body: unknown): string { - if (!body) return ""; - if (typeof body === "string") return body; - if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8"); - if (body instanceof ArrayBuffer) return Buffer.from(new Uint8Array(body)).toString("utf8"); + if (!body) { + return ""; + } + if (typeof body === "string") { + return body; + } + if (body instanceof Uint8Array) { + return Buffer.from(body).toString("utf8"); + } + if (body instanceof ArrayBuffer) { + return Buffer.from(new Uint8Array(body)).toString("utf8"); + } return ""; } diff --git a/src/gateway/test-helpers.server.ts b/src/gateway/test-helpers.server.ts index 154116813..99317df44 100644 --- a/src/gateway/test-helpers.server.ts +++ b/src/gateway/test-helpers.server.ts @@ -2,10 +2,9 @@ import fs from "node:fs/promises"; import { type AddressInfo, createServer } from "node:net"; import os from "node:os"; import path from "node:path"; - import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from "vitest"; import { WebSocket } from "ws"; - +import type { GatewayServerOptions } from "./server.js"; import { resolveMainSessionKeyFromConfig, type SessionEntry } from "../config/sessions.js"; import { resetAgentRunContextForTest } from "../infra/agent-events.js"; import { @@ -19,10 +18,8 @@ import { resetLogger, setLoggerOverride } from "../logging.js"; import { DEFAULT_AGENT_ID, toAgentStoreSessionKey } from "../routing/session-key.js"; import { getDeterministicFreePortBlock } from "../test-utils/ports.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; - -import { PROTOCOL_VERSION } from "./protocol/index.js"; import { buildDeviceAuthPayload } from "./device-auth.js"; -import type { GatewayServerOptions } from "./server.js"; +import { PROTOCOL_VERSION } from "./protocol/index.js"; import { agentCommand, cronIsolatedRun, @@ -56,7 +53,9 @@ export async function writeSessionStore(params: { mainKey?: string; }): Promise { const storePath = params.storePath ?? testState.sessionStorePath; - if (!storePath) throw new Error("writeSessionStore requires testState.sessionStorePath"); + if (!storePath) { + throw new Error("writeSessionStore requires testState.sessionStorePath"); + } const agentId = params.agentId ?? DEFAULT_AGENT_ID; const store: Record> = {}; for (const [requestKey, entry] of Object.entries(params.entries)) { @@ -78,22 +77,22 @@ export async function writeSessionStore(params: { async function setupGatewayTestHome() { previousHome = process.env.HOME; previousUserProfile = process.env.USERPROFILE; - previousStateDir = process.env.CLAWDBOT_STATE_DIR; - previousConfigPath = process.env.CLAWDBOT_CONFIG_PATH; - previousSkipBrowserControl = process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER; - previousSkipGmailWatcher = process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; - previousSkipCanvasHost = process.env.CLAWDBOT_SKIP_CANVAS_HOST; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gateway-home-")); + previousStateDir = process.env.OPENCLAW_STATE_DIR; + previousConfigPath = process.env.OPENCLAW_CONFIG_PATH; + previousSkipBrowserControl = process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER; + previousSkipGmailWatcher = process.env.OPENCLAW_SKIP_GMAIL_WATCHER; + previousSkipCanvasHost = process.env.OPENCLAW_SKIP_CANVAS_HOST; + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-home-")); process.env.HOME = tempHome; process.env.USERPROFILE = tempHome; - process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot"); - delete process.env.CLAWDBOT_CONFIG_PATH; + process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw"); + delete process.env.OPENCLAW_CONFIG_PATH; } function applyGatewaySkipEnv() { - process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; } async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) { @@ -105,8 +104,8 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) { } applyGatewaySkipEnv(); tempConfigRoot = options.uniqueConfigRoot - ? await fs.mkdtemp(path.join(tempHome, "moltbot-test-")) - : path.join(tempHome, ".clawdbot-test"); + ? await fs.mkdtemp(path.join(tempHome, "openclaw-test-")) + : path.join(tempHome, ".openclaw-test"); setTestConfigRoot(tempConfigRoot); sessionStoreSaveDelayMs.value = 0; testTailnetIPv4.value = undefined; @@ -148,21 +147,41 @@ async function cleanupGatewayTestHome(options: { restoreEnv: boolean }) { vi.useRealTimers(); resetLogger(); if (options.restoreEnv) { - if (previousHome === undefined) delete process.env.HOME; - else process.env.HOME = previousHome; - if (previousUserProfile === undefined) delete process.env.USERPROFILE; - else process.env.USERPROFILE = previousUserProfile; - if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = previousStateDir; - if (previousConfigPath === undefined) delete process.env.CLAWDBOT_CONFIG_PATH; - else process.env.CLAWDBOT_CONFIG_PATH = previousConfigPath; - if (previousSkipBrowserControl === undefined) - delete process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER; - else process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = previousSkipBrowserControl; - if (previousSkipGmailWatcher === undefined) delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; - else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher; - if (previousSkipCanvasHost === undefined) delete process.env.CLAWDBOT_SKIP_CANVAS_HOST; - else process.env.CLAWDBOT_SKIP_CANVAS_HOST = previousSkipCanvasHost; + if (previousHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = previousHome; + } + if (previousUserProfile === undefined) { + delete process.env.USERPROFILE; + } else { + process.env.USERPROFILE = previousUserProfile; + } + if (previousStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previousStateDir; + } + if (previousConfigPath === undefined) { + delete process.env.OPENCLAW_CONFIG_PATH; + } else { + process.env.OPENCLAW_CONFIG_PATH = previousConfigPath; + } + if (previousSkipBrowserControl === undefined) { + delete process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER; + } else { + process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = previousSkipBrowserControl; + } + if (previousSkipGmailWatcher === undefined) { + delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER; + } else { + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher; + } + if (previousSkipCanvasHost === undefined) { + delete process.env.OPENCLAW_SKIP_CANVAS_HOST; + } else { + process.env.OPENCLAW_SKIP_CANVAS_HOST = previousSkipCanvasHost; + } } if (options.restoreEnv && tempHome) { await fs.rm(tempHome, { @@ -259,7 +278,7 @@ export async function startGatewayServer(port: number, opts?: GatewayServerOptio export async function startServerWithClient(token?: string, opts?: GatewayServerOptions) { let port = await getFreePort(); - const prev = process.env.CLAWDBOT_GATEWAY_TOKEN; + const prev = process.env.OPENCLAW_GATEWAY_TOKEN; if (typeof token === "string") { testState.gatewayAuth = { mode: "token", token }; } @@ -269,9 +288,9 @@ export async function startServerWithClient(token?: string, opts?: GatewayServer ? (testState.gatewayAuth as { token?: string }).token : undefined); if (fallbackToken === undefined) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = fallbackToken; + process.env.OPENCLAW_GATEWAY_TOKEN = fallbackToken; } let server: Awaited> | null = null; @@ -281,7 +300,9 @@ export async function startServerWithClient(token?: string, opts?: GatewayServer break; } catch (err) { const code = (err as { cause?: { code?: string } }).cause?.code; - if (code !== "EADDRINUSE") throw err; + if (code !== "EADDRINUSE") { + throw err; + } port = await getFreePort(); } } @@ -348,19 +369,23 @@ export async function connectReq( ? undefined : typeof (testState.gatewayAuth as { token?: unknown } | undefined)?.token === "string" ? ((testState.gatewayAuth as { token?: string }).token ?? undefined) - : process.env.CLAWDBOT_GATEWAY_TOKEN; + : process.env.OPENCLAW_GATEWAY_TOKEN; const defaultPassword = opts?.skipDefaultAuth === true ? undefined : typeof (testState.gatewayAuth as { password?: unknown } | undefined)?.password === "string" ? ((testState.gatewayAuth as { password?: string }).password ?? undefined) - : process.env.CLAWDBOT_GATEWAY_PASSWORD; + : process.env.OPENCLAW_GATEWAY_PASSWORD; const token = opts?.token ?? defaultToken; const password = opts?.password ?? defaultPassword; const requestedScopes = Array.isArray(opts?.scopes) ? opts?.scopes : []; const device = (() => { - if (opts?.device === null) return undefined; - if (opts?.device) return opts.device; + if (opts?.device === null) { + return undefined; + } + if (opts?.device) { + return opts.device; + } const identity = loadOrCreateDeviceIdentity(); const signedAtMs = Date.now(); const payload = buildDeviceAuthPayload({ @@ -406,7 +431,9 @@ export async function connectReq( }), ); const isResponseForId = (o: unknown): boolean => { - if (!o || typeof o !== "object" || Array.isArray(o)) return false; + if (!o || typeof o !== "object" || Array.isArray(o)) { + return false; + } const rec = o as Record; return rec.type === "res" && rec.id === id; }; @@ -438,7 +465,9 @@ export async function rpcReq( }>( ws, (o) => { - if (!o || typeof o !== "object" || Array.isArray(o)) return false; + if (!o || typeof o !== "object" || Array.isArray(o)) { + return false; + } const rec = o as Record; return rec.type === "res" && rec.id === id; }, @@ -451,7 +480,9 @@ export async function waitForSystemEvent(timeoutMs = 2000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const events = peekSystemEvents(sessionKey); - if (events.length > 0) return events; + if (events.length > 0) { + return events; + } await new Promise((resolve) => setTimeout(resolve, 10)); } throw new Error("timeout waiting for system event"); diff --git a/src/gateway/tools-invoke-http.test.ts b/src/gateway/tools-invoke-http.test.ts index 93a295fc8..d24654d91 100644 --- a/src/gateway/tools-invoke-http.test.ts +++ b/src/gateway/tools-invoke-http.test.ts @@ -1,37 +1,37 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - import type { IncomingMessage, ServerResponse } from "node:http"; import { promises as fs } from "node:fs"; import path from "node:path"; - -import { installGatewayTestHooks, getFreePort, startGatewayServer } from "./test-helpers.server.js"; -import { resetTestPluginRegistry, setTestPluginRegistry, testState } from "./test-helpers.mocks.js"; -import { createTestRegistry } from "../test-utils/channel-plugins.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { CONFIG_PATH } from "../config/config.js"; +import { createTestRegistry } from "../test-utils/channel-plugins.js"; +import { resetTestPluginRegistry, setTestPluginRegistry, testState } from "./test-helpers.mocks.js"; +import { installGatewayTestHooks, getFreePort, startGatewayServer } from "./test-helpers.server.js"; installGatewayTestHooks({ scope: "suite" }); beforeEach(() => { // Ensure these tests are not affected by host env vars. - delete process.env.CLAWDBOT_GATEWAY_TOKEN; - delete process.env.CLAWDBOT_GATEWAY_PASSWORD; + delete process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_PASSWORD; }); const resolveGatewayToken = (): string => { const token = (testState.gatewayAuth as { token?: string } | undefined)?.token; - if (!token) throw new Error("test gateway token missing"); + if (!token) { + throw new Error("test gateway token missing"); + } return token; }; describe("POST /tools/invoke", () => { it("invokes a tool and returns {ok:true,result}", async () => { - // Allow the sessions_list tool for main agent. + // Allow the agents_list tool for main agent. testState.agentsConfig = { list: [ { id: "main", tools: { - allow: ["sessions_list"], + allow: ["agents_list"], }, }, ], @@ -46,7 +46,7 @@ describe("POST /tools/invoke", () => { const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, - body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }), + body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }), }); expect(res.status).toBe(200); @@ -63,10 +63,10 @@ describe("POST /tools/invoke", () => { list: [{ id: "main" }], } as any; - // minimal profile does NOT include sessions_list, but alsoAllow should. + // minimal profile does NOT include agents_list, but alsoAllow should. const { writeConfigFile } = await import("../config/config.js"); await writeConfigFile({ - tools: { profile: "minimal", alsoAllow: ["sessions_list"] }, + tools: { profile: "minimal", alsoAllow: ["agents_list"] }, } as any); const port = await getFreePort(); @@ -76,7 +76,7 @@ describe("POST /tools/invoke", () => { const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, - body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }), + body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }), }); expect(res.status).toBe(200); @@ -94,7 +94,7 @@ describe("POST /tools/invoke", () => { await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true }); await fs.writeFile( CONFIG_PATH, - JSON.stringify({ tools: { alsoAllow: ["sessions_list"] } }, null, 2), + JSON.stringify({ tools: { alsoAllow: ["agents_list"] } }, null, 2), "utf-8", ); @@ -105,7 +105,7 @@ describe("POST /tools/invoke", () => { const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, - body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }), + body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }), }); expect(res.status).toBe(200); @@ -121,7 +121,7 @@ describe("POST /tools/invoke", () => { { id: "main", tools: { - allow: ["sessions_list"], + allow: ["agents_list"], }, }, ], @@ -139,7 +139,7 @@ describe("POST /tools/invoke", () => { "content-type": "application/json", authorization: "Bearer secret", }, - body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }), + body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }), }); expect(res.status).toBe(200); @@ -171,7 +171,7 @@ describe("POST /tools/invoke", () => { { id: "main", tools: { - allow: ["sessions_list"], + allow: ["agents_list"], }, }, ], @@ -185,7 +185,7 @@ describe("POST /tools/invoke", () => { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify({ - tool: "sessions_list", + tool: "agents_list", action: "json", args: {}, sessionKey: "main", @@ -206,7 +206,7 @@ describe("POST /tools/invoke", () => { { id: "main", tools: { - allow: ["sessions_list"], + allow: ["agents_list"], }, }, ], @@ -221,7 +221,7 @@ describe("POST /tools/invoke", () => { const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }), + body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }), }); expect(res.status).toBe(401); @@ -235,7 +235,7 @@ describe("POST /tools/invoke", () => { { id: "main", tools: { - deny: ["sessions_list"], + deny: ["agents_list"], }, }, ], @@ -248,7 +248,7 @@ describe("POST /tools/invoke", () => { const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, - body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }), + body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }), }); expect(res.status).toBe(404); @@ -262,7 +262,7 @@ describe("POST /tools/invoke", () => { { id: "main", tools: { - allow: ["sessions_list"], + allow: ["agents_list"], }, }, ], @@ -280,7 +280,7 @@ describe("POST /tools/invoke", () => { const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, - body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }), + body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }), }); expect(res.status).toBe(404); @@ -294,14 +294,14 @@ describe("POST /tools/invoke", () => { { id: "main", tools: { - deny: ["sessions_list"], + deny: ["agents_list"], }, }, { id: "ops", default: true, tools: { - allow: ["sessions_list"], + allow: ["agents_list"], }, }, ], @@ -311,7 +311,7 @@ describe("POST /tools/invoke", () => { const port = await getFreePort(); const server = await startGatewayServer(port, { bind: "loopback" }); - const payload = { tool: "sessions_list", action: "json", args: {} }; + const payload = { tool: "agents_list", action: "json", args: {} }; const token = resolveGatewayToken(); const resDefault = await fetch(`http://127.0.0.1:${port}/tools/invoke`, { diff --git a/src/gateway/tools-invoke-http.ts b/src/gateway/tools-invoke-http.ts index fa45bf3dc..c0f837472 100644 --- a/src/gateway/tools-invoke-http.ts +++ b/src/gateway/tools-invoke-http.ts @@ -1,6 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; - -import { createMoltbotTools } from "../agents/moltbot-tools.js"; +import { createOpenClawTools } from "../agents/openclaw-tools.js"; import { filterToolsByPolicy, resolveEffectiveToolPolicy, @@ -18,12 +17,11 @@ import { import { loadConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { logWarn } from "../logger.js"; +import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js"; import { getPluginToolMeta } from "../plugins/tools.js"; import { isSubagentSessionKey } from "../routing/session-key.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; - import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js"; -import { getBearerToken, getHeader } from "./http-utils.js"; import { readJsonBodyOrError, sendInvalidRequest, @@ -31,8 +29,10 @@ import { sendMethodNotAllowed, sendUnauthorized, } from "./http-common.js"; +import { getBearerToken, getHeader } from "./http-utils.js"; const DEFAULT_BODY_BYTES = 2 * 1024 * 1024; +const MEMORY_TOOL_NAMES = new Set(["memory_search", "memory_get"]); type ToolsInvokeBody = { tool?: unknown; @@ -43,18 +43,48 @@ type ToolsInvokeBody = { }; function resolveSessionKeyFromBody(body: ToolsInvokeBody): string | undefined { - if (typeof body.sessionKey === "string" && body.sessionKey.trim()) return body.sessionKey.trim(); + if (typeof body.sessionKey === "string" && body.sessionKey.trim()) { + return body.sessionKey.trim(); + } return undefined; } +function resolveMemoryToolDisableReasons(cfg: ReturnType): string[] { + if (!process.env.VITEST) { + return []; + } + const reasons: string[] = []; + const plugins = cfg.plugins; + const slotRaw = plugins?.slots?.memory; + const slotDisabled = + slotRaw === null || (typeof slotRaw === "string" && slotRaw.trim().toLowerCase() === "none"); + const pluginsDisabled = plugins?.enabled === false; + const defaultDisabled = isTestDefaultMemorySlotDisabled(cfg); + + if (pluginsDisabled) { + reasons.push("plugins.enabled=false"); + } + if (slotDisabled) { + reasons.push(slotRaw === null ? "plugins.slots.memory=null" : 'plugins.slots.memory="none"'); + } + if (!pluginsDisabled && !slotDisabled && defaultDisabled) { + reasons.push("memory plugin disabled by test default"); + } + return reasons; +} + function mergeActionIntoArgsIfSupported(params: { toolSchema: unknown; action: string | undefined; args: Record; }): Record { const { toolSchema, action, args } = params; - if (!action) return args; - if (args.action !== undefined) return args; + if (!action) { + return args; + } + if (args.action !== undefined) { + return args; + } // TypeBox schemas are plain objects; many tools define an `action` property. const schemaObj = toolSchema as { properties?: Record } | null; const hasAction = Boolean( @@ -63,7 +93,9 @@ function mergeActionIntoArgsIfSupported(params: { schemaObj.properties && "action" in schemaObj.properties, ); - if (!hasAction) return args; + if (!hasAction) { + return args; + } return { ...args, action }; } @@ -73,7 +105,9 @@ export async function handleToolsInvokeHttpRequest( opts: { auth: ResolvedGatewayAuth; maxBodyBytes?: number; trustedProxies?: string[] }, ): Promise { const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); - if (url.pathname !== "/tools/invoke") return false; + if (url.pathname !== "/tools/invoke") { + return false; + } if (req.method !== "POST") { sendMethodNotAllowed(res, "POST"); @@ -94,7 +128,9 @@ export async function handleToolsInvokeHttpRequest( } const bodyUnknown = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? DEFAULT_BODY_BYTES); - if (bodyUnknown === undefined) return true; + if (bodyUnknown === undefined) { + return true; + } const body = (bodyUnknown ?? {}) as ToolsInvokeBody; const toolName = typeof body.tool === "string" ? body.tool.trim() : ""; @@ -103,22 +139,40 @@ export async function handleToolsInvokeHttpRequest( return true; } + if (process.env.VITEST && MEMORY_TOOL_NAMES.has(toolName)) { + const reasons = resolveMemoryToolDisableReasons(cfg); + if (reasons.length > 0) { + const suffix = reasons.length > 0 ? ` (${reasons.join(", ")})` : ""; + sendJson(res, 400, { + ok: false, + error: { + type: "invalid_request", + message: + `memory tools are disabled in tests${suffix}. ` + + 'Enable by setting plugins.slots.memory="memory-core" (and ensure plugins.enabled is not false).', + }, + }); + return true; + } + } + const action = typeof body.action === "string" ? body.action.trim() : undefined; const argsRaw = body.args; - const args = ( + const args = argsRaw && typeof argsRaw === "object" && !Array.isArray(argsRaw) ? (argsRaw as Record) - : {} - ) as Record; + : {}; const rawSessionKey = resolveSessionKeyFromBody(body); const sessionKey = !rawSessionKey || rawSessionKey === "main" ? resolveMainSessionKey(cfg) : rawSessionKey; // Resolve message channel/account hints (optional headers) for policy inheritance. - const messageChannel = normalizeMessageChannel(getHeader(req, "x-moltbot-message-channel") ?? ""); - const accountId = getHeader(req, "x-moltbot-account-id")?.trim() || undefined; + const messageChannel = normalizeMessageChannel( + getHeader(req, "x-openclaw-message-channel") ?? "", + ); + const accountId = getHeader(req, "x-openclaw-account-id")?.trim() || undefined; const { agentId, @@ -135,7 +189,9 @@ export async function handleToolsInvokeHttpRequest( const providerProfilePolicy = resolveToolProfilePolicy(providerProfile); const mergeAlsoAllow = (policy: typeof profilePolicy, alsoAllow?: string[]) => { - if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) return policy; + if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) { + return policy; + } return { ...policy, allow: Array.from(new Set([...policy.allow, ...alsoAllow])) }; }; @@ -155,7 +211,7 @@ export async function handleToolsInvokeHttpRequest( : undefined; // Build tool list (core + plugin tools). - const allTools = createMoltbotTools({ + const allTools = createOpenClawTools({ agentSessionKey: sessionKey, agentChannel: messageChannel ?? undefined, agentAccountId: accountId, diff --git a/src/gateway/ws-log.ts b/src/gateway/ws-log.ts index fccfc5b36..7c540267c 100644 --- a/src/gateway/ws-log.ts +++ b/src/gateway/ws-log.ts @@ -1,9 +1,9 @@ import chalk from "chalk"; import { isVerbose } from "../globals.js"; -import { parseAgentSessionKey } from "../routing/session-key.js"; import { shouldLogSubsystemToConsole } from "../logging/console.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; import { getDefaultRedactPatterns, redactSensitiveText } from "../logging/redact.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; +import { parseAgentSessionKey } from "../routing/session-key.js"; import { DEFAULT_WS_SLOW_MS, getGatewayWsLogStyle } from "./ws-logging.js"; const LOG_VALUE_LIMIT = 240; @@ -27,8 +27,12 @@ const wsLog = createSubsystemLogger("gateway/ws"); export function shortId(value: string): string { const s = value.trim(); - if (UUID_RE.test(s)) return `${s.slice(0, 8)}…${s.slice(-4)}`; - if (s.length <= 24) return s; + if (UUID_RE.test(s)) { + return `${s.slice(0, 8)}…${s.slice(-4)}`; + } + if (s.length <= 24) { + return s; + } return `${s.slice(0, 12)}…${s.slice(-4)}`; } @@ -36,13 +40,19 @@ export function formatForLog(value: unknown): string { try { if (value instanceof Error) { const parts: string[] = []; - if (value.name) parts.push(value.name); - if (value.message) parts.push(value.message); + if (value.name) { + parts.push(value.name); + } + if (value.message) { + parts.push(value.message); + } const code = "code" in value && (typeof value.code === "string" || typeof value.code === "number") ? String(value.code) : ""; - if (code) parts.push(`code=${code}`); + if (code) { + parts.push(`code=${code}`); + } const combined = parts.filter(Boolean).join(": ").trim(); if (combined) { return combined.length > LOG_VALUE_LIMIT @@ -57,7 +67,9 @@ export function formatForLog(value: unknown): string { const code = typeof rec.code === "string" || typeof rec.code === "number" ? String(rec.code) : ""; const parts = [name, rec.message.trim()].filter(Boolean); - if (code) parts.push(`code=${code}`); + if (code) { + parts.push(`code=${code}`); + } const combined = parts.join(": ").trim(); return combined.length > LOG_VALUE_LIMIT ? `${combined.slice(0, LOG_VALUE_LIMIT)}...` @@ -68,7 +80,9 @@ export function formatForLog(value: unknown): string { typeof value === "string" || typeof value === "number" ? String(value) : JSON.stringify(value); - if (!str) return ""; + if (!str) { + return ""; + } const redacted = redactSensitiveText(str, WS_LOG_REDACT_OPTIONS); return redacted.length > LOG_VALUE_LIMIT ? `${redacted.slice(0, LOG_VALUE_LIMIT)}...` @@ -80,12 +94,16 @@ export function formatForLog(value: unknown): string { function compactPreview(input: string, maxLen = 160): string { const oneLine = input.replace(/\s+/g, " ").trim(); - if (oneLine.length <= maxLen) return oneLine; + if (oneLine.length <= maxLen) { + return oneLine; + } return `${oneLine.slice(0, Math.max(0, maxLen - 1))}…`; } export function summarizeAgentEventForWsLog(payload: unknown): Record { - if (!payload || typeof payload !== "object") return {}; + if (!payload || typeof payload !== "object") { + return {}; + } const rec = payload as Record; const runId = typeof rec.runId === "string" ? rec.runId : undefined; const stream = typeof rec.stream === "string" ? rec.stream : undefined; @@ -95,7 +113,9 @@ export function summarizeAgentEventForWsLog(payload: unknown): Record) : undefined; const extra: Record = {}; - if (runId) extra.run = shortId(runId); + if (runId) { + extra.run = shortId(runId); + } if (sessionKey) { const parsed = parseAgentSessionKey(sessionKey); if (parsed) { @@ -105,47 +125,75 @@ export function summarizeAgentEventForWsLog(payload: unknown): Record 0) extra.media = mediaUrls.length; + if (mediaUrls && mediaUrls.length > 0) { + extra.media = mediaUrls.length; + } return extra; } if (stream === "tool") { const phase = typeof data.phase === "string" ? data.phase : undefined; const name = typeof data.name === "string" ? data.name : undefined; - if (phase || name) extra.tool = `${phase ?? "?"}:${name ?? "?"}`; + if (phase || name) { + extra.tool = `${phase ?? "?"}:${name ?? "?"}`; + } const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined; - if (toolCallId) extra.call = shortId(toolCallId); + if (toolCallId) { + extra.call = shortId(toolCallId); + } const meta = typeof data.meta === "string" ? data.meta : undefined; - if (meta?.trim()) extra.meta = meta; - if (typeof data.isError === "boolean") extra.err = data.isError; + if (meta?.trim()) { + extra.meta = meta; + } + if (typeof data.isError === "boolean") { + extra.err = data.isError; + } return extra; } if (stream === "lifecycle") { const phase = typeof data.phase === "string" ? data.phase : undefined; - if (phase) extra.phase = phase; - if (typeof data.aborted === "boolean") extra.aborted = data.aborted; + if (phase) { + extra.phase = phase; + } + if (typeof data.aborted === "boolean") { + extra.aborted = data.aborted; + } const error = typeof data.error === "string" ? data.error : undefined; - if (error?.trim()) extra.error = compactPreview(error, 120); + if (error?.trim()) { + extra.error = compactPreview(error, 120); + } return extra; } const reason = typeof data.reason === "string" ? data.reason : undefined; - if (reason?.trim()) extra.reason = reason; + if (reason?.trim()) { + extra.reason = reason; + } return extra; } export function logWs(direction: "in" | "out", kind: string, meta?: Record) { - if (!shouldLogSubsystemToConsole("gateway/ws")) return; + if (!shouldLogSubsystemToConsole("gateway/ws")) { + return; + } const style = getGatewayWsLogStyle(); if (!isVerbose()) { logWsOptimized(direction, kind, meta); @@ -172,7 +220,9 @@ export function logWs(direction: "in" | "out", kind: string, meta?: Record { const startedAt = wsInflightSince.get(inflightKey); - if (startedAt === undefined) return undefined; + if (startedAt === undefined) { + return undefined; + } wsInflightSince.delete(inflightKey); return now - startedAt; })() @@ -201,10 +251,18 @@ export function logWs(direction: "in" | "out", kind: string, meta?: Record Boolean(t), @@ -232,7 +292,9 @@ function logWsOptimized(direction: "in" | "out", kind: string, meta?: Record 2000) wsInflightOptimized.clear(); + if (wsInflightOptimized.size > 2000) { + wsInflightOptimized.clear(); + } return; } @@ -250,15 +312,21 @@ function logWsOptimized(direction: "in" | "out", kind: string, meta?: Record= DEFAULT_WS_SLOW_MS); - if (!shouldLog) return; + if (!shouldLog) { + return; + } const statusToken = ok === undefined ? undefined : ok ? chalk.greenBright("✓") : chalk.redBright("✗"); @@ -267,9 +335,15 @@ function logWsOptimized(direction: "in" | "out", kind: string, meta?: Record { - if (kind === "req" || kind === "res") return "⇄"; + if (kind === "req" || kind === "res") { + return "⇄"; + } return direction === "in" ? "←" : "→"; })(); const arrowColor = @@ -340,10 +416,18 @@ function logWsCompact(direction: "in" | "out", kind: string, meta?: Record Boolean(t), diff --git a/src/git-hooks.test.ts b/src/git-hooks.test.ts index ddb52a4f4..569f9fcbb 100644 --- a/src/git-hooks.test.ts +++ b/src/git-hooks.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; - import { filterOxfmtTargets, filterOutPartialTargets, @@ -12,7 +11,7 @@ import { import { setupGitHooks } from "../scripts/setup-git-hooks.js"; function makeTempDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-hooks-")); + return fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-hooks-")); } describe("format-staged helpers", () => { @@ -59,8 +58,12 @@ describe("setupGitHooks", () => { it("returns not-repo when not inside a work tree", () => { const runGit = vi.fn((args) => { - if (args[0] === "--version") return { status: 0, stdout: "git version" }; - if (args[0] === "rev-parse") return { status: 0, stdout: "false" }; + if (args[0] === "--version") { + return { status: 0, stdout: "git version" }; + } + if (args[0] === "rev-parse") { + return { status: 0, stdout: "false" }; + } return { status: 1, stdout: "" }; }); @@ -77,9 +80,15 @@ describe("setupGitHooks", () => { fs.chmodSync(hookPath, 0o644); const runGit = vi.fn((args) => { - if (args[0] === "--version") return { status: 0, stdout: "git version" }; - if (args[0] === "rev-parse") return { status: 0, stdout: "true" }; - if (args[0] === "config") return { status: 0, stdout: "" }; + if (args[0] === "--version") { + return { status: 0, stdout: "git version" }; + } + if (args[0] === "rev-parse") { + return { status: 0, stdout: "true" }; + } + if (args[0] === "config") { + return { status: 0, stdout: "" }; + } return { status: 1, stdout: "" }; }); diff --git a/src/globals.ts b/src/globals.ts index c3b9b2d1f..5523c41ec 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -17,18 +17,24 @@ export function shouldLogVerbose() { } export function logVerbose(message: string) { - if (!shouldLogVerbose()) return; + if (!shouldLogVerbose()) { + return; + } try { getLogger().debug({ message }, "verbose"); } catch { // ignore logger failures to avoid breaking verbose printing } - if (!globalVerbose) return; + if (!globalVerbose) { + return; + } console.log(theme.muted(message)); } export function logVerboseConsole(message: string) { - if (!globalVerbose) return; + if (!globalVerbose) { + return; + } console.log(theme.muted(message)); } diff --git a/src/hooks/bundled-dir.ts b/src/hooks/bundled-dir.ts index 6ced545e3..9f2560751 100644 --- a/src/hooks/bundled-dir.ts +++ b/src/hooks/bundled-dir.ts @@ -3,24 +3,30 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; export function resolveBundledHooksDir(): string | undefined { - const override = process.env.CLAWDBOT_BUNDLED_HOOKS_DIR?.trim(); - if (override) return override; + const override = process.env.OPENCLAW_BUNDLED_HOOKS_DIR?.trim(); + if (override) { + return override; + } // bun --compile: ship a sibling `hooks/bundled/` next to the executable. try { const execDir = path.dirname(process.execPath); const sibling = path.join(execDir, "hooks", "bundled"); - if (fs.existsSync(sibling)) return sibling; + if (fs.existsSync(sibling)) { + return sibling; + } } catch { // ignore } // npm: resolve `/dist/hooks/bundled` relative to this module (compiled hooks). - // This path works when installed via npm: node_modules/moltbot/dist/hooks/bundled-dir.js + // This path works when installed via npm: node_modules/openclaw/dist/hooks/bundled-dir.js try { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const distBundled = path.join(moduleDir, "bundled"); - if (fs.existsSync(distBundled)) return distBundled; + if (fs.existsSync(distBundled)) { + return distBundled; + } } catch { // ignore } @@ -31,7 +37,9 @@ export function resolveBundledHooksDir(): string | undefined { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(moduleDir, "..", ".."); const srcBundled = path.join(root, "src", "hooks", "bundled"); - if (fs.existsSync(srcBundled)) return srcBundled; + if (fs.existsSync(srcBundled)) { + return srcBundled; + } } catch { // ignore } diff --git a/src/hooks/bundled/README.md b/src/hooks/bundled/README.md index 77df25ee4..b842d7909 100644 --- a/src/hooks/bundled/README.md +++ b/src/hooks/bundled/README.md @@ -1,6 +1,6 @@ # Bundled Hooks -This directory contains hooks that ship with Clawdbot. These hooks are automatically discovered and can be enabled/disabled via CLI or configuration. +This directory contains hooks that ship with OpenClaw. These hooks are automatically discovered and can be enabled/disabled via CLI or configuration. ## Available Hooks @@ -10,12 +10,12 @@ Automatically saves session context to memory when you issue `/new`. **Events**: `command:new` **What it does**: Creates a dated memory file with LLM-generated slug based on conversation content. -**Output**: `/memory/YYYY-MM-DD-slug.md` (defaults to `~/clawd`) +**Output**: `/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`) **Enable**: ```bash -clawdbot hooks enable session-memory +openclaw hooks enable session-memory ``` ### 📝 command-logger @@ -24,12 +24,12 @@ Logs all command events to a centralized audit file. **Events**: `command` (all commands) **What it does**: Appends JSONL entries to command log file. -**Output**: `~/.clawdbot/logs/commands.log` +**Output**: `~/.openclaw/logs/commands.log` **Enable**: ```bash -clawdbot hooks enable command-logger +openclaw hooks enable command-logger ``` ### 😈 soul-evil @@ -39,12 +39,12 @@ Swaps injected `SOUL.md` content with `SOUL_EVIL.md` during a purge window or by **Events**: `agent:bootstrap` **What it does**: Overrides the injected SOUL content before the system prompt is built. **Output**: No files written; swaps happen in-memory only. -**Docs**: https://docs.molt.bot/hooks/soul-evil +**Docs**: https://docs.openclaw.ai/hooks/soul-evil **Enable**: ```bash -clawdbot hooks enable soul-evil +openclaw hooks enable soul-evil ``` ### 🚀 boot-md @@ -58,7 +58,7 @@ Runs `BOOT.md` whenever the gateway starts (after channels start). **Enable**: ```bash -clawdbot hooks enable boot-md +openclaw hooks enable boot-md ``` ## Hook Structure @@ -82,9 +82,9 @@ session-memory/ --- name: my-hook description: "Short description" -homepage: https://docs.molt.bot/hooks#my-hook +homepage: https://docs.openclaw.ai/hooks#my-hook metadata: - { "clawdbot": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } } + { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } } --- # Hook Title @@ -108,7 +108,7 @@ Documentation goes here... To create your own hooks, place them in: - **Workspace hooks**: `/hooks/` (highest precedence) -- **Managed hooks**: `~/.clawdbot/hooks/` (shared across workspaces) +- **Managed hooks**: `~/.openclaw/hooks/` (shared across workspaces) Custom hooks follow the same structure as bundled hooks. @@ -117,31 +117,31 @@ Custom hooks follow the same structure as bundled hooks. List all hooks: ```bash -clawdbot hooks list +openclaw hooks list ``` Show hook details: ```bash -clawdbot hooks info session-memory +openclaw hooks info session-memory ``` Check hook status: ```bash -clawdbot hooks check +openclaw hooks check ``` Enable/disable: ```bash -clawdbot hooks enable session-memory -clawdbot hooks disable command-logger +openclaw hooks enable session-memory +openclaw hooks disable command-logger ``` ## Configuration -Hooks can be configured in `~/.clawdbot/clawdbot.json`: +Hooks can be configured in `~/.openclaw/openclaw.json`: ```json { @@ -214,11 +214,11 @@ export default myHandler; Test your hooks by: 1. Place hook in workspace hooks directory -2. Restart gateway: `pkill -9 -f 'clawdbot.*gateway' && pnpm clawdbot gateway` -3. Enable the hook: `clawdbot hooks enable my-hook` +2. Restart gateway: `pkill -9 -f 'openclaw.*gateway' && pnpm openclaw gateway` +3. Enable the hook: `openclaw hooks enable my-hook` 4. Trigger the event (e.g., send `/new` command) 5. Check gateway logs for hook execution ## Documentation -Full documentation: https://docs.molt.bot/hooks +Full documentation: https://docs.openclaw.ai/hooks diff --git a/src/hooks/bundled/boot-md/HOOK.md b/src/hooks/bundled/boot-md/HOOK.md index 545a604fe..59755318c 100644 --- a/src/hooks/bundled/boot-md/HOOK.md +++ b/src/hooks/bundled/boot-md/HOOK.md @@ -1,15 +1,15 @@ --- name: boot-md description: "Run BOOT.md on gateway startup" -homepage: https://docs.molt.bot/hooks#boot-md +homepage: https://docs.openclaw.ai/hooks#boot-md metadata: { - "moltbot": + "openclaw": { "emoji": "🚀", "events": ["gateway:startup"], "requires": { "config": ["workspace.dir"] }, - "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with Moltbot" }], + "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], }, } --- diff --git a/src/hooks/bundled/boot-md/handler.ts b/src/hooks/bundled/boot-md/handler.ts index 013343587..4084d1798 100644 --- a/src/hooks/bundled/boot-md/handler.ts +++ b/src/hooks/bundled/boot-md/handler.ts @@ -1,11 +1,11 @@ import type { CliDeps } from "../../../cli/deps.js"; -import { createDefaultDeps } from "../../../cli/deps.js"; -import type { MoltbotConfig } from "../../../config/config.js"; -import { runBootOnce } from "../../../gateway/boot.js"; +import type { OpenClawConfig } from "../../../config/config.js"; import type { HookHandler } from "../../hooks.js"; +import { createDefaultDeps } from "../../../cli/deps.js"; +import { runBootOnce } from "../../../gateway/boot.js"; type BootHookContext = { - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; workspaceDir?: string; deps?: CliDeps; }; diff --git a/src/hooks/bundled/command-logger/HOOK.md b/src/hooks/bundled/command-logger/HOOK.md index 897c3e0fb..dd7636c7d 100644 --- a/src/hooks/bundled/command-logger/HOOK.md +++ b/src/hooks/bundled/command-logger/HOOK.md @@ -1,14 +1,14 @@ --- name: command-logger description: "Log all command events to a centralized audit file" -homepage: https://docs.molt.bot/hooks#command-logger +homepage: https://docs.openclaw.ai/hooks#command-logger metadata: { - "moltbot": + "openclaw": { "emoji": "📝", "events": ["command"], - "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with Moltbot" }], + "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], }, } --- @@ -22,7 +22,7 @@ Logs all command events (`/new`, `/reset`, `/stop`, etc.) to a centralized audit Every time you issue a command to the agent: 1. **Captures event details** - Command action, timestamp, session key, sender ID, source -2. **Appends to log file** - Writes a JSON line to `~/.clawdbot/logs/commands.log` +2. **Appends to log file** - Writes a JSON line to `~/.openclaw/logs/commands.log` 3. **Silent operation** - Runs in the background without user notifications ## Output Format @@ -43,7 +43,7 @@ Log entries are written in JSONL (JSON Lines) format: ## Log File Location -`~/.clawdbot/logs/commands.log` +`~/.openclaw/logs/commands.log` ## Requirements @@ -62,7 +62,7 @@ No configuration needed. The hook automatically: To disable this hook: ```bash -moltbot hooks disable command-logger +openclaw hooks disable command-logger ``` Or via config: @@ -86,13 +86,13 @@ The hook does not automatically rotate logs. To manage log size, you can: 1. **Manual rotation**: ```bash - mv ~/.clawdbot/logs/commands.log ~/.clawdbot/logs/commands.log.old + mv ~/.openclaw/logs/commands.log ~/.openclaw/logs/commands.log.old ``` 2. **Use logrotate** (Linux): - Create `/etc/logrotate.d/moltbot`: + Create `/etc/logrotate.d/openclaw`: ``` - /home/username/.clawdbot/logs/commands.log { + /home/username/.openclaw/logs/commands.log { weekly rotate 4 compress @@ -106,17 +106,17 @@ The hook does not automatically rotate logs. To manage log size, you can: View recent commands: ```bash -tail -n 20 ~/.clawdbot/logs/commands.log +tail -n 20 ~/.openclaw/logs/commands.log ``` Pretty-print with jq: ```bash -cat ~/.clawdbot/logs/commands.log | jq . +cat ~/.openclaw/logs/commands.log | jq . ``` Filter by action: ```bash -grep '"action":"new"' ~/.clawdbot/logs/commands.log | jq . +grep '"action":"new"' ~/.openclaw/logs/commands.log | jq . ``` diff --git a/src/hooks/bundled/command-logger/handler.ts b/src/hooks/bundled/command-logger/handler.ts index c89bfc0d1..16cd071ed 100644 --- a/src/hooks/bundled/command-logger/handler.ts +++ b/src/hooks/bundled/command-logger/handler.ts @@ -24,8 +24,8 @@ */ import fs from "node:fs/promises"; -import path from "node:path"; import os from "node:os"; +import path from "node:path"; import type { HookHandler } from "../../hooks.js"; /** @@ -39,7 +39,8 @@ const logCommand: HookHandler = async (event) => { try { // Create log directory - const logDir = path.join(os.homedir(), ".clawdbot", "logs"); + const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw"); + const logDir = path.join(stateDir, "logs"); await fs.mkdir(logDir, { recursive: true }); // Append to command log file diff --git a/src/hooks/bundled/session-memory/HOOK.md b/src/hooks/bundled/session-memory/HOOK.md index 41223eb05..20b598576 100644 --- a/src/hooks/bundled/session-memory/HOOK.md +++ b/src/hooks/bundled/session-memory/HOOK.md @@ -1,15 +1,15 @@ --- name: session-memory description: "Save session context to memory when /new command is issued" -homepage: https://docs.molt.bot/hooks#session-memory +homepage: https://docs.openclaw.ai/hooks#session-memory metadata: { - "moltbot": + "openclaw": { "emoji": "💾", "events": ["command:new"], "requires": { "config": ["workspace.dir"] }, - "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with Moltbot" }], + "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], }, } --- @@ -82,7 +82,7 @@ Example configuration: The hook automatically: -- Uses your workspace directory (`~/clawd` by default) +- Uses your workspace directory (`~/.openclaw/workspace` by default) - Uses your configured LLM for slug generation - Falls back to timestamp slugs if LLM is unavailable @@ -91,7 +91,7 @@ The hook automatically: To disable this hook: ```bash -moltbot hooks disable session-memory +openclaw hooks disable session-memory ``` Or remove it from your config: diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index 525e21059..5561a812a 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -1,12 +1,10 @@ import fs from "node:fs/promises"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - -import handler from "./handler.js"; -import { createHookEvent } from "../../hooks.js"; -import type { ClawdbotConfig } from "../../../config/config.js"; +import type { OpenClawConfig } from "../../../config/config.js"; import { makeTempWorkspace, writeWorkspaceFile } from "../../../test-helpers/workspace.js"; +import { createHookEvent } from "../../hooks.js"; +import handler from "./handler.js"; /** * Create a mock session JSONL file with various entry types @@ -33,7 +31,7 @@ function createMockSessionContent( describe("session-memory hook", () => { it("skips non-command events", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const event = createHookEvent("agent", "bootstrap", "agent:main:main", { workspaceDir: tempDir, @@ -47,7 +45,7 @@ describe("session-memory hook", () => { }); it("skips commands other than new", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const event = createHookEvent("command", "help", "agent:main:main", { workspaceDir: tempDir, @@ -61,7 +59,7 @@ describe("session-memory hook", () => { }); it("creates memory file with session content on /new command", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -78,7 +76,7 @@ describe("session-memory hook", () => { content: sessionContent, }); - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tempDir } }, }; @@ -98,7 +96,7 @@ describe("session-memory hook", () => { expect(files.length).toBe(1); // Read the memory file and verify content - const memoryContent = await fs.readFile(path.join(memoryDir, files[0]!), "utf-8"); + const memoryContent = await fs.readFile(path.join(memoryDir, files[0]), "utf-8"); expect(memoryContent).toContain("user: Hello there"); expect(memoryContent).toContain("assistant: Hi! How can I help?"); expect(memoryContent).toContain("user: What is 2+2?"); @@ -106,7 +104,7 @@ describe("session-memory hook", () => { }); it("filters out non-message entries (tool calls, system)", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -124,7 +122,7 @@ describe("session-memory hook", () => { content: sessionContent, }); - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tempDir } }, }; @@ -140,7 +138,7 @@ describe("session-memory hook", () => { const memoryDir = path.join(tempDir, "memory"); const files = await fs.readdir(memoryDir); - const memoryContent = await fs.readFile(path.join(memoryDir, files[0]!), "utf-8"); + const memoryContent = await fs.readFile(path.join(memoryDir, files[0]), "utf-8"); // Only user/assistant messages should be present expect(memoryContent).toContain("user: Hello"); @@ -153,7 +151,7 @@ describe("session-memory hook", () => { }); it("filters out command messages starting with /", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -169,7 +167,7 @@ describe("session-memory hook", () => { content: sessionContent, }); - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tempDir } }, }; @@ -185,7 +183,7 @@ describe("session-memory hook", () => { const memoryDir = path.join(tempDir, "memory"); const files = await fs.readdir(memoryDir); - const memoryContent = await fs.readFile(path.join(memoryDir, files[0]!), "utf-8"); + const memoryContent = await fs.readFile(path.join(memoryDir, files[0]), "utf-8"); // Command messages should be filtered out expect(memoryContent).not.toContain("/help"); @@ -196,7 +194,7 @@ describe("session-memory hook", () => { }); it("respects custom messages config (limits to N messages)", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -213,7 +211,7 @@ describe("session-memory hook", () => { }); // Configure to only include last 3 messages - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tempDir } }, hooks: { internal: { @@ -236,7 +234,7 @@ describe("session-memory hook", () => { const memoryDir = path.join(tempDir, "memory"); const files = await fs.readdir(memoryDir); - const memoryContent = await fs.readFile(path.join(memoryDir, files[0]!), "utf-8"); + const memoryContent = await fs.readFile(path.join(memoryDir, files[0]), "utf-8"); // Only last 3 messages should be present expect(memoryContent).not.toContain("user: Message 1\n"); @@ -247,7 +245,7 @@ describe("session-memory hook", () => { }); it("filters messages before slicing (fix for #2681)", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -274,7 +272,7 @@ describe("session-memory hook", () => { // Request 3 messages - if we sliced first, we'd only get 1-2 messages // because the last 3 lines include tool entries - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tempDir } }, hooks: { internal: { @@ -297,7 +295,7 @@ describe("session-memory hook", () => { const memoryDir = path.join(tempDir, "memory"); const files = await fs.readdir(memoryDir); - const memoryContent = await fs.readFile(path.join(memoryDir, files[0]!), "utf-8"); + const memoryContent = await fs.readFile(path.join(memoryDir, files[0]), "utf-8"); // Should have exactly 3 user/assistant messages (the last 3) expect(memoryContent).not.toContain("First message"); @@ -307,7 +305,7 @@ describe("session-memory hook", () => { }); it("handles empty session files gracefully", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -317,7 +315,7 @@ describe("session-memory hook", () => { content: "", }); - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tempDir } }, }; @@ -339,7 +337,7 @@ describe("session-memory hook", () => { }); it("handles session files with fewer messages than requested", async () => { - const tempDir = await makeTempWorkspace("clawdbot-session-memory-"); + const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -354,7 +352,7 @@ describe("session-memory hook", () => { content: sessionContent, }); - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tempDir } }, }; @@ -370,7 +368,7 @@ describe("session-memory hook", () => { const memoryDir = path.join(tempDir, "memory"); const files = await fs.readdir(memoryDir); - const memoryContent = await fs.readFile(path.join(memoryDir, files[0]!), "utf-8"); + const memoryContent = await fs.readFile(path.join(memoryDir, files[0]), "utf-8"); // Both messages should be included expect(memoryContent).toContain("user: Only message 1"); diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index 5b5a69c9c..dd99839d2 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -6,14 +6,14 @@ */ import fs from "node:fs/promises"; -import path from "node:path"; import os from "node:os"; +import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { MoltbotConfig } from "../../../config/config.js"; +import type { OpenClawConfig } from "../../../config/config.js"; +import type { HookHandler } from "../../hooks.js"; import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js"; import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js"; import { resolveHookConfig } from "../../config.js"; -import type { HookHandler } from "../../hooks.js"; /** * Read recent messages from session file for slug generation @@ -71,11 +71,11 @@ const saveSessionToMemory: HookHandler = async (event) => { console.log("[session-memory] Hook triggered for /new command"); const context = event.context || {}; - const cfg = context.cfg as MoltbotConfig | undefined; + const cfg = context.cfg as OpenClawConfig | undefined; const agentId = resolveAgentIdFromSessionKey(event.sessionKey); const workspaceDir = cfg ? resolveAgentWorkspaceDir(cfg, agentId) - : path.join(os.homedir(), "clawd"); + : path.join(os.homedir(), ".openclaw", "workspace"); const memoryDir = path.join(workspaceDir, "memory"); await fs.mkdir(memoryDir, { recursive: true }); @@ -117,8 +117,8 @@ const saveSessionToMemory: HookHandler = async (event) => { // Dynamically import the LLM slug generator (avoids module caching issues) // When compiled, handler is at dist/hooks/bundled/session-memory/handler.js // Going up ../.. puts us at dist/hooks/, so just add llm-slug-generator.js - const moltbotRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); - const slugGenPath = path.join(moltbotRoot, "llm-slug-generator.js"); + const openclawRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + const slugGenPath = path.join(openclawRoot, "llm-slug-generator.js"); const { generateSlugViaLLM } = await import(slugGenPath); // Use LLM to generate a descriptive slug @@ -129,7 +129,7 @@ const saveSessionToMemory: HookHandler = async (event) => { // If no slug, use timestamp if (!slug) { - const timeSlug = now.toISOString().split("T")[1]!.split(".")[0]!.replace(/:/g, ""); + const timeSlug = now.toISOString().split("T")[1].split(".")[0].replace(/:/g, ""); slug = timeSlug.slice(0, 4); // HHMM console.log("[session-memory] Using fallback timestamp slug:", slug); } @@ -141,7 +141,7 @@ const saveSessionToMemory: HookHandler = async (event) => { console.log("[session-memory] Full path:", memoryFilePath); // Format time as HH:MM:SS UTC - const timeStr = now.toISOString().split("T")[1]!.split(".")[0]; + const timeStr = now.toISOString().split("T")[1].split(".")[0]; // Extract context details const sessionId = (sessionEntry.sessionId as string) || "unknown"; diff --git a/src/hooks/bundled/soul-evil/HOOK.md b/src/hooks/bundled/soul-evil/HOOK.md index 4a3a554a2..c3bc81b2d 100644 --- a/src/hooks/bundled/soul-evil/HOOK.md +++ b/src/hooks/bundled/soul-evil/HOOK.md @@ -1,15 +1,15 @@ --- name: soul-evil description: "Swap SOUL.md with SOUL_EVIL.md during a purge window or by random chance" -homepage: https://docs.molt.bot/hooks/soul-evil +homepage: https://docs.openclaw.ai/hooks/soul-evil metadata: { - "moltbot": + "openclaw": { "emoji": "😈", "events": ["agent:bootstrap"], "requires": { "config": ["hooks.internal.entries.soul-evil.enabled"] }, - "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with Moltbot" }], + "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], }, } --- @@ -31,7 +31,7 @@ You can change the filename via hook config. ## Configuration -Add this to your config (`~/.clawdbot/moltbot.json`): +Add this to your config (`~/.openclaw/openclaw.json`): ```json { @@ -67,5 +67,5 @@ Add this to your config (`~/.clawdbot/moltbot.json`): ## Enable ```bash -moltbot hooks enable soul-evil +openclaw hooks enable soul-evil ``` diff --git a/src/hooks/bundled/soul-evil/README.md b/src/hooks/bundled/soul-evil/README.md index a49bfe33d..a90af5c07 100644 --- a/src/hooks/bundled/soul-evil/README.md +++ b/src/hooks/bundled/soul-evil/README.md @@ -1,11 +1,11 @@ # SOUL Evil Hook -Small persona swap hook for Clawdbot. +Small persona swap hook for OpenClaw. -Docs: https://docs.molt.bot/hooks/soul-evil +Docs: https://docs.openclaw.ai/hooks/soul-evil ## Setup -1. `clawdbot hooks enable soul-evil` +1. `openclaw hooks enable soul-evil` 2. Create `SOUL_EVIL.md` next to `SOUL.md` in your agent workspace 3. Configure `hooks.internal.entries.soul-evil` (see docs) diff --git a/src/hooks/bundled/soul-evil/handler.test.ts b/src/hooks/bundled/soul-evil/handler.test.ts index 133b428fc..8cb4be14c 100644 --- a/src/hooks/bundled/soul-evil/handler.test.ts +++ b/src/hooks/bundled/soul-evil/handler.test.ts @@ -1,23 +1,21 @@ import path from "node:path"; - import { describe, expect, it } from "vitest"; - -import handler from "./handler.js"; -import { createHookEvent } from "../../hooks.js"; +import type { OpenClawConfig } from "../../../config/config.js"; import type { AgentBootstrapHookContext } from "../../hooks.js"; -import type { MoltbotConfig } from "../../../config/config.js"; import { makeTempWorkspace, writeWorkspaceFile } from "../../../test-helpers/workspace.js"; +import { createHookEvent } from "../../hooks.js"; +import handler from "./handler.js"; describe("soul-evil hook", () => { it("skips subagent sessions", async () => { - const tempDir = await makeTempWorkspace("moltbot-soul-"); + const tempDir = await makeTempWorkspace("openclaw-soul-"); await writeWorkspaceFile({ dir: tempDir, name: "SOUL_EVIL.md", content: "chaotic", }); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { entries: { diff --git a/src/hooks/bundled/soul-evil/handler.ts b/src/hooks/bundled/soul-evil/handler.ts index 2df0956b2..88e5f94a7 100644 --- a/src/hooks/bundled/soul-evil/handler.ts +++ b/src/hooks/bundled/soul-evil/handler.ts @@ -1,4 +1,3 @@ -import type { MoltbotConfig } from "../../../config/config.js"; import { isSubagentSessionKey } from "../../../routing/session-key.js"; import { resolveHookConfig } from "../../config.js"; import { isAgentBootstrapEvent, type HookHandler } from "../../hooks.js"; @@ -7,21 +6,31 @@ import { applySoulEvilOverride, resolveSoulEvilConfigFromHook } from "../../soul const HOOK_KEY = "soul-evil"; const soulEvilHook: HookHandler = async (event) => { - if (!isAgentBootstrapEvent(event)) return; + if (!isAgentBootstrapEvent(event)) { + return; + } const context = event.context; - if (context.sessionKey && isSubagentSessionKey(context.sessionKey)) return; - const cfg = context.cfg as MoltbotConfig | undefined; + if (context.sessionKey && isSubagentSessionKey(context.sessionKey)) { + return; + } + const cfg = context.cfg; const hookConfig = resolveHookConfig(cfg, HOOK_KEY); - if (!hookConfig || hookConfig.enabled === false) return; + if (!hookConfig || hookConfig.enabled === false) { + return; + } const soulConfig = resolveSoulEvilConfigFromHook(hookConfig as Record, { warn: (message) => console.warn(`[soul-evil] ${message}`), }); - if (!soulConfig) return; + if (!soulConfig) { + return; + } const workspaceDir = context.workspaceDir; - if (!workspaceDir || !Array.isArray(context.bootstrapFiles)) return; + if (!workspaceDir || !Array.isArray(context.bootstrapFiles)) { + return; + } const updated = await applySoulEvilOverride({ files: context.bootstrapFiles, diff --git a/src/hooks/config.ts b/src/hooks/config.ts index b9b9c0fb2..04d4beac6 100644 --- a/src/hooks/config.ts +++ b/src/hooks/config.ts @@ -1,8 +1,8 @@ import fs from "node:fs"; import path from "node:path"; -import type { MoltbotConfig, HookConfig } from "../config/config.js"; -import { resolveHookKey } from "./frontmatter.js"; +import type { OpenClawConfig, HookConfig } from "../config/config.js"; import type { HookEligibilityContext, HookEntry } from "./types.js"; +import { resolveHookKey } from "./frontmatter.js"; const DEFAULT_CONFIG_VALUES: Record = { "browser.enabled": true, @@ -11,39 +11,53 @@ const DEFAULT_CONFIG_VALUES: Record = { }; function isTruthy(value: unknown): boolean { - if (value === undefined || value === null) return false; - if (typeof value === "boolean") return value; - if (typeof value === "number") return value !== 0; - if (typeof value === "string") return value.trim().length > 0; + if (value === undefined || value === null) { + return false; + } + if (typeof value === "boolean") { + return value; + } + if (typeof value === "number") { + return value !== 0; + } + if (typeof value === "string") { + return value.trim().length > 0; + } return true; } -export function resolveConfigPath(config: MoltbotConfig | undefined, pathStr: string) { +export function resolveConfigPath(config: OpenClawConfig | undefined, pathStr: string) { const parts = pathStr.split(".").filter(Boolean); let current: unknown = config; for (const part of parts) { - if (typeof current !== "object" || current === null) return undefined; + if (typeof current !== "object" || current === null) { + return undefined; + } current = (current as Record)[part]; } return current; } -export function isConfigPathTruthy(config: MoltbotConfig | undefined, pathStr: string): boolean { +export function isConfigPathTruthy(config: OpenClawConfig | undefined, pathStr: string): boolean { const value = resolveConfigPath(config, pathStr); if (value === undefined && pathStr in DEFAULT_CONFIG_VALUES) { - return DEFAULT_CONFIG_VALUES[pathStr] === true; + return DEFAULT_CONFIG_VALUES[pathStr]; } return isTruthy(value); } export function resolveHookConfig( - config: MoltbotConfig | undefined, + config: OpenClawConfig | undefined, hookKey: string, ): HookConfig | undefined { const hooks = config?.hooks?.internal?.entries; - if (!hooks || typeof hooks !== "object") return undefined; + if (!hooks || typeof hooks !== "object") { + return undefined; + } const entry = (hooks as Record)[hookKey]; - if (!entry || typeof entry !== "object") return undefined; + if (!entry || typeof entry !== "object") { + return undefined; + } return entry; } @@ -68,18 +82,20 @@ export function hasBinary(bin: string): boolean { export function shouldIncludeHook(params: { entry: HookEntry; - config?: MoltbotConfig; + config?: OpenClawConfig; eligibility?: HookEligibilityContext; }): boolean { const { entry, config, eligibility } = params; const hookKey = resolveHookKey(entry.hook.name, entry); const hookConfig = resolveHookConfig(config, hookKey); - const pluginManaged = entry.hook.source === "moltbot-plugin"; + const pluginManaged = entry.hook.source === "openclaw-plugin"; const osList = entry.metadata?.os ?? []; const remotePlatforms = eligibility?.remote?.platforms ?? []; // Check if explicitly disabled - if (!pluginManaged && hookConfig?.enabled === false) return false; + if (!pluginManaged && hookConfig?.enabled === false) { + return false; + } // Check OS requirement if ( @@ -99,8 +115,12 @@ export function shouldIncludeHook(params: { const requiredBins = entry.metadata?.requires?.bins ?? []; if (requiredBins.length > 0) { for (const bin of requiredBins) { - if (hasBinary(bin)) continue; - if (eligibility?.remote?.hasBin?.(bin)) continue; + if (hasBinary(bin)) { + continue; + } + if (eligibility?.remote?.hasBin?.(bin)) { + continue; + } return false; } } @@ -111,15 +131,21 @@ export function shouldIncludeHook(params: { const anyFound = requiredAnyBins.some((bin) => hasBinary(bin)) || eligibility?.remote?.hasAnyBin?.(requiredAnyBins); - if (!anyFound) return false; + if (!anyFound) { + return false; + } } // Check required environment variables const requiredEnv = entry.metadata?.requires?.env ?? []; if (requiredEnv.length > 0) { for (const envName of requiredEnv) { - if (process.env[envName]) continue; - if (hookConfig?.env?.[envName]) continue; + if (process.env[envName]) { + continue; + } + if (hookConfig?.env?.[envName]) { + continue; + } return false; } } @@ -128,7 +154,9 @@ export function shouldIncludeHook(params: { const requiredConfig = entry.metadata?.requires?.config ?? []; if (requiredConfig.length > 0) { for (const configPath of requiredConfig) { - if (!isConfigPathTruthy(config, configPath)) return false; + if (!isConfigPathTruthy(config, configPath)) { + return false; + } } } diff --git a/src/hooks/frontmatter.test.ts b/src/hooks/frontmatter.test.ts index d1eef9574..a20036f59 100644 --- a/src/hooks/frontmatter.test.ts +++ b/src/hooks/frontmatter.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { parseFrontmatter, - resolveMoltbotMetadata, + resolveOpenClawMetadata, resolveHookInvocationPolicy, } from "./frontmatter.js"; @@ -41,7 +41,7 @@ name: session-memory description: "Save session context" metadata: { - "moltbot": { + "openclaw": { "emoji": "💾", "events": ["command:new"] } @@ -57,9 +57,9 @@ metadata: expect(typeof result.metadata).toBe("string"); // Verify the metadata is valid JSON - const parsed = JSON.parse(result.metadata as string); - expect(parsed.moltbot.emoji).toBe("💾"); - expect(parsed.moltbot.events).toEqual(["command:new"]); + const parsed = JSON.parse(result.metadata); + expect(parsed.openclaw.emoji).toBe("💾"); + expect(parsed.openclaw.events).toEqual(["command:new"]); }); it("parses multi-line metadata with complex nested structure", () => { @@ -68,7 +68,7 @@ name: command-logger description: "Log all command events" metadata: { - "moltbot": + "openclaw": { "emoji": "📝", "events": ["command"], @@ -82,22 +82,22 @@ metadata: expect(result.name).toBe("command-logger"); expect(result.metadata).toBeDefined(); - const parsed = JSON.parse(result.metadata as string); - expect(parsed.moltbot.emoji).toBe("📝"); - expect(parsed.moltbot.events).toEqual(["command"]); - expect(parsed.moltbot.requires.config).toEqual(["workspace.dir"]); - expect(parsed.moltbot.install[0].kind).toBe("bundled"); + const parsed = JSON.parse(result.metadata); + expect(parsed.openclaw.emoji).toBe("📝"); + expect(parsed.openclaw.events).toEqual(["command"]); + expect(parsed.openclaw.requires.config).toEqual(["workspace.dir"]); + expect(parsed.openclaw.install[0].kind).toBe("bundled"); }); it("handles single-line metadata (inline JSON)", () => { const content = `--- name: simple-hook -metadata: {"moltbot": {"events": ["test"]}} +metadata: {"openclaw": {"events": ["test"]}} --- `; const result = parseFrontmatter(content); expect(result.name).toBe("simple-hook"); - expect(result.metadata).toBe('{"moltbot": {"events": ["test"]}}'); + expect(result.metadata).toBe('{"openclaw": {"events": ["test"]}}'); }); it("handles mixed single-line and multi-line values", () => { @@ -107,7 +107,7 @@ description: "A hook with mixed values" homepage: https://example.com metadata: { - "moltbot": { + "openclaw": { "events": ["command:new"] } } @@ -148,12 +148,12 @@ description: 'single-quoted' }); }); -describe("resolveMoltbotMetadata", () => { - it("extracts moltbot metadata from parsed frontmatter", () => { +describe("resolveOpenClawMetadata", () => { + it("extracts openclaw metadata from parsed frontmatter", () => { const frontmatter = { name: "test-hook", metadata: JSON.stringify({ - moltbot: { + openclaw: { emoji: "🔥", events: ["command:new", "command:reset"], requires: { @@ -164,7 +164,7 @@ describe("resolveMoltbotMetadata", () => { }), }; - const result = resolveMoltbotMetadata(frontmatter); + const result = resolveOpenClawMetadata(frontmatter); expect(result).toBeDefined(); expect(result?.emoji).toBe("🔥"); expect(result?.events).toEqual(["command:new", "command:reset"]); @@ -174,15 +174,15 @@ describe("resolveMoltbotMetadata", () => { it("returns undefined when metadata is missing", () => { const frontmatter = { name: "no-metadata" }; - const result = resolveMoltbotMetadata(frontmatter); + const result = resolveOpenClawMetadata(frontmatter); expect(result).toBeUndefined(); }); - it("returns undefined when moltbot key is missing", () => { + it("returns undefined when openclaw key is missing", () => { const frontmatter = { metadata: JSON.stringify({ other: "data" }), }; - const result = resolveMoltbotMetadata(frontmatter); + const result = resolveOpenClawMetadata(frontmatter); expect(result).toBeUndefined(); }); @@ -190,41 +190,41 @@ describe("resolveMoltbotMetadata", () => { const frontmatter = { metadata: "not valid json {", }; - const result = resolveMoltbotMetadata(frontmatter); + const result = resolveOpenClawMetadata(frontmatter); expect(result).toBeUndefined(); }); it("handles install specs", () => { const frontmatter = { metadata: JSON.stringify({ - moltbot: { + openclaw: { events: ["command"], install: [ - { id: "bundled", kind: "bundled", label: "Bundled with Moltbot" }, - { id: "npm", kind: "npm", package: "@moltbot/hook" }, + { id: "bundled", kind: "bundled", label: "Bundled with OpenClaw" }, + { id: "npm", kind: "npm", package: "@openclaw/hook" }, ], }, }), }; - const result = resolveMoltbotMetadata(frontmatter); + const result = resolveOpenClawMetadata(frontmatter); expect(result?.install).toHaveLength(2); expect(result?.install?.[0].kind).toBe("bundled"); expect(result?.install?.[1].kind).toBe("npm"); - expect(result?.install?.[1].package).toBe("@moltbot/hook"); + expect(result?.install?.[1].package).toBe("@openclaw/hook"); }); it("handles os restrictions", () => { const frontmatter = { metadata: JSON.stringify({ - moltbot: { + openclaw: { events: ["command"], os: ["darwin", "linux"], }, }), }; - const result = resolveMoltbotMetadata(frontmatter); + const result = resolveOpenClawMetadata(frontmatter); expect(result?.os).toEqual(["darwin", "linux"]); }); @@ -233,15 +233,15 @@ describe("resolveMoltbotMetadata", () => { const content = `--- name: session-memory description: "Save session context to memory when /new command is issued" -homepage: https://docs.molt.bot/hooks#session-memory +homepage: https://docs.openclaw.ai/hooks#session-memory metadata: { - "moltbot": + "openclaw": { "emoji": "💾", "events": ["command:new"], "requires": { "config": ["workspace.dir"] }, - "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with Moltbot" }], + "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], }, } --- @@ -253,28 +253,28 @@ metadata: expect(frontmatter.name).toBe("session-memory"); expect(frontmatter.metadata).toBeDefined(); - const moltbot = resolveMoltbotMetadata(frontmatter); - expect(moltbot).toBeDefined(); - expect(moltbot?.emoji).toBe("💾"); - expect(moltbot?.events).toEqual(["command:new"]); - expect(moltbot?.requires?.config).toEqual(["workspace.dir"]); - expect(moltbot?.install?.[0].kind).toBe("bundled"); + const openclaw = resolveOpenClawMetadata(frontmatter); + expect(openclaw).toBeDefined(); + expect(openclaw?.emoji).toBe("💾"); + expect(openclaw?.events).toEqual(["command:new"]); + expect(openclaw?.requires?.config).toEqual(["workspace.dir"]); + expect(openclaw?.install?.[0].kind).toBe("bundled"); }); it("parses YAML metadata map", () => { const content = `--- name: yaml-metadata metadata: - moltbot: + openclaw: emoji: disk events: - command:new --- `; const frontmatter = parseFrontmatter(content); - const moltbot = resolveMoltbotMetadata(frontmatter); - expect(moltbot?.emoji).toBe("disk"); - expect(moltbot?.events).toEqual(["command:new"]); + const openclaw = resolveOpenClawMetadata(frontmatter); + expect(openclaw?.emoji).toBe("disk"); + expect(openclaw?.events).toEqual(["command:new"]); }); }); diff --git a/src/hooks/frontmatter.ts b/src/hooks/frontmatter.ts index dc433f9ba..a213d0487 100644 --- a/src/hooks/frontmatter.ts +++ b/src/hooks/frontmatter.ts @@ -1,22 +1,23 @@ import JSON5 from "json5"; - -import { LEGACY_MANIFEST_KEY } from "../compat/legacy-names.js"; -import { parseFrontmatterBlock } from "../markdown/frontmatter.js"; -import { parseBooleanValue } from "../utils/boolean.js"; import type { - MoltbotHookMetadata, + OpenClawHookMetadata, HookEntry, HookInstallSpec, HookInvocationPolicy, ParsedHookFrontmatter, } from "./types.js"; +import { LEGACY_MANIFEST_KEYS, MANIFEST_KEY } from "../compat/legacy-names.js"; +import { parseFrontmatterBlock } from "../markdown/frontmatter.js"; +import { parseBooleanValue } from "../utils/boolean.js"; export function parseFrontmatter(content: string): ParsedHookFrontmatter { return parseFrontmatterBlock(content); } function normalizeStringList(input: unknown): string[] { - if (!input) return []; + if (!input) { + return []; + } if (Array.isArray(input)) { return input.map((value) => String(value).trim()).filter(Boolean); } @@ -30,7 +31,9 @@ function normalizeStringList(input: unknown): string[] { } function parseInstallSpec(input: unknown): HookInstallSpec | undefined { - if (!input || typeof input !== "object") return undefined; + if (!input || typeof input !== "object") { + return undefined; + } const raw = input as Record; const kindRaw = typeof raw.kind === "string" ? raw.kind : typeof raw.type === "string" ? raw.type : ""; @@ -40,15 +43,25 @@ function parseInstallSpec(input: unknown): HookInstallSpec | undefined { } const spec: HookInstallSpec = { - kind: kind as HookInstallSpec["kind"], + kind: kind, }; - if (typeof raw.id === "string") spec.id = raw.id; - if (typeof raw.label === "string") spec.label = raw.label; + if (typeof raw.id === "string") { + spec.id = raw.id; + } + if (typeof raw.label === "string") { + spec.label = raw.label; + } const bins = normalizeStringList(raw.bins); - if (bins.length > 0) spec.bins = bins; - if (typeof raw.package === "string") spec.package = raw.package; - if (typeof raw.repository === "string") spec.repository = raw.repository; + if (bins.length > 0) { + spec.bins = bins; + } + if (typeof raw.package === "string") { + spec.package = raw.package; + } + if (typeof raw.repository === "string") { + spec.repository = raw.repository; + } return spec; } @@ -63,18 +76,30 @@ function parseFrontmatterBool(value: string | undefined, fallback: boolean): boo return parsed === undefined ? fallback : parsed; } -export function resolveMoltbotMetadata( +export function resolveOpenClawMetadata( frontmatter: ParsedHookFrontmatter, -): MoltbotHookMetadata | undefined { +): OpenClawHookMetadata | undefined { const raw = getFrontmatterValue(frontmatter, "metadata"); - if (!raw) return undefined; + if (!raw) { + return undefined; + } try { - const parsed = JSON5.parse(raw) as { moltbot?: unknown } & Partial< - Record - >; - if (!parsed || typeof parsed !== "object") return undefined; - const metadataRaw = parsed.moltbot ?? parsed[LEGACY_MANIFEST_KEY]; - if (!metadataRaw || typeof metadataRaw !== "object") return undefined; + const parsed = JSON5.parse(raw); + if (!parsed || typeof parsed !== "object") { + return undefined; + } + const metadataRawCandidates = [MANIFEST_KEY, ...LEGACY_MANIFEST_KEYS]; + let metadataRaw: unknown; + for (const key of metadataRawCandidates) { + const candidate = parsed[key]; + if (candidate && typeof candidate === "object") { + metadataRaw = candidate; + break; + } + } + if (!metadataRaw || typeof metadataRaw !== "object") { + return undefined; + } const metadataObj = metadataRaw as Record; const requiresRaw = typeof metadataObj.requires === "object" && metadataObj.requires !== null diff --git a/src/hooks/gmail-ops.ts b/src/hooks/gmail-ops.ts index 681127524..b8fbd4aba 100644 --- a/src/hooks/gmail-ops.ts +++ b/src/hooks/gmail-ops.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; - +import { formatCliCommand } from "../cli/command-format.js"; import { - type MoltbotConfig, + type OpenClawConfig, CONFIG_PATH, loadConfig, readConfigFileSnapshot, @@ -11,8 +11,16 @@ import { } from "../config/config.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime } from "../runtime.js"; -import { formatCliCommand } from "../cli/command-format.js"; import { displayPath } from "../utils.js"; +import { + ensureDependency, + ensureGcloudAuth, + ensureSubscription, + ensureTailscaleEndpoint, + ensureTopic, + resolveProjectIdFromGogCredentials, + runGcloud, +} from "./gmail-setup-utils.js"; import { buildDefaultHookUrl, buildGogWatchServeArgs, @@ -35,15 +43,6 @@ import { parseTopicPath, resolveGmailHookRuntimeConfig, } from "./gmail.js"; -import { - ensureDependency, - ensureGcloudAuth, - ensureSubscription, - ensureTailscaleEndpoint, - ensureTopic, - resolveProjectIdFromGogCredentials, - runGcloud, -} from "./gmail-setup-utils.js"; export type GmailSetupOptions = { account: string; @@ -210,7 +209,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) { true, ); - const nextConfig: MoltbotConfig = { + const nextConfig: OpenClawConfig = { ...baseConfig, hooks: { ...baseConfig.hooks, @@ -278,7 +277,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) { defaultRuntime.log(`- push endpoint: ${pushEndpoint}`); defaultRuntime.log(`- hook url: ${hookUrl}`); defaultRuntime.log(`- config: ${displayPath(CONFIG_PATH)}`); - defaultRuntime.log(`Next: ${formatCliCommand("moltbot webhooks gmail run")}`); + defaultRuntime.log(`Next: ${formatCliCommand("openclaw webhooks gmail run")}`); } export async function runGmailService(opts: GmailRunOptions) { @@ -332,7 +331,9 @@ export async function runGmailService(opts: GmailRunOptions) { }, renewMs); const shutdown = () => { - if (shuttingDown) return; + if (shuttingDown) { + return; + } shuttingDown = true; clearInterval(renewTimer); child.kill("SIGTERM"); @@ -342,10 +343,14 @@ export async function runGmailService(opts: GmailRunOptions) { process.on("SIGTERM", shutdown); child.on("exit", () => { - if (shuttingDown) return; + if (shuttingDown) { + return; + } defaultRuntime.log("gog watch serve exited; restarting in 2s"); setTimeout(() => { - if (shuttingDown) return; + if (shuttingDown) { + return; + } child = spawnGogServe(runtimeConfig); }, 2000); }); @@ -365,7 +370,9 @@ async function startGmailWatch( const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 }); if (result.code !== 0) { const message = result.stderr || result.stdout || "gog watch start failed"; - if (fatal) throw new Error(message); + if (fatal) { + throw new Error(message); + } defaultRuntime.error(message); } } diff --git a/src/hooks/gmail-setup-utils.test.ts b/src/hooks/gmail-setup-utils.test.ts index 3e87afa6d..1876dd8ea 100644 --- a/src/hooks/gmail-setup-utils.test.ts +++ b/src/hooks/gmail-setup-utils.test.ts @@ -1,7 +1,6 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { beforeEach, describe, expect, it, vi } from "vitest"; const itUnix = process.platform === "win32" ? it.skip : it; @@ -14,7 +13,7 @@ describe("resolvePythonExecutablePath", () => { itUnix( "resolves a working python path and caches the result", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-python-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-python-")); const originalPath = process.env.PATH; try { const realPython = path.join(tmp, "python-real"); diff --git a/src/hooks/gmail-setup-utils.ts b/src/hooks/gmail-setup-utils.ts index fe3663e3e..4a95b10ab 100644 --- a/src/hooks/gmail-setup-utils.ts +++ b/src/hooks/gmail-setup-utils.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import path from "node:path"; - import { hasBinary } from "../agents/skills.js"; import { runCommandWithTimeout, type SpawnResult } from "../process/exec.js"; import { resolveUserPath } from "../utils.js"; @@ -11,8 +10,12 @@ const MAX_OUTPUT_CHARS = 800; function trimOutput(value: string): string { const trimmed = value.trim(); - if (!trimmed) return ""; - if (trimmed.length <= MAX_OUTPUT_CHARS) return trimmed; + if (!trimmed) { + return ""; + } + if (trimmed.length <= MAX_OUTPUT_CHARS) { + return trimmed; + } return `${trimmed.slice(0, MAX_OUTPUT_CHARS)}…`; } @@ -23,8 +26,12 @@ function formatCommandFailure(command: string, result: SpawnResult): string { const stderr = trimOutput(result.stderr); const stdout = trimOutput(result.stdout); const lines = [`${command} failed (code=${code}${signal}${killed})`]; - if (stderr) lines.push(`stderr: ${stderr}`); - if (stdout) lines.push(`stdout: ${stdout}`); + if (stderr) { + lines.push(`stderr: ${stderr}`); + } + if (stdout) { + lines.push(`stdout: ${stdout}`); + } return lines.join("\n"); } @@ -35,8 +42,12 @@ function formatCommandResult(command: string, result: SpawnResult): string { const stderr = trimOutput(result.stderr); const stdout = trimOutput(result.stdout); const lines = [`${command} exited (code=${code}${signal}${killed})`]; - if (stderr) lines.push(`stderr: ${stderr}`); - if (stdout) lines.push(`stdout: ${stdout}`); + if (stderr) { + lines.push(`stderr: ${stderr}`); + } + if (stdout) { + lines.push(`stdout: ${stdout}`); + } return lines.join("\n"); } @@ -57,7 +68,9 @@ function findExecutablesOnPath(bins: string[]): string[] { for (const part of parts) { for (const bin of bins) { const candidate = path.join(part, bin); - if (seen.has(candidate)) continue; + if (seen.has(candidate)) { + continue; + } try { fs.accessSync(candidate, fs.constants.X_OK); matches.push(candidate); @@ -73,13 +86,17 @@ function findExecutablesOnPath(bins: string[]): string[] { function ensurePathIncludes(dirPath: string, position: "append" | "prepend") { const pathEnv = process.env.PATH ?? ""; const parts = pathEnv.split(path.delimiter).filter(Boolean); - if (parts.includes(dirPath)) return; + if (parts.includes(dirPath)) { + return; + } const next = position === "prepend" ? [dirPath, ...parts] : [...parts, dirPath]; process.env.PATH = next.join(path.delimiter); } function ensureGcloudOnPath(): boolean { - if (hasBinary("gcloud")) return true; + if (hasBinary("gcloud")) { + return true; + } const candidates = [ "/opt/homebrew/share/google-cloud-sdk/bin/gcloud", "/usr/local/share/google-cloud-sdk/bin/gcloud", @@ -108,9 +125,13 @@ export async function resolvePythonExecutablePath(): Promise [candidate, "-c", "import os, sys; print(os.path.realpath(sys.executable))"], { timeoutMs: 2_000 }, ); - if (res.code !== 0) continue; + if (res.code !== 0) { + continue; + } const resolved = res.stdout.trim().split(/\s+/)[0]; - if (!resolved) continue; + if (!resolved) { + continue; + } try { fs.accessSync(resolved, fs.constants.X_OK); cachedPythonPath = resolved; @@ -124,9 +145,13 @@ export async function resolvePythonExecutablePath(): Promise } async function gcloudEnv(): Promise { - if (process.env.CLOUDSDK_PYTHON) return undefined; + if (process.env.CLOUDSDK_PYTHON) { + return undefined; + } const pythonPath = await resolvePythonExecutablePath(); - if (!pythonPath) return undefined; + if (!pythonPath) { + return undefined; + } return { CLOUDSDK_PYTHON: pythonPath }; } @@ -141,8 +166,12 @@ async function runGcloudCommand( } export async function ensureDependency(bin: string, brewArgs: string[]) { - if (bin === "gcloud" && ensureGcloudOnPath()) return; - if (hasBinary(bin)) return; + if (bin === "gcloud" && ensureGcloudOnPath()) { + return; + } + if (hasBinary(bin)) { + return; + } if (process.platform !== "darwin") { throw new Error(`${bin} not installed; install it and retry`); } @@ -167,7 +196,9 @@ export async function ensureGcloudAuth() { ["auth", "list", "--filter", "status:ACTIVE", "--format", "value(account)"], 30_000, ); - if (res.code === 0 && res.stdout.trim()) return; + if (res.code === 0 && res.stdout.trim()) { + return; + } const login = await runGcloudCommand(["auth", "login"], 600_000); if (login.code !== 0) { throw new Error(login.stderr || "gcloud auth login failed"); @@ -187,7 +218,9 @@ export async function ensureTopic(projectId: string, topicName: string) { ["pubsub", "topics", "describe", topicName, "--project", projectId], 30_000, ); - if (describe.code === 0) return; + if (describe.code === 0) { + return; + } await runGcloud(["pubsub", "topics", "create", topicName, "--project", projectId]); } @@ -235,7 +268,9 @@ export async function ensureTailscaleEndpoint(params: { target?: string; token?: string; }): Promise { - if (params.mode === "off") return ""; + if (params.mode === "off") { + return ""; + } const statusArgs = ["status", "--json"]; const statusCommand = formatCommand("tailscale", statusArgs); @@ -249,7 +284,7 @@ export async function ensureTailscaleEndpoint(params: { try { parsed = JSON.parse(status.stdout) as { Self?: { DNSName?: string } }; } catch (err) { - throw new Error(formatJsonParseFailure(statusCommand, status, err)); + throw new Error(formatJsonParseFailure(statusCommand, status, err), { cause: err }); } const dnsName = parsed.Self?.DNSName?.replace(/\.$/, ""); if (!dnsName) { @@ -283,13 +318,17 @@ export async function ensureTailscaleEndpoint(params: { export async function resolveProjectIdFromGogCredentials(): Promise { const candidates = gogCredentialsPaths(); for (const candidate of candidates) { - if (!fs.existsSync(candidate)) continue; + if (!fs.existsSync(candidate)) { + continue; + } try { const raw = fs.readFileSync(candidate, "utf-8"); const parsed = JSON.parse(raw) as Record; const clientId = extractGogClientId(parsed); const projectNumber = extractProjectNumber(clientId); - if (!projectNumber) continue; + if (!projectNumber) { + continue; + } const res = await runGcloudCommand( [ "projects", @@ -301,9 +340,13 @@ export async function resolveProjectIdFromGogCredentials(): Promise): string | null { } function extractProjectNumber(clientId: string | null): string | null { - if (!clientId) return null; + if (!clientId) { + return null; + } const match = clientId.match(/^(\d+)-/); return match?.[1] ?? null; } diff --git a/src/hooks/gmail-watcher.ts b/src/hooks/gmail-watcher.ts index bf1b08b35..16512e355 100644 --- a/src/hooks/gmail-watcher.ts +++ b/src/hooks/gmail-watcher.ts @@ -6,17 +6,17 @@ */ import { type ChildProcess, spawn } from "node:child_process"; +import type { OpenClawConfig } from "../config/config.js"; import { hasBinary } from "../agents/skills.js"; -import type { MoltbotConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { runCommandWithTimeout } from "../process/exec.js"; +import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js"; import { buildGogWatchServeArgs, buildGogWatchStartArgs, type GmailHookRuntimeConfig, resolveGmailHookRuntimeConfig, } from "./gmail.js"; -import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js"; const log = createSubsystemLogger("gmail-watcher"); @@ -75,12 +75,16 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess { child.stdout?.on("data", (data: Buffer) => { const line = data.toString().trim(); - if (line) log.info(`[gog] ${line}`); + if (line) { + log.info(`[gog] ${line}`); + } }); child.stderr?.on("data", (data: Buffer) => { const line = data.toString().trim(); - if (!line) return; + if (!line) { + return; + } if (isAddressInUseError(line)) { addressInUse = true; } @@ -92,11 +96,13 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess { }); child.on("exit", (code, signal) => { - if (shuttingDown) return; + if (shuttingDown) { + return; + } if (addressInUse) { log.warn( "gog serve failed to bind (address already in use); stopping restarts. " + - "Another watcher is likely running. Set CLAWDBOT_SKIP_GMAIL_WATCHER=1 or stop the other process.", + "Another watcher is likely running. Set OPENCLAW_SKIP_GMAIL_WATCHER=1 or stop the other process.", ); watcherProcess = null; return; @@ -104,7 +110,9 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess { log.warn(`gog exited (code=${code}, signal=${signal}); restarting in 5s`); watcherProcess = null; setTimeout(() => { - if (shuttingDown || !currentConfig) return; + if (shuttingDown || !currentConfig) { + return; + } watcherProcess = spawnGogServe(currentConfig); }, 5000); }); @@ -121,7 +129,7 @@ export type GmailWatcherStartResult = { * Start the Gmail watcher service. * Called automatically by the gateway if hooks.gmail is configured. */ -export async function startGmailWatcher(cfg: MoltbotConfig): Promise { +export async function startGmailWatcher(cfg: OpenClawConfig): Promise { // Check if gmail hooks are configured if (!cfg.hooks?.enabled) { return { started: false, reason: "hooks not enabled" }; @@ -180,7 +188,9 @@ export async function startGmailWatcher(cfg: MoltbotConfig): Promise { - if (shuttingDown) return; + if (shuttingDown) { + return; + } void startGmailWatch(runtimeConfig); }, renewMs); diff --git a/src/hooks/gmail.test.ts b/src/hooks/gmail.test.ts index d14ef7447..514e89c07 100644 --- a/src/hooks/gmail.test.ts +++ b/src/hooks/gmail.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { type MoltbotConfig, DEFAULT_GATEWAY_PORT } from "../config/config.js"; +import { type OpenClawConfig, DEFAULT_GATEWAY_PORT } from "../config/config.js"; import { buildDefaultHookUrl, buildTopicPath, @@ -11,12 +11,12 @@ const baseConfig = { hooks: { token: "hook-token", gmail: { - account: "moltbot@gmail.com", + account: "openclaw@gmail.com", topic: "projects/demo/topics/gog-gmail-watch", pushToken: "push-token", }, }, -} satisfies MoltbotConfig; +} satisfies OpenClawConfig; describe("gmail hook config", () => { it("builds default hook url", () => { @@ -37,7 +37,7 @@ describe("gmail hook config", () => { const result = resolveGmailHookRuntimeConfig(baseConfig, {}); expect(result.ok).toBe(true); if (result.ok) { - expect(result.value.account).toBe("moltbot@gmail.com"); + expect(result.value.account).toBe("openclaw@gmail.com"); expect(result.value.label).toBe("INBOX"); expect(result.value.includeBody).toBe(true); expect(result.value.serve.port).toBe(8788); @@ -50,7 +50,7 @@ describe("gmail hook config", () => { { hooks: { gmail: { - account: "moltbot@gmail.com", + account: "openclaw@gmail.com", topic: "projects/demo/topics/gog-gmail-watch", pushToken: "push-token", }, @@ -67,7 +67,7 @@ describe("gmail hook config", () => { hooks: { token: "hook-token", gmail: { - account: "moltbot@gmail.com", + account: "openclaw@gmail.com", topic: "projects/demo/topics/gog-gmail-watch", pushToken: "push-token", tailscale: { mode: "funnel" }, @@ -89,7 +89,7 @@ describe("gmail hook config", () => { hooks: { token: "hook-token", gmail: { - account: "moltbot@gmail.com", + account: "openclaw@gmail.com", topic: "projects/demo/topics/gog-gmail-watch", pushToken: "push-token", serve: { path: "/gmail-pubsub" }, @@ -112,7 +112,7 @@ describe("gmail hook config", () => { hooks: { token: "hook-token", gmail: { - account: "moltbot@gmail.com", + account: "openclaw@gmail.com", topic: "projects/demo/topics/gog-gmail-watch", pushToken: "push-token", serve: { path: "/custom" }, @@ -135,7 +135,7 @@ describe("gmail hook config", () => { hooks: { token: "hook-token", gmail: { - account: "moltbot@gmail.com", + account: "openclaw@gmail.com", topic: "projects/demo/topics/gog-gmail-watch", pushToken: "push-token", serve: { path: "/custom" }, diff --git a/src/hooks/gmail.ts b/src/hooks/gmail.ts index 1eff9e13d..5b3c89032 100644 --- a/src/hooks/gmail.ts +++ b/src/hooks/gmail.ts @@ -1,7 +1,6 @@ import { randomBytes } from "node:crypto"; - import { - type MoltbotConfig, + type OpenClawConfig, DEFAULT_GATEWAY_PORT, type HooksGmailTailscaleMode, resolveGatewayPort, @@ -71,7 +70,9 @@ export function mergeHookPresets(existing: string[] | undefined, preset: string) export function normalizeHooksPath(raw?: string): string { const base = raw?.trim() || DEFAULT_HOOKS_PATH; - if (base === "/") return DEFAULT_HOOKS_PATH; + if (base === "/") { + return DEFAULT_HOOKS_PATH; + } const withSlash = base.startsWith("/") ? base : `/${base}`; return withSlash.replace(/\/+$/, ""); } @@ -80,7 +81,9 @@ export function normalizeServePath(raw?: string): string { const base = raw?.trim() || DEFAULT_GMAIL_SERVE_PATH; // Tailscale funnel/serve strips the set-path prefix before proxying. // To accept requests at / externally, gog must listen on "/". - if (base === "/") return "/"; + if (base === "/") { + return "/"; + } const withSlash = base.startsWith("/") ? base : `/${base}`; return withSlash.replace(/\/+$/, ""); } @@ -95,7 +98,7 @@ export function buildDefaultHookUrl( } export function resolveGmailHookRuntimeConfig( - cfg: MoltbotConfig, + cfg: OpenClawConfig, overrides: GmailHookOverrides, ): { ok: true; value: GmailHookRuntimeConfig } | { ok: false; error: string } { const hooks = cfg.hooks; @@ -253,7 +256,9 @@ export function buildTopicPath(projectId: string, topicName: string): string { export function parseTopicPath(topic: string): { projectId: string; topicName: string } | null { const match = topic.trim().match(/^projects\/([^/]+)\/topics\/([^/]+)$/i); - if (!match) return null; + if (!match) { + return null; + } return { projectId: match[1] ?? "", topicName: match[2] ?? "" }; } diff --git a/src/hooks/hooks-install.e2e.test.ts b/src/hooks/hooks-install.e2e.test.ts index 165f786cb..0bb0fcc63 100644 --- a/src/hooks/hooks-install.e2e.test.ts +++ b/src/hooks/hooks-install.e2e.test.ts @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const tempDirs: string[] = []; async function makeTempDir() { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hooks-e2e-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hooks-e2e-")); tempDirs.push(dir); return dir; } @@ -21,24 +21,24 @@ describe("hooks install (e2e)", () => { workspaceDir = path.join(baseDir, "workspace"); await fs.mkdir(workspaceDir, { recursive: true }); - prevStateDir = process.env.CLAWDBOT_STATE_DIR; - prevBundledDir = process.env.CLAWDBOT_BUNDLED_HOOKS_DIR; - process.env.CLAWDBOT_STATE_DIR = path.join(baseDir, "state"); - process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = path.join(baseDir, "bundled-none"); + prevStateDir = process.env.OPENCLAW_STATE_DIR; + prevBundledDir = process.env.OPENCLAW_BUNDLED_HOOKS_DIR; + process.env.OPENCLAW_STATE_DIR = path.join(baseDir, "state"); + process.env.OPENCLAW_BUNDLED_HOOKS_DIR = path.join(baseDir, "bundled-none"); vi.resetModules(); }); afterEach(async () => { if (prevStateDir === undefined) { - delete process.env.CLAWDBOT_STATE_DIR; + delete process.env.OPENCLAW_STATE_DIR; } else { - process.env.CLAWDBOT_STATE_DIR = prevStateDir; + process.env.OPENCLAW_STATE_DIR = prevStateDir; } if (prevBundledDir === undefined) { - delete process.env.CLAWDBOT_BUNDLED_HOOKS_DIR; + delete process.env.OPENCLAW_BUNDLED_HOOKS_DIR; } else { - process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = prevBundledDir; + process.env.OPENCLAW_BUNDLED_HOOKS_DIR = prevBundledDir; } vi.resetModules(); @@ -63,7 +63,7 @@ describe("hooks install (e2e)", () => { { name: "@acme/hello-hooks", version: "0.0.0", - moltbot: { hooks: ["./hooks/hello-hook"] }, + openclaw: { hooks: ["./hooks/hello-hook"] }, }, null, 2, @@ -77,7 +77,7 @@ describe("hooks install (e2e)", () => { "---", 'name: "hello-hook"', 'description: "Test hook"', - 'metadata: {"moltbot":{"events":["command:new"]}}', + 'metadata: {"openclaw":{"events":["command:new"]}}', "---", "", "# Hello Hook", @@ -95,7 +95,9 @@ describe("hooks install (e2e)", () => { const { installHooksFromPath } = await import("./install.js"); const installResult = await installHooksFromPath({ path: packDir }); expect(installResult.ok).toBe(true); - if (!installResult.ok) return; + if (!installResult.ok) { + return; + } const { clearInternalHooks, createInternalHookEvent, triggerInternalHook } = await import("./internal-hooks.js"); diff --git a/src/hooks/hooks-status.ts b/src/hooks/hooks-status.ts index 0a5aded7c..0a8018e11 100644 --- a/src/hooks/hooks-status.ts +++ b/src/hooks/hooks-status.ts @@ -1,9 +1,8 @@ import path from "node:path"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { HookEligibilityContext, HookEntry, HookInstallSpec } from "./types.js"; import { CONFIG_DIR } from "../utils.js"; import { hasBinary, isConfigPathTruthy, resolveConfigPath, resolveHookConfig } from "./config.js"; -import type { HookEligibilityContext, HookEntry, HookInstallSpec } from "./types.js"; import { loadWorkspaceHookEntries } from "./workspace.js"; export type HookStatusConfigCheck = { @@ -65,7 +64,9 @@ function resolveHookKey(entry: HookEntry): string { function normalizeInstallOptions(entry: HookEntry): HookInstallOption[] { const install = entry.metadata?.install ?? []; - if (install.length === 0) return []; + if (install.length === 0) { + return []; + } // For hooks, we just list all install options return install.map((spec, index) => { @@ -75,7 +76,7 @@ function normalizeInstallOptions(entry: HookEntry): HookInstallOption[] { if (!label) { if (spec.kind === "bundled") { - label = "Bundled with Moltbot"; + label = "Bundled with OpenClaw"; } else if (spec.kind === "npm" && spec.package) { label = `Install ${spec.package} (npm)`; } else if (spec.kind === "git" && spec.repository) { @@ -91,12 +92,12 @@ function normalizeInstallOptions(entry: HookEntry): HookInstallOption[] { function buildHookStatus( entry: HookEntry, - config?: MoltbotConfig, + config?: OpenClawConfig, eligibility?: HookEligibilityContext, ): HookStatusEntry { const hookKey = resolveHookKey(entry); const hookConfig = resolveHookConfig(config, hookKey); - const managedByPlugin = entry.hook.source === "moltbot-plugin"; + const managedByPlugin = entry.hook.source === "openclaw-plugin"; const disabled = managedByPlugin ? false : hookConfig?.enabled === false; const always = entry.metadata?.always === true; const emoji = entry.metadata?.emoji ?? entry.frontmatter.emoji; @@ -115,8 +116,12 @@ function buildHookStatus( const requiredOs = entry.metadata?.os ?? []; const missingBins = requiredBins.filter((bin) => { - if (hasBinary(bin)) return false; - if (eligibility?.remote?.hasBin?.(bin)) return false; + if (hasBinary(bin)) { + return false; + } + if (eligibility?.remote?.hasBin?.(bin)) { + return false; + } return true; }); @@ -138,8 +143,12 @@ function buildHookStatus( const missingEnv: string[] = []; for (const envName of requiredEnv) { - if (process.env[envName]) continue; - if (hookConfig?.env?.[envName]) continue; + if (process.env[envName]) { + continue; + } + if (hookConfig?.env?.[envName]) { + continue; + } missingEnv.push(envName); } @@ -202,7 +211,7 @@ function buildHookStatus( export function buildWorkspaceHookStatus( workspaceDir: string, opts?: { - config?: MoltbotConfig; + config?: OpenClawConfig; managedHooksDir?: string; entries?: HookEntry[]; eligibility?: HookEligibilityContext; diff --git a/src/hooks/install.test.ts b/src/hooks/install.test.ts index e9deb8e86..97bc7682a 100644 --- a/src/hooks/install.test.ts +++ b/src/hooks/install.test.ts @@ -1,15 +1,15 @@ +import JSZip from "jszip"; import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import JSZip from "jszip"; import * as tar from "tar"; import { afterEach, describe, expect, it } from "vitest"; const tempDirs: string[] = []; function makeTempDir() { - const dir = path.join(os.tmpdir(), `moltbot-hook-install-${randomUUID()}`); + const dir = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`); fs.mkdirSync(dir, { recursive: true }); tempDirs.push(dir); return dir; @@ -35,9 +35,9 @@ describe("installHooksFromArchive", () => { zip.file( "package/package.json", JSON.stringify({ - name: "@moltbot/zip-hooks", + name: "@openclaw/zip-hooks", version: "0.0.1", - moltbot: { hooks: ["./hooks/zip-hook"] }, + openclaw: { hooks: ["./hooks/zip-hook"] }, }), ); zip.file( @@ -46,7 +46,7 @@ describe("installHooksFromArchive", () => { "---", "name: zip-hook", "description: Zip hook", - 'metadata: {"moltbot":{"events":["command:new"]}}', + 'metadata: {"openclaw":{"events":["command:new"]}}', "---", "", "# Zip Hook", @@ -61,7 +61,9 @@ describe("installHooksFromArchive", () => { const result = await installHooksFromArchive({ archivePath, hooksDir }); expect(result.ok).toBe(true); - if (!result.ok) return; + if (!result.ok) { + return; + } expect(result.hookPackId).toBe("zip-hooks"); expect(result.hooks).toContain("zip-hook"); expect(result.targetDir).toBe(path.join(stateDir, "hooks", "zip-hooks")); @@ -78,9 +80,9 @@ describe("installHooksFromArchive", () => { fs.writeFileSync( path.join(pkgDir, "package.json"), JSON.stringify({ - name: "@moltbot/tar-hooks", + name: "@openclaw/tar-hooks", version: "0.0.1", - moltbot: { hooks: ["./hooks/tar-hook"] }, + openclaw: { hooks: ["./hooks/tar-hook"] }, }), "utf-8", ); @@ -90,7 +92,7 @@ describe("installHooksFromArchive", () => { "---", "name: tar-hook", "description: Tar hook", - 'metadata: {"moltbot":{"events":["command:new"]}}', + 'metadata: {"openclaw":{"events":["command:new"]}}', "---", "", "# Tar Hook", @@ -109,7 +111,9 @@ describe("installHooksFromArchive", () => { const result = await installHooksFromArchive({ archivePath, hooksDir }); expect(result.ok).toBe(true); - if (!result.ok) return; + if (!result.ok) { + return; + } expect(result.hookPackId).toBe("tar-hooks"); expect(result.hooks).toContain("tar-hook"); expect(result.targetDir).toBe(path.join(stateDir, "hooks", "tar-hooks")); @@ -128,7 +132,7 @@ describe("installHooksFromPath", () => { "---", "name: my-hook", "description: My hook", - 'metadata: {"moltbot":{"events":["command:new"]}}', + 'metadata: {"openclaw":{"events":["command:new"]}}', "---", "", "# My Hook", @@ -142,7 +146,9 @@ describe("installHooksFromPath", () => { const result = await installHooksFromPath({ path: hookDir, hooksDir }); expect(result.ok).toBe(true); - if (!result.ok) return; + if (!result.ok) { + return; + } expect(result.hookPackId).toBe("my-hook"); expect(result.hooks).toEqual(["my-hook"]); expect(result.targetDir).toBe(path.join(stateDir, "hooks", "my-hook")); diff --git a/src/hooks/install.ts b/src/hooks/install.ts index 14ae54fa0..4594f99df 100644 --- a/src/hooks/install.ts +++ b/src/hooks/install.ts @@ -1,10 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - -import { LEGACY_MANIFEST_KEY } from "../compat/legacy-names.js"; -import { runCommandWithTimeout } from "../process/exec.js"; -import { CONFIG_DIR, resolveUserPath } from "../utils.js"; +import { MANIFEST_KEY } from "../compat/legacy-names.js"; import { extractArchive, fileExists, @@ -12,6 +9,8 @@ import { resolveArchiveKind, resolvePackedRootDir, } from "../infra/archive.js"; +import { runCommandWithTimeout } from "../process/exec.js"; +import { CONFIG_DIR, resolveUserPath } from "../utils.js"; import { parseFrontmatter } from "./frontmatter.js"; export type HookInstallLogger = { @@ -23,9 +22,7 @@ type HookPackageManifest = { name?: string; version?: string; dependencies?: Record; - moltbot?: { hooks?: string[] }; - [LEGACY_MANIFEST_KEY]?: { hooks?: string[] }; -}; +} & Partial>; export type InstallHooksResult = | { @@ -41,13 +38,17 @@ const defaultLogger: HookInstallLogger = {}; function unscopedPackageName(name: string): string { const trimmed = name.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed; } function safeDirName(input: string): string { const trimmed = input.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } return trimmed.replaceAll("/", "__"); } @@ -56,14 +57,14 @@ export function resolveHookInstallDir(hookId: string, hooksDir?: string): string return path.join(hooksBase, safeDirName(hookId)); } -async function ensureMoltbotHooks(manifest: HookPackageManifest) { - const hooks = manifest.moltbot?.hooks ?? manifest[LEGACY_MANIFEST_KEY]?.hooks; +async function ensureOpenClawHooks(manifest: HookPackageManifest) { + const hooks = manifest[MANIFEST_KEY]?.hooks; if (!Array.isArray(hooks)) { - throw new Error("package.json missing moltbot.hooks"); + throw new Error("package.json missing openclaw.hooks"); } const list = hooks.map((e) => (typeof e === "string" ? e.trim() : "")).filter(Boolean); if (list.length === 0) { - throw new Error("package.json moltbot.hooks is empty"); + throw new Error("package.json openclaw.hooks is empty"); } return list; } @@ -122,7 +123,7 @@ async function installHookPackageFromDir(params: { let hookEntries: string[]; try { - hookEntries = await ensureMoltbotHooks(manifest); + hookEntries = await ensureOpenClawHooks(manifest); } catch (err) { return { ok: false, error: String(err) }; } @@ -295,7 +296,7 @@ export async function installHooksFromArchive(params: { return { ok: false, error: `unsupported archive: ${archivePath}` }; } - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hook-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-")); const extractDir = path.join(tmpDir, "extract"); await fs.mkdir(extractDir, { recursive: true }); @@ -351,9 +352,11 @@ export async function installHooksFromNpmSpec(params: { const dryRun = params.dryRun ?? false; const expectedHookPackId = params.expectedHookPackId; const spec = params.spec.trim(); - if (!spec) return { ok: false, error: "missing npm spec" }; + if (!spec) { + return { ok: false, error: "missing npm spec" }; + } - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hook-pack-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-pack-")); logger.info?.(`Downloading ${spec}…`); const res = await runCommandWithTimeout(["npm", "pack", spec], { timeoutMs: Math.max(timeoutMs, 300_000), diff --git a/src/hooks/installs.ts b/src/hooks/installs.ts index c0c35ff86..f493ec29e 100644 --- a/src/hooks/installs.ts +++ b/src/hooks/installs.ts @@ -1,9 +1,9 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { HookInstallRecord } from "../config/types.hooks.js"; export type HookInstallUpdate = HookInstallRecord & { hookId: string }; -export function recordHookInstall(cfg: MoltbotConfig, update: HookInstallUpdate): MoltbotConfig { +export function recordHookInstall(cfg: OpenClawConfig, update: HookInstallUpdate): OpenClawConfig { const { hookId, ...record } = update; const installs = { ...cfg.hooks?.internal?.installs, diff --git a/src/hooks/internal-hooks.ts b/src/hooks/internal-hooks.ts index 1b866d444..e92b19366 100644 --- a/src/hooks/internal-hooks.ts +++ b/src/hooks/internal-hooks.ts @@ -1,19 +1,19 @@ /** - * Hook system for moltbot agent events + * Hook system for OpenClaw agent events * * Provides an extensible event-driven hook system for agent events * like command processing, session lifecycle, etc. */ import type { WorkspaceBootstrapFile } from "../agents/workspace.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; export type InternalHookEventType = "command" | "session" | "agent" | "gateway"; export type AgentBootstrapHookContext = { workspaceDir: string; bootstrapFiles: WorkspaceBootstrapFile[]; - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; sessionKey?: string; sessionId?: string; agentId?: string; @@ -167,9 +167,15 @@ export function createInternalHookEvent( } export function isAgentBootstrapEvent(event: InternalHookEvent): event is AgentBootstrapHookEvent { - if (event.type !== "agent" || event.action !== "bootstrap") return false; + if (event.type !== "agent" || event.action !== "bootstrap") { + return false; + } const context = event.context as Partial | null; - if (!context || typeof context !== "object") return false; - if (typeof context.workspaceDir !== "string") return false; + if (!context || typeof context !== "object") { + return false; + } + if (typeof context.workspaceDir !== "string") { + return false; + } return Array.isArray(context.bootstrapFiles); } diff --git a/src/hooks/llm-slug-generator.ts b/src/hooks/llm-slug-generator.ts index 7baf7aca6..95161b66b 100644 --- a/src/hooks/llm-slug-generator.ts +++ b/src/hooks/llm-slug-generator.ts @@ -5,20 +5,20 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveDefaultAgentId, resolveAgentWorkspaceDir, resolveAgentDir, } from "../agents/agent-scope.js"; +import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; /** * Generate a short 1-2 word filename slug from session content using LLM */ export async function generateSlugViaLLM(params: { sessionContent: string; - cfg: MoltbotConfig; + cfg: OpenClawConfig; }): Promise { let tempSessionFile: string | null = null; @@ -28,7 +28,7 @@ export async function generateSlugViaLLM(params: { const agentDir = resolveAgentDir(params.cfg, agentId); // Create a temporary session file for this one-off LLM call - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-slug-")); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-slug-")); tempSessionFile = path.join(tempDir, "session.jsonl"); const prompt = `Based on this conversation, generate a short 1-2 word filename slug (lowercase, hyphen-separated, no file extension). diff --git a/src/hooks/loader.test.ts b/src/hooks/loader.test.ts index a812c2dc2..7bf4e11fa 100644 --- a/src/hooks/loader.test.ts +++ b/src/hooks/loader.test.ts @@ -2,14 +2,14 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { loadInternalHooks } from "./loader.js"; +import type { OpenClawConfig } from "../config/config.js"; import { clearInternalHooks, getRegisteredEventKeys, triggerInternalHook, createInternalHookEvent, } from "./internal-hooks.js"; -import type { MoltbotConfig } from "../config/config.js"; +import { loadInternalHooks } from "./loader.js"; describe("loader", () => { let tmpDir: string; @@ -18,21 +18,21 @@ describe("loader", () => { beforeEach(async () => { clearInternalHooks(); // Create a temp directory for test modules - tmpDir = path.join(os.tmpdir(), `moltbot-test-${Date.now()}`); + tmpDir = path.join(os.tmpdir(), `openclaw-test-${Date.now()}`); await fs.mkdir(tmpDir, { recursive: true }); // Disable bundled hooks during tests by setting env var to non-existent directory - originalBundledDir = process.env.CLAWDBOT_BUNDLED_HOOKS_DIR; - process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = "/nonexistent/bundled/hooks"; + originalBundledDir = process.env.OPENCLAW_BUNDLED_HOOKS_DIR; + process.env.OPENCLAW_BUNDLED_HOOKS_DIR = "/nonexistent/bundled/hooks"; }); afterEach(async () => { clearInternalHooks(); // Restore original env var if (originalBundledDir === undefined) { - delete process.env.CLAWDBOT_BUNDLED_HOOKS_DIR; + delete process.env.OPENCLAW_BUNDLED_HOOKS_DIR; } else { - process.env.CLAWDBOT_BUNDLED_HOOKS_DIR = originalBundledDir; + process.env.OPENCLAW_BUNDLED_HOOKS_DIR = originalBundledDir; } // Clean up temp directory try { @@ -44,7 +44,7 @@ describe("loader", () => { describe("loadInternalHooks", () => { it("should return 0 when hooks are not enabled", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: false, @@ -57,7 +57,7 @@ describe("loader", () => { }); it("should return 0 when hooks config is missing", async () => { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const count = await loadInternalHooks(cfg, tmpDir); expect(count).toBe(0); }); @@ -72,7 +72,7 @@ describe("loader", () => { `; await fs.writeFile(handlerPath, handlerCode, "utf-8"); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: true, @@ -101,7 +101,7 @@ describe("loader", () => { await fs.writeFile(handler1Path, "export default async function() {}", "utf-8"); await fs.writeFile(handler2Path, "export default async function() {}", "utf-8"); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: true, @@ -131,7 +131,7 @@ describe("loader", () => { `; await fs.writeFile(handlerPath, handlerCode, "utf-8"); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: true, @@ -153,7 +153,7 @@ describe("loader", () => { it("should handle module loading errors gracefully", async () => { const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: true, @@ -184,7 +184,7 @@ describe("loader", () => { const handlerPath = path.join(tmpDir, "bad-export.js"); await fs.writeFile(handlerPath, 'export default "not a function";', "utf-8"); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: true, @@ -213,7 +213,7 @@ describe("loader", () => { // Get relative path from cwd const relativePath = path.relative(process.cwd(), handlerPath); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: true, @@ -245,7 +245,7 @@ describe("loader", () => { `; await fs.writeFile(handlerPath, handlerCode, "utf-8"); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { internal: { enabled: true, diff --git a/src/hooks/loader.ts b/src/hooks/loader.ts index d246045a9..2b7bc5395 100644 --- a/src/hooks/loader.ts +++ b/src/hooks/loader.ts @@ -5,14 +5,14 @@ * and from directory-based discovery (bundled, managed, workspace) */ -import { pathToFileURL } from "node:url"; import path from "node:path"; -import { registerInternalHook } from "./internal-hooks.js"; -import type { MoltbotConfig } from "../config/config.js"; +import { pathToFileURL } from "node:url"; +import type { OpenClawConfig } from "../config/config.js"; import type { InternalHookHandler } from "./internal-hooks.js"; -import { loadWorkspaceHookEntries } from "./workspace.js"; import { resolveHookConfig } from "./config.js"; import { shouldIncludeHook } from "./config.js"; +import { registerInternalHook } from "./internal-hooks.js"; +import { loadWorkspaceHookEntries } from "./workspace.js"; /** * Load and register all hook handlers @@ -21,7 +21,7 @@ import { shouldIncludeHook } from "./config.js"; * 1. Directory-based discovery (bundled, managed, workspace) * 2. Legacy config handlers (backwards compatibility) * - * @param cfg - Moltbot configuration + * @param cfg - OpenClaw configuration * @param workspaceDir - Workspace directory for hook discovery * @returns Number of handlers successfully loaded * @@ -33,7 +33,10 @@ import { shouldIncludeHook } from "./config.js"; * console.log(`Loaded ${count} hook handlers`); * ``` */ -export async function loadInternalHooks(cfg: MoltbotConfig, workspaceDir: string): Promise { +export async function loadInternalHooks( + cfg: OpenClawConfig, + workspaceDir: string, +): Promise { // Check if hooks are enabled if (!cfg.hooks?.internal?.enabled) { return 0; diff --git a/src/hooks/plugin-hooks.ts b/src/hooks/plugin-hooks.ts index f65dadbfd..faf34323b 100644 --- a/src/hooks/plugin-hooks.ts +++ b/src/hooks/plugin-hooks.ts @@ -1,11 +1,10 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; - -import type { MoltbotPluginApi } from "../plugins/types.js"; +import type { OpenClawPluginApi } from "../plugins/types.js"; +import type { InternalHookHandler } from "./internal-hooks.js"; import type { HookEntry } from "./types.js"; import { shouldIncludeHook } from "./config.js"; import { loadHookEntriesFromDir } from "./workspace.js"; -import type { InternalHookHandler } from "./internal-hooks.js"; export type PluginHookLoadResult = { hooks: HookEntry[]; @@ -14,17 +13,19 @@ export type PluginHookLoadResult = { errors: string[]; }; -function resolveHookDir(api: MoltbotPluginApi, dir: string): string { - if (path.isAbsolute(dir)) return dir; +function resolveHookDir(api: OpenClawPluginApi, dir: string): string { + if (path.isAbsolute(dir)) { + return dir; + } return path.resolve(path.dirname(api.source), dir); } -function normalizePluginHookEntry(api: MoltbotPluginApi, entry: HookEntry): HookEntry { +function normalizePluginHookEntry(api: OpenClawPluginApi, entry: HookEntry): HookEntry { return { ...entry, hook: { ...entry.hook, - source: "moltbot-plugin", + source: "openclaw-plugin", pluginId: api.id, }, metadata: { @@ -37,7 +38,7 @@ function normalizePluginHookEntry(api: MoltbotPluginApi, entry: HookEntry): Hook async function loadHookHandler( entry: HookEntry, - api: MoltbotPluginApi, + api: OpenClawPluginApi, ): Promise { try { const url = pathToFileURL(entry.hook.handlerPath).href; @@ -57,13 +58,13 @@ async function loadHookHandler( } export async function registerPluginHooksFromDir( - api: MoltbotPluginApi, + api: OpenClawPluginApi, dir: string, ): Promise { const resolvedDir = resolveHookDir(api, dir); const hooks = loadHookEntriesFromDir({ dir: resolvedDir, - source: "moltbot-plugin", + source: "openclaw-plugin", pluginId: api.id, }); diff --git a/src/hooks/soul-evil.test.ts b/src/hooks/soul-evil.test.ts index 6db0e8c4d..b6d41904c 100644 --- a/src/hooks/soul-evil.test.ts +++ b/src/hooks/soul-evil.test.ts @@ -1,15 +1,13 @@ import path from "node:path"; - import { describe, expect, it } from "vitest"; - +import { DEFAULT_SOUL_FILENAME, type WorkspaceBootstrapFile } from "../agents/workspace.js"; +import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js"; import { applySoulEvilOverride, decideSoulEvil, DEFAULT_SOUL_EVIL_FILENAME, resolveSoulEvilConfigFromHook, } from "./soul-evil.js"; -import { DEFAULT_SOUL_FILENAME, type WorkspaceBootstrapFile } from "../agents/workspace.js"; -import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js"; const makeFiles = (overrides?: Partial) => [ { @@ -116,7 +114,7 @@ describe("decideSoulEvil", () => { describe("applySoulEvilOverride", () => { it("replaces SOUL content when evil is active and file exists", async () => { - const tempDir = await makeTempWorkspace("moltbot-soul-"); + const tempDir = await makeTempWorkspace("openclaw-soul-"); await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_SOUL_EVIL_FILENAME, @@ -140,7 +138,7 @@ describe("applySoulEvilOverride", () => { }); it("leaves SOUL content when evil file is missing", async () => { - const tempDir = await makeTempWorkspace("moltbot-soul-"); + const tempDir = await makeTempWorkspace("openclaw-soul-"); const files = makeFiles({ path: path.join(tempDir, DEFAULT_SOUL_FILENAME), }); @@ -158,7 +156,7 @@ describe("applySoulEvilOverride", () => { }); it("uses custom evil filename when configured", async () => { - const tempDir = await makeTempWorkspace("moltbot-soul-"); + const tempDir = await makeTempWorkspace("openclaw-soul-"); await writeWorkspaceFile({ dir: tempDir, name: "SOUL_EVIL_CUSTOM.md", @@ -182,7 +180,7 @@ describe("applySoulEvilOverride", () => { }); it("warns and skips when evil file is empty", async () => { - const tempDir = await makeTempWorkspace("moltbot-soul-"); + const tempDir = await makeTempWorkspace("openclaw-soul-"); await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_SOUL_EVIL_FILENAME, @@ -209,7 +207,7 @@ describe("applySoulEvilOverride", () => { }); it("leaves files untouched when SOUL.md is not in bootstrap files", async () => { - const tempDir = await makeTempWorkspace("moltbot-soul-"); + const tempDir = await makeTempWorkspace("openclaw-soul-"); await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_SOUL_EVIL_FILENAME, diff --git a/src/hooks/soul-evil.ts b/src/hooks/soul-evil.ts index 934d0a48c..fc1591737 100644 --- a/src/hooks/soul-evil.ts +++ b/src/hooks/soul-evil.ts @@ -1,8 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; - -import { resolveUserTimezone } from "../agents/date-time.js"; import type { WorkspaceBootstrapFile } from "../agents/workspace.js"; +import { resolveUserTimezone } from "../agents/date-time.js"; import { parseDurationMs } from "../cli/parse-duration.js"; import { resolveUserPath } from "../utils.js"; @@ -44,7 +43,9 @@ export function resolveSoulEvilConfigFromHook( entry: Record | undefined, log?: SoulEvilLog, ): SoulEvilConfig | null { - if (!entry) return null; + if (!entry) { + return null; + } const file = typeof entry.file === "string" ? entry.file : undefined; if (entry.file !== undefined && !file) { log?.warn?.("soul-evil config: file must be a string"); @@ -80,23 +81,33 @@ export function resolveSoulEvilConfigFromHook( log?.warn?.("soul-evil config: purge must be an object"); } - if (!file && chance === undefined && !purge) return null; + if (!file && chance === undefined && !purge) { + return null; + } return { file, chance, purge }; } function clampChance(value?: number): number { - if (typeof value !== "number" || !Number.isFinite(value)) return 0; + if (typeof value !== "number" || !Number.isFinite(value)) { + return 0; + } return Math.min(1, Math.max(0, value)); } function parsePurgeAt(raw?: string): number | null { - if (!raw) return null; + if (!raw) { + return null; + } const trimmed = raw.trim(); const match = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(trimmed); - if (!match) return null; + if (!match) { + return null; + } const hour = Number.parseInt(match[1] ?? "", 10); const minute = Number.parseInt(match[2] ?? "", 10); - if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null; + if (!Number.isFinite(hour) || !Number.isFinite(minute)) { + return null; + } return hour * 60 + minute; } @@ -111,9 +122,13 @@ function timeOfDayMsInTimezone(date: Date, timeZone: string): number | null { }).formatToParts(date); const map: Record = {}; for (const part of parts) { - if (part.type !== "literal") map[part.type] = part.value; + if (part.type !== "literal") { + map[part.type] = part.value; + } + } + if (!map.hour || !map.minute || !map.second) { + return null; } - if (!map.hour || !map.minute || !map.second) return null; const hour = Number.parseInt(map.hour, 10); const minute = Number.parseInt(map.minute, 10); const second = Number.parseInt(map.second, 10); @@ -132,9 +147,13 @@ function isWithinDailyPurgeWindow(params: { now: Date; timeZone: string; }): boolean { - if (!params.at || !params.duration) return false; + if (!params.at || !params.duration) { + return false; + } const startMinutes = parsePurgeAt(params.at); - if (startMinutes === null) return false; + if (startMinutes === null) { + return false; + } let durationMs: number; try { @@ -142,13 +161,19 @@ function isWithinDailyPurgeWindow(params: { } catch { return false; } - if (!Number.isFinite(durationMs) || durationMs <= 0) return false; + if (!Number.isFinite(durationMs) || durationMs <= 0) { + return false; + } const dayMs = 24 * 60 * 60 * 1000; - if (durationMs >= dayMs) return true; + if (durationMs >= dayMs) { + return true; + } const nowMs = timeOfDayMsInTimezone(params.now, params.timeZone); - if (nowMs === null) return false; + if (nowMs === null) { + return false; + } const startMs = startMinutes * 60 * 1000; const endMs = startMs + durationMs; @@ -204,7 +229,9 @@ export async function applySoulEvilOverride(params: { now: params.now, random: params.random, }); - if (!decision.useEvil) return params.files; + if (!decision.useEvil) { + return params.files; + } const workspaceDir = resolveUserPath(params.workspaceDir); const evilPath = path.join(workspaceDir, decision.fileName); @@ -235,11 +262,15 @@ export async function applySoulEvilOverride(params: { let replaced = false; const updated = params.files.map((file) => { - if (file.name !== "SOUL.md") return file; + if (file.name !== "SOUL.md") { + return file; + } replaced = true; return { ...file, content: evilContent, missing: false }; }); - if (!replaced) return params.files; + if (!replaced) { + return params.files; + } params.log?.debug?.( `SOUL_EVIL active (${decision.reason ?? "unknown"}) using ${decision.fileName}`, diff --git a/src/hooks/types.ts b/src/hooks/types.ts index 3818dfd92..6675a4326 100644 --- a/src/hooks/types.ts +++ b/src/hooks/types.ts @@ -7,7 +7,7 @@ export type HookInstallSpec = { bins?: string[]; }; -export type MoltbotHookMetadata = { +export type OpenClawHookMetadata = { always?: boolean; hookKey?: string; emoji?: string; @@ -35,7 +35,7 @@ export type ParsedHookFrontmatter = Record; export type Hook = { name: string; description: string; - source: "moltbot-bundled" | "moltbot-managed" | "moltbot-workspace" | "moltbot-plugin"; + source: "openclaw-bundled" | "openclaw-managed" | "openclaw-workspace" | "openclaw-plugin"; pluginId?: string; filePath: string; // Path to HOOK.md baseDir: string; // Directory containing hook @@ -47,7 +47,7 @@ export type HookSource = Hook["source"]; export type HookEntry = { hook: Hook; frontmatter: ParsedHookFrontmatter; - metadata?: MoltbotHookMetadata; + metadata?: OpenClawHookMetadata; invocation?: HookInvocationPolicy; }; diff --git a/src/hooks/workspace.ts b/src/hooks/workspace.ts index 7f371ac10..e476279fe 100644 --- a/src/hooks/workspace.ts +++ b/src/hooks/workspace.ts @@ -1,16 +1,6 @@ import fs from "node:fs"; import path from "node:path"; - -import { LEGACY_MANIFEST_KEY } from "../compat/legacy-names.js"; -import type { MoltbotConfig } from "../config/config.js"; -import { CONFIG_DIR, resolveUserPath } from "../utils.js"; -import { resolveBundledHooksDir } from "./bundled-dir.js"; -import { shouldIncludeHook } from "./config.js"; -import { - parseFrontmatter, - resolveMoltbotMetadata, - resolveHookInvocationPolicy, -} from "./frontmatter.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { Hook, HookEligibilityContext, @@ -19,16 +9,23 @@ import type { HookSource, ParsedHookFrontmatter, } from "./types.js"; +import { MANIFEST_KEY } from "../compat/legacy-names.js"; +import { CONFIG_DIR, resolveUserPath } from "../utils.js"; +import { resolveBundledHooksDir } from "./bundled-dir.js"; +import { shouldIncludeHook } from "./config.js"; +import { + parseFrontmatter, + resolveOpenClawMetadata, + resolveHookInvocationPolicy, +} from "./frontmatter.js"; type HookPackageManifest = { name?: string; - moltbot?: { hooks?: string[] }; - [LEGACY_MANIFEST_KEY]?: { hooks?: string[] }; -}; +} & Partial>; function filterHookEntries( entries: HookEntry[], - config?: MoltbotConfig, + config?: OpenClawConfig, eligibility?: HookEligibilityContext, ): HookEntry[] { return entries.filter((entry) => shouldIncludeHook({ entry, config, eligibility })); @@ -36,7 +33,9 @@ function filterHookEntries( function readHookPackageManifest(dir: string): HookPackageManifest | null { const manifestPath = path.join(dir, "package.json"); - if (!fs.existsSync(manifestPath)) return null; + if (!fs.existsSync(manifestPath)) { + return null; + } try { const raw = fs.readFileSync(manifestPath, "utf-8"); return JSON.parse(raw) as HookPackageManifest; @@ -46,8 +45,10 @@ function readHookPackageManifest(dir: string): HookPackageManifest | null { } function resolvePackageHooks(manifest: HookPackageManifest): string[] { - const raw = manifest.moltbot?.hooks ?? manifest[LEGACY_MANIFEST_KEY]?.hooks; - if (!Array.isArray(raw)) return []; + const raw = manifest[MANIFEST_KEY]?.hooks; + if (!Array.isArray(raw)) { + return []; + } return raw.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); } @@ -58,7 +59,9 @@ function loadHookFromDir(params: { nameHint?: string; }): Hook | null { const hookMdPath = path.join(params.hookDir, "HOOK.md"); - if (!fs.existsSync(hookMdPath)) return null; + if (!fs.existsSync(hookMdPath)) { + return null; + } try { const content = fs.readFileSync(hookMdPath, "utf-8"); @@ -85,7 +88,7 @@ function loadHookFromDir(params: { return { name, description, - source: params.source as Hook["source"], + source: params.source, pluginId: params.pluginId, filePath: hookMdPath, baseDir: params.hookDir, @@ -103,16 +106,22 @@ function loadHookFromDir(params: { function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?: string }): Hook[] { const { dir, source, pluginId } = params; - if (!fs.existsSync(dir)) return []; + if (!fs.existsSync(dir)) { + return []; + } const stat = fs.statSync(dir); - if (!stat.isDirectory()) return []; + if (!stat.isDirectory()) { + return []; + } const hooks: Hook[] = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - if (!entry.isDirectory()) continue; + if (!entry.isDirectory()) { + continue; + } const hookDir = path.join(dir, entry.name); const manifest = readHookPackageManifest(hookDir); @@ -127,7 +136,9 @@ function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?: pluginId, nameHint: path.basename(resolvedHookDir), }); - if (hook) hooks.push(hook); + if (hook) { + hooks.push(hook); + } } continue; } @@ -138,7 +149,9 @@ function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?: pluginId, nameHint: entry.name, }); - if (hook) hooks.push(hook); + if (hook) { + hooks.push(hook); + } } return hooks; @@ -169,7 +182,7 @@ export function loadHookEntriesFromDir(params: { pluginId: params.pluginId, }, frontmatter, - metadata: resolveMoltbotMetadata(frontmatter), + metadata: resolveOpenClawMetadata(frontmatter), invocation: resolveHookInvocationPolicy(frontmatter), }; return entry; @@ -179,7 +192,7 @@ export function loadHookEntriesFromDir(params: { function loadHookEntries( workspaceDir: string, opts?: { - config?: MoltbotConfig; + config?: OpenClawConfig; managedHooksDir?: string; bundledHooksDir?: string; }, @@ -195,31 +208,39 @@ function loadHookEntries( const bundledHooks = bundledHooksDir ? loadHooksFromDir({ dir: bundledHooksDir, - source: "moltbot-bundled", + source: "openclaw-bundled", }) : []; const extraHooks = extraDirs.flatMap((dir) => { const resolved = resolveUserPath(dir); return loadHooksFromDir({ dir: resolved, - source: "moltbot-workspace", // Extra dirs treated as workspace + source: "openclaw-workspace", // Extra dirs treated as workspace }); }); const managedHooks = loadHooksFromDir({ dir: managedHooksDir, - source: "moltbot-managed", + source: "openclaw-managed", }); const workspaceHooks = loadHooksFromDir({ dir: workspaceHooksDir, - source: "moltbot-workspace", + source: "openclaw-workspace", }); const merged = new Map(); // Precedence: extra < bundled < managed < workspace (workspace wins) - for (const hook of extraHooks) merged.set(hook.name, hook); - for (const hook of bundledHooks) merged.set(hook.name, hook); - for (const hook of managedHooks) merged.set(hook.name, hook); - for (const hook of workspaceHooks) merged.set(hook.name, hook); + for (const hook of extraHooks) { + merged.set(hook.name, hook); + } + for (const hook of bundledHooks) { + merged.set(hook.name, hook); + } + for (const hook of managedHooks) { + merged.set(hook.name, hook); + } + for (const hook of workspaceHooks) { + merged.set(hook.name, hook); + } return Array.from(merged.values()).map((hook) => { let frontmatter: ParsedHookFrontmatter = {}; @@ -232,7 +253,7 @@ function loadHookEntries( return { hook, frontmatter, - metadata: resolveMoltbotMetadata(frontmatter), + metadata: resolveOpenClawMetadata(frontmatter), invocation: resolveHookInvocationPolicy(frontmatter), }; }); @@ -241,7 +262,7 @@ function loadHookEntries( export function buildWorkspaceHookSnapshot( workspaceDir: string, opts?: { - config?: MoltbotConfig; + config?: OpenClawConfig; managedHooksDir?: string; bundledHooksDir?: string; entries?: HookEntry[]; @@ -265,7 +286,7 @@ export function buildWorkspaceHookSnapshot( export function loadWorkspaceHookEntries( workspaceDir: string, opts?: { - config?: MoltbotConfig; + config?: OpenClawConfig; managedHooksDir?: string; bundledHooksDir?: string; }, diff --git a/src/imessage/accounts.ts b/src/imessage/accounts.ts index 25e825d83..ed8ad886e 100644 --- a/src/imessage/accounts.ts +++ b/src/imessage/accounts.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { IMessageAccountConfig } from "../config/types.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; @@ -10,34 +10,42 @@ export type ResolvedIMessageAccount = { configured: boolean; }; -function listConfiguredAccountIds(cfg: MoltbotConfig): string[] { +function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = cfg.channels?.imessage?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } -export function listIMessageAccountIds(cfg: MoltbotConfig): string[] { +export function listIMessageAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } -export function resolveDefaultIMessageAccountId(cfg: MoltbotConfig): string { +export function resolveDefaultIMessageAccountId(cfg: OpenClawConfig): string { const ids = listIMessageAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } function resolveAccountConfig( - cfg: MoltbotConfig, + cfg: OpenClawConfig, accountId: string, ): IMessageAccountConfig | undefined { const accounts = cfg.channels?.imessage?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as IMessageAccountConfig | undefined; } -function mergeIMessageAccountConfig(cfg: MoltbotConfig, accountId: string): IMessageAccountConfig { +function mergeIMessageAccountConfig(cfg: OpenClawConfig, accountId: string): IMessageAccountConfig { const { accounts: _ignored, ...base } = (cfg.channels?.imessage ?? {}) as IMessageAccountConfig & { accounts?: unknown }; const account = resolveAccountConfig(cfg, accountId) ?? {}; @@ -45,7 +53,7 @@ function mergeIMessageAccountConfig(cfg: MoltbotConfig, accountId: string): IMes } export function resolveIMessageAccount(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; }): ResolvedIMessageAccount { const accountId = normalizeAccountId(params.accountId); @@ -75,7 +83,7 @@ export function resolveIMessageAccount(params: { }; } -export function listEnabledIMessageAccounts(cfg: MoltbotConfig): ResolvedIMessageAccount[] { +export function listEnabledIMessageAccounts(cfg: OpenClawConfig): ResolvedIMessageAccount[] { return listIMessageAccountIds(cfg) .map((accountId) => resolveIMessageAccount({ cfg, accountId })) .filter((account) => account.enabled); diff --git a/src/imessage/client.ts b/src/imessage/client.ts index 52d255ad8..9811de083 100644 --- a/src/imessage/client.ts +++ b/src/imessage/client.ts @@ -1,6 +1,5 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { createInterface, type Interface } from "node:readline"; - import type { RuntimeEnv } from "../runtime.js"; import { resolveUserPath } from "../utils.js"; @@ -60,7 +59,9 @@ export class IMessageRpcClient { } async start(): Promise { - if (this.child) return; + if (this.child) { + return; + } const args = ["rpc"]; if (this.dbPath) { args.push("--db", this.dbPath); @@ -73,14 +74,18 @@ export class IMessageRpcClient { this.reader.on("line", (line) => { const trimmed = line.trim(); - if (!trimmed) return; + if (!trimmed) { + return; + } this.handleLine(trimmed); }); child.stderr?.on("data", (chunk) => { const lines = chunk.toString().split(/\r?\n/); for (const line of lines) { - if (!line.trim()) continue; + if (!line.trim()) { + continue; + } this.runtime?.error?.(`imsg rpc: ${line.trim()}`); } }); @@ -102,7 +107,9 @@ export class IMessageRpcClient { } async stop(): Promise { - if (!this.child) return; + if (!this.child) { + return; + } this.reader?.close(); this.reader = null; this.child.stdin?.end(); @@ -113,7 +120,9 @@ export class IMessageRpcClient { this.closed, new Promise((resolve) => { setTimeout(() => { - if (!child.killed) child.kill("SIGTERM"); + if (!child.killed) { + child.kill("SIGTERM"); + } resolve(); }, 500); }), @@ -175,8 +184,12 @@ export class IMessageRpcClient { if (parsed.id !== undefined && parsed.id !== null) { const key = String(parsed.id); const pending = this.pending.get(key); - if (!pending) return; - if (pending.timer) clearTimeout(pending.timer); + if (!pending) { + return; + } + if (pending.timer) { + clearTimeout(pending.timer); + } this.pending.delete(key); if (parsed.error) { @@ -184,11 +197,15 @@ export class IMessageRpcClient { const details = parsed.error.data; const code = parsed.error.code; const suffixes = [] as string[]; - if (typeof code === "number") suffixes.push(`code=${code}`); + if (typeof code === "number") { + suffixes.push(`code=${code}`); + } if (details !== undefined) { const detailText = typeof details === "string" ? details : JSON.stringify(details, null, 2); - if (detailText) suffixes.push(detailText); + if (detailText) { + suffixes.push(detailText); + } } const msg = suffixes.length > 0 ? `${baseMessage}: ${suffixes.join(" ")}` : baseMessage; pending.reject(new Error(msg)); @@ -208,7 +225,9 @@ export class IMessageRpcClient { private failAll(err: Error) { for (const [key, pending] of this.pending.entries()) { - if (pending.timer) clearTimeout(pending.timer); + if (pending.timer) { + clearTimeout(pending.timer); + } pending.reject(err); this.pending.delete(key); } diff --git a/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts b/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts index 7f0fdd368..099e8508d 100644 --- a/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts +++ b/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { monitorIMessageProvider } from "./monitor.js"; const requestMock = vi.fn(); @@ -36,7 +35,7 @@ vi.mock("../pairing/pairing-store.js", () => ({ })); vi.mock("../config/sessions.js", () => ({ - resolveStorePath: vi.fn(() => "/tmp/moltbot-sessions.json"), + resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), readSessionUpdatedAt: vi.fn(() => undefined), recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined), @@ -64,7 +63,9 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); async function waitForSubscribe() { for (let i = 0; i < 5; i += 1) { - if (requestMock.mock.calls.some((call) => call[0] === "watch.subscribe")) return; + if (requestMock.mock.calls.some((call) => call[0] === "watch.subscribe")) { + return; + } await flush(); } } @@ -80,11 +81,13 @@ beforeEach(() => { }, session: { mainKey: "main" }, messages: { - groupChat: { mentionPatterns: ["@clawd"] }, + groupChat: { mentionPatterns: ["@openclaw"] }, }, }; requestMock.mockReset().mockImplementation((method: string) => { - if (method === "watch.subscribe") return Promise.resolve({ subscription: 1 }); + if (method === "watch.subscribe") { + return Promise.resolve({ subscription: 1 }); + } return Promise.resolve({}); }); stopMock.mockReset().mockResolvedValue(undefined); @@ -219,7 +222,7 @@ describe("monitorIMessageProvider", () => { chat_id: 123, sender: "+15550001111", is_from_me: false, - text: "@clawd hello", + text: "@openclaw hello", is_group: true, }, }, @@ -364,7 +367,7 @@ describe("monitorIMessageProvider", () => { chat_id: 42, sender: "+15550002222", is_from_me: false, - text: "@clawd ping", + text: "@openclaw ping", is_group: true, chat_name: "Lobster Squad", participants: ["+1555", "+1556"], @@ -413,7 +416,7 @@ describe("monitorIMessageProvider", () => { chat_id: 202, sender: "+15550003333", is_from_me: false, - text: "@clawd hi", + text: "@openclaw hi", is_group: true, }, }, @@ -448,7 +451,7 @@ describe("monitorIMessageProvider", () => { chat_id: 303, sender: "+15550003333", is_from_me: false, - text: "@clawd hi", + text: "@openclaw hi", is_group: true, }, }, @@ -474,7 +477,7 @@ describe("monitorIMessageProvider", () => { chat_name: "Test Group", sender: "+15550001111", is_from_me: false, - text: "@clawd hi", + text: "@openclaw hi", is_group: true, created_at: "2026-01-17T00:00:00Z", }, @@ -489,7 +492,7 @@ describe("monitorIMessageProvider", () => { const ctx = replyMock.mock.calls[0]?.[0]; const body = ctx?.Body ?? ""; expect(body).toContain("Test Group id:99"); - expect(body).toContain("+15550001111: @clawd hi"); + expect(body).toContain("+15550001111: @openclaw hi"); }); it("includes reply context when imessage reply metadata is present", async () => { diff --git a/src/imessage/monitor.updates-last-route-chat-id-direct-messages.test.ts b/src/imessage/monitor.updates-last-route-chat-id-direct-messages.test.ts index 78744feda..96123bd58 100644 --- a/src/imessage/monitor.updates-last-route-chat-id-direct-messages.test.ts +++ b/src/imessage/monitor.updates-last-route-chat-id-direct-messages.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { monitorIMessageProvider } from "./monitor.js"; const requestMock = vi.fn(); @@ -36,7 +35,7 @@ vi.mock("../pairing/pairing-store.js", () => ({ })); vi.mock("../config/sessions.js", () => ({ - resolveStorePath: vi.fn(() => "/tmp/moltbot-sessions.json"), + resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), readSessionUpdatedAt: vi.fn(() => undefined), recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined), @@ -64,7 +63,9 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); async function waitForSubscribe() { for (let i = 0; i < 5; i += 1) { - if (requestMock.mock.calls.some((call) => call[0] === "watch.subscribe")) return; + if (requestMock.mock.calls.some((call) => call[0] === "watch.subscribe")) { + return; + } await flush(); } } @@ -80,11 +81,13 @@ beforeEach(() => { }, session: { mainKey: "main" }, messages: { - groupChat: { mentionPatterns: ["@clawd"] }, + groupChat: { mentionPatterns: ["@openclaw"] }, }, }; requestMock.mockReset().mockImplementation((method: string) => { - if (method === "watch.subscribe") return Promise.resolve({ subscription: 1 }); + if (method === "watch.subscribe") { + return Promise.resolve({ subscription: 1 }); + } return Promise.resolve({}); }); stopMock.mockReset().mockResolvedValue(undefined); @@ -133,8 +136,12 @@ describe("monitorIMessageProvider", () => { it("does not trigger unhandledRejection when aborting during shutdown", async () => { requestMock.mockImplementation((method: string) => { - if (method === "watch.subscribe") return Promise.resolve({ subscription: 1 }); - if (method === "watch.unsubscribe") return Promise.reject(new Error("imsg rpc closed")); + if (method === "watch.subscribe") { + return Promise.resolve({ subscription: 1 }); + } + if (method === "watch.unsubscribe") { + return Promise.reject(new Error("imsg rpc closed")); + } return Promise.resolve({}); }); diff --git a/src/imessage/monitor/deliver.ts b/src/imessage/monitor/deliver.ts index c07bc2b08..eb5b72dd3 100644 --- a/src/imessage/monitor/deliver.ts +++ b/src/imessage/monitor/deliver.ts @@ -1,10 +1,10 @@ +import type { ReplyPayload } from "../../auto-reply/types.js"; +import type { RuntimeEnv } from "../../runtime.js"; +import type { createIMessageRpcClient } from "../client.js"; import { chunkTextWithMode, resolveChunkMode } from "../../auto-reply/chunk.js"; import { loadConfig } from "../../config/config.js"; import { resolveMarkdownTableMode } from "../../config/markdown-tables.js"; import { convertMarkdownTables } from "../../markdown/tables.js"; -import type { ReplyPayload } from "../../auto-reply/types.js"; -import type { RuntimeEnv } from "../../runtime.js"; -import type { createIMessageRpcClient } from "../client.js"; import { sendMessageIMessage } from "../send.js"; export async function deliverReplies(params: { @@ -28,7 +28,9 @@ export async function deliverReplies(params: { const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const rawText = payload.text ?? ""; const text = convertMarkdownTables(rawText, tableMode); - if (!text && mediaList.length === 0) continue; + if (!text && mediaList.length === 0) { + continue; + } if (mediaList.length === 0) { for (const chunk of chunkTextWithMode(text, textLimit, chunkMode)) { await sendMessageIMessage(target, chunk, { diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index d8d4b99ec..08fb36aea 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -1,8 +1,9 @@ import fs from "node:fs/promises"; - +import type { IMessagePayload, MonitorIMessageOpts } from "./types.js"; import { resolveHumanDelayConfig } from "../../agents/identity.js"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; +import { dispatchInboundMessage } from "../../auto-reply/dispatch.js"; import { formatInboundEnvelope, formatInboundFromLabel, @@ -12,8 +13,6 @@ import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js"; -import { dispatchInboundMessage } from "../../auto-reply/dispatch.js"; -import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, @@ -21,8 +20,10 @@ import { recordPendingHistoryEntryIfEnabled, type HistoryEntry, } from "../../auto-reply/reply/history.js"; +import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js"; import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js"; +import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { logInboundDrop } from "../../channels/logging.js"; import { createReplyPrefixContext } from "../../channels/reply-prefix.js"; import { recordInboundSession } from "../../channels/session.js"; @@ -42,7 +43,6 @@ import { } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { truncateUtf16Safe } from "../../utils.js"; -import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { resolveIMessageAccount } from "../accounts.js"; import { createIMessageRpcClient } from "../client.js"; import { probeIMessage } from "../probe.js"; @@ -54,11 +54,10 @@ import { } from "../targets.js"; import { deliverReplies } from "./deliver.js"; import { normalizeAllowList, resolveRuntime } from "./runtime.js"; -import type { IMessagePayload, MonitorIMessageOpts } from "./types.js"; /** * Try to detect remote host from an SSH wrapper script like: - * exec ssh -T moltbot@192.168.64.3 /opt/homebrew/bin/imsg "$@" + * exec ssh -T openclaw@192.168.64.3 /opt/homebrew/bin/imsg "$@" * exec ssh -T mac-mini imsg "$@" * Returns the user@host or host portion if found, undefined otherwise. */ @@ -70,9 +69,11 @@ async function detectRemoteHostFromCliPath(cliPath: string): Promise { const sender = entry.message.sender?.trim(); - if (!sender) return null; + if (!sender) { + return null; + } const conversationId = entry.message.chat_id != null ? `chat:${entry.message.chat_id}` @@ -158,13 +165,19 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P }, shouldDebounce: (entry) => { const text = entry.message.text?.trim() ?? ""; - if (!text) return false; - if (entry.message.attachments && entry.message.attachments.length > 0) return false; + if (!text) { + return false; + } + if (entry.message.attachments && entry.message.attachments.length > 0) { + return false; + } return !hasControlCommand(text, cfg); }, onFlush: async (entries) => { const last = entries.at(-1); - if (!last) return; + if (!last) { + return; + } if (entries.length === 1) { await handleMessageNow(last.message); return; @@ -188,9 +201,13 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P async function handleMessageNow(message: IMessagePayload) { const senderRaw = message.sender ?? ""; const sender = senderRaw.trim(); - if (!sender) return; + if (!sender) { + return; + } const senderNormalized = normalizeIMessageHandle(sender); - if (message.is_from_me) return; + if (message.is_from_me) { + return; + } const chatId = message.chat_id ?? undefined; const chatGuid = message.chat_guid ?? undefined; @@ -220,7 +237,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P ); const isGroup = Boolean(message.is_group) || treatAsGroupByConfig; - if (isGroup && !chatId) return; + if (isGroup && !chatId) { + return; + } const groupId = isGroup ? groupIdCandidate : undefined; const storeAllowFrom = await readChannelAllowFromStore("imessage").catch(() => []); @@ -273,7 +292,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P chatIdentifier, })); if (!isGroup) { - if (dmPolicy === "disabled") return; + if (dmPolicy === "disabled") { + return; + } if (!dmAuthorized) { if (dmPolicy === "pairing") { const senderId = normalizeIMessageHandle(sender); @@ -336,7 +357,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P const kind = mediaKindFromMime(mediaType ?? undefined); const placeholder = kind ? `` : attachments?.length ? "" : ""; const bodyText = messageText || placeholder; - if (!bodyText) return; + if (!bodyText) { + return; + } const replyContext = describeReplyContext(message); const createdAt = message.created_at ? Date.parse(message.created_at) : undefined; const historyKey = isGroup @@ -580,7 +603,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P const handleMessage = async (raw: unknown) => { const params = raw as { message?: IMessagePayload | null }; const message = params?.message ?? null; - if (!message) return; + if (!message) { + return; + } await inboundDebouncer.enqueue({ message }); }; @@ -594,7 +619,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P runtime, check: async () => { const probe = await probeIMessage(2000, { cliPath, dbPath, runtime }); - if (probe.ok) return { ok: true }; + if (probe.ok) { + return { ok: true }; + } if (probe.fatal) { throw new Error(probe.error ?? "imsg rpc unavailable"); } @@ -602,7 +629,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P }, }); - if (opts.abortSignal?.aborted) return; + if (opts.abortSignal?.aborted) { + return; + } const client = await createIMessageRpcClient({ cliPath, @@ -644,7 +673,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P subscriptionId = result?.subscription ?? null; await client.waitForClose(); } catch (err) { - if (abort?.aborted) return; + if (abort?.aborted) { + return; + } runtime.error?.(danger(`imessage: monitor failed: ${String(err)}`)); throw err; } finally { diff --git a/src/imessage/monitor/types.ts b/src/imessage/monitor/types.ts index d6c2d9177..2f13b3ecf 100644 --- a/src/imessage/monitor/types.ts +++ b/src/imessage/monitor/types.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; export type IMessageAttachment = { @@ -31,7 +31,7 @@ export type MonitorIMessageOpts = { cliPath?: string; dbPath?: string; accountId?: string; - config?: MoltbotConfig; + config?: OpenClawConfig; allowFrom?: Array; groupAllowFrom?: Array; includeAttachments?: boolean; diff --git a/src/imessage/probe.test.ts b/src/imessage/probe.test.ts index 5a3e030e7..3faa7cb2a 100644 --- a/src/imessage/probe.test.ts +++ b/src/imessage/probe.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { probeIMessage } from "./probe.js"; const detectBinaryMock = vi.hoisted(() => vi.fn()); diff --git a/src/imessage/probe.ts b/src/imessage/probe.ts index b81a6f7f3..92d131565 100644 --- a/src/imessage/probe.ts +++ b/src/imessage/probe.ts @@ -1,7 +1,7 @@ +import type { RuntimeEnv } from "../runtime.js"; import { detectBinary } from "../commands/onboard-helpers.js"; import { loadConfig } from "../config/config.js"; import { runCommandWithTimeout } from "../process/exec.js"; -import type { RuntimeEnv } from "../runtime.js"; import { createIMessageRpcClient } from "./client.js"; export type IMessageProbe = { @@ -26,7 +26,9 @@ const rpcSupportCache = new Map(); async function probeRpcSupport(cliPath: string): Promise { const cached = rpcSupportCache.get(cliPath); - if (cached) return cached; + if (cached) { + return cached; + } try { const result = await runCommandWithTimeout([cliPath, "rpc", "--help"], { timeoutMs: 2000 }); const combined = `${result.stdout}\n${result.stderr}`.trim(); diff --git a/src/imessage/send.ts b/src/imessage/send.ts index 30972ef09..adc7052c3 100644 --- a/src/imessage/send.ts +++ b/src/imessage/send.ts @@ -1,9 +1,9 @@ import { loadConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; +import { convertMarkdownTables } from "../markdown/tables.js"; import { mediaKindFromMime } from "../media/constants.js"; import { saveMediaBuffer } from "../media/store.js"; import { loadWebMedia } from "../web/media.js"; -import { convertMarkdownTables } from "../markdown/tables.js"; import { resolveIMessageAccount } from "./accounts.js"; import { createIMessageRpcClient, type IMessageRpcClient } from "./client.js"; import { formatIMessageChatTarget, type IMessageService, parseIMessageTarget } from "./targets.js"; @@ -26,7 +26,9 @@ export type IMessageSendResult = { }; function resolveMessageId(result: Record | null | undefined): string | null { - if (!result) return null; + if (!result) { + return null; + } const raw = (typeof result.messageId === "string" && result.messageId.trim()) || (typeof result.message_id === "string" && result.message_id.trim()) || @@ -83,7 +85,9 @@ export async function sendMessageIMessage( filePath = resolved.path; if (!message.trim()) { const kind = mediaKindFromMime(resolved.contentType ?? undefined); - if (kind) message = kind === "image" ? "" : ``; + if (kind) { + message = kind === "image" ? "" : ``; + } } } @@ -101,10 +105,12 @@ export async function sendMessageIMessage( const params: Record = { text: message, - service: (service || "auto") as IMessageService, + service: service || "auto", region, }; - if (filePath) params.file = filePath; + if (filePath) { + params.file = filePath; + } if (target.kind === "chat_id") { params.chat_id = target.chatId; @@ -119,7 +125,7 @@ export async function sendMessageIMessage( const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath })); const shouldClose = !opts.client; try { - const result = await client.request>("send", params, { + const result = await client.request<{ ok?: string }>("send", params, { timeoutMs: opts.timeoutMs, }); const resolvedId = resolveMessageId(result); diff --git a/src/imessage/targets.test.ts b/src/imessage/targets.test.ts index 6350167a3..3a0118215 100644 --- a/src/imessage/targets.test.ts +++ b/src/imessage/targets.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { formatIMessageChatTarget, isAllowedIMessageSender, diff --git a/src/imessage/targets.ts b/src/imessage/targets.ts index 03fdcf306..3819e1f93 100644 --- a/src/imessage/targets.ts +++ b/src/imessage/targets.ts @@ -29,11 +29,19 @@ function stripPrefix(value: string, prefix: string): string { export function normalizeIMessageHandle(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } const lowered = trimmed.toLowerCase(); - if (lowered.startsWith("imessage:")) return normalizeIMessageHandle(trimmed.slice(9)); - if (lowered.startsWith("sms:")) return normalizeIMessageHandle(trimmed.slice(4)); - if (lowered.startsWith("auto:")) return normalizeIMessageHandle(trimmed.slice(5)); + if (lowered.startsWith("imessage:")) { + return normalizeIMessageHandle(trimmed.slice(9)); + } + if (lowered.startsWith("sms:")) { + return normalizeIMessageHandle(trimmed.slice(4)); + } + if (lowered.startsWith("auto:")) { + return normalizeIMessageHandle(trimmed.slice(5)); + } // Normalize chat_id/chat_guid/chat_identifier prefixes case-insensitively for (const prefix of CHAT_ID_PREFIXES) { @@ -55,21 +63,29 @@ export function normalizeIMessageHandle(raw: string): string { } } - if (trimmed.includes("@")) return trimmed.toLowerCase(); + if (trimmed.includes("@")) { + return trimmed.toLowerCase(); + } const normalized = normalizeE164(trimmed); - if (normalized) return normalized; + if (normalized) { + return normalized; + } return trimmed.replace(/\s+/g, ""); } export function parseIMessageTarget(raw: string): IMessageTarget { const trimmed = raw.trim(); - if (!trimmed) throw new Error("iMessage target is required"); + if (!trimmed) { + throw new Error("iMessage target is required"); + } const lower = trimmed.toLowerCase(); for (const { prefix, service } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); - if (!remainder) throw new Error(`${prefix} target is required`); + if (!remainder) { + throw new Error(`${prefix} target is required`); + } const remainderLower = remainder.toLowerCase(); const isChatTarget = CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) || @@ -96,7 +112,9 @@ export function parseIMessageTarget(raw: string): IMessageTarget { for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (!value) throw new Error("chat_guid is required"); + if (!value) { + throw new Error("chat_guid is required"); + } return { kind: "chat_guid", chatGuid: value }; } } @@ -104,7 +122,9 @@ export function parseIMessageTarget(raw: string): IMessageTarget { for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (!value) throw new Error("chat_identifier is required"); + if (!value) { + throw new Error("chat_identifier is required"); + } return { kind: "chat_identifier", chatIdentifier: value }; } } @@ -114,13 +134,17 @@ export function parseIMessageTarget(raw: string): IMessageTarget { export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget { const trimmed = raw.trim(); - if (!trimmed) return { kind: "handle", handle: "" }; + if (!trimmed) { + return { kind: "handle", handle: "" }; + } const lower = trimmed.toLowerCase(); for (const { prefix } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); - if (!remainder) return { kind: "handle", handle: "" }; + if (!remainder) { + return { kind: "handle", handle: "" }; + } return parseIMessageAllowTarget(remainder); } } @@ -129,21 +153,27 @@ export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); const chatId = Number.parseInt(value, 10); - if (Number.isFinite(chatId)) return { kind: "chat_id", chatId }; + if (Number.isFinite(chatId)) { + return { kind: "chat_id", chatId }; + } } } for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (value) return { kind: "chat_guid", chatGuid: value }; + if (value) { + return { kind: "chat_guid", chatGuid: value }; + } } } for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (value) return { kind: "chat_identifier", chatIdentifier: value }; + if (value) { + return { kind: "chat_identifier", chatIdentifier: value }; + } } } @@ -158,8 +188,12 @@ export function isAllowedIMessageSender(params: { chatIdentifier?: string | null; }): boolean { const allowFrom = params.allowFrom.map((entry) => String(entry).trim()); - if (allowFrom.length === 0) return true; - if (allowFrom.includes("*")) return true; + if (allowFrom.length === 0) { + return true; + } + if (allowFrom.includes("*")) { + return true; + } const senderNormalized = normalizeIMessageHandle(params.sender); const chatId = params.chatId ?? undefined; @@ -167,22 +201,34 @@ export function isAllowedIMessageSender(params: { const chatIdentifier = params.chatIdentifier?.trim(); for (const entry of allowFrom) { - if (!entry) continue; + if (!entry) { + continue; + } const parsed = parseIMessageAllowTarget(entry); if (parsed.kind === "chat_id" && chatId !== undefined) { - if (parsed.chatId === chatId) return true; + if (parsed.chatId === chatId) { + return true; + } } else if (parsed.kind === "chat_guid" && chatGuid) { - if (parsed.chatGuid === chatGuid) return true; + if (parsed.chatGuid === chatGuid) { + return true; + } } else if (parsed.kind === "chat_identifier" && chatIdentifier) { - if (parsed.chatIdentifier === chatIdentifier) return true; + if (parsed.chatIdentifier === chatIdentifier) { + return true; + } } else if (parsed.kind === "handle" && senderNormalized) { - if (parsed.handle === senderNormalized) return true; + if (parsed.handle === senderNormalized) { + return true; + } } } return false; } export function formatIMessageChatTarget(chatId?: number | null): string { - if (!chatId || !Number.isFinite(chatId)) return ""; + if (!chatId || !Number.isFinite(chatId)) { + return ""; + } return `chat_id:${chatId}`; } diff --git a/src/index.ts b/src/index.ts index 6a02a828d..61d96ccee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node import process from "node:process"; import { fileURLToPath } from "node:url"; - import { getReplyFromConfig } from "./auto-reply/reply.js"; import { applyTemplate } from "./auto-reply/templating.js"; import { monitorWebChannel } from "./channel-web.js"; @@ -19,8 +18,9 @@ import { import { ensureBinary } from "./infra/binaries.js"; import { loadDotEnv } from "./infra/dotenv.js"; import { normalizeEnv } from "./infra/env.js"; +import { formatUncaughtError } from "./infra/errors.js"; import { isMainModule } from "./infra/is-main.js"; -import { ensureMoltbotCliOnPath } from "./infra/path-env.js"; +import { ensureOpenClawCliOnPath } from "./infra/path-env.js"; import { describePortOwner, ensurePortAvailable, @@ -28,7 +28,6 @@ import { PortInUseError, } from "./infra/ports.js"; import { assertSupportedRuntime } from "./infra/runtime-guard.js"; -import { formatUncaughtError } from "./infra/errors.js"; import { installUnhandledRejectionHandler } from "./infra/unhandled-rejections.js"; import { enableConsoleCapture } from "./logging.js"; import { runCommandWithTimeout, runExec } from "./process/exec.js"; @@ -36,7 +35,7 @@ import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js"; loadDotEnv({ quiet: true }); normalizeEnv(); -ensureMoltbotCliOnPath(); +ensureOpenClawCliOnPath(); // Capture all console output into structured logs while keeping stdout/stderr behavior. enableConsoleCapture(); @@ -83,12 +82,12 @@ if (isMain) { installUnhandledRejectionHandler(); process.on("uncaughtException", (error) => { - console.error("[moltbot] Uncaught exception:", formatUncaughtError(error)); + console.error("[openclaw] Uncaught exception:", formatUncaughtError(error)); process.exit(1); }); void program.parseAsync(process.argv).catch((err) => { - console.error("[moltbot] CLI failed:", formatUncaughtError(err)); + console.error("[openclaw] CLI failed:", formatUncaughtError(err)); process.exit(1); }); } diff --git a/src/infra/agent-events.test.ts b/src/infra/agent-events.test.ts index 1ce051bdb..f86425894 100644 --- a/src/infra/agent-events.test.ts +++ b/src/infra/agent-events.test.ts @@ -39,9 +39,15 @@ describe("agent-events sequencing", () => { test("preserves compaction ordering on the event bus", async () => { const phases: Array = []; const stop = onAgentEvent((evt) => { - if (evt.runId !== "run-1") return; - if (evt.stream !== "compaction") return; - if (typeof evt.data?.phase === "string") phases.push(evt.data.phase); + if (evt.runId !== "run-1") { + return; + } + if (evt.stream !== "compaction") { + return; + } + if (typeof evt.data?.phase === "string") { + phases.push(evt.data.phase); + } }); emitAgentEvent({ runId: "run-1", stream: "compaction", data: { phase: "start" } }); diff --git a/src/infra/agent-events.ts b/src/infra/agent-events.ts index 5c41c3c95..23557cdda 100644 --- a/src/infra/agent-events.ts +++ b/src/infra/agent-events.ts @@ -23,7 +23,9 @@ const listeners = new Set<(evt: AgentEventPayload) => void>(); const runContextById = new Map(); export function registerAgentRunContext(runId: string, context: AgentRunContext) { - if (!runId) return; + if (!runId) { + return; + } const existing = runContextById.get(runId); if (!existing) { runContextById.set(runId, { ...context }); diff --git a/src/infra/archive.test.ts b/src/infra/archive.test.ts index ccf998894..10ea1a601 100644 --- a/src/infra/archive.test.ts +++ b/src/infra/archive.test.ts @@ -1,7 +1,7 @@ +import JSZip from "jszip"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import JSZip from "jszip"; import * as tar from "tar"; import { afterEach, describe, expect, it } from "vitest"; import { extractArchive, resolveArchiveKind, resolvePackedRootDir } from "./archive.js"; @@ -9,7 +9,7 @@ import { extractArchive, resolveArchiveKind, resolvePackedRootDir } from "./arch const tempDirs: string[] = []; async function makeTempDir() { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-archive-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-archive-")); tempDirs.push(dir); return dir; } diff --git a/src/infra/archive.ts b/src/infra/archive.ts index 35ad4fa04..305b8f147 100644 --- a/src/infra/archive.ts +++ b/src/infra/archive.ts @@ -1,7 +1,7 @@ +import JSZip from "jszip"; import fs from "node:fs/promises"; import path from "node:path"; import * as tar from "tar"; -import JSZip from "jszip"; export type ArchiveKind = "tar" | "zip"; @@ -14,8 +14,12 @@ const TAR_SUFFIXES = [".tgz", ".tar.gz", ".tar"]; export function resolveArchiveKind(filePath: string): ArchiveKind | null { const lower = filePath.toLowerCase(); - if (lower.endsWith(".zip")) return "zip"; - if (TAR_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return "tar"; + if (lower.endsWith(".zip")) { + return "zip"; + } + if (TAR_SUFFIXES.some((suffix) => lower.endsWith(suffix))) { + return "tar"; + } return null; } @@ -23,7 +27,9 @@ export async function resolvePackedRootDir(extractDir: string): Promise const direct = path.join(extractDir, "package"); try { const stat = await fs.stat(direct); - if (stat.isDirectory()) return direct; + if (stat.isDirectory()) { + return direct; + } } catch { // ignore } @@ -57,7 +63,9 @@ export async function withTimeout( }), ]); } finally { - if (timeoutId) clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } } } diff --git a/src/infra/backoff.ts b/src/infra/backoff.ts index f1d07b579..153eca162 100644 --- a/src/infra/backoff.ts +++ b/src/infra/backoff.ts @@ -14,12 +14,14 @@ export function computeBackoff(policy: BackoffPolicy, attempt: number) { } export async function sleepWithAbort(ms: number, abortSignal?: AbortSignal) { - if (ms <= 0) return; + if (ms <= 0) { + return; + } try { await delay(ms, undefined, { signal: abortSignal }); } catch (err) { if (abortSignal?.aborted) { - throw new Error("aborted"); + throw new Error("aborted", { cause: err }); } throw err; } diff --git a/src/infra/binaries.test.ts b/src/infra/binaries.test.ts index 50b48d372..4deee7bd0 100644 --- a/src/infra/binaries.test.ts +++ b/src/infra/binaries.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import type { runExec } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; import { ensureBinary } from "./binaries.js"; diff --git a/src/infra/bonjour-ciao.ts b/src/infra/bonjour-ciao.ts index 17df4e78c..878997c62 100644 --- a/src/infra/bonjour-ciao.ts +++ b/src/infra/bonjour-ciao.ts @@ -1,5 +1,4 @@ import { logDebug } from "../logger.js"; - import { formatBonjourError } from "./bonjour-errors.js"; export function ignoreCiaoCancellationRejection(reason: unknown): boolean { diff --git a/src/infra/bonjour-discovery.test.ts b/src/infra/bonjour-discovery.test.ts index 6720b2800..c58a4eeed 100644 --- a/src/infra/bonjour-discovery.test.ts +++ b/src/infra/bonjour-discovery.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from "vitest"; - import type { runCommandWithTimeout } from "../process/exec.js"; import { discoverGatewayBeacons } from "./bonjour-discovery.js"; -import { WIDE_AREA_DISCOVERY_DOMAIN } from "./widearea-dns.js"; + +const WIDE_AREA_DOMAIN = "openclaw.internal."; describe("bonjour-discovery", () => { it("discovers beacons on darwin across local + wide-area domains", async () => { @@ -17,8 +17,8 @@ describe("bonjour-discovery", () => { if (domain === "local.") { return { stdout: [ - "Add 2 3 local. _moltbot-gw._tcp. Peter\\226\\128\\153s Mac Studio Gateway", - "Add 2 3 local. _moltbot-gw._tcp. Laptop Gateway", + "Add 2 3 local. _openclaw-gw._tcp. Peter\\226\\128\\153s Mac Studio Gateway", + "Add 2 3 local. _openclaw-gw._tcp. Laptop Gateway", "", ].join("\n"), stderr: "", @@ -27,12 +27,11 @@ describe("bonjour-discovery", () => { killed: false, }; } - if (domain === WIDE_AREA_DISCOVERY_DOMAIN) { + if (domain === WIDE_AREA_DOMAIN) { return { - stdout: [ - `Add 2 3 ${WIDE_AREA_DISCOVERY_DOMAIN} _moltbot-gw._tcp. Tailnet Gateway`, - "", - ].join("\n"), + stdout: [`Add 2 3 ${WIDE_AREA_DOMAIN} _openclaw-gw._tcp. Tailnet Gateway`, ""].join( + "\n", + ), stderr: "", code: 0, signal: null, @@ -65,7 +64,7 @@ describe("bonjour-discovery", () => { return { stdout: [ - `${instance}._moltbot-gw._tcp. can be reached at ${host}:18789`, + `${instance}._openclaw-gw._tcp. can be reached at ${host}:18789`, txtParts.join(" "), "", ].join("\n"), @@ -82,6 +81,7 @@ describe("bonjour-discovery", () => { const beacons = await discoverGatewayBeacons({ platform: "darwin", timeoutMs: 1234, + wideAreaDomain: WIDE_AREA_DOMAIN, run: run as unknown as typeof runCommandWithTimeout, }); @@ -95,24 +95,26 @@ describe("bonjour-discovery", () => { ]), ); expect(beacons.map((b) => b.domain)).toEqual( - expect.arrayContaining(["local.", WIDE_AREA_DISCOVERY_DOMAIN]), + expect.arrayContaining(["local.", WIDE_AREA_DOMAIN]), ); const browseCalls = calls.filter((c) => c.argv[0] === "dns-sd" && c.argv[1] === "-B"); expect(browseCalls.map((c) => c.argv[3])).toEqual( - expect.arrayContaining(["local.", WIDE_AREA_DISCOVERY_DOMAIN]), + expect.arrayContaining(["local.", WIDE_AREA_DOMAIN]), ); expect(browseCalls.every((c) => c.timeoutMs === 1234)).toBe(true); }); it("decodes dns-sd octal escapes in TXT displayName", async () => { const run = vi.fn(async (argv: string[], options: { timeoutMs: number }) => { - if (options.timeoutMs < 0) throw new Error("invalid timeout"); + if (options.timeoutMs < 0) { + throw new Error("invalid timeout"); + } const domain = argv[3] ?? ""; if (argv[0] === "dns-sd" && argv[1] === "-B" && domain === "local.") { return { - stdout: ["Add 2 3 local. _moltbot-gw._tcp. Studio Gateway", ""].join("\n"), + stdout: ["Add 2 3 local. _openclaw-gw._tcp. Studio Gateway", ""].join("\n"), stderr: "", code: 0, signal: null, @@ -123,7 +125,7 @@ describe("bonjour-discovery", () => { if (argv[0] === "dns-sd" && argv[1] === "-L") { return { stdout: [ - "Studio Gateway._moltbot-gw._tcp. can be reached at studio.local:18789", + "Studio Gateway._openclaw-gw._tcp. can be reached at studio.local:18789", "txtvers=1 displayName=Peter\\226\\128\\153s\\032Mac\\032Studio lanHost=studio.local gatewayPort=18789 sshPort=22", "", ].join("\n"), @@ -164,8 +166,8 @@ describe("bonjour-discovery", () => { it("falls back to tailnet DNS probing for wide-area when split DNS is not configured", async () => { const calls: Array<{ argv: string[]; timeoutMs: number }> = []; - const zone = WIDE_AREA_DISCOVERY_DOMAIN.replace(/\.$/, ""); - const serviceBase = `_moltbot-gw._tcp.${zone}`; + const zone = WIDE_AREA_DOMAIN.replace(/\.$/, ""); + const serviceBase = `_openclaw-gw._tcp.${zone}`; const studioService = `studio-gateway.${serviceBase}`; const run = vi.fn(async (argv: string[], options: { timeoutMs: number }) => { @@ -231,7 +233,7 @@ describe("bonjour-discovery", () => { `"transport=gateway"`, `"sshPort=22"`, `"tailnetDns=peters-mac-studio-1.sheep-coho.ts.net"`, - `"cliPath=/opt/homebrew/bin/moltbot"`, + `"cliPath=/opt/homebrew/bin/openclaw"`, "", ].join(" "), stderr: "", @@ -248,13 +250,14 @@ describe("bonjour-discovery", () => { const beacons = await discoverGatewayBeacons({ platform: "darwin", timeoutMs: 1200, - domains: [WIDE_AREA_DISCOVERY_DOMAIN], + domains: [WIDE_AREA_DOMAIN], + wideAreaDomain: WIDE_AREA_DOMAIN, run: run as unknown as typeof runCommandWithTimeout, }); expect(beacons).toEqual([ expect.objectContaining({ - domain: WIDE_AREA_DISCOVERY_DOMAIN, + domain: WIDE_AREA_DOMAIN, instanceName: "studio-gateway", displayName: "Studio", host: `studio.${zone}`, @@ -262,7 +265,7 @@ describe("bonjour-discovery", () => { tailnetDns: "peters-mac-studio-1.sheep-coho.ts.net", gatewayPort: 18789, sshPort: 22, - cliPath: "/opt/homebrew/bin/moltbot", + cliPath: "/opt/homebrew/bin/openclaw", }), ]); @@ -286,12 +289,12 @@ describe("bonjour-discovery", () => { await discoverGatewayBeacons({ platform: "darwin", timeoutMs: 1, - domains: ["local", "moltbot.internal"], + domains: ["local", "openclaw.internal"], run: run as unknown as typeof runCommandWithTimeout, }); expect(calls.filter((c) => c[1] === "-B").map((c) => c[3])).toEqual( - expect.arrayContaining(["local.", "moltbot.internal."]), + expect.arrayContaining(["local.", "openclaw.internal."]), ); calls.length = 0; diff --git a/src/infra/bonjour-discovery.ts b/src/infra/bonjour-discovery.ts index e7777484f..f0ee29615 100644 --- a/src/infra/bonjour-discovery.ts +++ b/src/infra/bonjour-discovery.ts @@ -1,5 +1,5 @@ import { runCommandWithTimeout } from "../process/exec.js"; -import { WIDE_AREA_DISCOVERY_DOMAIN } from "./widearea-dns.js"; +import { resolveWideAreaDiscoveryDomain } from "./widearea-dns.js"; export type GatewayBonjourBeacon = { instanceName: string; @@ -22,13 +22,13 @@ export type GatewayBonjourBeacon = { export type GatewayBonjourDiscoverOpts = { timeoutMs?: number; domains?: string[]; + wideAreaDomain?: string | null; platform?: NodeJS.Platform; run?: typeof runCommandWithTimeout; }; const DEFAULT_TIMEOUT_MS = 2000; - -const DEFAULT_DOMAINS = ["local.", WIDE_AREA_DISCOVERY_DOMAIN] as const; +const GATEWAY_SERVICE_TYPE = "_openclaw-gw._tcp"; function decodeDnsSdEscapes(value: string): string { let decoded = false; @@ -36,7 +36,9 @@ function decodeDnsSdEscapes(value: string): string { let pending = ""; const flush = () => { - if (!pending) return; + if (!pending) { + return; + } bytes.push(...Buffer.from(pending, "utf8")); pending = ""; }; @@ -61,16 +63,22 @@ function decodeDnsSdEscapes(value: string): string { pending += ch; } - if (!decoded) return value; + if (!decoded) { + return value; + } flush(); return Buffer.from(bytes).toString("utf8"); } function isTailnetIPv4(address: string): boolean { const parts = address.split("."); - if (parts.length !== 4) return false; + if (parts.length !== 4) { + return false; + } const octets = parts.map((p) => Number.parseInt(p, 10)); - if (octets.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) return false; + if (octets.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) { + return false; + } // Tailscale IPv4 range: 100.64.0.0/10 const [a, b] = octets; return a === 100 && b >= 64 && b <= 127; @@ -89,7 +97,9 @@ function parseDigTxt(stdout: string): string[] { const tokens: string[] = []; for (const raw of stdout.split("\n")) { const line = raw.trim(); - if (!line) continue; + if (!line) { + continue; + } const matches = Array.from(line.matchAll(/"([^"]*)"/g), (m) => m[1] ?? ""); for (const m of matches) { const unescaped = m.replaceAll("\\\\", "\\").replaceAll('\\"', '"').replaceAll("\\n", "\n"); @@ -105,14 +115,22 @@ function parseDigSrv(stdout: string): { host: string; port: number } | null { .split("\n") .map((l) => l.trim()) .find(Boolean); - if (!line) return null; + if (!line) { + return null; + } const parts = line.split(/\s+/).filter(Boolean); - if (parts.length < 4) return null; + if (parts.length < 4) { + return null; + } const port = Number.parseInt(parts[2] ?? "", 10); const hostRaw = parts[3] ?? ""; - if (!Number.isFinite(port) || port <= 0) return null; + if (!Number.isFinite(port) || port <= 0) { + return null; + } const host = hostRaw.replace(/\.$/, ""); - if (!host) return null; + if (!host) { + return null; + } return { host, port }; } @@ -121,13 +139,21 @@ function parseTailscaleStatusIPv4s(stdout: string): string[] { const out: string[] = []; const addIps = (value: unknown) => { - if (!value || typeof value !== "object") return; + if (!value || typeof value !== "object") { + return; + } const ips = (value as { TailscaleIPs?: unknown }).TailscaleIPs; - if (!Array.isArray(ips)) return; + if (!Array.isArray(ips)) { + return; + } for (const ip of ips) { - if (typeof ip !== "string") continue; + if (typeof ip !== "string") { + continue; + } const trimmed = ip.trim(); - if (trimmed && isTailnetIPv4(trimmed)) out.push(trimmed); + if (trimmed && isTailnetIPv4(trimmed)) { + out.push(trimmed); + } } }; @@ -144,7 +170,9 @@ function parseTailscaleStatusIPv4s(stdout: string): string[] { } function parseIntOrNull(value: string | undefined): number | undefined { - if (!value) return undefined; + if (!value) { + return undefined; + } const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : undefined; } @@ -153,10 +181,14 @@ function parseTxtTokens(tokens: string[]): Record { const txt: Record = {}; for (const token of tokens) { const idx = token.indexOf("="); - if (idx <= 0) continue; + if (idx <= 0) { + continue; + } const key = token.slice(0, idx).trim(); const value = decodeDnsSdEscapes(token.slice(idx + 1).trim()); - if (!key) continue; + if (!key) { + continue; + } txt[key] = value; } return txt; @@ -166,9 +198,13 @@ function parseDnsSdBrowse(stdout: string): string[] { const instances = new Set(); for (const raw of stdout.split("\n")) { const line = raw.trim(); - if (!line || !line.includes("_moltbot-gw._tcp")) continue; - if (!line.includes("Add")) continue; - const match = line.match(/_moltbot-gw\._tcp\.?\s+(.+)$/); + if (!line || !line.includes(GATEWAY_SERVICE_TYPE)) { + continue; + } + if (!line.includes("Add")) { + continue; + } + const match = line.match(/_openclaw-gw\._tcp\.?\s+(.+)$/); if (match?.[1]) { instances.add(decodeDnsSdEscapes(match[1].trim())); } @@ -182,7 +218,9 @@ function parseDnsSdResolve(stdout: string, instanceName: string): GatewayBonjour let txt: Record = {}; for (const raw of stdout.split("\n")) { const line = raw.trim(); - if (!line) continue; + if (!line) { + continue; + } if (line.includes("can be reached at")) { const match = line.match(/can be reached at\s+([^\s:]+):(\d+)/i); @@ -202,21 +240,37 @@ function parseDnsSdResolve(stdout: string, instanceName: string): GatewayBonjour } beacon.txt = Object.keys(txt).length ? txt : undefined; - if (txt.displayName) beacon.displayName = decodeDnsSdEscapes(txt.displayName); - if (txt.lanHost) beacon.lanHost = txt.lanHost; - if (txt.tailnetDns) beacon.tailnetDns = txt.tailnetDns; - if (txt.cliPath) beacon.cliPath = txt.cliPath; + if (txt.displayName) { + beacon.displayName = decodeDnsSdEscapes(txt.displayName); + } + if (txt.lanHost) { + beacon.lanHost = txt.lanHost; + } + if (txt.tailnetDns) { + beacon.tailnetDns = txt.tailnetDns; + } + if (txt.cliPath) { + beacon.cliPath = txt.cliPath; + } beacon.gatewayPort = parseIntOrNull(txt.gatewayPort); beacon.sshPort = parseIntOrNull(txt.sshPort); if (txt.gatewayTls) { const raw = txt.gatewayTls.trim().toLowerCase(); beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes"; } - if (txt.gatewayTlsSha256) beacon.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256; - if (txt.role) beacon.role = txt.role; - if (txt.transport) beacon.transport = txt.transport; + if (txt.gatewayTlsSha256) { + beacon.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256; + } + if (txt.role) { + beacon.role = txt.role; + } + if (txt.transport) { + beacon.transport = txt.transport; + } - if (!beacon.displayName) beacon.displayName = decodedInstanceName; + if (!beacon.displayName) { + beacon.displayName = decodedInstanceName; + } return beacon; } @@ -225,17 +279,19 @@ async function discoverViaDnsSd( timeoutMs: number, run: typeof runCommandWithTimeout, ): Promise { - const browse = await run(["dns-sd", "-B", "_moltbot-gw._tcp", domain], { + const browse = await run(["dns-sd", "-B", GATEWAY_SERVICE_TYPE, domain], { timeoutMs, }); const instances = parseDnsSdBrowse(browse.stdout); const results: GatewayBonjourBeacon[] = []; for (const instance of instances) { - const resolved = await run(["dns-sd", "-L", instance, "_moltbot-gw._tcp", domain], { + const resolved = await run(["dns-sd", "-L", instance, GATEWAY_SERVICE_TYPE, domain], { timeoutMs, }); const parsed = parseDnsSdResolve(resolved.stdout, instance); - if (parsed) results.push({ ...parsed, domain }); + if (parsed) { + results.push({ ...parsed, domain }); + } } return results; } @@ -245,7 +301,9 @@ async function discoverWideAreaViaTailnetDns( timeoutMs: number, run: typeof runCommandWithTimeout, ): Promise { - if (domain !== WIDE_AREA_DISCOVERY_DOMAIN) return []; + if (!domain || domain === "local.") { + return []; + } const startedAt = Date.now(); const remainingMs = () => timeoutMs - (Date.now() - startedAt); @@ -257,18 +315,24 @@ async function discoverWideAreaViaTailnetDns( timeoutMs: Math.max(1, Math.min(700, remainingMs())), }); ips = parseTailscaleStatusIPv4s(res.stdout); - if (ips.length > 0) break; + if (ips.length > 0) { + break; + } } catch { // ignore } } - if (ips.length === 0) return []; - if (remainingMs() <= 0) return []; + if (ips.length === 0) { + return []; + } + if (remainingMs() <= 0) { + return []; + } // Keep scans bounded: this is a fallback and should not block long. ips = ips.slice(0, 40); - const probeName = `_moltbot-gw._tcp.${domain.replace(/\.$/, "")}`; + const probeName = `${GATEWAY_SERVICE_TYPE}.${domain.replace(/\.$/, "")}`; const concurrency = 6; let nextIndex = 0; @@ -278,19 +342,27 @@ async function discoverWideAreaViaTailnetDns( const worker = async () => { while (nameserver === null) { const budget = remainingMs(); - if (budget <= 0) return; + if (budget <= 0) { + return; + } const i = nextIndex; nextIndex += 1; - if (i >= ips.length) return; + if (i >= ips.length) { + return; + } const ip = ips[i] ?? ""; - if (!ip) continue; + if (!ip) { + continue; + } try { const probe = await run( ["dig", "+short", "+time=1", "+tries=1", `@${ip}`, probeName, "PTR"], { timeoutMs: Math.max(1, Math.min(250, budget)) }, ); const lines = parseDigShortLines(probe.stdout); - if (lines.length === 0) continue; + if (lines.length === 0) { + continue; + } nameserver = ip; ptrs = lines; return; @@ -302,23 +374,33 @@ async function discoverWideAreaViaTailnetDns( await Promise.all(Array.from({ length: Math.min(concurrency, ips.length) }, () => worker())); - if (!nameserver || ptrs.length === 0) return []; - if (remainingMs() <= 0) return []; + if (!nameserver || ptrs.length === 0) { + return []; + } + if (remainingMs() <= 0) { + return []; + } const nameserverArg = `@${String(nameserver)}`; const results: GatewayBonjourBeacon[] = []; for (const ptr of ptrs) { const budget = remainingMs(); - if (budget <= 0) break; + if (budget <= 0) { + break; + } const ptrName = ptr.trim().replace(/\.$/, ""); - if (!ptrName) continue; - const instanceName = ptrName.replace(/\.?_moltbot-gw\._tcp\..*$/, ""); + if (!ptrName) { + continue; + } + const instanceName = ptrName.replace(/\.?_openclaw-gw\._tcp\..*$/, ""); const srv = await run(["dig", "+short", "+time=1", "+tries=1", nameserverArg, ptrName, "SRV"], { timeoutMs: Math.max(1, Math.min(350, budget)), }).catch(() => null); const srvParsed = srv ? parseDigSrv(srv.stdout) : null; - if (!srvParsed) continue; + if (!srvParsed) { + continue; + } const txtBudget = remainingMs(); if (txtBudget <= 0) { @@ -354,9 +436,15 @@ async function discoverWideAreaViaTailnetDns( const raw = txtMap.gatewayTls.trim().toLowerCase(); beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes"; } - if (txtMap.gatewayTlsSha256) beacon.gatewayTlsFingerprintSha256 = txtMap.gatewayTlsSha256; - if (txtMap.role) beacon.role = txtMap.role; - if (txtMap.transport) beacon.transport = txtMap.transport; + if (txtMap.gatewayTlsSha256) { + beacon.gatewayTlsFingerprintSha256 = txtMap.gatewayTlsSha256; + } + if (txtMap.role) { + beacon.role = txtMap.role; + } + if (txtMap.transport) { + beacon.transport = txtMap.transport; + } results.push(beacon); } @@ -370,10 +458,14 @@ function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] { for (const raw of stdout.split("\n")) { const line = raw.trimEnd(); - if (!line) continue; - if (line.startsWith("=") && line.includes("_moltbot-gw._tcp")) { - if (current) results.push(current); - const marker = " _moltbot-gw._tcp"; + if (!line) { + continue; + } + if (line.startsWith("=") && line.includes(GATEWAY_SERVICE_TYPE)) { + if (current) { + results.push(current); + } + const marker = ` ${GATEWAY_SERVICE_TYPE}`; const idx = line.indexOf(marker); const left = idx >= 0 ? line.slice(0, idx).trim() : line; const parts = left.split(/\s+/); @@ -385,18 +477,24 @@ function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] { continue; } - if (!current) continue; + if (!current) { + continue; + } const trimmed = line.trim(); if (trimmed.startsWith("hostname =")) { const match = trimmed.match(/hostname\s*=\s*\[([^\]]+)\]/); - if (match?.[1]) current.host = match[1]; + if (match?.[1]) { + current.host = match[1]; + } continue; } if (trimmed.startsWith("port =")) { const match = trimmed.match(/port\s*=\s*\[(\d+)\]/); - if (match?.[1]) current.port = parseIntOrNull(match[1]); + if (match?.[1]) { + current.port = parseIntOrNull(match[1]); + } continue; } @@ -404,23 +502,39 @@ function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] { const tokens = Array.from(trimmed.matchAll(/"([^"]*)"/g), (m) => m[1]); const txt = parseTxtTokens(tokens); current.txt = Object.keys(txt).length ? txt : undefined; - if (txt.displayName) current.displayName = txt.displayName; - if (txt.lanHost) current.lanHost = txt.lanHost; - if (txt.tailnetDns) current.tailnetDns = txt.tailnetDns; - if (txt.cliPath) current.cliPath = txt.cliPath; + if (txt.displayName) { + current.displayName = txt.displayName; + } + if (txt.lanHost) { + current.lanHost = txt.lanHost; + } + if (txt.tailnetDns) { + current.tailnetDns = txt.tailnetDns; + } + if (txt.cliPath) { + current.cliPath = txt.cliPath; + } current.gatewayPort = parseIntOrNull(txt.gatewayPort); current.sshPort = parseIntOrNull(txt.sshPort); if (txt.gatewayTls) { const raw = txt.gatewayTls.trim().toLowerCase(); current.gatewayTls = raw === "1" || raw === "true" || raw === "yes"; } - if (txt.gatewayTlsSha256) current.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256; - if (txt.role) current.role = txt.role; - if (txt.transport) current.transport = txt.transport; + if (txt.gatewayTlsSha256) { + current.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256; + } + if (txt.role) { + current.role = txt.role; + } + if (txt.transport) { + current.transport = txt.transport; + } } } - if (current) results.push(current); + if (current) { + results.push(current); + } return results; } @@ -429,7 +543,7 @@ async function discoverViaAvahi( timeoutMs: number, run: typeof runCommandWithTimeout, ): Promise { - const args = ["avahi-browse", "-rt", "_moltbot-gw._tcp"]; + const args = ["avahi-browse", "-rt", GATEWAY_SERVICE_TYPE]; if (domain && domain !== "local.") { // avahi-browse wants a plain domain (no trailing dot) args.push("-d", domain.replace(/\.$/, "")); @@ -447,8 +561,10 @@ export async function discoverGatewayBeacons( const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; const platform = opts.platform ?? process.platform; const run = opts.run ?? runCommandWithTimeout; + const wideAreaDomain = resolveWideAreaDiscoveryDomain({ configDomain: opts.wideAreaDomain }); const domainsRaw = Array.isArray(opts.domains) ? opts.domains : []; - const domains = (domainsRaw.length > 0 ? domainsRaw : [...DEFAULT_DOMAINS]) + const defaultDomains = ["local.", ...(wideAreaDomain ? [wideAreaDomain] : [])]; + const domains = (domainsRaw.length > 0 ? domainsRaw : defaultDomains) .map((d) => String(d).trim()) .filter(Boolean) .map((d) => (d.endsWith(".") ? d : `${d}.`)); @@ -460,15 +576,15 @@ export async function discoverGatewayBeacons( ); const discovered = perDomain.flatMap((r) => (r.status === "fulfilled" ? r.value : [])); - const wantsWideArea = domains.includes(WIDE_AREA_DISCOVERY_DOMAIN); - const hasWideArea = discovered.some((b) => b.domain === WIDE_AREA_DISCOVERY_DOMAIN); + const wantsWideArea = wideAreaDomain ? domains.includes(wideAreaDomain) : false; + const hasWideArea = wideAreaDomain + ? discovered.some((b) => b.domain === wideAreaDomain) + : false; - if (wantsWideArea && !hasWideArea) { - const fallback = await discoverWideAreaViaTailnetDns( - WIDE_AREA_DISCOVERY_DOMAIN, - timeoutMs, - run, - ).catch(() => []); + if (wantsWideArea && !hasWideArea && wideAreaDomain) { + const fallback = await discoverWideAreaViaTailnetDns(wideAreaDomain, timeoutMs, run).catch( + () => [], + ); return [...discovered, ...fallback]; } diff --git a/src/infra/bonjour.test.ts b/src/infra/bonjour.test.ts index ef93f7ffb..a9320e021 100644 --- a/src/infra/bonjour.test.ts +++ b/src/infra/bonjour.test.ts @@ -1,7 +1,5 @@ import os from "node:os"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import * as logging from "../logging.js"; const mocks = vi.hoisted(() => ({ @@ -65,7 +63,9 @@ describe("gateway bonjour advertiser", () => { afterEach(() => { for (const key of Object.keys(process.env)) { - if (!(key in prevEnv)) delete process.env[key]; + if (!(key in prevEnv)) { + delete process.env[key]; + } } for (const [key, value] of Object.entries(prevEnv)) { process.env[key] = value; @@ -86,6 +86,8 @@ describe("gateway bonjour advertiser", () => { process.env.NODE_ENV = "development"; vi.spyOn(os, "hostname").mockReturnValue("test-host"); + process.env.OPENCLAW_MDNS_HOSTNAME = "test-host"; + process.env.OPENCLAW_MDNS_HOSTNAME = "test-host"; const destroy = vi.fn().mockResolvedValue(undefined); const advertise = vi.fn().mockImplementation( @@ -111,12 +113,12 @@ describe("gateway bonjour advertiser", () => { gatewayPort: 18789, sshPort: 2222, tailnetDns: "host.tailnet.ts.net", - cliPath: "/opt/homebrew/bin/moltbot", + cliPath: "/opt/homebrew/bin/openclaw", }); expect(createService).toHaveBeenCalledTimes(1); const [gatewayCall] = createService.mock.calls as Array<[Record]>; - expect(gatewayCall?.[0]?.type).toBe("moltbot-gw"); + expect(gatewayCall?.[0]?.type).toBe("openclaw-gw"); const gatewayType = asString(gatewayCall?.[0]?.type, ""); expect(gatewayType.length).toBeLessThanOrEqual(15); expect(gatewayCall?.[0]?.port).toBe(18789); @@ -126,7 +128,7 @@ describe("gateway bonjour advertiser", () => { expect((gatewayCall?.[0]?.txt as Record)?.gatewayPort).toBe("18789"); expect((gatewayCall?.[0]?.txt as Record)?.sshPort).toBe("2222"); expect((gatewayCall?.[0]?.txt as Record)?.cliPath).toBe( - "/opt/homebrew/bin/moltbot", + "/opt/homebrew/bin/openclaw", ); expect((gatewayCall?.[0]?.txt as Record)?.transport).toBe("gateway"); @@ -163,7 +165,7 @@ describe("gateway bonjour advertiser", () => { const started = await startGatewayBonjourAdvertiser({ gatewayPort: 18789, sshPort: 2222, - cliPath: "/opt/homebrew/bin/moltbot", + cliPath: "/opt/homebrew/bin/openclaw", minimal: true, }); @@ -180,6 +182,7 @@ describe("gateway bonjour advertiser", () => { process.env.NODE_ENV = "development"; vi.spyOn(os, "hostname").mockReturnValue("test-host"); + process.env.OPENCLAW_MDNS_HOSTNAME = "test-host"; const destroy = vi.fn().mockResolvedValue(undefined); const advertise = vi.fn().mockResolvedValue(undefined); @@ -217,6 +220,7 @@ describe("gateway bonjour advertiser", () => { process.env.NODE_ENV = "development"; vi.spyOn(os, "hostname").mockReturnValue("test-host"); + process.env.OPENCLAW_MDNS_HOSTNAME = "test-host"; const destroy = vi.fn().mockResolvedValue(undefined); const advertise = vi.fn().mockResolvedValue(undefined); @@ -261,6 +265,7 @@ describe("gateway bonjour advertiser", () => { vi.useFakeTimers(); vi.spyOn(os, "hostname").mockReturnValue("test-host"); + process.env.OPENCLAW_MDNS_HOSTNAME = "test-host"; const destroy = vi.fn().mockResolvedValue(undefined); const advertise = vi @@ -308,6 +313,7 @@ describe("gateway bonjour advertiser", () => { process.env.NODE_ENV = "development"; vi.spyOn(os, "hostname").mockReturnValue("test-host"); + process.env.OPENCLAW_MDNS_HOSTNAME = "test-host"; const destroy = vi.fn().mockResolvedValue(undefined); const advertise = vi.fn(() => { @@ -364,10 +370,10 @@ describe("gateway bonjour advertiser", () => { }); const [gatewayCall] = createService.mock.calls as Array<[ServiceCall]>; - expect(gatewayCall?.[0]?.name).toBe("Mac (Moltbot)"); + expect(gatewayCall?.[0]?.name).toBe("openclaw (OpenClaw)"); expect(gatewayCall?.[0]?.domain).toBe("local"); - expect(gatewayCall?.[0]?.hostname).toBe("Mac"); - expect((gatewayCall?.[0]?.txt as Record)?.lanHost).toBe("Mac.local"); + expect(gatewayCall?.[0]?.hostname).toBe("openclaw"); + expect((gatewayCall?.[0]?.txt as Record)?.lanHost).toBe("openclaw.local"); await started.stop(); }); diff --git a/src/infra/bonjour.ts b/src/infra/bonjour.ts index cc0d50c0e..7d405741a 100644 --- a/src/infra/bonjour.ts +++ b/src/infra/bonjour.ts @@ -1,5 +1,3 @@ -import os from "node:os"; - import { logDebug, logWarn } from "../logger.js"; import { getLogger } from "../logging.js"; import { ignoreCiaoCancellationRejection } from "./bonjour-ciao.js"; @@ -28,20 +26,26 @@ export type GatewayBonjourAdvertiseOpts = { }; function isDisabledByEnv() { - if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_BONJOUR)) return true; - if (process.env.NODE_ENV === "test") return true; - if (process.env.VITEST) return true; + if (isTruthyEnvValue(process.env.OPENCLAW_DISABLE_BONJOUR)) { + return true; + } + if (process.env.NODE_ENV === "test") { + return true; + } + if (process.env.VITEST) { + return true; + } return false; } function safeServiceName(name: string) { const trimmed = name.trim(); - return trimmed.length > 0 ? trimmed : "Moltbot"; + return trimmed.length > 0 ? trimmed : "OpenClaw"; } function prettifyInstanceName(name: string) { const normalized = name.trim().replace(/\s+/g, " "); - return normalized.replace(/\s+\(Moltbot\)\s*$/i, "").trim() || normalized; + return normalized.replace(/\s+\(OpenClaw\)\s*$/i, "").trim() || normalized; } type BonjourService = { @@ -90,16 +94,19 @@ export async function startGatewayBonjourAdvertiser( // mDNS service instance names are single DNS labels; dots in hostnames (like // `Mac.localdomain`) can confuse some resolvers/browsers and break discovery. // Keep only the first label and normalize away a trailing `.local`. + const hostnameRaw = + process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || + process.env.CLAWDBOT_MDNS_HOSTNAME?.trim() || + "openclaw"; const hostname = - os - .hostname() + hostnameRaw .replace(/\.local$/i, "") .split(".")[0] - .trim() || "moltbot"; + .trim() || "openclaw"; const instanceName = typeof opts.instanceName === "string" && opts.instanceName.trim() ? opts.instanceName.trim() - : `${hostname} (Moltbot)`; + : `${hostname} (OpenClaw)`; const displayName = prettifyInstanceName(instanceName); const txtBase: Record = { @@ -140,7 +147,7 @@ export async function startGatewayBonjourAdvertiser( const gateway = responder.createService({ name: safeServiceName(instanceName), - type: "moltbot-gw", + type: "openclaw-gw", protocol: Protocol.TCP, port: opts.gatewayPort, domain: "local", @@ -211,8 +218,12 @@ export async function startGatewayBonjourAdvertiser( const watchdog = setInterval(() => { for (const { label, svc } of services) { const stateUnknown = (svc as { serviceState?: unknown }).serviceState; - if (typeof stateUnknown !== "string") continue; - if (stateUnknown === "announced" || stateUnknown === "announcing") continue; + if (typeof stateUnknown !== "string") { + continue; + } + if (stateUnknown === "announced" || stateUnknown === "announcing") { + continue; + } let key = label; try { @@ -222,7 +233,9 @@ export async function startGatewayBonjourAdvertiser( } const now = Date.now(); const last = lastRepairAttempt.get(key) ?? 0; - if (now - last < 30_000) continue; + if (now - last < 30_000) { + continue; + } lastRepairAttempt.set(key, now); logWarn( diff --git a/src/infra/brew.test.ts b/src/infra/brew.test.ts index 5b09a2367..87e34a3a9 100644 --- a/src/infra/brew.test.ts +++ b/src/infra/brew.test.ts @@ -1,14 +1,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { resolveBrewExecutable, resolveBrewPathDirs } from "./brew.js"; describe("brew helpers", () => { it("resolves brew from ~/.linuxbrew/bin when executable exists", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-brew-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-brew-")); try { const homebrewBin = path.join(tmp, ".linuxbrew", "bin"); await fs.mkdir(homebrewBin, { recursive: true }); @@ -24,7 +22,7 @@ describe("brew helpers", () => { }); it("prefers HOMEBREW_PREFIX/bin/brew when present", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-brew-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-brew-")); try { const prefix = path.join(tmp, "prefix"); const prefixBin = path.join(prefix, "bin"); diff --git a/src/infra/brew.ts b/src/infra/brew.ts index 9155729c2..0b600ee7c 100644 --- a/src/infra/brew.ts +++ b/src/infra/brew.ts @@ -12,7 +12,9 @@ function isExecutable(filePath: string): boolean { } function normalizePathValue(value: unknown): string | undefined { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const trimmed = value.trim(); return trimmed ? trimmed : undefined; } @@ -51,10 +53,14 @@ export function resolveBrewExecutable(opts?: { const candidates: string[] = []; const brewFile = normalizePathValue(env.HOMEBREW_BREW_FILE); - if (brewFile) candidates.push(brewFile); + if (brewFile) { + candidates.push(brewFile); + } const prefix = normalizePathValue(env.HOMEBREW_PREFIX); - if (prefix) candidates.push(path.join(prefix, "bin", "brew")); + if (prefix) { + candidates.push(path.join(prefix, "bin", "brew")); + } // Linuxbrew defaults. candidates.push(path.join(homeDir, ".linuxbrew", "bin", "brew")); @@ -64,7 +70,9 @@ export function resolveBrewExecutable(opts?: { candidates.push("/opt/homebrew/bin/brew", "/usr/local/bin/brew"); for (const candidate of candidates) { - if (isExecutable(candidate)) return candidate; + if (isExecutable(candidate)) { + return candidate; + } } return undefined; diff --git a/src/infra/canvas-host-url.ts b/src/infra/canvas-host-url.ts index 47fc50742..fe537bb8e 100644 --- a/src/infra/canvas-host-url.ts +++ b/src/infra/canvas-host-url.ts @@ -11,23 +11,39 @@ type CanvasHostUrlParams = { const isLoopbackHost = (value: string) => { const normalized = value.trim().toLowerCase(); - if (!normalized) return false; - if (normalized === "localhost") return true; - if (normalized === "::1") return true; - if (normalized === "0.0.0.0" || normalized === "::") return true; + if (!normalized) { + return false; + } + if (normalized === "localhost") { + return true; + } + if (normalized === "::1") { + return true; + } + if (normalized === "0.0.0.0" || normalized === "::") { + return true; + } return normalized.startsWith("127."); }; const normalizeHost = (value: HostSource, rejectLoopback: boolean) => { - if (!value) return ""; + if (!value) { + return ""; + } const trimmed = value.trim(); - if (!trimmed) return ""; - if (rejectLoopback && isLoopbackHost(trimmed)) return ""; + if (!trimmed) { + return ""; + } + if (rejectLoopback && isLoopbackHost(trimmed)) { + return ""; + } return trimmed; }; const parseHostHeader = (value: HostSource) => { - if (!value) return ""; + if (!value) { + return ""; + } try { return new URL(`http://${String(value).trim()}`).hostname; } catch { @@ -36,13 +52,17 @@ const parseHostHeader = (value: HostSource) => { }; const parseForwardedProto = (value: HostSource | HostSource[]) => { - if (Array.isArray(value)) return value[0]; + if (Array.isArray(value)) { + return value[0]; + } return value; }; export function resolveCanvasHostUrl(params: CanvasHostUrlParams) { const port = params.canvasPort; - if (!port) return undefined; + if (!port) { + return undefined; + } const scheme = params.scheme ?? @@ -53,7 +73,9 @@ export function resolveCanvasHostUrl(params: CanvasHostUrlParams) { const localAddress = normalizeHost(params.localAddress, Boolean(override || requestHost)); const host = override || requestHost || localAddress; - if (!host) return undefined; + if (!host) { + return undefined; + } const formatted = host.includes(":") ? `[${host}]` : host; return `${scheme}://${formatted}:${port}`; } diff --git a/src/infra/channel-activity.test.ts b/src/infra/channel-activity.test.ts index fa84c1b24..a12d47bfb 100644 --- a/src/infra/channel-activity.test.ts +++ b/src/infra/channel-activity.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getChannelActivity, recordChannelActivity, diff --git a/src/infra/channel-activity.ts b/src/infra/channel-activity.ts index e56757256..f1365548a 100644 --- a/src/infra/channel-activity.ts +++ b/src/infra/channel-activity.ts @@ -15,7 +15,9 @@ function keyFor(channel: ChannelId, accountId: string) { function ensureEntry(channel: ChannelId, accountId: string): ActivityEntry { const key = keyFor(channel, accountId); const existing = activity.get(key); - if (existing) return existing; + if (existing) { + return existing; + } const created: ActivityEntry = { inboundAt: null, outboundAt: null }; activity.set(key, created); return created; @@ -30,8 +32,12 @@ export function recordChannelActivity(params: { const at = typeof params.at === "number" ? params.at : Date.now(); const accountId = params.accountId?.trim() || "default"; const entry = ensureEntry(params.channel, accountId); - if (params.direction === "inbound") entry.inboundAt = at; - if (params.direction === "outbound") entry.outboundAt = at; + if (params.direction === "inbound") { + entry.inboundAt = at; + } + if (params.direction === "outbound") { + entry.outboundAt = at; + } } export function getChannelActivity(params: { diff --git a/src/infra/channel-summary.ts b/src/infra/channel-summary.ts index 830d8b9be..d95a3adfe 100644 --- a/src/infra/channel-summary.ts +++ b/src/infra/channel-summary.ts @@ -1,6 +1,6 @@ -import { listChannelPlugins } from "../channels/plugins/index.js"; import type { ChannelAccountSnapshot, ChannelPlugin } from "../channels/plugins/types.js"; -import { type MoltbotConfig, loadConfig } from "../config/config.js"; +import { listChannelPlugins } from "../channels/plugins/index.js"; +import { type OpenClawConfig, loadConfig } from "../config/config.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { theme } from "../terminal/theme.js"; @@ -24,7 +24,9 @@ type ChannelAccountEntry = { const formatAccountLabel = (params: { accountId: string; name?: string }) => { const base = params.accountId || DEFAULT_ACCOUNT_ID; - if (params.name?.trim()) return `${base} (${params.name.trim()})`; + if (params.name?.trim()) { + return `${base} (${params.name.trim()})`; + } return base; }; @@ -34,12 +36,14 @@ const accountLine = (label: string, details: string[]) => const resolveAccountEnabled = ( plugin: ChannelPlugin, account: unknown, - cfg: MoltbotConfig, + cfg: OpenClawConfig, ): boolean => { if (plugin.config.isEnabled) { return plugin.config.isEnabled(account, cfg); } - if (!account || typeof account !== "object") return true; + if (!account || typeof account !== "object") { + return true; + } const enabled = (account as { enabled?: boolean }).enabled; return enabled !== false; }; @@ -47,7 +51,7 @@ const resolveAccountEnabled = ( const resolveAccountConfigured = async ( plugin: ChannelPlugin, account: unknown, - cfg: MoltbotConfig, + cfg: OpenClawConfig, ): Promise => { if (plugin.config.isConfigured) { return await plugin.config.isConfigured(account, cfg); @@ -58,7 +62,7 @@ const resolveAccountConfigured = async ( const buildAccountSnapshot = (params: { plugin: ChannelPlugin; account: unknown; - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId: string; enabled: boolean; configured: boolean; @@ -76,7 +80,7 @@ const buildAccountSnapshot = (params: { const formatAllowFrom = (params: { plugin: ChannelPlugin; - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; allowFrom: Array; }) => { @@ -93,13 +97,17 @@ const formatAllowFrom = (params: { const buildAccountDetails = (params: { entry: ChannelAccountEntry; plugin: ChannelPlugin; - cfg: MoltbotConfig; + cfg: OpenClawConfig; includeAllowFrom: boolean; }): string[] => { const details: string[] = []; const snapshot = params.entry.snapshot; - if (snapshot.enabled === false) details.push("disabled"); - if (snapshot.dmPolicy) details.push(`dm:${snapshot.dmPolicy}`); + if (snapshot.enabled === false) { + details.push("disabled"); + } + if (snapshot.dmPolicy) { + details.push(`dm:${snapshot.dmPolicy}`); + } if (snapshot.tokenSource && snapshot.tokenSource !== "none") { details.push(`token:${snapshot.tokenSource}`); } @@ -109,10 +117,18 @@ const buildAccountDetails = (params: { if (snapshot.appTokenSource && snapshot.appTokenSource !== "none") { details.push(`app:${snapshot.appTokenSource}`); } - if (snapshot.baseUrl) details.push(snapshot.baseUrl); - if (snapshot.port != null) details.push(`port:${snapshot.port}`); - if (snapshot.cliPath) details.push(`cli:${snapshot.cliPath}`); - if (snapshot.dbPath) details.push(`db:${snapshot.dbPath}`); + if (snapshot.baseUrl) { + details.push(snapshot.baseUrl); + } + if (snapshot.port != null) { + details.push(`port:${snapshot.port}`); + } + if (snapshot.cliPath) { + details.push(`cli:${snapshot.cliPath}`); + } + if (snapshot.dbPath) { + details.push(`db:${snapshot.dbPath}`); + } if (params.includeAllowFrom && snapshot.allowFrom?.length) { const formatted = formatAllowFrom({ @@ -129,7 +145,7 @@ const buildAccountDetails = (params: { }; export async function buildChannelSummary( - cfg?: MoltbotConfig, + cfg?: OpenClawConfig, options?: ChannelSummaryOptions, ): Promise { const effective = cfg ?? loadConfig(); @@ -174,7 +190,7 @@ export async function buildChannelSummary( }) : undefined; - const summaryRecord = summary as Record | undefined; + const summaryRecord = summary; const linked = summaryRecord && typeof summaryRecord.linked === "boolean" ? summaryRecord.linked : null; const configured = @@ -204,7 +220,9 @@ export async function buildChannelSummary( const authAgeMs = summaryRecord && typeof summaryRecord.authAgeMs === "number" ? summaryRecord.authAgeMs : null; const self = summaryRecord?.self as { e164?: string | null } | undefined; - if (self?.e164) line += ` ${self.e164}`; + if (self?.e164) { + line += ` ${self.e164}`; + } if (authAgeMs != null && authAgeMs >= 0) { line += ` auth ${formatAge(authAgeMs)}`; } @@ -236,12 +254,20 @@ export async function buildChannelSummary( } export function formatAge(ms: number): string { - if (ms < 0) return "unknown"; + if (ms < 0) { + return "unknown"; + } const minutes = Math.round(ms / 60_000); - if (minutes < 1) return "just now"; - if (minutes < 60) return `${minutes}m ago`; + if (minutes < 1) { + return "just now"; + } + if (minutes < 60) { + return `${minutes}m ago`; + } const hours = Math.round(minutes / 60); - if (hours < 48) return `${hours}h ago`; + if (hours < 48) { + return `${hours}h ago`; + } const days = Math.round(hours / 24); return `${days}d ago`; } diff --git a/src/infra/channels-status-issues.ts b/src/infra/channels-status-issues.ts index 61ed9d76e..b5e5a610b 100644 --- a/src/infra/channels-status-issues.ts +++ b/src/infra/channels-status-issues.ts @@ -1,14 +1,18 @@ -import { listChannelPlugins } from "../channels/plugins/index.js"; import type { ChannelAccountSnapshot, ChannelStatusIssue } from "../channels/plugins/types.js"; +import { listChannelPlugins } from "../channels/plugins/index.js"; export function collectChannelStatusIssues(payload: Record): ChannelStatusIssue[] { const issues: ChannelStatusIssue[] = []; const accountsByChannel = payload.channelAccounts as Record | undefined; for (const plugin of listChannelPlugins()) { const collect = plugin.status?.collectStatusIssues; - if (!collect) continue; + if (!collect) { + continue; + } const raw = accountsByChannel?.[plugin.id]; - if (!Array.isArray(raw)) continue; + if (!Array.isArray(raw)) { + continue; + } issues.push(...collect(raw as ChannelAccountSnapshot[])); } diff --git a/src/infra/clipboard.ts b/src/infra/clipboard.ts index dd8aaf510..c7daebf22 100644 --- a/src/infra/clipboard.ts +++ b/src/infra/clipboard.ts @@ -14,7 +14,9 @@ export async function copyToClipboard(value: string): Promise { timeoutMs: 3_000, input: value, }); - if (result.code === 0 && !result.killed) return true; + if (result.code === 0 && !result.killed) { + return true; + } } catch { // keep trying the next fallback } diff --git a/src/infra/control-ui-assets.test.ts b/src/infra/control-ui-assets.test.ts index b64c4c685..1936e5af6 100644 --- a/src/infra/control-ui-assets.test.ts +++ b/src/infra/control-ui-assets.test.ts @@ -1,14 +1,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { resolveControlUiDistIndexPath, resolveControlUiRepoRoot } from "./control-ui-assets.js"; describe("control UI assets helpers", () => { it("resolves repo root from src argv1", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-ui-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-")); try { await fs.mkdir(path.join(tmp, "ui"), { recursive: true }); await fs.writeFile(path.join(tmp, "ui", "vite.config.ts"), "export {};\n"); @@ -23,7 +21,7 @@ describe("control UI assets helpers", () => { }); it("resolves repo root from dist argv1", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-ui-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-")); try { await fs.mkdir(path.join(tmp, "ui"), { recursive: true }); await fs.writeFile(path.join(tmp, "ui", "vite.config.ts"), "export {};\n"); @@ -37,11 +35,46 @@ describe("control UI assets helpers", () => { } }); - it("resolves dist control-ui index path for dist argv1", () => { + it("resolves dist control-ui index path for dist argv1", async () => { const argv1 = path.resolve("/tmp", "pkg", "dist", "index.js"); const distDir = path.dirname(argv1); - expect(resolveControlUiDistIndexPath(argv1)).toBe( + expect(await resolveControlUiDistIndexPath(argv1)).toBe( path.join(distDir, "control-ui", "index.html"), ); }); + + it("resolves dist control-ui index path from package root argv1", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-")); + try { + await fs.writeFile(path.join(tmp, "package.json"), JSON.stringify({ name: "openclaw" })); + await fs.writeFile(path.join(tmp, "openclaw.mjs"), "export {};\n"); + await fs.mkdir(path.join(tmp, "dist", "control-ui"), { recursive: true }); + await fs.writeFile(path.join(tmp, "dist", "control-ui", "index.html"), "\n"); + + expect(await resolveControlUiDistIndexPath(path.join(tmp, "openclaw.mjs"))).toBe( + path.join(tmp, "dist", "control-ui", "index.html"), + ); + } finally { + await fs.rm(tmp, { recursive: true, force: true }); + } + }); + + it("resolves dist control-ui index path from .bin argv1", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-")); + try { + const binDir = path.join(tmp, "node_modules", ".bin"); + const pkgRoot = path.join(tmp, "node_modules", "openclaw"); + await fs.mkdir(binDir, { recursive: true }); + await fs.mkdir(path.join(pkgRoot, "dist", "control-ui"), { recursive: true }); + await fs.writeFile(path.join(binDir, "openclaw"), "#!/usr/bin/env node\n"); + await fs.writeFile(path.join(pkgRoot, "package.json"), JSON.stringify({ name: "openclaw" })); + await fs.writeFile(path.join(pkgRoot, "dist", "control-ui", "index.html"), "\n"); + + expect(await resolveControlUiDistIndexPath(path.join(binDir, "openclaw"))).toBe( + path.join(pkgRoot, "dist", "control-ui", "index.html"), + ); + } finally { + await fs.rm(tmp, { recursive: true, force: true }); + } + }); }); diff --git a/src/infra/control-ui-assets.ts b/src/infra/control-ui-assets.ts index bd705ebf3..97a850885 100644 --- a/src/infra/control-ui-assets.ts +++ b/src/infra/control-ui-assets.ts @@ -1,19 +1,23 @@ import fs from "node:fs"; import path from "node:path"; - import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { resolveOpenClawPackageRoot } from "./openclaw-root.js"; export function resolveControlUiRepoRoot( argv1: string | undefined = process.argv[1], ): string | null { - if (!argv1) return null; + if (!argv1) { + return null; + } const normalized = path.resolve(argv1); const parts = normalized.split(path.sep); const srcIndex = parts.lastIndexOf("src"); if (srcIndex !== -1) { const root = parts.slice(0, srcIndex).join(path.sep); - if (fs.existsSync(path.join(root, "ui", "vite.config.ts"))) return root; + if (fs.existsSync(path.join(root, "ui", "vite.config.ts"))) { + return root; + } } let dir = path.dirname(normalized); @@ -25,21 +29,34 @@ export function resolveControlUiRepoRoot( return dir; } const parent = path.dirname(dir); - if (parent === dir) break; + if (parent === dir) { + break; + } dir = parent; } return null; } -export function resolveControlUiDistIndexPath( +export async function resolveControlUiDistIndexPath( argv1: string | undefined = process.argv[1], -): string | null { - if (!argv1) return null; +): Promise { + if (!argv1) { + return null; + } const normalized = path.resolve(argv1); + + // Case 1: entrypoint is directly inside dist/ (e.g., dist/entry.js) const distDir = path.dirname(normalized); - if (path.basename(distDir) !== "dist") return null; - return path.join(distDir, "control-ui", "index.html"); + if (path.basename(distDir) === "dist") { + return path.join(distDir, "control-ui", "index.html"); + } + + const packageRoot = await resolveOpenClawPackageRoot({ argv1: normalized }); + if (!packageRoot) { + return null; + } + return path.join(packageRoot, "dist", "control-ui", "index.html"); } export type EnsureControlUiAssetsResult = { @@ -53,9 +70,13 @@ function summarizeCommandOutput(text: string): string | undefined { .split(/\r?\n/g) .map((l) => l.trim()) .filter(Boolean); - if (!lines.length) return undefined; + if (!lines.length) { + return undefined; + } const last = lines.at(-1); - if (!last) return undefined; + if (!last) { + return undefined; + } return last.length > 240 ? `${last.slice(0, 239)}…` : last; } @@ -63,7 +84,7 @@ export async function ensureControlUiAssetsBuilt( runtime: RuntimeEnv = defaultRuntime, opts?: { timeoutMs?: number }, ): Promise { - const indexFromDist = resolveControlUiDistIndexPath(process.argv[1]); + const indexFromDist = await resolveControlUiDistIndexPath(process.argv[1]); if (indexFromDist && fs.existsSync(indexFromDist)) { return { ok: true, built: false }; } diff --git a/src/infra/dedupe.test.ts b/src/infra/dedupe.test.ts index 3d41938a4..366f0d52f 100644 --- a/src/infra/dedupe.test.ts +++ b/src/infra/dedupe.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { createDedupeCache } from "./dedupe.js"; describe("createDedupeCache", () => { diff --git a/src/infra/dedupe.ts b/src/infra/dedupe.ts index ad49d65bd..850e2145a 100644 --- a/src/infra/dedupe.ts +++ b/src/infra/dedupe.ts @@ -33,15 +33,19 @@ export function createDedupeCache(options: DedupeCacheOptions): DedupeCache { return; } while (cache.size > maxSize) { - const oldestKey = cache.keys().next().value as string | undefined; - if (!oldestKey) break; + const oldestKey = cache.keys().next().value; + if (!oldestKey) { + break; + } cache.delete(oldestKey); } }; return { check: (key, now = Date.now()) => { - if (!key) return false; + if (!key) { + return false; + } const existing = cache.get(key); if (existing !== undefined && (ttlMs <= 0 || now - existing < ttlMs)) { touch(key, now); diff --git a/src/infra/device-auth-store.ts b/src/infra/device-auth-store.ts index 0f3515c03..62a27c97a 100644 --- a/src/infra/device-auth-store.ts +++ b/src/infra/device-auth-store.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import path from "node:path"; - import { resolveStateDir } from "../config/paths.js"; export type DeviceAuthEntry = { @@ -27,22 +26,32 @@ function normalizeRole(role: string): string { } function normalizeScopes(scopes: string[] | undefined): string[] { - if (!Array.isArray(scopes)) return []; + if (!Array.isArray(scopes)) { + return []; + } const out = new Set(); for (const scope of scopes) { const trimmed = scope.trim(); - if (trimmed) out.add(trimmed); + if (trimmed) { + out.add(trimmed); + } } - return [...out].sort(); + return [...out].toSorted(); } function readStore(filePath: string): DeviceAuthStore | null { try { - if (!fs.existsSync(filePath)) return null; + if (!fs.existsSync(filePath)) { + return null; + } const raw = fs.readFileSync(filePath, "utf8"); const parsed = JSON.parse(raw) as DeviceAuthStore; - if (parsed?.version !== 1 || typeof parsed.deviceId !== "string") return null; - if (!parsed.tokens || typeof parsed.tokens !== "object") return null; + if (parsed?.version !== 1 || typeof parsed.deviceId !== "string") { + return null; + } + if (!parsed.tokens || typeof parsed.tokens !== "object") { + return null; + } return parsed; } catch { return null; @@ -66,11 +75,17 @@ export function loadDeviceAuthToken(params: { }): DeviceAuthEntry | null { const filePath = resolveDeviceAuthPath(params.env); const store = readStore(filePath); - if (!store) return null; - if (store.deviceId !== params.deviceId) return null; + if (!store) { + return null; + } + if (store.deviceId !== params.deviceId) { + return null; + } const role = normalizeRole(params.role); const entry = store.tokens[role]; - if (!entry || typeof entry.token !== "string") return null; + if (!entry || typeof entry.token !== "string") { + return null; + } return entry; } @@ -110,9 +125,13 @@ export function clearDeviceAuthToken(params: { }): void { const filePath = resolveDeviceAuthPath(params.env); const store = readStore(filePath); - if (!store || store.deviceId !== params.deviceId) return; + if (!store || store.deviceId !== params.deviceId) { + return; + } const role = normalizeRole(params.role); - if (!store.tokens[role]) return; + if (!store.tokens[role]) { + return; + } const next: DeviceAuthStore = { version: 1, deviceId: store.deviceId, diff --git a/src/infra/device-identity.ts b/src/infra/device-identity.ts index c5381785c..a5502fb26 100644 --- a/src/infra/device-identity.ts +++ b/src/infra/device-identity.ts @@ -17,7 +17,7 @@ type StoredIdentity = { createdAtMs: number; }; -const DEFAULT_DIR = path.join(os.homedir(), ".clawdbot", "identity"); +const DEFAULT_DIR = path.join(os.homedir(), ".openclaw", "identity"); const DEFAULT_FILE = path.join(DEFAULT_DIR, "device.json"); function ensureDir(filePath: string) { diff --git a/src/infra/device-pairing.test.ts b/src/infra/device-pairing.test.ts index 5461498d9..c1605debd 100644 --- a/src/infra/device-pairing.test.ts +++ b/src/infra/device-pairing.test.ts @@ -11,7 +11,7 @@ import { describe("device pairing tokens", () => { test("preserves existing token scopes when rotating without scopes", async () => { - const baseDir = await mkdtemp(join(tmpdir(), "moltbot-device-pairing-")); + const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-")); const request = await requestDevicePairing( { deviceId: "device-1", diff --git a/src/infra/device-pairing.ts b/src/infra/device-pairing.ts index b190199eb..c2193af3f 100644 --- a/src/infra/device-pairing.ts +++ b/src/infra/device-pairing.ts @@ -164,47 +164,69 @@ function normalizeRole(role: string | undefined): string | null { function mergeRoles(...items: Array): string[] | undefined { const roles = new Set(); for (const item of items) { - if (!item) continue; + if (!item) { + continue; + } if (Array.isArray(item)) { for (const role of item) { const trimmed = role.trim(); - if (trimmed) roles.add(trimmed); + if (trimmed) { + roles.add(trimmed); + } } } else { const trimmed = item.trim(); - if (trimmed) roles.add(trimmed); + if (trimmed) { + roles.add(trimmed); + } } } - if (roles.size === 0) return undefined; + if (roles.size === 0) { + return undefined; + } return [...roles]; } function mergeScopes(...items: Array): string[] | undefined { const scopes = new Set(); for (const item of items) { - if (!item) continue; + if (!item) { + continue; + } for (const scope of item) { const trimmed = scope.trim(); - if (trimmed) scopes.add(trimmed); + if (trimmed) { + scopes.add(trimmed); + } } } - if (scopes.size === 0) return undefined; + if (scopes.size === 0) { + return undefined; + } return [...scopes]; } function normalizeScopes(scopes: string[] | undefined): string[] { - if (!Array.isArray(scopes)) return []; + if (!Array.isArray(scopes)) { + return []; + } const out = new Set(); for (const scope of scopes) { const trimmed = scope.trim(); - if (trimmed) out.add(trimmed); + if (trimmed) { + out.add(trimmed); + } } - return [...out].sort(); + return [...out].toSorted(); } function scopesAllow(requested: string[], allowed: string[]): boolean { - if (requested.length === 0) return true; - if (allowed.length === 0) return false; + if (requested.length === 0) { + return true; + } + if (allowed.length === 0) { + return false; + } const allowedSet = new Set(allowed); return requested.every((scope) => allowedSet.has(scope)); } @@ -215,8 +237,8 @@ function newToken() { export async function listDevicePairing(baseDir?: string): Promise { const state = await loadState(baseDir); - const pending = Object.values(state.pendingById).sort((a, b) => b.ts - a.ts); - const paired = Object.values(state.pairedByDeviceId).sort( + const pending = Object.values(state.pendingById).toSorted((a, b) => b.ts - a.ts); + const paired = Object.values(state.pairedByDeviceId).toSorted( (a, b) => b.approvedAtMs - a.approvedAtMs, ); return { pending, paired }; @@ -278,7 +300,9 @@ export async function approveDevicePairing( return await withLock(async () => { const state = await loadState(baseDir); const pending = state.pendingById[requestId]; - if (!pending) return null; + if (!pending) { + return null; + } const now = Date.now(); const existing = state.pairedByDeviceId[pending.deviceId]; const roles = mergeRoles(existing?.roles, existing?.role, pending.roles, pending.role); @@ -328,7 +352,9 @@ export async function rejectDevicePairing( return await withLock(async () => { const state = await loadState(baseDir); const pending = state.pendingById[requestId]; - if (!pending) return null; + if (!pending) { + return null; + } delete state.pendingById[requestId]; await persistState(state, baseDir); return { requestId, deviceId: pending.deviceId }; @@ -343,7 +369,9 @@ export async function updatePairedDeviceMetadata( return await withLock(async () => { const state = await loadState(baseDir); const existing = state.pairedByDeviceId[normalizeDeviceId(deviceId)]; - if (!existing) return; + if (!existing) { + return; + } const roles = mergeRoles(existing.roles, existing.role, patch.role); const scopes = mergeScopes(existing.scopes, patch.scopes); state.pairedByDeviceId[deviceId] = { @@ -363,7 +391,9 @@ export async function updatePairedDeviceMetadata( export function summarizeDeviceTokens( tokens: Record | undefined, ): DeviceAuthTokenSummary[] | undefined { - if (!tokens) return undefined; + if (!tokens) { + return undefined; + } const summaries = Object.values(tokens) .map((token) => ({ role: token.role, @@ -373,7 +403,7 @@ export function summarizeDeviceTokens( revokedAtMs: token.revokedAtMs, lastUsedAtMs: token.lastUsedAtMs, })) - .sort((a, b) => a.role.localeCompare(b.role)); + .toSorted((a, b) => a.role.localeCompare(b.role)); return summaries.length > 0 ? summaries : undefined; } @@ -387,13 +417,23 @@ export async function verifyDeviceToken(params: { return await withLock(async () => { const state = await loadState(params.baseDir); const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)]; - if (!device) return { ok: false, reason: "device-not-paired" }; + if (!device) { + return { ok: false, reason: "device-not-paired" }; + } const role = normalizeRole(params.role); - if (!role) return { ok: false, reason: "role-missing" }; + if (!role) { + return { ok: false, reason: "role-missing" }; + } const entry = device.tokens?.[role]; - if (!entry) return { ok: false, reason: "token-missing" }; - if (entry.revokedAtMs) return { ok: false, reason: "token-revoked" }; - if (entry.token !== params.token) return { ok: false, reason: "token-mismatch" }; + if (!entry) { + return { ok: false, reason: "token-missing" }; + } + if (entry.revokedAtMs) { + return { ok: false, reason: "token-revoked" }; + } + if (entry.token !== params.token) { + return { ok: false, reason: "token-mismatch" }; + } const requestedScopes = normalizeScopes(params.scopes); if (!scopesAllow(requestedScopes, entry.scopes)) { return { ok: false, reason: "scope-mismatch" }; @@ -416,9 +456,13 @@ export async function ensureDeviceToken(params: { return await withLock(async () => { const state = await loadState(params.baseDir); const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)]; - if (!device) return null; + if (!device) { + return null; + } const role = normalizeRole(params.role); - if (!role) return null; + if (!role) { + return null; + } const requestedScopes = normalizeScopes(params.scopes); const tokens = device.tokens ? { ...device.tokens } : {}; const existing = tokens[role]; @@ -454,9 +498,13 @@ export async function rotateDeviceToken(params: { return await withLock(async () => { const state = await loadState(params.baseDir); const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)]; - if (!device) return null; + if (!device) { + return null; + } const role = normalizeRole(params.role); - if (!role) return null; + if (!role) { + return null; + } const tokens = device.tokens ? { ...device.tokens } : {}; const existing = tokens[role]; const requestedScopes = normalizeScopes(params.scopes ?? existing?.scopes ?? device.scopes); @@ -489,10 +537,16 @@ export async function revokeDeviceToken(params: { return await withLock(async () => { const state = await loadState(params.baseDir); const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)]; - if (!device) return null; + if (!device) { + return null; + } const role = normalizeRole(params.role); - if (!role) return null; - if (!device.tokens?.[role]) return null; + if (!role) { + return null; + } + if (!device.tokens?.[role]) { + return null; + } const tokens = { ...device.tokens }; const entry = { ...tokens[role], revokedAtMs: Date.now() }; tokens[role] = entry; diff --git a/src/infra/diagnostic-events.test.ts b/src/infra/diagnostic-events.test.ts index 2235c9c8a..50fa72e00 100644 --- a/src/infra/diagnostic-events.test.ts +++ b/src/infra/diagnostic-events.test.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from "vitest"; - import { emitDiagnosticEvent, onDiagnosticEvent, diff --git a/src/infra/diagnostic-events.ts b/src/infra/diagnostic-events.ts index 586177b0b..b0de66614 100644 --- a/src/infra/diagnostic-events.ts +++ b/src/infra/diagnostic-events.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; export type DiagnosticSessionState = "idle" | "processing" | "waiting"; @@ -149,7 +149,7 @@ export type DiagnosticEventInput = DiagnosticEventPayload extends infer Event let seq = 0; const listeners = new Set<(evt: DiagnosticEventPayload) => void>(); -export function isDiagnosticsEnabled(config?: MoltbotConfig): boolean { +export function isDiagnosticsEnabled(config?: OpenClawConfig): boolean { return config?.diagnostics?.enabled === true; } diff --git a/src/infra/diagnostic-flags.test.ts b/src/infra/diagnostic-flags.test.ts index bdff0e567..b2d94a8da 100644 --- a/src/infra/diagnostic-flags.test.ts +++ b/src/infra/diagnostic-flags.test.ts @@ -1,15 +1,14 @@ import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { isDiagnosticFlagEnabled, resolveDiagnosticFlags } from "./diagnostic-flags.js"; describe("diagnostic flags", () => { it("merges config + env flags", () => { const cfg = { diagnostics: { flags: ["telegram.http", "cache.*"] }, - } as MoltbotConfig; + } as OpenClawConfig; const env = { - CLAWDBOT_DIAGNOSTICS: "foo,bar", + OPENCLAW_DIAGNOSTICS: "foo,bar", } as NodeJS.ProcessEnv; const flags = resolveDiagnosticFlags(cfg, env); @@ -20,12 +19,12 @@ describe("diagnostic flags", () => { }); it("treats env true as wildcard", () => { - const env = { CLAWDBOT_DIAGNOSTICS: "1" } as NodeJS.ProcessEnv; + const env = { OPENCLAW_DIAGNOSTICS: "1" } as NodeJS.ProcessEnv; expect(isDiagnosticFlagEnabled("anything.here", undefined, env)).toBe(true); }); it("treats env false as disabled", () => { - const env = { CLAWDBOT_DIAGNOSTICS: "0" } as NodeJS.ProcessEnv; + const env = { OPENCLAW_DIAGNOSTICS: "0" } as NodeJS.ProcessEnv; expect(isDiagnosticFlagEnabled("telegram.http", undefined, env)).toBe(false); }); }); diff --git a/src/infra/diagnostic-flags.ts b/src/infra/diagnostic-flags.ts index 8ffd71b76..af0276192 100644 --- a/src/infra/diagnostic-flags.ts +++ b/src/infra/diagnostic-flags.ts @@ -1,18 +1,26 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; -const DIAGNOSTICS_ENV = "CLAWDBOT_DIAGNOSTICS"; +const DIAGNOSTICS_ENV = "OPENCLAW_DIAGNOSTICS"; function normalizeFlag(value: string): string { return value.trim().toLowerCase(); } function parseEnvFlags(raw?: string): string[] { - if (!raw) return []; + if (!raw) { + return []; + } const trimmed = raw.trim(); - if (!trimmed) return []; + if (!trimmed) { + return []; + } const lowered = trimmed.toLowerCase(); - if (["0", "false", "off", "none"].includes(lowered)) return []; - if (["1", "true", "all", "*"].includes(lowered)) return ["*"]; + if (["0", "false", "off", "none"].includes(lowered)) { + return []; + } + if (["1", "true", "all", "*"].includes(lowered)) { + return ["*"]; + } return trimmed .split(/[,\s]+/) .map(normalizeFlag) @@ -24,7 +32,9 @@ function uniqueFlags(flags: string[]): string[] { const out: string[] = []; for (const flag of flags) { const normalized = normalizeFlag(flag); - if (!normalized || seen.has(normalized)) continue; + if (!normalized || seen.has(normalized)) { + continue; + } seen.add(normalized); out.push(normalized); } @@ -32,7 +42,7 @@ function uniqueFlags(flags: string[]): string[] { } export function resolveDiagnosticFlags( - cfg?: MoltbotConfig, + cfg?: OpenClawConfig, env: NodeJS.ProcessEnv = process.env, ): string[] { const configFlags = Array.isArray(cfg?.diagnostics?.flags) ? cfg?.diagnostics?.flags : []; @@ -42,27 +52,39 @@ export function resolveDiagnosticFlags( export function matchesDiagnosticFlag(flag: string, enabledFlags: string[]): boolean { const target = normalizeFlag(flag); - if (!target) return false; + if (!target) { + return false; + } for (const raw of enabledFlags) { const enabled = normalizeFlag(raw); - if (!enabled) continue; - if (enabled === "*" || enabled === "all") return true; + if (!enabled) { + continue; + } + if (enabled === "*" || enabled === "all") { + return true; + } if (enabled.endsWith(".*")) { const prefix = enabled.slice(0, -2); - if (target === prefix || target.startsWith(`${prefix}.`)) return true; + if (target === prefix || target.startsWith(`${prefix}.`)) { + return true; + } } if (enabled.endsWith("*")) { const prefix = enabled.slice(0, -1); - if (target.startsWith(prefix)) return true; + if (target.startsWith(prefix)) { + return true; + } + } + if (enabled === target) { + return true; } - if (enabled === target) return true; } return false; } export function isDiagnosticFlagEnabled( flag: string, - cfg?: MoltbotConfig, + cfg?: OpenClawConfig, env: NodeJS.ProcessEnv = process.env, ): boolean { const flags = resolveDiagnosticFlags(cfg, env); diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index 5aa753881..c9cab5456 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { loadDotEnv } from "./dotenv.js"; async function writeEnvFile(filePath: string, contents: string) { @@ -12,15 +10,15 @@ async function writeEnvFile(filePath: string, contents: string) { } describe("loadDotEnv", () => { - it("loads ~/.clawdbot/.env as fallback without overriding CWD .env", async () => { + it("loads ~/.openclaw/.env as fallback without overriding CWD .env", async () => { const prevEnv = { ...process.env }; const prevCwd = process.cwd(); - const base = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-dotenv-test-")); + const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-dotenv-test-")); const cwdDir = path.join(base, "cwd"); const stateDir = path.join(base, "state"); - process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.OPENCLAW_STATE_DIR = stateDir; await writeEnvFile(path.join(stateDir, ".env"), "FOO=from-global\nBAR=1\n"); await writeEnvFile(path.join(cwdDir, ".env"), "FOO=from-cwd\n"); @@ -36,11 +34,16 @@ describe("loadDotEnv", () => { process.chdir(prevCwd); for (const key of Object.keys(process.env)) { - if (!(key in prevEnv)) delete process.env[key]; + if (!(key in prevEnv)) { + delete process.env[key]; + } } for (const [key, value] of Object.entries(prevEnv)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } } }); @@ -48,11 +51,11 @@ describe("loadDotEnv", () => { const prevEnv = { ...process.env }; const prevCwd = process.cwd(); - const base = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-dotenv-test-")); + const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-dotenv-test-")); const cwdDir = path.join(base, "cwd"); const stateDir = path.join(base, "state"); - process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.OPENCLAW_STATE_DIR = stateDir; process.env.FOO = "from-shell"; await writeEnvFile(path.join(stateDir, ".env"), "FOO=from-global\n"); @@ -66,11 +69,16 @@ describe("loadDotEnv", () => { process.chdir(prevCwd); for (const key of Object.keys(process.env)) { - if (!(key in prevEnv)) delete process.env[key]; + if (!(key in prevEnv)) { + delete process.env[key]; + } } for (const [key, value] of Object.entries(prevEnv)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } } }); }); diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 6a0669dec..e6474b407 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -1,8 +1,6 @@ +import dotenv from "dotenv"; import fs from "node:fs"; import path from "node:path"; - -import dotenv from "dotenv"; - import { resolveConfigDir } from "../utils.js"; export function loadDotEnv(opts?: { quiet?: boolean }) { @@ -11,10 +9,12 @@ export function loadDotEnv(opts?: { quiet?: boolean }) { // Load from process CWD first (dotenv default). dotenv.config({ quiet }); - // Then load global fallback: ~/.clawdbot/.env (or CLAWDBOT_STATE_DIR/.env), + // Then load global fallback: ~/.openclaw/.env (or OPENCLAW_STATE_DIR/.env), // without overriding any env vars already present. const globalEnvPath = path.join(resolveConfigDir(process.env), ".env"); - if (!fs.existsSync(globalEnvPath)) return; + if (!fs.existsSync(globalEnvPath)) { + return; + } dotenv.config({ quiet, path: globalEnvPath, override: false }); } diff --git a/src/infra/env-file.ts b/src/infra/env-file.ts index de7a27f2d..c20222a6c 100644 --- a/src/infra/env-file.ts +++ b/src/infra/env-file.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import path from "node:path"; - import { resolveConfigDir } from "../utils.js"; function escapeRegExp(value: string): string { @@ -30,11 +29,15 @@ export function upsertSharedEnvVar(params: { const nextLines = lines.map((line) => { const match = line.match(matcher); - if (!match) return line; + if (!match) { + return line; + } replaced = true; const prefix = match[1] ?? ""; const next = `${prefix}${key}=${value}`; - if (next !== line) updated = true; + if (next !== line) { + updated = true; + } return next; }); diff --git a/src/infra/env.test.ts b/src/infra/env.test.ts index dac8bc359..c03510371 100644 --- a/src/infra/env.test.ts +++ b/src/infra/env.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { isTruthyEnvValue, normalizeZaiEnv } from "./env.js"; describe("normalizeZaiEnv", () => { @@ -13,10 +12,16 @@ describe("normalizeZaiEnv", () => { expect(process.env.ZAI_API_KEY).toBe("zai-legacy"); - if (prevZai === undefined) delete process.env.ZAI_API_KEY; - else process.env.ZAI_API_KEY = prevZai; - if (prevZAi === undefined) delete process.env.Z_AI_API_KEY; - else process.env.Z_AI_API_KEY = prevZAi; + if (prevZai === undefined) { + delete process.env.ZAI_API_KEY; + } else { + process.env.ZAI_API_KEY = prevZai; + } + if (prevZAi === undefined) { + delete process.env.Z_AI_API_KEY; + } else { + process.env.Z_AI_API_KEY = prevZAi; + } }); it("does not override existing ZAI_API_KEY", () => { @@ -29,10 +34,16 @@ describe("normalizeZaiEnv", () => { expect(process.env.ZAI_API_KEY).toBe("zai-current"); - if (prevZai === undefined) delete process.env.ZAI_API_KEY; - else process.env.ZAI_API_KEY = prevZai; - if (prevZAi === undefined) delete process.env.Z_AI_API_KEY; - else process.env.Z_AI_API_KEY = prevZAi; + if (prevZai === undefined) { + delete process.env.ZAI_API_KEY; + } else { + process.env.ZAI_API_KEY = prevZai; + } + if (prevZAi === undefined) { + delete process.env.Z_AI_API_KEY; + } else { + process.env.Z_AI_API_KEY = prevZAi; + } }); }); diff --git a/src/infra/env.ts b/src/infra/env.ts index 2139c65a7..47c16fed1 100644 --- a/src/infra/env.ts +++ b/src/infra/env.ts @@ -12,17 +12,27 @@ type AcceptedEnvOption = { }; function formatEnvValue(value: string, redact?: boolean): string { - if (redact) return ""; + if (redact) { + return ""; + } const singleLine = value.replace(/\s+/g, " ").trim(); - if (singleLine.length <= 160) return singleLine; + if (singleLine.length <= 160) { + return singleLine; + } return `${singleLine.slice(0, 160)}…`; } export function logAcceptedEnvOption(option: AcceptedEnvOption): void { - if (process.env.VITEST || process.env.NODE_ENV === "test") return; - if (loggedEnv.has(option.key)) return; + if (process.env.VITEST || process.env.NODE_ENV === "test") { + return; + } + if (loggedEnv.has(option.key)) { + return; + } const rawValue = option.value ?? process.env[option.key]; - if (!rawValue || !rawValue.trim()) return; + if (!rawValue || !rawValue.trim()) { + return; + } loggedEnv.add(option.key); log.info(`env: ${option.key}=${formatEnvValue(rawValue, option.redact)} (${option.description})`); } diff --git a/src/infra/errors.ts b/src/infra/errors.ts index 9175b3622..9f41ee4e5 100644 --- a/src/infra/errors.ts +++ b/src/infra/errors.ts @@ -1,8 +1,14 @@ export function extractErrorCode(err: unknown): string | undefined { - if (!err || typeof err !== "object") return undefined; + if (!err || typeof err !== "object") { + return undefined; + } const code = (err as { code?: unknown }).code; - if (typeof code === "string") return code; - if (typeof code === "number") return String(code); + if (typeof code === "string") { + return code; + } + if (typeof code === "number") { + return String(code); + } return undefined; } @@ -10,7 +16,9 @@ export function formatErrorMessage(err: unknown): string { if (err instanceof Error) { return err.message || err.name || "Error"; } - if (typeof err === "string") return err; + if (typeof err === "string") { + return err; + } if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") { return String(err); } diff --git a/src/infra/exec-approval-forwarder.test.ts b/src/infra/exec-approval-forwarder.test.ts index 8e9bba7d8..60f8ad148 100644 --- a/src/infra/exec-approval-forwarder.test.ts +++ b/src/infra/exec-approval-forwarder.test.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { createExecApprovalForwarder } from "./exec-approval-forwarder.js"; const baseRequest = { @@ -24,7 +23,7 @@ describe("exec approval forwarder", () => { const deliver = vi.fn().mockResolvedValue([]); const cfg = { approvals: { exec: { enabled: true, mode: "session" } }, - } as MoltbotConfig; + } as OpenClawConfig; const forwarder = createExecApprovalForwarder({ getConfig: () => cfg, @@ -59,7 +58,7 @@ describe("exec approval forwarder", () => { targets: [{ channel: "telegram", to: "123" }], }, }, - } as MoltbotConfig; + } as OpenClawConfig; const forwarder = createExecApprovalForwarder({ getConfig: () => cfg, diff --git a/src/infra/exec-approval-forwarder.ts b/src/infra/exec-approval-forwarder.ts index 776fbb1ec..8ce0748cc 100644 --- a/src/infra/exec-approval-forwarder.ts +++ b/src/infra/exec-approval-forwarder.ts @@ -1,14 +1,14 @@ -import type { MoltbotConfig } from "../config/config.js"; -import { loadConfig } from "../config/config.js"; -import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { ExecApprovalForwardingConfig, ExecApprovalForwardTarget, } from "../config/types.approvals.js"; +import type { ExecApprovalDecision } from "./exec-approvals.js"; +import { loadConfig } from "../config/config.js"; +import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; import { isDeliverableMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js"; -import type { ExecApprovalDecision } from "./exec-approvals.js"; import { deliverOutboundPayloads } from "./outbound/deliver.js"; import { resolveSessionDeliveryTarget } from "./outbound/targets.js"; @@ -52,11 +52,11 @@ export type ExecApprovalForwarder = { }; export type ExecApprovalForwarderDeps = { - getConfig?: () => MoltbotConfig; + getConfig?: () => OpenClawConfig; deliver?: typeof deliverOutboundPayloads; nowMs?: () => number; resolveSessionTarget?: (params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; request: ExecApprovalRequest; }) => ExecApprovalForwardTarget | null; }; @@ -82,18 +82,28 @@ function shouldForward(params: { request: ExecApprovalRequest; }): boolean { const config = params.config; - if (!config?.enabled) return false; + if (!config?.enabled) { + return false; + } if (config.agentFilter?.length) { const agentId = params.request.request.agentId ?? parseAgentSessionKey(params.request.request.sessionKey)?.agentId; - if (!agentId) return false; - if (!config.agentFilter.includes(agentId)) return false; + if (!agentId) { + return false; + } + if (!config.agentFilter.includes(agentId)) { + return false; + } } if (config.sessionFilter?.length) { const sessionKey = params.request.request.sessionKey; - if (!sessionKey) return false; - if (!matchSessionFilter(sessionKey, config.sessionFilter)) return false; + if (!sessionKey) { + return false; + } + if (!matchSessionFilter(sessionKey, config.sessionFilter)) { + return false; + } } return true; } @@ -108,11 +118,21 @@ function buildTargetKey(target: ExecApprovalForwardTarget): string { function buildRequestMessage(request: ExecApprovalRequest, nowMs: number) { const lines: string[] = ["🔒 Exec approval required", `ID: ${request.id}`]; lines.push(`Command: ${request.request.command}`); - if (request.request.cwd) lines.push(`CWD: ${request.request.cwd}`); - if (request.request.host) lines.push(`Host: ${request.request.host}`); - if (request.request.agentId) lines.push(`Agent: ${request.request.agentId}`); - if (request.request.security) lines.push(`Security: ${request.request.security}`); - if (request.request.ask) lines.push(`Ask: ${request.request.ask}`); + if (request.request.cwd) { + lines.push(`CWD: ${request.request.cwd}`); + } + if (request.request.host) { + lines.push(`Host: ${request.request.host}`); + } + if (request.request.agentId) { + lines.push(`Agent: ${request.request.agentId}`); + } + if (request.request.security) { + lines.push(`Security: ${request.request.security}`); + } + if (request.request.ask) { + lines.push(`Ask: ${request.request.ask}`); + } const expiresIn = Math.max(0, Math.round((request.expiresAtMs - nowMs) / 1000)); lines.push(`Expires in: ${expiresIn}s`); lines.push("Reply with: /approve allow-once|allow-always|deny"); @@ -120,8 +140,12 @@ function buildRequestMessage(request: ExecApprovalRequest, nowMs: number) { } function decisionLabel(decision: ExecApprovalDecision): string { - if (decision === "allow-once") return "allowed once"; - if (decision === "allow-always") return "allowed always"; + if (decision === "allow-once") { + return "allowed once"; + } + if (decision === "allow-always") { + return "allowed always"; + } return "denied"; } @@ -136,20 +160,28 @@ function buildExpiredMessage(request: ExecApprovalRequest) { } function defaultResolveSessionTarget(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; request: ExecApprovalRequest; }): ExecApprovalForwardTarget | null { const sessionKey = params.request.request.sessionKey?.trim(); - if (!sessionKey) return null; + if (!sessionKey) { + return null; + } const parsed = parseAgentSessionKey(sessionKey); const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main"; const storePath = resolveStorePath(params.cfg.session?.store, { agentId }); const store = loadSessionStore(storePath); const entry = store[sessionKey]; - if (!entry) return null; + if (!entry) { + return null; + } const target = resolveSessionDeliveryTarget({ entry, requestedChannel: "last" }); - if (!target.channel || !target.to) return null; - if (!isDeliverableMessageChannel(target.channel)) return null; + if (!target.channel || !target.to) { + return null; + } + if (!isDeliverableMessageChannel(target.channel)) { + return null; + } return { channel: target.channel, to: target.to, @@ -159,16 +191,20 @@ function defaultResolveSessionTarget(params: { } async function deliverToTargets(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; targets: ForwardTarget[]; text: string; deliver: typeof deliverOutboundPayloads; shouldSend?: () => boolean; }) { const deliveries = params.targets.map(async (target) => { - if (params.shouldSend && !params.shouldSend()) return; + if (params.shouldSend && !params.shouldSend()) { + return; + } const channel = normalizeMessageChannel(target.channel) ?? target.channel; - if (!isDeliverableMessageChannel(channel)) return; + if (!isDeliverableMessageChannel(channel)) { + return; + } try { await params.deliver({ cfg: params.cfg, @@ -197,7 +233,9 @@ export function createExecApprovalForwarder( const handleRequested = async (request: ExecApprovalRequest) => { const cfg = getConfig(); const config = cfg.approvals?.exec; - if (!shouldForward({ config, request })) return; + if (!shouldForward({ config, request })) { + return; + } const mode = normalizeMode(config?.mode); const targets: ForwardTarget[] = []; @@ -218,19 +256,25 @@ export function createExecApprovalForwarder( const explicitTargets = config?.targets ?? []; for (const target of explicitTargets) { const key = buildTargetKey(target); - if (seen.has(key)) continue; + if (seen.has(key)) { + continue; + } seen.add(key); targets.push({ ...target, source: "target" }); } } - if (targets.length === 0) return; + if (targets.length === 0) { + return; + } const expiresInMs = Math.max(0, request.expiresAtMs - nowMs()); const timeoutId = setTimeout(() => { void (async () => { const entry = pending.get(request.id); - if (!entry) return; + if (!entry) { + return; + } pending.delete(request.id); const expiredText = buildExpiredMessage(request); await deliverToTargets({ cfg, targets: entry.targets, text: expiredText, deliver }); @@ -241,7 +285,9 @@ export function createExecApprovalForwarder( const pendingEntry: PendingApproval = { request, targets, timeoutId }; pending.set(request.id, pendingEntry); - if (pending.get(request.id) !== pendingEntry) return; + if (pending.get(request.id) !== pendingEntry) { + return; + } const text = buildRequestMessage(request, nowMs()); await deliverToTargets({ @@ -255,8 +301,12 @@ export function createExecApprovalForwarder( const handleResolved = async (resolved: ExecApprovalResolved) => { const entry = pending.get(resolved.id); - if (!entry) return; - if (entry.timeoutId) clearTimeout(entry.timeoutId); + if (!entry) { + return; + } + if (entry.timeoutId) { + clearTimeout(entry.timeoutId); + } pending.delete(resolved.id); const cfg = getConfig(); @@ -266,7 +316,9 @@ export function createExecApprovalForwarder( const stop = () => { for (const entry of pending.values()) { - if (entry.timeoutId) clearTimeout(entry.timeoutId); + if (entry.timeoutId) { + clearTimeout(entry.timeoutId); + } } pending.clear(); }; diff --git a/src/infra/exec-approvals.test.ts b/src/infra/exec-approvals.test.ts index d63be8c75..091f34e14 100644 --- a/src/infra/exec-approvals.test.ts +++ b/src/infra/exec-approvals.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it, vi } from "vitest"; - import { analyzeArgvCommand, analyzeShellCommand, @@ -29,7 +27,7 @@ function makePathEnv(binDir: string): NodeJS.ProcessEnv { } function makeTempDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-exec-approvals-")); + return fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-exec-approvals-")); } describe("exec approvals allowlist matching", () => { @@ -381,7 +379,7 @@ describe("exec approvals wildcard agent", () => { const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(dir); try { - const approvalsPath = path.join(dir, ".clawdbot", "exec-approvals.json"); + const approvalsPath = path.join(dir, ".openclaw", "exec-approvals.json"); fs.mkdirSync(path.dirname(approvalsPath), { recursive: true }); fs.writeFileSync( approvalsPath, diff --git a/src/infra/exec-approvals.ts b/src/infra/exec-approvals.ts index 0830ed89a..61a757ba8 100644 --- a/src/infra/exec-approvals.ts +++ b/src/infra/exec-approvals.ts @@ -3,7 +3,6 @@ import fs from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; - import { DEFAULT_AGENT_ID } from "../routing/session-key.js"; export type ExecHost = "sandbox" | "gateway" | "node"; @@ -61,8 +60,8 @@ const DEFAULT_SECURITY: ExecSecurity = "deny"; const DEFAULT_ASK: ExecAsk = "on-miss"; const DEFAULT_ASK_FALLBACK: ExecSecurity = "deny"; const DEFAULT_AUTO_ALLOW_SKILLS = false; -const DEFAULT_SOCKET = "~/.clawdbot/exec-approvals.sock"; -const DEFAULT_FILE = "~/.clawdbot/exec-approvals.json"; +const DEFAULT_SOCKET = "~/.openclaw/exec-approvals.sock"; +const DEFAULT_FILE = "~/.openclaw/exec-approvals.json"; export const DEFAULT_SAFE_BINS = ["jq", "grep", "cut", "sort", "uniq", "head", "tail", "tr", "wc"]; function hashExecApprovalsRaw(raw: string | null): string { @@ -73,9 +72,15 @@ function hashExecApprovalsRaw(raw: string | null): string { } function expandHome(value: string): string { - if (!value) return value; - if (value === "~") return os.homedir(); - if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2)); + if (!value) { + return value; + } + if (value === "~") { + return os.homedir(); + } + if (value.startsWith("~/")) { + return path.join(os.homedir(), value.slice(2)); + } return value; } @@ -100,12 +105,18 @@ function mergeLegacyAgent( const seen = new Set(); const pushEntry = (entry: ExecAllowlistEntry) => { const key = normalizeAllowlistPattern(entry.pattern); - if (!key || seen.has(key)) return; + if (!key || seen.has(key)) { + return; + } seen.add(key); allowlist.push(entry); }; - for (const entry of current.allowlist ?? []) pushEntry(entry); - for (const entry of legacy.allowlist ?? []) pushEntry(entry); + for (const entry of current.allowlist ?? []) { + pushEntry(entry); + } + for (const entry of legacy.allowlist ?? []) { + pushEntry(entry); + } return { security: current.security ?? legacy.security, @@ -124,10 +135,14 @@ function ensureDir(filePath: string) { function ensureAllowlistIds( allowlist: ExecAllowlistEntry[] | undefined, ): ExecAllowlistEntry[] | undefined { - if (!Array.isArray(allowlist) || allowlist.length === 0) return allowlist; + if (!Array.isArray(allowlist) || allowlist.length === 0) { + return allowlist; + } let changed = false; const next = allowlist.map((entry) => { - if (entry.id) return entry; + if (entry.id) { + return entry; + } changed = true; return { ...entry, id: crypto.randomUUID() }; }); @@ -248,12 +263,16 @@ export function ensureExecApprovals(): ExecApprovalsFile { } function normalizeSecurity(value: ExecSecurity | undefined, fallback: ExecSecurity): ExecSecurity { - if (value === "allowlist" || value === "full" || value === "deny") return value; + if (value === "allowlist" || value === "full" || value === "deny") { + return value; + } return fallback; } function normalizeAsk(value: ExecAsk | undefined, fallback: ExecAsk): ExecAsk { - if (value === "always" || value === "off" || value === "on-miss") return value; + if (value === "always" || value === "off" || value === "on-miss") { + return value; + } return fallback; } @@ -345,7 +364,9 @@ type CommandResolution = { function isExecutableFile(filePath: string): boolean { try { const stat = fs.statSync(filePath); - if (!stat.isFile()) return false; + if (!stat.isFile()) { + return false; + } if (process.platform !== "win32") { fs.accessSync(filePath, fs.constants.X_OK); } @@ -357,11 +378,15 @@ function isExecutableFile(filePath: string): boolean { function parseFirstToken(command: string): string | null { const trimmed = command.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const first = trimmed[0]; if (first === '"' || first === "'") { const end = trimmed.indexOf(first, 1); - if (end > 1) return trimmed.slice(1, end); + if (end > 1) { + return trimmed.slice(1, end); + } return trimmed.slice(1); } const match = /^[^\s]+/.exec(trimmed); @@ -398,7 +423,9 @@ function resolveExecutablePath(rawExecutable: string, cwd?: string, env?: NodeJS for (const entry of entries) { for (const ext of extensions) { const candidate = path.join(entry, expanded + ext); - if (isExecutableFile(candidate)) return candidate; + if (isExecutableFile(candidate)) { + return candidate; + } } } return undefined; @@ -410,7 +437,9 @@ export function resolveCommandResolution( env?: NodeJS.ProcessEnv, ): CommandResolution | null { const rawExecutable = parseFirstToken(command); - if (!rawExecutable) return null; + if (!rawExecutable) { + return null; + } const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env); const executableName = resolvedPath ? path.basename(resolvedPath) : rawExecutable; return { rawExecutable, resolvedPath, executableName }; @@ -422,7 +451,9 @@ export function resolveCommandResolutionFromArgv( env?: NodeJS.ProcessEnv, ): CommandResolution | null { const rawExecutable = argv[0]?.trim(); - if (!rawExecutable) return null; + if (!rawExecutable) { + return null; + } const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env); const executableName = resolvedPath ? path.basename(resolvedPath) : rawExecutable; return { rawExecutable, resolvedPath, executableName }; @@ -474,7 +505,9 @@ function globToRegExp(pattern: string): RegExp { function matchesPattern(pattern: string, target: string): boolean { const trimmed = pattern.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } const expanded = trimmed.startsWith("~") ? expandHome(trimmed) : trimmed; const hasWildcard = /[*?]/.test(expanded); let normalizedPattern = expanded; @@ -493,13 +526,23 @@ function resolveAllowlistCandidatePath( resolution: CommandResolution | null, cwd?: string, ): string | undefined { - if (!resolution) return undefined; - if (resolution.resolvedPath) return resolution.resolvedPath; + if (!resolution) { + return undefined; + } + if (resolution.resolvedPath) { + return resolution.resolvedPath; + } const raw = resolution.rawExecutable?.trim(); - if (!raw) return undefined; + if (!raw) { + return undefined; + } const expanded = raw.startsWith("~") ? expandHome(raw) : raw; - if (!expanded.includes("/") && !expanded.includes("\\")) return undefined; - if (path.isAbsolute(expanded)) return expanded; + if (!expanded.includes("/") && !expanded.includes("\\")) { + return undefined; + } + if (path.isAbsolute(expanded)) { + return expanded; + } const base = cwd && cwd.trim() ? cwd.trim() : process.cwd(); return path.resolve(base, expanded); } @@ -508,14 +551,22 @@ export function matchAllowlist( entries: ExecAllowlistEntry[], resolution: CommandResolution | null, ): ExecAllowlistEntry | null { - if (!entries.length || !resolution?.resolvedPath) return null; + if (!entries.length || !resolution?.resolvedPath) { + return null; + } const resolvedPath = resolution.resolvedPath; for (const entry of entries) { const pattern = entry.pattern?.trim(); - if (!pattern) continue; + if (!pattern) { + continue; + } const hasPath = pattern.includes("/") || pattern.includes("\\") || pattern.includes("~"); - if (!hasPath) continue; - if (matchesPattern(pattern, resolvedPath)) return entry; + if (!hasPath) { + continue; + } + if (matchesPattern(pattern, resolvedPath)) { + return entry; + } } return null; } @@ -579,12 +630,16 @@ function iterateQuoteAware( continue; } if (inSingle) { - if (ch === "'") inSingle = false; + if (ch === "'") { + inSingle = false; + } buf += ch; continue; } if (inDouble) { - if (ch === '"') inDouble = false; + if (ch === '"') { + inDouble = false; + } buf += ch; continue; } @@ -805,10 +860,18 @@ export function analyzeArgvCommand(params: { function isPathLikeToken(value: string): boolean { const trimmed = value.trim(); - if (!trimmed) return false; - if (trimmed === "-") return false; - if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~")) return true; - if (trimmed.startsWith("/")) return true; + if (!trimmed) { + return false; + } + if (trimmed === "-") { + return false; + } + if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~")) { + return true; + } + if (trimmed.startsWith("/")) { + return true; + } return /^[A-Za-z]:[\\/]/.test(trimmed); } @@ -821,7 +884,9 @@ function defaultFileExists(filePath: string): boolean { } export function normalizeSafeBins(entries?: string[]): Set { - if (!Array.isArray(entries)) return new Set(); + if (!Array.isArray(entries)) { + return new Set(); + } const normalized = entries .map((entry) => entry.trim().toLowerCase()) .filter((entry) => entry.length > 0); @@ -829,7 +894,9 @@ export function normalizeSafeBins(entries?: string[]): Set { } export function resolveSafeBins(entries?: string[] | null): Set { - if (entries === undefined) return normalizeSafeBins(DEFAULT_SAFE_BINS); + if (entries === undefined) { + return normalizeSafeBins(DEFAULT_SAFE_BINS); + } return normalizeSafeBins(entries ?? []); } @@ -840,22 +907,34 @@ export function isSafeBinUsage(params: { cwd?: string; fileExists?: (filePath: string) => boolean; }): boolean { - if (params.safeBins.size === 0) return false; + if (params.safeBins.size === 0) { + return false; + } const resolution = params.resolution; const execName = resolution?.executableName?.toLowerCase(); - if (!execName) return false; + if (!execName) { + return false; + } const matchesSafeBin = params.safeBins.has(execName) || (process.platform === "win32" && params.safeBins.has(path.parse(execName).name)); - if (!matchesSafeBin) return false; - if (!resolution?.resolvedPath) return false; + if (!matchesSafeBin) { + return false; + } + if (!resolution?.resolvedPath) { + return false; + } const cwd = params.cwd ?? process.cwd(); const exists = params.fileExists ?? defaultFileExists; const argv = params.argv.slice(1); for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; - if (!token) continue; - if (token === "-") continue; + if (!token) { + continue; + } + if (token === "-") { + continue; + } if (token.startsWith("-")) { const eqIndex = token.indexOf("="); if (eqIndex > 0) { @@ -866,8 +945,12 @@ export function isSafeBinUsage(params: { } continue; } - if (isPathLikeToken(token)) return false; - if (exists(path.resolve(cwd, token))) return false; + if (isPathLikeToken(token)) { + return false; + } + if (exists(path.resolve(cwd, token))) { + return false; + } } return true; } @@ -897,7 +980,9 @@ function evaluateSegments( ? { ...segment.resolution, resolvedPath: candidatePath } : segment.resolution; const match = matchAllowlist(params.allowlist, candidateResolution); - if (match) matches.push(match); + if (match) { + matches.push(match); + } const safe = isSafeBinUsage({ argv: segment.argv, resolution: segment.resolution, @@ -993,12 +1078,16 @@ function splitCommandChain(command: string): string[] | null { continue; } if (inSingle) { - if (ch === "'") inSingle = false; + if (ch === "'") { + inSingle = false; + } buf += ch; continue; } if (inDouble) { - if (ch === '"') inDouble = false; + if (ch === '"') { + inDouble = false; + } buf += ch; continue; } @@ -1014,19 +1103,25 @@ function splitCommandChain(command: string): string[] | null { } if (ch === "&" && command[i + 1] === "&") { - if (!pushPart()) invalidChain = true; + if (!pushPart()) { + invalidChain = true; + } i += 1; foundChain = true; continue; } if (ch === "|" && command[i + 1] === "|") { - if (!pushPart()) invalidChain = true; + if (!pushPart()) { + invalidChain = true; + } i += 1; foundChain = true; continue; } if (ch === ";") { - if (!pushPart()) invalidChain = true; + if (!pushPart()) { + invalidChain = true; + } foundChain = true; continue; } @@ -1035,8 +1130,12 @@ function splitCommandChain(command: string): string[] | null { } const pushedFinal = pushPart(); - if (!foundChain) return null; - if (invalidChain || !pushedFinal) return null; + if (!foundChain) { + return null; + } + if (invalidChain || !pushedFinal) { + return null; + } return parts.length > 0 ? parts : null; } @@ -1187,8 +1286,12 @@ export function addAllowlistEntry( const existing = agents[target] ?? {}; const allowlist = Array.isArray(existing.allowlist) ? existing.allowlist : []; const trimmed = pattern.trim(); - if (!trimmed) return; - if (allowlist.some((entry) => entry.pattern === trimmed)) return; + if (!trimmed) { + return; + } + if (allowlist.some((entry) => entry.pattern === trimmed)) { + return; + } allowlist.push({ id: crypto.randomUUID(), pattern: trimmed, lastUsedAt: Date.now() }); agents[target] = { ...existing, allowlist }; approvals.agents = agents; @@ -1214,14 +1317,18 @@ export async function requestExecApprovalViaSocket(params: { timeoutMs?: number; }): Promise { const { socketPath, token, request } = params; - if (!socketPath || !token) return null; + if (!socketPath || !token) { + return null; + } const timeoutMs = params.timeoutMs ?? 15_000; return await new Promise((resolve) => { const client = new net.Socket(); let settled = false; let buffer = ""; const finish = (value: ExecApprovalDecision | null) => { - if (settled) return; + if (settled) { + return; + } settled = true; try { client.destroy(); @@ -1250,7 +1357,9 @@ export async function requestExecApprovalViaSocket(params: { const line = buffer.slice(0, idx).trim(); buffer = buffer.slice(idx + 1); idx = buffer.indexOf("\n"); - if (!line) continue; + if (!line) { + continue; + } try { const msg = JSON.parse(line) as { type?: string; decision?: ExecApprovalDecision }; if (msg?.type === "decision" && msg.decision) { diff --git a/src/infra/exec-host.ts b/src/infra/exec-host.ts index 904b6503a..d9d11aa92 100644 --- a/src/infra/exec-host.ts +++ b/src/infra/exec-host.ts @@ -39,14 +39,18 @@ export async function requestExecHostViaSocket(params: { timeoutMs?: number; }): Promise { const { socketPath, token, request } = params; - if (!socketPath || !token) return null; + if (!socketPath || !token) { + return null; + } const timeoutMs = params.timeoutMs ?? 20_000; return await new Promise((resolve) => { const client = new net.Socket(); let settled = false; let buffer = ""; const finish = (value: ExecHostResponse | null) => { - if (settled) return; + if (settled) { + return; + } settled = true; try { client.destroy(); @@ -85,7 +89,9 @@ export async function requestExecHostViaSocket(params: { const line = buffer.slice(0, idx).trim(); buffer = buffer.slice(idx + 1); idx = buffer.indexOf("\n"); - if (!line) continue; + if (!line) { + continue; + } try { const msg = JSON.parse(line) as { type?: string; diff --git a/src/infra/exec-safety.ts b/src/infra/exec-safety.ts index fe957d936..f7ee77f01 100644 --- a/src/infra/exec-safety.ts +++ b/src/infra/exec-safety.ts @@ -4,21 +4,41 @@ const QUOTE_CHARS = /["']/; const BARE_NAME_PATTERN = /^[A-Za-z0-9._+-]+$/; function isLikelyPath(value: string): boolean { - if (value.startsWith(".") || value.startsWith("~")) return true; - if (value.includes("/") || value.includes("\\")) return true; + if (value.startsWith(".") || value.startsWith("~")) { + return true; + } + if (value.includes("/") || value.includes("\\")) { + return true; + } return /^[A-Za-z]:[\\/]/.test(value); } export function isSafeExecutableValue(value: string | null | undefined): boolean { - if (!value) return false; + if (!value) { + return false; + } const trimmed = value.trim(); - if (!trimmed) return false; - if (trimmed.includes("\0")) return false; - if (CONTROL_CHARS.test(trimmed)) return false; - if (SHELL_METACHARS.test(trimmed)) return false; - if (QUOTE_CHARS.test(trimmed)) return false; + if (!trimmed) { + return false; + } + if (trimmed.includes("\0")) { + return false; + } + if (CONTROL_CHARS.test(trimmed)) { + return false; + } + if (SHELL_METACHARS.test(trimmed)) { + return false; + } + if (QUOTE_CHARS.test(trimmed)) { + return false; + } - if (isLikelyPath(trimmed)) return true; - if (trimmed.startsWith("-")) return false; + if (isLikelyPath(trimmed)) { + return true; + } + if (trimmed.startsWith("-")) { + return false; + } return BARE_NAME_PATTERN.test(trimmed); } diff --git a/src/infra/fetch.test.ts b/src/infra/fetch.test.ts index 6a41f71f5..6fb471106 100644 --- a/src/infra/fetch.test.ts +++ b/src/infra/fetch.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { wrapFetchWithAbortSignal } from "./fetch.js"; describe("wrapFetchWithAbortSignal", () => { @@ -30,10 +29,14 @@ describe("wrapFetchWithAbortSignal", () => { const fakeSignal = { aborted: false, addEventListener: (event: string, handler: () => void) => { - if (event === "abort") abortHandler = handler; + if (event === "abort") { + abortHandler = handler; + } }, removeEventListener: (event: string, handler: () => void) => { - if (event === "abort" && abortHandler === handler) abortHandler = null; + if (event === "abort" && abortHandler === handler) { + abortHandler = null; + } }, } as AbortSignal; diff --git a/src/infra/fetch.ts b/src/infra/fetch.ts index 61012e485..86fd789dd 100644 --- a/src/infra/fetch.ts +++ b/src/infra/fetch.ts @@ -14,8 +14,12 @@ function withDuplex( typeof Request !== "undefined" && input instanceof Request && input.body != null; - if (!hasInitBody && !hasRequestBody) return init; - if (init && "duplex" in (init as Record)) return init; + if (!hasInitBody && !hasRequestBody) { + return init; + } + if (init && "duplex" in (init as Record)) { + return init; + } return init ? ({ ...init, duplex: "half" as const } as RequestInitWithDuplex) : ({ duplex: "half" as const } as RequestInitWithDuplex); @@ -25,7 +29,9 @@ export function wrapFetchWithAbortSignal(fetchImpl: typeof fetch): typeof fetch const wrapped = ((input: RequestInfo | URL, init?: RequestInit) => { const patchedInit = withDuplex(init, input); const signal = patchedInit?.signal; - if (!signal) return fetchImpl(input, patchedInit); + if (!signal) { + return fetchImpl(input, patchedInit); + } if (typeof AbortSignal !== "undefined" && signal instanceof AbortSignal) { return fetchImpl(input, patchedInit); } @@ -62,6 +68,8 @@ export function wrapFetchWithAbortSignal(fetchImpl: typeof fetch): typeof fetch export function resolveFetch(fetchImpl?: typeof fetch): typeof fetch | undefined { const resolved = fetchImpl ?? globalThis.fetch; - if (!resolved) return undefined; + if (!resolved) { + return undefined; + } return wrapFetchWithAbortSignal(resolved); } diff --git a/src/infra/format-duration.ts b/src/infra/format-duration.ts index 04ff89593..b6cb694d7 100644 --- a/src/infra/format-duration.ts +++ b/src/infra/format-duration.ts @@ -7,7 +7,9 @@ export function formatDurationSeconds( ms: number, options: FormatDurationSecondsOptions = {}, ): string { - if (!Number.isFinite(ms)) return "unknown"; + if (!Number.isFinite(ms)) { + return "unknown"; + } const decimals = options.decimals ?? 1; const unit = options.unit ?? "s"; const seconds = Math.max(0, ms) / 1000; @@ -22,8 +24,12 @@ export type FormatDurationMsOptions = { }; export function formatDurationMs(ms: number, options: FormatDurationMsOptions = {}): string { - if (!Number.isFinite(ms)) return "unknown"; - if (ms < 1000) return `${ms}ms`; + if (!Number.isFinite(ms)) { + return "unknown"; + } + if (ms < 1000) { + return `${ms}ms`; + } return formatDurationSeconds(ms, { decimals: options.decimals ?? 2, unit: options.unit ?? "s", diff --git a/src/infra/fs-safe.ts b/src/infra/fs-safe.ts index 52a94b46f..fc8d4ce52 100644 --- a/src/infra/fs-safe.ts +++ b/src/infra/fs-safe.ts @@ -1,6 +1,6 @@ -import { constants as fsConstants } from "node:fs"; import type { Stats } from "node:fs"; import type { FileHandle } from "node:fs/promises"; +import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; @@ -55,7 +55,7 @@ export async function openFileWithinRoot(params: { } const supportsNoFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants; - const flags = fsConstants.O_RDONLY | (supportsNoFollow ? (fsConstants.O_NOFOLLOW as number) : 0); + const flags = fsConstants.O_RDONLY | (supportsNoFollow ? fsConstants.O_NOFOLLOW : 0); let handle: FileHandle; try { @@ -94,7 +94,9 @@ export async function openFileWithinRoot(params: { return { handle, realPath, stat }; } catch (err) { await handle.close().catch(() => {}); - if (err instanceof SafeOpenError) throw err; + if (err instanceof SafeOpenError) { + throw err; + } if (isNotFoundError(err)) { throw new SafeOpenError("not-found", "file not found"); } diff --git a/src/infra/gateway-lock.test.ts b/src/infra/gateway-lock.test.ts index 54c1db6b9..12a93fd58 100644 --- a/src/infra/gateway-lock.test.ts +++ b/src/infra/gateway-lock.test.ts @@ -3,22 +3,20 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it, vi } from "vitest"; - -import { acquireGatewayLock, GatewayLockError } from "./gateway-lock.js"; import { resolveConfigPath, resolveGatewayLockDir, resolveStateDir } from "../config/paths.js"; +import { acquireGatewayLock, GatewayLockError } from "./gateway-lock.js"; async function makeEnv() { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-gateway-lock-")); - const configPath = path.join(dir, "moltbot.json"); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-lock-")); + const configPath = path.join(dir, "openclaw.json"); await fs.writeFile(configPath, "{}", "utf8"); await fs.mkdir(resolveGatewayLockDir(), { recursive: true }); return { env: { ...process.env, - CLAWDBOT_STATE_DIR: dir, - CLAWDBOT_CONFIG_PATH: configPath, + OPENCLAW_STATE_DIR: dir, + OPENCLAW_CONFIG_PATH: configPath, }, cleanup: async () => { await fs.rm(dir, { recursive: true, force: true }); diff --git a/src/infra/gateway-lock.ts b/src/infra/gateway-lock.ts index aa65e7d81..ef89f42a1 100644 --- a/src/infra/gateway-lock.ts +++ b/src/infra/gateway-lock.ts @@ -1,8 +1,7 @@ import { createHash } from "node:crypto"; -import fs from "node:fs/promises"; import fsSync from "node:fs"; +import fs from "node:fs/promises"; import path from "node:path"; - import { resolveConfigPath, resolveGatewayLockDir, resolveStateDir } from "../config/paths.js"; const DEFAULT_TIMEOUT_MS = 5000; @@ -44,7 +43,9 @@ export class GatewayLockError extends Error { type LockOwnerStatus = "alive" | "dead" | "unknown"; function isAlive(pid: number): boolean { - if (!Number.isFinite(pid) || pid <= 0) return false; + if (!Number.isFinite(pid) || pid <= 0) { + return false; + } try { process.kill(pid, 0); return true; @@ -66,14 +67,14 @@ function parseProcCmdline(raw: string): string[] { function isGatewayArgv(args: string[]): boolean { const normalized = args.map(normalizeProcArg); - if (!normalized.includes("gateway")) return false; + if (!normalized.includes("gateway")) { + return false; + } const entryCandidates = [ "dist/index.js", - "dist/index.mjs", "dist/entry.js", - "moltbot.mjs", - "dist/entry.mjs", + "openclaw.mjs", "scripts/run-node.mjs", "src/index.ts", ]; @@ -82,7 +83,7 @@ function isGatewayArgv(args: string[]): boolean { } const exe = normalized[0] ?? ""; - return exe.endsWith("/moltbot") || exe === "moltbot"; + return exe.endsWith("/openclaw") || exe === "openclaw"; } function readLinuxCmdline(pid: number): string[] | null { @@ -98,7 +99,9 @@ function readLinuxStartTime(pid: number): number | null { try { const raw = fsSync.readFileSync(`/proc/${pid}/stat`, "utf8").trim(); const closeParen = raw.lastIndexOf(")"); - if (closeParen < 0) return null; + if (closeParen < 0) { + return null; + } const rest = raw.slice(closeParen + 1).trim(); const fields = rest.split(/\s+/); const startTime = Number.parseInt(fields[19] ?? "", 10); @@ -113,18 +116,26 @@ function resolveGatewayOwnerStatus( payload: LockPayload | null, platform: NodeJS.Platform, ): LockOwnerStatus { - if (!isAlive(pid)) return "dead"; - if (platform !== "linux") return "alive"; + if (!isAlive(pid)) { + return "dead"; + } + if (platform !== "linux") { + return "alive"; + } const payloadStartTime = payload?.startTime; if (Number.isFinite(payloadStartTime)) { const currentStartTime = readLinuxStartTime(pid); - if (currentStartTime == null) return "unknown"; + if (currentStartTime == null) { + return "unknown"; + } return currentStartTime === payloadStartTime ? "alive" : "dead"; } const args = readLinuxCmdline(pid); - if (!args) return "unknown"; + if (!args) { + return "unknown"; + } return isGatewayArgv(args) ? "alive" : "dead"; } @@ -132,9 +143,15 @@ async function readLockPayload(lockPath: string): Promise { try { const raw = await fs.readFile(lockPath, "utf8"); const parsed = JSON.parse(raw) as Partial; - if (typeof parsed.pid !== "number") return null; - if (typeof parsed.createdAt !== "string") return null; - if (typeof parsed.configPath !== "string") return null; + if (typeof parsed.pid !== "number") { + return null; + } + if (typeof parsed.createdAt !== "string") { + return null; + } + if (typeof parsed.configPath !== "string") { + return null; + } const startTime = typeof parsed.startTime === "number" ? parsed.startTime : undefined; return { pid: parsed.pid, @@ -162,7 +179,7 @@ export async function acquireGatewayLock( const env = opts.env ?? process.env; const allowInTests = opts.allowInTests === true; if ( - env.CLAWDBOT_ALLOW_MULTI_GATEWAY === "1" || + env.OPENCLAW_ALLOW_MULTI_GATEWAY === "1" || (!allowInTests && (env.VITEST || env.NODE_ENV === "test")) ) { return null; diff --git a/src/infra/git-commit.ts b/src/infra/git-commit.ts index da351f17f..996f8dad1 100644 --- a/src/infra/git-commit.ts +++ b/src/infra/git-commit.ts @@ -3,9 +3,13 @@ import { createRequire } from "node:module"; import path from "node:path"; const formatCommit = (value?: string | null) => { - if (!value) return null; + if (!value) { + return null; + } const trimmed = value.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } return trimmed.length > 7 ? trimmed.slice(0, 7) : trimmed; }; @@ -30,7 +34,9 @@ const resolveGitHead = (startDir: string) => { // ignore missing .git at this level } const parent = path.dirname(current); - if (parent === current) break; + if (parent === current) { + break; + } current = parent; } return null; @@ -64,7 +70,9 @@ const readCommitFromBuildInfo = () => { }; export const resolveCommitHash = (options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) => { - if (cachedCommit !== undefined) return cachedCommit; + if (cachedCommit !== undefined) { + return cachedCommit; + } const env = options.env ?? process.env; const envCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim(); const normalized = formatCommit(envCommit); diff --git a/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts b/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts index 18c1ab063..88c6b98e8 100644 --- a/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts +++ b/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts @@ -2,17 +2,17 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; +import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; +import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; +import { setWhatsAppRuntime } from "../../extensions/whatsapp/src/runtime.js"; import * as replyModule from "../auto-reply/reply.js"; -import type { MoltbotConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; -import { runHeartbeatOnce } from "./heartbeat-runner.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createPluginRuntime } from "../plugins/runtime/index.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; -import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; -import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; -import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; -import { setWhatsAppRuntime } from "../../extensions/whatsapp/src/runtime.js"; +import { runHeartbeatOnce } from "./heartbeat-runner.js"; // Avoid pulling optional runtime deps during isolated runs. vi.mock("jiti", () => ({ createJiti: () => () => ({}) })); @@ -31,11 +31,11 @@ beforeEach(() => { describe("resolveHeartbeatIntervalMs", () => { it("respects ackMaxChars for heartbeat acks", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -93,11 +93,11 @@ describe("resolveHeartbeatIntervalMs", () => { }); it("sends HEARTBEAT_OK when visibility.showOk is true", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -155,11 +155,11 @@ describe("resolveHeartbeatIntervalMs", () => { }); it("skips heartbeat LLM calls when visibility disables all output", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -222,11 +222,11 @@ describe("resolveHeartbeatIntervalMs", () => { }); it("skips delivery for markup-wrapped HEARTBEAT_OK", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -283,13 +283,13 @@ describe("resolveHeartbeatIntervalMs", () => { }); it("does not regress updatedAt when restoring heartbeat sessions", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { const originalUpdatedAt = 1000; const bumpedUpdatedAt = 2000; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -356,11 +356,11 @@ describe("resolveHeartbeatIntervalMs", () => { }); it("skips WhatsApp delivery when not linked or running", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -416,13 +416,13 @@ describe("resolveHeartbeatIntervalMs", () => { }); it("passes through accountId for telegram heartbeats", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = ""; try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -484,13 +484,13 @@ describe("resolveHeartbeatIntervalMs", () => { }); it("does not pre-resolve telegram accountId (allows config-only account tokens)", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = ""; try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, diff --git a/src/infra/heartbeat-runner.returns-default-unset.test.ts b/src/infra/heartbeat-runner.returns-default-unset.test.ts index cbe92ba93..a43ff6834 100644 --- a/src/infra/heartbeat-runner.returns-default-unset.test.ts +++ b/src/infra/heartbeat-runner.returns-default-unset.test.ts @@ -2,16 +2,23 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; +import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; +import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; +import { setWhatsAppRuntime } from "../../extensions/whatsapp/src/runtime.js"; import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js"; import * as replyModule from "../auto-reply/reply.js"; -import type { MoltbotConfig } from "../config/config.js"; import { resolveAgentIdFromSessionKey, resolveAgentMainSessionKey, resolveMainSessionKey, resolveStorePath, } from "../config/sessions.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; +import { createPluginRuntime } from "../plugins/runtime/index.js"; import { buildAgentPeerSessionKey } from "../routing/session-key.js"; +import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { isHeartbeatEnabledForAgent, resolveHeartbeatIntervalMs, @@ -19,13 +26,6 @@ import { runHeartbeatOnce, } from "./heartbeat-runner.js"; import { resolveHeartbeatDeliveryTarget } from "./outbound/targets.js"; -import { setActivePluginRegistry } from "../plugins/runtime.js"; -import { createPluginRuntime } from "../plugins/runtime/index.js"; -import { createTestRegistry } from "../test-utils/channel-plugins.js"; -import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; -import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; -import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; -import { setWhatsAppRuntime } from "../../extensions/whatsapp/src/runtime.js"; // Avoid pulling optional runtime deps during isolated runs. vi.mock("jiti", () => ({ createJiti: () => () => ({}) })); @@ -95,7 +95,7 @@ describe("resolveHeartbeatPrompt", () => { }); it("uses a trimmed override when configured", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { prompt: " ping " } } }, }; expect(resolveHeartbeatPrompt(cfg)).toBe("ping"); @@ -104,7 +104,7 @@ describe("resolveHeartbeatPrompt", () => { describe("isHeartbeatEnabledForAgent", () => { it("enables only explicit heartbeat agents when configured", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { every: "30m" } }, list: [{ id: "main" }, { id: "ops", heartbeat: { every: "1h" } }], @@ -115,7 +115,7 @@ describe("isHeartbeatEnabledForAgent", () => { }); it("falls back to default agent when no explicit heartbeat entries", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { every: "30m" } }, list: [{ id: "main" }, { id: "ops" }], @@ -133,7 +133,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }; it("respects target none", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { target: "none" } } }, }; expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual({ @@ -146,7 +146,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("uses last route by default", () => { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const entry = { ...baseEntry, lastChannel: "whatsapp" as const, @@ -162,7 +162,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("normalizes explicit WhatsApp targets when allowFrom is '*'", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { target: "whatsapp", to: "whatsapp:(555) 123" }, @@ -180,7 +180,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("skips when last route is webchat", () => { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const entry = { ...baseEntry, lastChannel: "webchat" as const, @@ -196,7 +196,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("applies allowFrom fallback for WhatsApp targets", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { target: "whatsapp", to: "+1999" } } }, channels: { whatsapp: { allowFrom: ["+1555", "+1666"] } }, }; @@ -216,7 +216,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("keeps WhatsApp group targets even with allowFrom set", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["+1555"] } }, }; const entry = { @@ -234,7 +234,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("normalizes prefixed WhatsApp group targets for heartbeat delivery", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["+1555"] } }, }; const entry = { @@ -252,7 +252,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("keeps explicit telegram targets", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { target: "telegram", to: "123" } } }, }; expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual({ @@ -265,7 +265,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { }); it("prefers per-agent heartbeat overrides when provided", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { target: "telegram", to: "123" } } }, }; const heartbeat = { target: "whatsapp", to: "+1555" } as const; @@ -287,7 +287,7 @@ describe("resolveHeartbeatDeliveryTarget", () => { describe("runHeartbeatOnce", () => { it("skips when agent heartbeat is not enabled", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { every: "30m" } }, list: [{ id: "main" }, { id: "ops", heartbeat: { every: "1h" } }], @@ -302,7 +302,7 @@ describe("runHeartbeatOnce", () => { }); it("skips outside active hours", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { userTimezone: "UTC", @@ -326,11 +326,11 @@ describe("runHeartbeatOnce", () => { }); it("uses the last non-empty payload for delivery", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -384,11 +384,11 @@ describe("runHeartbeatOnce", () => { }); it("uses per-agent heartbeat overrides and session keys", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { heartbeat: { every: "30m", prompt: "Default prompt" }, @@ -454,12 +454,12 @@ describe("runHeartbeatOnce", () => { }); it("runs heartbeats in the explicit session key when configured", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { const groupId = "120363401234567890@g.us"; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -537,11 +537,11 @@ describe("runHeartbeatOnce", () => { }); it("suppresses duplicate heartbeat payloads within 24h", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -593,11 +593,11 @@ describe("runHeartbeatOnce", () => { }); it("can include reasoning payloads when enabled", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -665,11 +665,11 @@ describe("runHeartbeatOnce", () => { }); it("delivers reasoning even when the main heartbeat reply is HEARTBEAT_OK", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, @@ -736,11 +736,11 @@ describe("runHeartbeatOnce", () => { }); it("loads the default agent session from templated stores", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, heartbeat: { every: "5m" } }, list: [{ id: "work", default: true }], @@ -800,7 +800,7 @@ describe("runHeartbeatOnce", () => { }); it("skips heartbeat when HEARTBEAT.md is effectively empty (saves API calls)", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const workspaceDir = path.join(tmpDir, "workspace"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); @@ -814,7 +814,7 @@ describe("runHeartbeatOnce", () => { "utf-8", ); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: workspaceDir, @@ -872,7 +872,7 @@ describe("runHeartbeatOnce", () => { }); it("runs heartbeat when HEARTBEAT.md has actionable content", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const workspaceDir = path.join(tmpDir, "workspace"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); @@ -886,7 +886,7 @@ describe("runHeartbeatOnce", () => { "utf-8", ); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: workspaceDir, @@ -942,7 +942,7 @@ describe("runHeartbeatOnce", () => { }); it("runs heartbeat when HEARTBEAT.md does not exist (lets LLM decide)", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const workspaceDir = path.join(tmpDir, "workspace"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); @@ -950,7 +950,7 @@ describe("runHeartbeatOnce", () => { await fs.mkdir(workspaceDir, { recursive: true }); // Don't create HEARTBEAT.md - it doesn't exist - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: workspaceDir, diff --git a/src/infra/heartbeat-runner.scheduler.test.ts b/src/infra/heartbeat-runner.scheduler.test.ts index 3cfd75c81..e95058880 100644 --- a/src/infra/heartbeat-runner.scheduler.test.ts +++ b/src/infra/heartbeat-runner.scheduler.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { startHeartbeatRunner } from "./heartbeat-runner.js"; describe("startHeartbeatRunner", () => { @@ -17,7 +17,7 @@ describe("startHeartbeatRunner", () => { const runner = startHeartbeatRunner({ cfg: { agents: { defaults: { heartbeat: { every: "30m" } } }, - } as MoltbotConfig, + } as OpenClawConfig, runOnce: runSpy, }); @@ -36,7 +36,7 @@ describe("startHeartbeatRunner", () => { { id: "ops", heartbeat: { every: "15m" } }, ], }, - } as MoltbotConfig); + } as OpenClawConfig); await vi.advanceTimersByTimeAsync(10 * 60_000 + 1_000); diff --git a/src/infra/heartbeat-runner.sender-prefers-delivery-target.test.ts b/src/infra/heartbeat-runner.sender-prefers-delivery-target.test.ts index 0ff1d7cd0..405d41877 100644 --- a/src/infra/heartbeat-runner.sender-prefers-delivery-target.test.ts +++ b/src/infra/heartbeat-runner.sender-prefers-delivery-target.test.ts @@ -2,19 +2,18 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; - -import * as replyModule from "../auto-reply/reply.js"; -import type { MoltbotConfig } from "../config/config.js"; -import { resolveMainSessionKey } from "../config/sessions.js"; -import { setActivePluginRegistry } from "../plugins/runtime.js"; -import { createPluginRuntime } from "../plugins/runtime/index.js"; -import { createTestRegistry } from "../test-utils/channel-plugins.js"; +import type { OpenClawConfig } from "../config/config.js"; import { slackPlugin } from "../../extensions/slack/src/channel.js"; import { setSlackRuntime } from "../../extensions/slack/src/runtime.js"; import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; import { setWhatsAppRuntime } from "../../extensions/whatsapp/src/runtime.js"; +import * as replyModule from "../auto-reply/reply.js"; +import { resolveMainSessionKey } from "../config/sessions.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; +import { createPluginRuntime } from "../plugins/runtime/index.js"; +import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { runHeartbeatOnce } from "./heartbeat-runner.js"; // Avoid pulling optional runtime deps during isolated runs. @@ -36,11 +35,11 @@ beforeEach(() => { describe("runHeartbeatOnce", () => { it("uses the delivery target as sender when lastTo differs", async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-hb-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); const storePath = path.join(tmpDir, "sessions.json"); const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { workspace: tmpDir, diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 8e0c9a4ee..9b6b77aa5 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -1,6 +1,10 @@ import fs from "node:fs/promises"; import path from "node:path"; - +import type { ReplyPayload } from "../auto-reply/types.js"; +import type { ChannelHeartbeatDeps } from "../channels/plugins/types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; +import type { OutboundSendDeps } from "./outbound/deliver.js"; import { resolveAgentConfig, resolveAgentWorkspaceDir, @@ -16,13 +20,10 @@ import { resolveHeartbeatPrompt as resolveHeartbeatPromptText, stripHeartbeatToken, } from "../auto-reply/heartbeat.js"; -import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; -import type { ReplyPayload } from "../auto-reply/types.js"; +import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js"; import { getChannelPlugin } from "../channels/plugins/index.js"; -import type { ChannelHeartbeatDeps } from "../channels/plugins/types.js"; import { parseDurationMs } from "../cli/parse-duration.js"; -import type { MoltbotConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; import { canonicalizeMainSessionAlias, @@ -33,14 +34,13 @@ import { saveSessionStore, updateSessionStore, } from "../config/sessions.js"; -import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; import { formatErrorMessage } from "../infra/errors.js"; import { peekSystemEvents } from "../infra/system-events.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { getQueueSize } from "../process/command-queue.js"; import { CommandLane } from "../process/lanes.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js"; import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js"; import { @@ -49,7 +49,6 @@ import { requestHeartbeatNow, setHeartbeatWakeHandler, } from "./heartbeat-wake.js"; -import type { OutboundSendDeps } from "./outbound/deliver.js"; import { deliverOutboundPayloads } from "./outbound/deliver.js"; import { resolveHeartbeatDeliveryTarget, @@ -97,7 +96,7 @@ const EXEC_EVENT_PROMPT = "Please relay the command output to the user in a helpful way. If the command succeeded, share the relevant output. " + "If it failed, explain what went wrong."; -function resolveActiveHoursTimezone(cfg: MoltbotConfig, raw?: string): string { +function resolveActiveHoursTimezone(cfg: OpenClawConfig, raw?: string): string { const trimmed = raw?.trim(); if (!trimmed || trimmed === "user") { return resolveUserTimezone(cfg.agents?.defaults?.userTimezone); @@ -115,13 +114,19 @@ function resolveActiveHoursTimezone(cfg: MoltbotConfig, raw?: string): string { } function parseActiveHoursTime(opts: { allow24: boolean }, raw?: string): number | null { - if (!raw || !ACTIVE_HOURS_TIME_PATTERN.test(raw)) return null; + if (!raw || !ACTIVE_HOURS_TIME_PATTERN.test(raw)) { + return null; + } const [hourStr, minuteStr] = raw.split(":"); const hour = Number(hourStr); const minute = Number(minuteStr); - if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null; + if (!Number.isFinite(hour) || !Number.isFinite(minute)) { + return null; + } if (hour === 24) { - if (!opts.allow24 || minute !== 0) return null; + if (!opts.allow24 || minute !== 0) { + return null; + } return 24 * 60; } return hour * 60 + minute; @@ -137,11 +142,15 @@ function resolveMinutesInTimeZone(nowMs: number, timeZone: string): number | nul }).formatToParts(new Date(nowMs)); const map: Record = {}; for (const part of parts) { - if (part.type !== "literal") map[part.type] = part.value; + if (part.type !== "literal") { + map[part.type] = part.value; + } } const hour = Number(map.hour); const minute = Number(map.minute); - if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null; + if (!Number.isFinite(hour) || !Number.isFinite(minute)) { + return null; + } return hour * 60 + minute; } catch { return null; @@ -149,21 +158,29 @@ function resolveMinutesInTimeZone(nowMs: number, timeZone: string): number | nul } function isWithinActiveHours( - cfg: MoltbotConfig, + cfg: OpenClawConfig, heartbeat?: HeartbeatConfig, nowMs?: number, ): boolean { const active = heartbeat?.activeHours; - if (!active) return true; + if (!active) { + return true; + } const startMin = parseActiveHoursTime({ allow24: false }, active.start); const endMin = parseActiveHoursTime({ allow24: true }, active.end); - if (startMin === null || endMin === null) return true; - if (startMin === endMin) return true; + if (startMin === null || endMin === null) { + return true; + } + if (startMin === endMin) { + return true; + } const timeZone = resolveActiveHoursTimezone(cfg, active.timezone); const currentMin = resolveMinutesInTimeZone(nowMs ?? Date.now(), timeZone); - if (currentMin === null) return true; + if (currentMin === null) { + return true; + } if (endMin > startMin) { return currentMin >= startMin && currentMin < endMin; @@ -181,15 +198,15 @@ type HeartbeatAgentState = { export type HeartbeatRunner = { stop: () => void; - updateConfig: (cfg: MoltbotConfig) => void; + updateConfig: (cfg: OpenClawConfig) => void; }; -function hasExplicitHeartbeatAgents(cfg: MoltbotConfig) { +function hasExplicitHeartbeatAgents(cfg: OpenClawConfig) { const list = cfg.agents?.list ?? []; return list.some((entry) => Boolean(entry?.heartbeat)); } -export function isHeartbeatEnabledForAgent(cfg: MoltbotConfig, agentId?: string): boolean { +export function isHeartbeatEnabledForAgent(cfg: OpenClawConfig, agentId?: string): boolean { const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg)); const list = cfg.agents?.list ?? []; const hasExplicit = hasExplicitHeartbeatAgents(cfg); @@ -201,16 +218,23 @@ export function isHeartbeatEnabledForAgent(cfg: MoltbotConfig, agentId?: string) return resolvedAgentId === resolveDefaultAgentId(cfg); } -function resolveHeartbeatConfig(cfg: MoltbotConfig, agentId?: string): HeartbeatConfig | undefined { +function resolveHeartbeatConfig( + cfg: OpenClawConfig, + agentId?: string, +): HeartbeatConfig | undefined { const defaults = cfg.agents?.defaults?.heartbeat; - if (!agentId) return defaults; + if (!agentId) { + return defaults; + } const overrides = resolveAgentConfig(cfg, agentId)?.heartbeat; - if (!defaults && !overrides) return overrides; + if (!defaults && !overrides) { + return overrides; + } return { ...defaults, ...overrides }; } export function resolveHeartbeatSummaryForAgent( - cfg: MoltbotConfig, + cfg: OpenClawConfig, agentId?: string, ): HeartbeatSummary { const defaults = cfg.agents?.defaults?.heartbeat; @@ -257,7 +281,7 @@ export function resolveHeartbeatSummaryForAgent( }; } -function resolveHeartbeatAgents(cfg: MoltbotConfig): HeartbeatAgent[] { +function resolveHeartbeatAgents(cfg: OpenClawConfig): HeartbeatAgent[] { const list = cfg.agents?.list ?? []; if (hasExplicitHeartbeatAgents(cfg)) { return list @@ -273,7 +297,7 @@ function resolveHeartbeatAgents(cfg: MoltbotConfig): HeartbeatAgent[] { } export function resolveHeartbeatIntervalMs( - cfg: MoltbotConfig, + cfg: OpenClawConfig, overrideEvery?: string, heartbeat?: HeartbeatConfig, ) { @@ -282,24 +306,30 @@ export function resolveHeartbeatIntervalMs( heartbeat?.every ?? cfg.agents?.defaults?.heartbeat?.every ?? DEFAULT_HEARTBEAT_EVERY; - if (!raw) return null; + if (!raw) { + return null; + } const trimmed = String(raw).trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } let ms: number; try { ms = parseDurationMs(trimmed, { defaultUnit: "m" }); } catch { return null; } - if (ms <= 0) return null; + if (ms <= 0) { + return null; + } return ms; } -export function resolveHeartbeatPrompt(cfg: MoltbotConfig, heartbeat?: HeartbeatConfig) { +export function resolveHeartbeatPrompt(cfg: OpenClawConfig, heartbeat?: HeartbeatConfig) { return resolveHeartbeatPromptText(heartbeat?.prompt ?? cfg.agents?.defaults?.heartbeat?.prompt); } -function resolveHeartbeatAckMaxChars(cfg: MoltbotConfig, heartbeat?: HeartbeatConfig) { +function resolveHeartbeatAckMaxChars(cfg: OpenClawConfig, heartbeat?: HeartbeatConfig) { return Math.max( 0, heartbeat?.ackMaxChars ?? @@ -309,7 +339,7 @@ function resolveHeartbeatAckMaxChars(cfg: MoltbotConfig, heartbeat?: HeartbeatCo } function resolveHeartbeatSession( - cfg: MoltbotConfig, + cfg: OpenClawConfig, agentId?: string, heartbeat?: HeartbeatConfig, ) { @@ -360,11 +390,17 @@ function resolveHeartbeatSession( function resolveHeartbeatReplyPayload( replyResult: ReplyPayload | ReplyPayload[] | undefined, ): ReplyPayload | undefined { - if (!replyResult) return undefined; - if (!Array.isArray(replyResult)) return replyResult; + if (!replyResult) { + return undefined; + } + if (!Array.isArray(replyResult)) { + return replyResult; + } for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) { const payload = replyResult[idx]; - if (!payload) continue; + if (!payload) { + continue; + } if (payload.text || payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) { return payload; } @@ -388,17 +424,27 @@ async function restoreHeartbeatUpdatedAt(params: { updatedAt?: number; }) { const { storePath, sessionKey, updatedAt } = params; - if (typeof updatedAt !== "number") return; + if (typeof updatedAt !== "number") { + return; + } const store = loadSessionStore(storePath); const entry = store[sessionKey]; - if (!entry) return; + if (!entry) { + return; + } const nextUpdatedAt = Math.max(entry.updatedAt ?? 0, updatedAt); - if (entry.updatedAt === nextUpdatedAt) return; + if (entry.updatedAt === nextUpdatedAt) { + return; + } await updateSessionStore(storePath, (nextStore) => { const nextEntry = nextStore[sessionKey] ?? entry; - if (!nextEntry) return; + if (!nextEntry) { + return; + } const resolvedUpdatedAt = Math.max(nextEntry.updatedAt ?? 0, updatedAt); - if (nextEntry.updatedAt === resolvedUpdatedAt) return; + if (nextEntry.updatedAt === resolvedUpdatedAt) { + return; + } nextStore[sessionKey] = { ...nextEntry, updatedAt: resolvedUpdatedAt }; }); } @@ -428,7 +474,7 @@ function normalizeHeartbeatReply( } export async function runHeartbeatOnce(opts: { - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; agentId?: string; heartbeat?: HeartbeatConfig; reason?: string; @@ -522,7 +568,9 @@ export async function runHeartbeatOnce(opts: { visibility.showOk && delivery.channel !== "none" && delivery.to, ); const maybeSendHeartbeatOk = async () => { - if (!canAttemptHeartbeatOk || delivery.channel === "none" || !delivery.to) return false; + if (!canAttemptHeartbeatOk || delivery.channel === "none" || !delivery.to) { + return false; + } const heartbeatPlugin = getChannelPlugin(delivery.channel); if (heartbeatPlugin?.heartbeat?.checkReady) { const readiness = await heartbeatPlugin.heartbeat.checkReady({ @@ -530,7 +578,9 @@ export async function runHeartbeatOnce(opts: { accountId: delivery.accountId, deps: opts.deps, }); - if (!readiness.ok) return false; + if (!readiness.ok) { + return false; + } } await deliverOutboundPayloads({ cfg, @@ -755,7 +805,7 @@ export async function runHeartbeatOnce(opts: { } export function startHeartbeatRunner(opts: { - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; runtime?: RuntimeEnv; abortSignal?: AbortSignal; runOnce?: typeof runHeartbeatOnce; @@ -782,18 +832,26 @@ export function startHeartbeatRunner(opts: { }; const scheduleNext = () => { - if (state.stopped) return; + if (state.stopped) { + return; + } if (state.timer) { clearTimeout(state.timer); state.timer = null; } - if (state.agents.size === 0) return; + if (state.agents.size === 0) { + return; + } const now = Date.now(); let nextDue = Number.POSITIVE_INFINITY; for (const agent of state.agents.values()) { - if (agent.nextDueMs < nextDue) nextDue = agent.nextDueMs; + if (agent.nextDueMs < nextDue) { + nextDue = agent.nextDueMs; + } + } + if (!Number.isFinite(nextDue)) { + return; } - if (!Number.isFinite(nextDue)) return; const delay = Math.max(0, nextDue - now); state.timer = setTimeout(() => { requestHeartbeatNow({ reason: "interval", coalesceMs: 0 }); @@ -801,8 +859,10 @@ export function startHeartbeatRunner(opts: { state.timer.unref?.(); }; - const updateConfig = (cfg: MoltbotConfig) => { - if (state.stopped) return; + const updateConfig = (cfg: OpenClawConfig) => { + if (state.stopped) { + return; + } const now = Date.now(); const prevAgents = state.agents; const prevEnabled = prevAgents.size > 0; @@ -810,7 +870,9 @@ export function startHeartbeatRunner(opts: { const intervals: number[] = []; for (const agent of resolveHeartbeatAgents(cfg)) { const intervalMs = resolveHeartbeatIntervalMs(cfg, undefined, agent.heartbeat); - if (!intervalMs) continue; + if (!intervalMs) { + continue; + } intervals.push(intervalMs); const prevState = prevAgents.get(agent.agentId); const nextDueMs = resolveNextDue(now, intervalMs, prevState); @@ -877,11 +939,15 @@ export function startHeartbeatRunner(opts: { agent.lastRunMs = now; agent.nextDueMs = now + agent.intervalMs; } - if (res.status === "ran") ran = true; + if (res.status === "ran") { + ran = true; + } } scheduleNext(); - if (ran) return { status: "ran", durationMs: Date.now() - startedAt }; + if (ran) { + return { status: "ran", durationMs: Date.now() - startedAt }; + } return { status: "skipped", reason: isInterval ? "not-due" : "disabled" }; }; @@ -891,7 +957,9 @@ export function startHeartbeatRunner(opts: { const cleanup = () => { state.stopped = true; setHeartbeatWakeHandler(null); - if (state.timer) clearTimeout(state.timer); + if (state.timer) { + clearTimeout(state.timer); + } state.timer = null; }; diff --git a/src/infra/heartbeat-visibility.test.ts b/src/infra/heartbeat-visibility.test.ts index 33b795207..1a7ab6df7 100644 --- a/src/infra/heartbeat-visibility.test.ts +++ b/src/infra/heartbeat-visibility.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js"; describe("resolveHeartbeatVisibility", () => { it("returns default values when no config is provided", () => { - const cfg = {} as MoltbotConfig; + const cfg = {} as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "telegram" }); expect(result).toEqual({ @@ -25,7 +25,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "telegram" }); @@ -52,7 +52,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "telegram" }); @@ -88,7 +88,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, @@ -120,7 +120,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, @@ -151,7 +151,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "telegram" }); @@ -174,7 +174,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, @@ -195,7 +195,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "whatsapp" }); @@ -215,7 +215,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "discord" }); @@ -237,7 +237,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "slack" }); @@ -259,7 +259,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "webchat" }); @@ -271,7 +271,7 @@ describe("resolveHeartbeatVisibility", () => { }); it("webchat returns defaults when no channel defaults configured", () => { - const cfg = {} as MoltbotConfig; + const cfg = {} as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, channel: "webchat" }); @@ -291,7 +291,7 @@ describe("resolveHeartbeatVisibility", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = resolveHeartbeatVisibility({ cfg, diff --git a/src/infra/heartbeat-visibility.ts b/src/infra/heartbeat-visibility.ts index 808e818b4..d7b7bc12e 100644 --- a/src/infra/heartbeat-visibility.ts +++ b/src/infra/heartbeat-visibility.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { ChannelHeartbeatVisibilityConfig } from "../config/types.channels.js"; import type { GatewayMessageChannel } from "../utils/message-channel.js"; @@ -20,7 +20,7 @@ const DEFAULT_VISIBILITY: ResolvedHeartbeatVisibility = { * For webchat, uses channels.defaults.heartbeat since webchat doesn't have per-channel config. */ export function resolveHeartbeatVisibility(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: GatewayMessageChannel; accountId?: string; }): ResolvedHeartbeatVisibility { diff --git a/src/infra/heartbeat-wake.ts b/src/infra/heartbeat-wake.ts index eb26bf499..8e981ffc1 100644 --- a/src/infra/heartbeat-wake.ts +++ b/src/infra/heartbeat-wake.ts @@ -15,12 +15,16 @@ const DEFAULT_COALESCE_MS = 250; const DEFAULT_RETRY_MS = 1_000; function schedule(coalesceMs: number) { - if (timer) return; + if (timer) { + return; + } timer = setTimeout(async () => { timer = null; scheduled = false; const active = handler; - if (!active) return; + if (!active) { + return; + } if (running) { scheduled = true; schedule(coalesceMs); @@ -43,7 +47,9 @@ function schedule(coalesceMs: number) { schedule(DEFAULT_RETRY_MS); } finally { running = false; - if (pendingReason || scheduled) schedule(coalesceMs); + if (pendingReason || scheduled) { + schedule(coalesceMs); + } } }, coalesceMs); timer.unref?.(); diff --git a/src/infra/is-main.test.ts b/src/infra/is-main.test.ts index 5bb41a336..a94c2a816 100644 --- a/src/infra/is-main.test.ts +++ b/src/infra/is-main.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { isMainModule } from "./is-main.js"; describe("isMainModule", () => { @@ -28,7 +27,7 @@ describe("isMainModule", () => { it("returns false when running under PM2 but this module is imported", () => { expect( isMainModule({ - currentFile: "/repo/node_modules/moltbot/dist/index.js", + currentFile: "/repo/node_modules/openclaw/dist/index.js", argv: ["node", "/repo/app.js"], cwd: "/repo", env: { pm_exec_path: "/repo/app.js", pm_id: "0" }, diff --git a/src/infra/is-main.ts b/src/infra/is-main.ts index 15ccd38c4..23c036cc3 100644 --- a/src/infra/is-main.ts +++ b/src/infra/is-main.ts @@ -9,7 +9,9 @@ type IsMainModuleOptions = { }; function normalizePathCandidate(candidate: string | undefined, cwd: string): string | undefined { - if (!candidate) return undefined; + if (!candidate) { + return undefined; + } const resolved = path.resolve(cwd, candidate); try { diff --git a/src/infra/json-file.ts b/src/infra/json-file.ts index 19c8169f7..f34259df2 100644 --- a/src/infra/json-file.ts +++ b/src/infra/json-file.ts @@ -3,7 +3,9 @@ import path from "node:path"; export function loadJsonFile(pathname: string): unknown { try { - if (!fs.existsSync(pathname)) return undefined; + if (!fs.existsSync(pathname)) { + return undefined; + } const raw = fs.readFileSync(pathname, "utf8"); return JSON.parse(raw) as unknown; } catch { diff --git a/src/infra/machine-name.ts b/src/infra/machine-name.ts index 4c9211d5f..4d31be7f2 100644 --- a/src/infra/machine-name.ts +++ b/src/infra/machine-name.ts @@ -24,21 +24,27 @@ function fallbackHostName() { os .hostname() .replace(/\.local$/i, "") - .trim() || "moltbot" + .trim() || "openclaw" ); } export async function getMachineDisplayName(): Promise { - if (cachedPromise) return cachedPromise; + if (cachedPromise) { + return cachedPromise; + } cachedPromise = (async () => { if (process.env.VITEST || process.env.NODE_ENV === "test") { return fallbackHostName(); } if (process.platform === "darwin") { const computerName = await tryScutil("ComputerName"); - if (computerName) return computerName; + if (computerName) { + return computerName; + } const localHostName = await tryScutil("LocalHostName"); - if (localHostName) return localHostName; + if (localHostName) { + return localHostName; + } } return fallbackHostName(); })(); diff --git a/src/infra/net/ssrf.pinning.test.ts b/src/infra/net/ssrf.pinning.test.ts index 42bc54b66..653996083 100644 --- a/src/infra/net/ssrf.pinning.test.ts +++ b/src/infra/net/ssrf.pinning.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { createPinnedLookup, resolvePinnedHostname } from "./ssrf.js"; describe("ssrf pinning", () => { @@ -15,8 +14,11 @@ describe("ssrf pinning", () => { const first = await new Promise<{ address: string; family?: number }>((resolve, reject) => { pinned.lookup("example.com", (err, address, family) => { - if (err) reject(err); - else resolve({ address: address as string, family }); + if (err) { + reject(err); + } else { + resolve({ address: address, family }); + } }); }); expect(first.address).toBe("93.184.216.34"); @@ -24,8 +26,11 @@ describe("ssrf pinning", () => { const all = await new Promise((resolve, reject) => { pinned.lookup("example.com", { all: true }, (err, addresses) => { - if (err) reject(err); - else resolve(addresses); + if (err) { + reject(err); + } else { + resolve(addresses); + } }); }); expect(Array.isArray(all)).toBe(true); @@ -52,8 +57,11 @@ describe("ssrf pinning", () => { const result = await new Promise<{ address: string }>((resolve, reject) => { lookup("other.test", (err, address) => { - if (err) reject(err); - else resolve({ address: address as string }); + if (err) { + reject(err); + } else { + resolve({ address: address }); + } }); }); diff --git a/src/infra/net/ssrf.ts b/src/infra/net/ssrf.ts index 297df0f03..026113706 100644 --- a/src/infra/net/ssrf.ts +++ b/src/infra/net/ssrf.ts @@ -1,5 +1,5 @@ -import { lookup as dnsLookup } from "node:dns/promises"; import { lookup as dnsLookupCb, type LookupAddress } from "node:dns"; +import { lookup as dnsLookup } from "node:dns/promises"; import { Agent, type Dispatcher } from "undici"; type LookupCallback = ( @@ -30,9 +30,13 @@ function normalizeHostname(hostname: string): string { function parseIpv4(address: string): number[] | null { const parts = address.split("."); - if (parts.length !== 4) return null; + if (parts.length !== 4) { + return null; + } const numbers = parts.map((part) => Number.parseInt(part, 10)); - if (numbers.some((value) => Number.isNaN(value) || value < 0 || value > 255)) return null; + if (numbers.some((value) => Number.isNaN(value) || value < 0 || value > 255)) { + return null; + } return numbers; } @@ -43,10 +47,14 @@ function parseIpv4FromMappedIpv6(mapped: string): number[] | null { const parts = mapped.split(":").filter(Boolean); if (parts.length === 1) { const value = Number.parseInt(parts[0], 16); - if (Number.isNaN(value) || value < 0 || value > 0xffff_ffff) return null; + if (Number.isNaN(value) || value < 0 || value > 0xffff_ffff) { + return null; + } return [(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff]; } - if (parts.length !== 2) return null; + if (parts.length !== 2) { + return null; + } const high = Number.parseInt(parts[0], 16); const low = Number.parseInt(parts[1], 16); if ( @@ -65,13 +73,27 @@ function parseIpv4FromMappedIpv6(mapped: string): number[] | null { function isPrivateIpv4(parts: number[]): boolean { const [octet1, octet2] = parts; - if (octet1 === 0) return true; - if (octet1 === 10) return true; - if (octet1 === 127) return true; - if (octet1 === 169 && octet2 === 254) return true; - if (octet1 === 172 && octet2 >= 16 && octet2 <= 31) return true; - if (octet1 === 192 && octet2 === 168) return true; - if (octet1 === 100 && octet2 >= 64 && octet2 <= 127) return true; + if (octet1 === 0) { + return true; + } + if (octet1 === 10) { + return true; + } + if (octet1 === 127) { + return true; + } + if (octet1 === 169 && octet2 === 254) { + return true; + } + if (octet1 === 172 && octet2 >= 16 && octet2 <= 31) { + return true; + } + if (octet1 === 192 && octet2 === 168) { + return true; + } + if (octet1 === 100 && octet2 >= 64 && octet2 <= 127) { + return true; + } return false; } @@ -80,28 +102,40 @@ export function isPrivateIpAddress(address: string): boolean { if (normalized.startsWith("[") && normalized.endsWith("]")) { normalized = normalized.slice(1, -1); } - if (!normalized) return false; + if (!normalized) { + return false; + } if (normalized.startsWith("::ffff:")) { const mapped = normalized.slice("::ffff:".length); const ipv4 = parseIpv4FromMappedIpv6(mapped); - if (ipv4) return isPrivateIpv4(ipv4); + if (ipv4) { + return isPrivateIpv4(ipv4); + } } if (normalized.includes(":")) { - if (normalized === "::" || normalized === "::1") return true; + if (normalized === "::" || normalized === "::1") { + return true; + } return PRIVATE_IPV6_PREFIXES.some((prefix) => normalized.startsWith(prefix)); } const ipv4 = parseIpv4(normalized); - if (!ipv4) return false; + if (!ipv4) { + return false; + } return isPrivateIpv4(ipv4); } export function isBlockedHostname(hostname: string): boolean { const normalized = normalizeHostname(hostname); - if (!normalized) return false; - if (BLOCKED_HOSTNAMES.has(normalized)) return true; + if (!normalized) { + return false; + } + if (BLOCKED_HOSTNAMES.has(normalized)) { + return true; + } return ( normalized.endsWith(".localhost") || normalized.endsWith(".local") || @@ -134,7 +168,9 @@ export function createPinnedLookup(params: { return ((host: string, options?: unknown, callback?: unknown) => { const cb: LookupCallback = typeof options === "function" ? (options as LookupCallback) : (callback as LookupCallback); - if (!cb) return; + if (!cb) { + return; + } const normalized = normalizeHostname(host); if (!normalized || normalized !== normalizedHost) { if (typeof options === "function" || options === undefined) { @@ -219,7 +255,9 @@ export function createPinnedDispatcher(pinned: PinnedHostname): Dispatcher { } export async function closeDispatcher(dispatcher?: Dispatcher | null): Promise { - if (!dispatcher) return; + if (!dispatcher) { + return; + } const candidate = dispatcher as { close?: () => Promise | void; destroy?: () => void }; try { if (typeof candidate.close === "function") { diff --git a/src/infra/node-pairing.ts b/src/infra/node-pairing.ts index f852ff420..0d1089e82 100644 --- a/src/infra/node-pairing.ts +++ b/src/infra/node-pairing.ts @@ -149,8 +149,8 @@ function newToken() { export async function listNodePairing(baseDir?: string): Promise { const state = await loadState(baseDir); - const pending = Object.values(state.pendingById).sort((a, b) => b.ts - a.ts); - const paired = Object.values(state.pairedByNodeId).sort( + const pending = Object.values(state.pendingById).toSorted((a, b) => b.ts - a.ts); + const paired = Object.values(state.pairedByNodeId).toSorted( (a, b) => b.approvedAtMs - a.approvedAtMs, ); return { pending, paired }; @@ -216,7 +216,9 @@ export async function approveNodePairing( return await withLock(async () => { const state = await loadState(baseDir); const pending = state.pendingById[requestId]; - if (!pending) return null; + if (!pending) { + return null; + } const now = Date.now(); const existing = state.pairedByNodeId[pending.nodeId]; @@ -252,7 +254,9 @@ export async function rejectNodePairing( return await withLock(async () => { const state = await loadState(baseDir); const pending = state.pendingById[requestId]; - if (!pending) return null; + if (!pending) { + return null; + } delete state.pendingById[requestId]; await persistState(state, baseDir); return { requestId, nodeId: pending.nodeId }; @@ -267,7 +271,9 @@ export async function verifyNodeToken( const state = await loadState(baseDir); const normalized = normalizeNodeId(nodeId); const node = state.pairedByNodeId[normalized]; - if (!node) return { ok: false }; + if (!node) { + return { ok: false }; + } return node.token === token ? { ok: true, node } : { ok: false }; } @@ -280,7 +286,9 @@ export async function updatePairedNodeMetadata( const state = await loadState(baseDir); const normalized = normalizeNodeId(nodeId); const existing = state.pairedByNodeId[normalized]; - if (!existing) return; + if (!existing) { + return; + } const next: NodePairingPairedNode = { ...existing, @@ -313,9 +321,13 @@ export async function renamePairedNode( const state = await loadState(baseDir); const normalized = normalizeNodeId(nodeId); const existing = state.pairedByNodeId[normalized]; - if (!existing) return null; + if (!existing) { + return null; + } const trimmed = displayName.trim(); - if (!trimmed) throw new Error("displayName required"); + if (!trimmed) { + throw new Error("displayName required"); + } const next: NodePairingPairedNode = { ...existing, displayName: trimmed }; state.pairedByNodeId[normalized] = next; await persistState(state, baseDir); diff --git a/src/infra/node-shell.test.ts b/src/infra/node-shell.test.ts index 8f95a29a8..55683eaba 100644 --- a/src/infra/node-shell.test.ts +++ b/src/infra/node-shell.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { buildNodeShellCommand } from "./node-shell.js"; describe("buildNodeShellCommand", () => { diff --git a/src/infra/moltbot-root.ts b/src/infra/openclaw-root.ts similarity index 86% rename from src/infra/moltbot-root.ts rename to src/infra/openclaw-root.ts index 950ce46e2..157ffbc30 100644 --- a/src/infra/moltbot-root.ts +++ b/src/infra/openclaw-root.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -const CORE_PACKAGE_NAMES = new Set(["moltbot", "moltbot"]); +const CORE_PACKAGE_NAMES = new Set(["openclaw"]); async function readPackageName(dir: string): Promise { try { @@ -18,9 +18,13 @@ async function findPackageRoot(startDir: string, maxDepth = 12): Promise { - if (platform === "darwin") return `macos ${macosVersion()} (${arch})`; - if (platform === "win32") return `windows ${release} (${arch})`; + if (platform === "darwin") { + return `macos ${macosVersion()} (${arch})`; + } + if (platform === "win32") { + return `windows ${release} (${arch})`; + } return `${platform} ${release} (${arch})`; })(); return { platform, arch, release, label }; diff --git a/src/infra/outbound/agent-delivery.test.ts b/src/infra/outbound/agent-delivery.test.ts index 27a10b58c..ac76d327a 100644 --- a/src/infra/outbound/agent-delivery.test.ts +++ b/src/infra/outbound/agent-delivery.test.ts @@ -12,7 +12,7 @@ vi.mock("./targets.js", async () => { }; }); -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { resolveAgentDeliveryPlan, resolveAgentOutboundTarget } from "./agent-delivery.js"; describe("agent delivery helpers", () => { @@ -45,7 +45,7 @@ describe("agent delivery helpers", () => { }); const resolved = resolveAgentOutboundTarget({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, plan, targetMode: "implicit", }); @@ -68,7 +68,7 @@ describe("agent delivery helpers", () => { mocks.resolveOutboundTarget.mockClear(); const resolved = resolveAgentOutboundTarget({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, plan, targetMode: "explicit", validateExplicitTarget: false, diff --git a/src/infra/outbound/agent-delivery.ts b/src/infra/outbound/agent-delivery.ts index 2c83c7086..c2398943d 100644 --- a/src/infra/outbound/agent-delivery.ts +++ b/src/infra/outbound/agent-delivery.ts @@ -1,6 +1,8 @@ -import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js"; import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; +import type { OutboundTargetResolution } from "./targets.js"; +import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js"; import { normalizeAccountId } from "../../utils/account-id.js"; import { INTERNAL_MESSAGE_CHANNEL, @@ -14,8 +16,6 @@ import { resolveSessionDeliveryTarget, type SessionDeliveryTarget, } from "./targets.js"; -import type { MoltbotConfig } from "../../config/config.js"; -import type { OutboundTargetResolution } from "./targets.js"; export type AgentDeliveryPlan = { baseDelivery: SessionDeliveryTarget; @@ -52,7 +52,9 @@ export function resolveAgentDeliveryPlan(params: { }); const resolvedChannel = (() => { - if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL; + if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) { + return INTERNAL_MESSAGE_CHANNEL; + } if (requestedChannel === "last") { if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) { return baseDelivery.channel; @@ -60,7 +62,9 @@ export function resolveAgentDeliveryPlan(params: { return params.wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL; } - if (isGatewayMessageChannel(requestedChannel)) return requestedChannel; + if (isGatewayMessageChannel(requestedChannel)) { + return requestedChannel; + } if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) { return baseDelivery.channel; @@ -98,7 +102,7 @@ export function resolveAgentDeliveryPlan(params: { } export function resolveAgentOutboundTarget(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; plan: AgentDeliveryPlan; targetMode?: ChannelOutboundTargetMode; validateExplicitTarget?: boolean; diff --git a/src/infra/outbound/channel-adapters.ts b/src/infra/outbound/channel-adapters.ts index b66d1edbf..c48fbb395 100644 --- a/src/infra/outbound/channel-adapters.ts +++ b/src/infra/outbound/channel-adapters.ts @@ -19,6 +19,8 @@ const DISCORD_ADAPTER: ChannelMessageAdapter = { }; export function getChannelMessageAdapter(channel: ChannelId): ChannelMessageAdapter { - if (channel === "discord") return DISCORD_ADAPTER; + if (channel === "discord") { + return DISCORD_ADAPTER; + } return DEFAULT_ADAPTER; } diff --git a/src/infra/outbound/channel-selection.ts b/src/infra/outbound/channel-selection.ts index 833603ff3..6ef5d1617 100644 --- a/src/infra/outbound/channel-selection.ts +++ b/src/infra/outbound/channel-selection.ts @@ -1,6 +1,6 @@ -import { listChannelPlugins } from "../../channels/plugins/index.js"; import type { ChannelPlugin } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { listChannelPlugins } from "../../channels/plugins/index.js"; import { listDeliverableMessageChannels, type DeliverableMessageChannel, @@ -16,44 +16,56 @@ function isKnownChannel(value: string): boolean { } function isAccountEnabled(account: unknown): boolean { - if (!account || typeof account !== "object") return true; + if (!account || typeof account !== "object") { + return true; + } const enabled = (account as { enabled?: boolean }).enabled; return enabled !== false; } -async function isPluginConfigured(plugin: ChannelPlugin, cfg: MoltbotConfig): Promise { +async function isPluginConfigured(plugin: ChannelPlugin, cfg: OpenClawConfig): Promise { const accountIds = plugin.config.listAccountIds(cfg); - if (accountIds.length === 0) return false; + if (accountIds.length === 0) { + return false; + } for (const accountId of accountIds) { const account = plugin.config.resolveAccount(cfg, accountId); const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : isAccountEnabled(account); - if (!enabled) continue; - if (!plugin.config.isConfigured) return true; + if (!enabled) { + continue; + } + if (!plugin.config.isConfigured) { + return true; + } const configured = await plugin.config.isConfigured(account, cfg); - if (configured) return true; + if (configured) { + return true; + } } return false; } export async function listConfiguredMessageChannels( - cfg: MoltbotConfig, + cfg: OpenClawConfig, ): Promise { const channels: MessageChannelId[] = []; for (const plugin of listChannelPlugins()) { - if (!isKnownChannel(plugin.id)) continue; + if (!isKnownChannel(plugin.id)) { + continue; + } if (await isPluginConfigured(plugin, cfg)) { - channels.push(plugin.id as MessageChannelId); + channels.push(plugin.id); } } return channels; } export async function resolveMessageChannelSelection(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel?: string | null; }): Promise<{ channel: MessageChannelId; configured: MessageChannelId[] }> { const normalized = normalizeMessageChannel(params.channel); diff --git a/src/infra/outbound/channel-target.ts b/src/infra/outbound/channel-target.ts index dae736abf..21b577e7c 100644 --- a/src/infra/outbound/channel-target.ts +++ b/src/infra/outbound/channel-target.ts @@ -24,7 +24,9 @@ export function applyTargetToParams(params: { throw new Error("Use `target` for actions that accept a destination."); } - if (!target) return; + if (!target) { + return; + } if (mode === "channelId") { params.args.channelId = target; return; diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index ebbe30c44..417e037f0 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -1,11 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { signalOutbound } from "../../channels/plugins/outbound/signal.js"; import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js"; import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js"; -import { markdownToSignalTextChunks } from "../../signal/format.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { markdownToSignalTextChunks } from "../../signal/format.js"; import { createIMessageTestPlugin, createOutboundTestPlugin, @@ -38,7 +37,7 @@ describe("deliverOutboundPayloads", () => { }); it("chunks telegram markdown and passes through accountId", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, }; const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; @@ -71,7 +70,7 @@ describe("deliverOutboundPayloads", () => { it("passes explicit accountId to sendTelegram", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, }; @@ -93,7 +92,7 @@ describe("deliverOutboundPayloads", () => { it("uses signal media maxBytes from config", async () => { const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", timestamp: 123 }); - const cfg: MoltbotConfig = { channels: { signal: { mediaMaxMb: 2 } } }; + const cfg: OpenClawConfig = { channels: { signal: { mediaMaxMb: 2 } } }; const results = await deliverOutboundPayloads({ cfg, @@ -118,7 +117,7 @@ describe("deliverOutboundPayloads", () => { it("chunks Signal markdown using the format-first chunker", async () => { const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", timestamp: 123 }); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { signal: { textChunkLimit: 20 } }, }; const text = `Intro\\n\\n\`\`\`\`md\\n${"y".repeat(60)}\\n\`\`\`\\n\\nOutro`; @@ -152,7 +151,7 @@ describe("deliverOutboundPayloads", () => { .fn() .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" }) .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { whatsapp: { textChunkLimit: 2 } }, }; @@ -170,7 +169,7 @@ describe("deliverOutboundPayloads", () => { it("respects newline chunk mode for WhatsApp", async () => { const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { whatsapp: { textChunkLimit: 4000, chunkMode: "newline" } }, }; @@ -229,7 +228,7 @@ describe("deliverOutboundPayloads", () => { ]), ); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { matrix: { textChunkLimit: 4000, chunkMode: "newline" } }, }; const text = "```js\nconst a = 1;\nconst b = 2;\n```\nAfter"; @@ -256,7 +255,7 @@ describe("deliverOutboundPayloads", () => { }, ]), ); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { mediaMaxMb: 3 } }, }; @@ -293,7 +292,7 @@ describe("deliverOutboundPayloads", () => { .mockRejectedValueOnce(new Error("fail")) .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); const onError = vi.fn(); - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const results = await deliverOutboundPayloads({ cfg, @@ -313,7 +312,7 @@ describe("deliverOutboundPayloads", () => { it("passes normalized payload to onError", async () => { const sendWhatsApp = vi.fn().mockRejectedValue(new Error("boom")); const onError = vi.fn(); - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; await deliverOutboundPayloads({ cfg, @@ -334,7 +333,7 @@ describe("deliverOutboundPayloads", () => { it("mirrors delivered output when mirror options are provided", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, }; mocks.appendAssistantMessageToSessionTranscript.mockClear(); diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 1abbd3557..de7931a64 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -1,29 +1,29 @@ +import type { ReplyPayload } from "../../auto-reply/types.js"; +import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import type { sendMessageDiscord } from "../../discord/send.js"; +import type { sendMessageIMessage } from "../../imessage/send.js"; +import type { sendMessageSlack } from "../../slack/send.js"; +import type { sendMessageTelegram } from "../../telegram/send.js"; +import type { sendMessageWhatsApp } from "../../web/outbound.js"; +import type { NormalizedOutboundPayload } from "./payloads.js"; +import type { OutboundChannel } from "./targets.js"; import { chunkByParagraph, chunkMarkdownTextWithMode, resolveChunkMode, resolveTextChunkLimit, } from "../../auto-reply/chunk.js"; -import type { ReplyPayload } from "../../auto-reply/types.js"; import { resolveChannelMediaMaxBytes } from "../../channels/plugins/media-limits.js"; import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js"; -import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; import { resolveMarkdownTableMode } from "../../config/markdown-tables.js"; -import type { sendMessageDiscord } from "../../discord/send.js"; -import type { sendMessageIMessage } from "../../imessage/send.js"; -import { markdownToSignalTextChunks, type SignalTextStyleRange } from "../../signal/format.js"; -import { sendMessageSignal } from "../../signal/send.js"; -import type { sendMessageSlack } from "../../slack/send.js"; -import type { sendMessageTelegram } from "../../telegram/send.js"; -import type { sendMessageWhatsApp } from "../../web/outbound.js"; import { appendAssistantMessageToSessionTranscript, resolveMirroredTranscriptText, } from "../../config/sessions.js"; -import type { NormalizedOutboundPayload } from "./payloads.js"; +import { markdownToSignalTextChunks, type SignalTextStyleRange } from "../../signal/format.js"; +import { sendMessageSignal } from "../../signal/send.js"; import { normalizeReplyPayloadsForDelivery } from "./payloads.js"; -import type { OutboundChannel } from "./targets.js"; export type { NormalizedOutboundPayload } from "./payloads.js"; export { normalizeOutboundPayloads } from "./payloads.js"; @@ -82,7 +82,7 @@ function throwIfAborted(abortSignal?: AbortSignal): void { // Channel docking: outbound delivery delegates to plugin.outbound adapters. async function createChannelHandler(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: Exclude; to: string; accountId?: string; @@ -114,7 +114,7 @@ async function createChannelHandler(params: { function createPluginHandler(params: { outbound?: ChannelOutboundAdapter; - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: Exclude; to: string; accountId?: string; @@ -124,7 +124,9 @@ function createPluginHandler(params: { gifPlayback?: boolean; }): ChannelHandler | null { const outbound = params.outbound; - if (!outbound?.sendText || !outbound?.sendMedia) return null; + if (!outbound?.sendText || !outbound?.sendMedia) { + return null; + } const sendText = outbound.sendText; const sendMedia = outbound.sendMedia; const chunker = outbound.chunker ?? null; @@ -175,7 +177,7 @@ function createPluginHandler(params: { } export async function deliverOutboundPayloads(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: Exclude; to: string; accountId?: string; @@ -244,10 +246,14 @@ export async function deliverOutboundPayloads(params: { ? chunkMarkdownTextWithMode(text, textLimit, "newline") : chunkByParagraph(text, textLimit); - if (!blockChunks.length && text) blockChunks.push(text); + if (!blockChunks.length && text) { + blockChunks.push(text); + } for (const blockChunk of blockChunks) { const chunks = handler.chunker(blockChunk, textLimit); - if (!chunks.length && blockChunk) chunks.push(blockChunk); + if (!chunks.length && blockChunk) { + chunks.push(blockChunk); + } for (const chunk of chunks) { throwIfAborted(abortSignal); results.push(await handler.sendText(chunk)); @@ -346,7 +352,9 @@ export async function deliverOutboundPayloads(params: { } } } catch (err) { - if (!params.bestEffort) throw err; + if (!params.bestEffort) { + throw err; + } params.onError?.(err, payloadSummary); } } diff --git a/src/infra/outbound/directory-cache.ts b/src/infra/outbound/directory-cache.ts index e957daeb6..8dccac50f 100644 --- a/src/infra/outbound/directory-cache.ts +++ b/src/infra/outbound/directory-cache.ts @@ -1,5 +1,5 @@ import type { ChannelDirectoryEntryKind, ChannelId } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; type CacheEntry = { value: T; @@ -21,14 +21,16 @@ export function buildDirectoryCacheKey(key: DirectoryCacheKey): string { export class DirectoryCache { private readonly cache = new Map>(); - private lastConfigRef: MoltbotConfig | null = null; + private lastConfigRef: OpenClawConfig | null = null; constructor(private readonly ttlMs: number) {} - get(key: string, cfg: MoltbotConfig): T | undefined { + get(key: string, cfg: OpenClawConfig): T | undefined { this.resetIfConfigChanged(cfg); const entry = this.cache.get(key); - if (!entry) return undefined; + if (!entry) { + return undefined; + } if (Date.now() - entry.fetchedAt > this.ttlMs) { this.cache.delete(key); return undefined; @@ -36,23 +38,27 @@ export class DirectoryCache { return entry.value; } - set(key: string, value: T, cfg: MoltbotConfig): void { + set(key: string, value: T, cfg: OpenClawConfig): void { this.resetIfConfigChanged(cfg); this.cache.set(key, { value, fetchedAt: Date.now() }); } clearMatching(match: (key: string) => boolean): void { for (const key of this.cache.keys()) { - if (match(key)) this.cache.delete(key); + if (match(key)) { + this.cache.delete(key); + } } } - clear(cfg?: MoltbotConfig): void { + clear(cfg?: OpenClawConfig): void { this.cache.clear(); - if (cfg) this.lastConfigRef = cfg; + if (cfg) { + this.lastConfigRef = cfg; + } } - private resetIfConfigChanged(cfg: MoltbotConfig): void { + private resetIfConfigChanged(cfg: OpenClawConfig): void { if (this.lastConfigRef && this.lastConfigRef !== cfg) { this.cache.clear(); } diff --git a/src/infra/outbound/envelope.test.ts b/src/infra/outbound/envelope.test.ts index e0e6a928f..71effdee8 100644 --- a/src/infra/outbound/envelope.test.ts +++ b/src/infra/outbound/envelope.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; - -import { buildOutboundResultEnvelope } from "./envelope.js"; import type { OutboundDeliveryJson } from "./format.js"; +import { buildOutboundResultEnvelope } from "./envelope.js"; describe("buildOutboundResultEnvelope", () => { it("flattens delivery-only payloads by default", () => { diff --git a/src/infra/outbound/format.test.ts b/src/infra/outbound/format.test.ts index e91330143..950bb3e5f 100644 --- a/src/infra/outbound/format.test.ts +++ b/src/infra/outbound/format.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { buildOutboundDeliveryJson, formatGatewaySummary, diff --git a/src/infra/outbound/format.ts b/src/infra/outbound/format.ts index 01391f8a2..4772ee917 100644 --- a/src/infra/outbound/format.ts +++ b/src/infra/outbound/format.ts @@ -1,7 +1,7 @@ -import { getChannelPlugin } from "../../channels/plugins/index.js"; -import { getChatChannelMeta, normalizeChatChannelId } from "../../channels/registry.js"; import type { ChannelId } from "../../channels/plugins/types.js"; import type { OutboundDeliveryResult } from "./deliver.js"; +import { getChannelPlugin } from "../../channels/plugins/index.js"; +import { getChatChannelMeta, normalizeChatChannelId } from "../../channels/registry.js"; export type OutboundDeliveryJson = { channel: string; @@ -31,9 +31,13 @@ type OutboundDeliveryMeta = { const resolveChannelLabel = (channel: string) => { const pluginLabel = getChannelPlugin(channel as ChannelId)?.meta.label; - if (pluginLabel) return pluginLabel; + if (pluginLabel) { + return pluginLabel; + } const normalized = normalizeChatChannelId(channel); - if (normalized) return getChatChannelMeta(normalized).label; + if (normalized) { + return getChatChannelMeta(normalized).label; + } return channel; }; @@ -48,10 +52,18 @@ export function formatOutboundDeliverySummary( const label = resolveChannelLabel(result.channel); const base = `✅ Sent via ${label}. Message ID: ${result.messageId}`; - if ("chatId" in result) return `${base} (chat ${result.chatId})`; - if ("channelId" in result) return `${base} (channel ${result.channelId})`; - if ("roomId" in result) return `${base} (room ${result.roomId})`; - if ("conversationId" in result) return `${base} (conversation ${result.conversationId})`; + if ("chatId" in result) { + return `${base} (chat ${result.chatId})`; + } + if ("channelId" in result) { + return `${base} (channel ${result.channelId})`; + } + if ("roomId" in result) { + return `${base} (room ${result.roomId})`; + } + if ("conversationId" in result) { + return `${base} (conversation ${result.conversationId})`; + } return base; } diff --git a/src/infra/outbound/message-action-runner.test.ts b/src/infra/outbound/message-action-runner.test.ts index 9ae25561d..ed432b070 100644 --- a/src/infra/outbound/message-action-runner.test.ts +++ b/src/infra/outbound/message-action-runner.test.ts @@ -1,15 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import type { MoltbotConfig } from "../../config/config.js"; -import { setActivePluginRegistry } from "../../plugins/runtime.js"; -import { createIMessageTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; +import type { ChannelPlugin } from "../../channels/plugins/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { slackPlugin } from "../../../extensions/slack/src/channel.js"; import { telegramPlugin } from "../../../extensions/telegram/src/channel.js"; import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js"; +import { jsonResult } from "../../agents/tools/common.js"; +import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { createIMessageTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; import { loadWebMedia } from "../../web/media.js"; import { runMessageAction } from "./message-action-runner.js"; -import { jsonResult } from "../../agents/tools/common.js"; -import type { ChannelPlugin } from "../../channels/plugins/types.js"; vi.mock("../../web/media.js", async () => { const actual = await vi.importActual("../../web/media.js"); @@ -26,7 +25,7 @@ const slackConfig = { appToken: "xapp-test", }, }, -} as MoltbotConfig; +} as OpenClawConfig; const whatsappConfig = { channels: { @@ -34,7 +33,7 @@ const whatsappConfig = { allowFrom: ["*"], }, }, -} as MoltbotConfig; +} as OpenClawConfig; describe("runMessageAction context isolation", () => { beforeEach(async () => { @@ -263,7 +262,7 @@ describe("runMessageAction context isolation", () => { token: "tg-test", }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = await runMessageAction({ cfg: multiConfig, @@ -305,7 +304,7 @@ describe("runMessageAction context isolation", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; await expect( runMessageAction({ @@ -423,7 +422,7 @@ describe("runMessageAction sendAttachment hydration", () => { password: "test-password", }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = await runMessageAction({ cfg, @@ -491,7 +490,7 @@ describe("runMessageAction accountId defaults", () => { it("propagates defaultAccountId into params", async () => { await runMessageAction({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, action: "send", params: { channel: "discord", diff --git a/src/infra/outbound/message-action-runner.threading.test.ts b/src/infra/outbound/message-action-runner.threading.test.ts index 68a719fc5..b467823dd 100644 --- a/src/infra/outbound/message-action-runner.threading.test.ts +++ b/src/infra/outbound/message-action-runner.threading.test.ts @@ -1,9 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { slackPlugin } from "../../../extensions/slack/src/channel.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; -import { slackPlugin } from "../../../extensions/slack/src/channel.js"; const mocks = vi.hoisted(() => ({ executeSendAction: vi.fn(), @@ -39,7 +38,7 @@ const slackConfig = { appToken: "xapp-test", }, }, -} as MoltbotConfig; +} as OpenClawConfig; describe("runMessageAction Slack threading", () => { beforeEach(async () => { diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 8f99ad791..e19ae8528 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -1,35 +1,37 @@ +import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import path from "node:path"; import { fileURLToPath } from "node:url"; - -import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import { - readNumberParam, - readStringArrayParam, - readStringParam, -} from "../../agents/tools/common.js"; -import { resolveSessionAgentId } from "../../agents/agent-scope.js"; -import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js"; -import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js"; import type { ChannelId, ChannelMessageActionName, ChannelThreadingToolContext, } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import type { OutboundSendDeps } from "./deliver.js"; +import type { MessagePollResult, MessageSendResult } from "./message.js"; +import { resolveSessionAgentId } from "../../agents/agent-scope.js"; +import { + readNumberParam, + readStringArrayParam, + readStringParam, +} from "../../agents/tools/common.js"; +import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js"; +import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js"; +import { extensionForMime } from "../../media/mime.js"; +import { parseSlackTarget } from "../../slack/targets.js"; import { isDeliverableMessageChannel, normalizeMessageChannel, type GatewayClientMode, type GatewayClientName, } from "../../utils/message-channel.js"; +import { loadWebMedia } from "../../web/media.js"; import { listConfiguredMessageChannels, resolveMessageChannelSelection, } from "./channel-selection.js"; import { applyTargetToParams } from "./channel-target.js"; -import { ensureOutboundSessionEntry, resolveOutboundSessionRoute } from "./outbound-session.js"; -import type { OutboundSendDeps } from "./deliver.js"; -import type { MessagePollResult, MessageSendResult } from "./message.js"; +import { actionHasTarget, actionRequiresTarget } from "./message-action-spec.js"; import { applyCrossContextDecoration, buildCrossContextDecoration, @@ -38,11 +40,8 @@ import { shouldApplyCrossContextMarker, } from "./outbound-policy.js"; import { executePollAction, executeSendAction } from "./outbound-send-service.js"; -import { actionHasTarget, actionRequiresTarget } from "./message-action-spec.js"; +import { ensureOutboundSessionEntry, resolveOutboundSessionRoute } from "./outbound-session.js"; import { resolveChannelTarget, type ResolvedMessagingTarget } from "./target-resolver.js"; -import { loadWebMedia } from "../../web/media.js"; -import { extensionForMime } from "../../media/mime.js"; -import { parseSlackTarget } from "../../slack/targets.js"; export type MessageActionRunnerGateway = { url?: string; @@ -54,7 +53,7 @@ export type MessageActionRunnerGateway = { }; export type RunMessageActionParams = { - cfg: MoltbotConfig; + cfg: OpenClawConfig; action: ChannelMessageActionName; params: Record; defaultAccountId?: string; @@ -123,7 +122,9 @@ export function getToolResult( } function extractToolPayload(result: AgentToolResult): unknown { - if (result.details !== undefined) return result.details; + if (result.details !== undefined) { + return result.details; + } const textBlock = Array.isArray(result.content) ? result.content.find( (block) => @@ -168,7 +169,7 @@ function applyCrossContextMessageDecoration({ } async function maybeApplyCrossContextMarker(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; action: ChannelMessageActionName; target: string; @@ -188,7 +189,9 @@ async function maybeApplyCrossContextMarker(params: { toolContext: params.toolContext, accountId: params.accountId ?? undefined, }); - if (!decoration) return params.message; + if (!decoration) { + return params.message; + } return applyCrossContextMessageDecoration({ params: params.args, message: params.message, @@ -199,11 +202,17 @@ async function maybeApplyCrossContextMarker(params: { function readBooleanParam(params: Record, key: string): boolean | undefined { const raw = params[key]; - if (typeof raw === "boolean") return raw; + if (typeof raw === "boolean") { + return raw; + } if (typeof raw === "string") { const trimmed = raw.trim().toLowerCase(); - if (trimmed === "true") return true; - if (trimmed === "false") return false; + if (trimmed === "true") { + return true; + } + if (trimmed === "false") { + return false; + } } return undefined; } @@ -213,18 +222,28 @@ function resolveSlackAutoThreadId(params: { toolContext?: ChannelThreadingToolContext; }): string | undefined { const context = params.toolContext; - if (!context?.currentThreadTs || !context.currentChannelId) return undefined; + if (!context?.currentThreadTs || !context.currentChannelId) { + return undefined; + } // Only mirror auto-threading when Slack would reply in the active thread for this channel. - if (context.replyToMode !== "all" && context.replyToMode !== "first") return undefined; + if (context.replyToMode !== "all" && context.replyToMode !== "first") { + return undefined; + } const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" }); - if (!parsedTarget || parsedTarget.kind !== "channel") return undefined; - if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) return undefined; - if (context.replyToMode === "first" && context.hasRepliedRef?.value) return undefined; + if (!parsedTarget || parsedTarget.kind !== "channel") { + return undefined; + } + if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) { + return undefined; + } + if (context.replyToMode === "first" && context.hasRepliedRef?.value) { + return undefined; + } return context.currentThreadTs; } function resolveAttachmentMaxBytes(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; accountId?: string | null; }): number | undefined { @@ -266,14 +285,20 @@ function inferAttachmentFilename(params: { if (mediaHint.startsWith("file://")) { const filePath = fileURLToPath(mediaHint); const base = path.basename(filePath); - if (base) return base; + if (base) { + return base; + } } else if (/^https?:\/\//i.test(mediaHint)) { const url = new URL(mediaHint); const base = path.basename(url.pathname); - if (base) return base; + if (base) { + return base; + } } else { const base = path.basename(mediaHint); - if (base) return base; + if (base) { + return base; + } } } catch { // fall through to content-type based default @@ -287,9 +312,13 @@ function normalizeBase64Payload(params: { base64?: string; contentType?: string base64?: string; contentType?: string; } { - if (!params.base64) return { base64: params.base64, contentType: params.contentType }; + if (!params.base64) { + return { base64: params.base64, contentType: params.contentType }; + } const match = /^data:([^;]+);base64,(.*)$/i.exec(params.base64.trim()); - if (!match) return { base64: params.base64, contentType: params.contentType }; + if (!match) { + return { base64: params.base64, contentType: params.contentType }; + } const [, mime, payload] = match; return { base64: payload, @@ -298,14 +327,16 @@ function normalizeBase64Payload(params: { base64?: string; contentType?: string } async function hydrateSetGroupIconParams(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; accountId?: string | null; args: Record; action: ChannelMessageActionName; dryRun?: boolean; }): Promise { - if (params.action !== "setGroupIcon") return; + if (params.action !== "setGroupIcon") { + return; + } const mediaHint = readStringParam(params.args, "media", { trim: false }); const fileHint = @@ -355,14 +386,16 @@ async function hydrateSetGroupIconParams(params: { } async function hydrateSendAttachmentParams(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; accountId?: string | null; args: Record; action: ChannelMessageActionName; dryRun?: boolean; }): Promise { - if (params.action !== "sendAttachment") return; + if (params.action !== "sendAttachment") { + return; + } const mediaHint = readStringParam(params.args, "media", { trim: false }); const fileHint = @@ -372,7 +405,9 @@ async function hydrateSendAttachmentParams(params: { readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType"); const caption = readStringParam(params.args, "caption", { allowEmpty: true })?.trim(); const message = readStringParam(params.args, "message", { allowEmpty: true })?.trim(); - if (!caption && message) params.args.caption = message; + if (!caption && message) { + params.args.caption = message; + } const rawBuffer = readStringParam(params.args, "buffer", { trim: false }); const normalized = normalizeBase64Payload({ @@ -416,7 +451,9 @@ async function hydrateSendAttachmentParams(params: { function parseButtonsParam(params: Record): void { const raw = params.buttons; - if (typeof raw !== "string") return; + if (typeof raw !== "string") { + return; + } const trimmed = raw.trim(); if (!trimmed) { delete params.buttons; @@ -431,7 +468,9 @@ function parseButtonsParam(params: Record): void { function parseCardParam(params: Record): void { const raw = params.card; - if (typeof raw !== "string") return; + if (typeof raw !== "string") { + return; + } const trimmed = raw.trim(); if (!trimmed) { delete params.card; @@ -444,7 +483,7 @@ function parseCardParam(params: Record): void { } } -async function resolveChannel(cfg: MoltbotConfig, params: Record) { +async function resolveChannel(cfg: OpenClawConfig, params: Record) { const channelHint = readStringParam(params, "channel"); const selection = await resolveMessageChannelSelection({ cfg, @@ -454,7 +493,7 @@ async function resolveChannel(cfg: MoltbotConfig, params: Record; @@ -499,7 +538,7 @@ async function resolveActionTarget(params: { } type ResolvedActionContext = { - cfg: MoltbotConfig; + cfg: OpenClawConfig; params: Record; channel: ChannelId; accountId?: string | null; @@ -511,7 +550,9 @@ type ResolvedActionContext = { abortSignal?: AbortSignal; }; function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined { - if (!input.gateway) return undefined; + if (!input.gateway) { + return undefined; + } return { url: input.gateway.url, token: input.gateway.token, @@ -562,7 +603,9 @@ async function handleBroadcastAction( channel: targetChannel, input: target, }); - if (!resolved.ok) throw resolved.error; + if (!resolved.ok) { + throw resolved.error; + } const sendResult = await runMessageAction({ ...input, action: "send", @@ -579,7 +622,9 @@ async function handleBroadcastAction( result: sendResult.kind === "send" ? sendResult.sendResult : undefined, }); } catch (err) { - if (isAbortError(err)) throw err; + if (isAbortError(err)) { + throw err; + } results.push({ channel: targetChannel, to: target, @@ -591,7 +636,7 @@ async function handleBroadcastAction( } return { kind: "broadcast", - channel: (targetChannels[0] ?? "discord") as ChannelId, + channel: targetChannels[0] ?? "discord", action: "broadcast", handledBy: input.dryRun ? "dry-run" : "core", payload: { results }, @@ -640,17 +685,25 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise(); const pushMedia = (value?: string | null) => { const trimmed = value?.trim(); - if (!trimmed) return; - if (seenMedia.has(trimmed)) return; + if (!trimmed) { + return; + } + if (seenMedia.has(trimmed)) { + return; + } seenMedia.add(trimmed); mergedMediaUrls.push(trimmed); }; pushMedia(mediaHint); - for (const url of parsed.mediaUrls ?? []) pushMedia(url); + for (const url of parsed.mediaUrls ?? []) { + pushMedia(url); + } pushMedia(parsed.mediaUrl); message = parsed.text; params.message = message; - if (!params.replyTo && parsed.replyToId) params.replyTo = parsed.replyToId; + if (!params.replyTo && parsed.replyToId) { + params.replyTo = parsed.replyToId; + } if (!params.media) { // Use path/filePath if media not set, then fall back to parsed directives params.media = mergedMediaUrls[0] || undefined; diff --git a/src/infra/outbound/message-action-spec.ts b/src/infra/outbound/message-action-spec.ts index 639e641d0..12e5b0e71 100644 --- a/src/infra/outbound/message-action-spec.ts +++ b/src/infra/outbound/message-action-spec.ts @@ -75,15 +75,25 @@ export function actionHasTarget( params: Record, ): boolean { const to = typeof params.to === "string" ? params.to.trim() : ""; - if (to) return true; + if (to) { + return true; + } const channelId = typeof params.channelId === "string" ? params.channelId.trim() : ""; - if (channelId) return true; + if (channelId) { + return true; + } const aliases = ACTION_TARGET_ALIASES[action]; - if (!aliases) return false; + if (!aliases) { + return false; + } return aliases.some((alias) => { const value = params[alias]; - if (typeof value === "string") return value.trim().length > 0; - if (typeof value === "number") return Number.isFinite(value); + if (typeof value === "string") { + return value.trim().length > 0; + } + if (typeof value === "number") { + return Number.isFinite(value); + } return false; }); } diff --git a/src/infra/outbound/message.test.ts b/src/infra/outbound/message.test.ts index eebd53d97..9bd8f0d1b 100644 --- a/src/infra/outbound/message.test.ts +++ b/src/infra/outbound/message.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js"; import { createIMessageTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; const loadMessage = async () => await import("./message.js"); diff --git a/src/infra/outbound/message.ts b/src/infra/outbound/message.ts index 517b72ee5..1efcf601d 100644 --- a/src/infra/outbound/message.ts +++ b/src/infra/outbound/message.ts @@ -1,9 +1,8 @@ +import type { OpenClawConfig } from "../../config/config.js"; +import type { PollInput } from "../../polls.js"; import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; -import type { ChannelId } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js"; import { callGateway, randomIdempotencyKey } from "../../gateway/call.js"; -import type { PollInput } from "../../polls.js"; import { normalizePollInput } from "../../polls.js"; import { GATEWAY_CLIENT_MODES, @@ -18,7 +17,6 @@ import { type OutboundSendDeps, } from "./deliver.js"; import { normalizeReplyPayloadsForDelivery } from "./payloads.js"; -import type { OutboundChannel } from "./targets.js"; import { resolveOutboundTarget } from "./targets.js"; export type MessageGatewayOptions = { @@ -41,7 +39,7 @@ type MessageSendParams = { dryRun?: boolean; bestEffort?: boolean; deps?: OutboundSendDeps; - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; gateway?: MessageGatewayOptions; idempotencyKey?: string; mirror?: { @@ -71,7 +69,7 @@ type MessagePollParams = { durationHours?: number; channel?: string; dryRun?: boolean; - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; gateway?: MessageGatewayOptions; idempotencyKey?: string; }; @@ -116,7 +114,7 @@ export async function sendMessage(params: MessageSendParams): Promise; + const outboundChannel = channel; const resolvedTarget = resolveOutboundTarget({ channel: outboundChannel, to: params.to, @@ -157,7 +155,9 @@ export async function sendMessage(params: MessageSendParams): Promise { it("blocks cross-provider sends by default", () => { @@ -41,7 +40,7 @@ describe("outbound policy", () => { tools: { message: { crossContext: { allowAcrossProviders: true } }, }, - } as MoltbotConfig; + } as OpenClawConfig; expect(() => enforceCrossContextPolicy({ @@ -58,7 +57,7 @@ describe("outbound policy", () => { const cfg = { ...slackConfig, tools: { message: { crossContext: { allowWithinProvider: false } } }, - } as MoltbotConfig; + } as OpenClawConfig; expect(() => enforceCrossContextPolicy({ diff --git a/src/infra/outbound/outbound-policy.ts b/src/infra/outbound/outbound-policy.ts index 9b3470949..809c523dc 100644 --- a/src/infra/outbound/outbound-policy.ts +++ b/src/infra/outbound/outbound-policy.ts @@ -1,11 +1,11 @@ -import { normalizeTargetForProvider } from "./target-normalization.js"; import type { ChannelId, ChannelMessageActionName, ChannelThreadingToolContext, } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { getChannelMessageAdapter } from "./channel-adapters.js"; +import { normalizeTargetForProvider } from "./target-normalization.js"; import { formatTargetDisplay, lookupDirectoryDisplay } from "./target-resolver.js"; export type CrossContextDecoration = { @@ -39,16 +39,26 @@ function resolveContextGuardTarget( action: ChannelMessageActionName, params: Record, ): string | undefined { - if (!CONTEXT_GUARDED_ACTIONS.has(action)) return undefined; - - if (action === "thread-reply" || action === "thread-create") { - if (typeof params.channelId === "string") return params.channelId; - if (typeof params.to === "string") return params.to; + if (!CONTEXT_GUARDED_ACTIONS.has(action)) { return undefined; } - if (typeof params.to === "string") return params.to; - if (typeof params.channelId === "string") return params.channelId; + if (action === "thread-reply" || action === "thread-create") { + if (typeof params.channelId === "string") { + return params.channelId; + } + if (typeof params.to === "string") { + return params.to; + } + return undefined; + } + + if (typeof params.to === "string") { + return params.to; + } + if (typeof params.channelId === "string") { + return params.channelId; + } return undefined; } @@ -62,10 +72,14 @@ function isCrossContextTarget(params: { toolContext?: ChannelThreadingToolContext; }): boolean { const currentTarget = params.toolContext?.currentChannelId?.trim(); - if (!currentTarget) return false; + if (!currentTarget) { + return false; + } const normalizedTarget = normalizeTarget(params.channel, params.target); const normalizedCurrent = normalizeTarget(params.channel, currentTarget); - if (!normalizedTarget || !normalizedCurrent) return false; + if (!normalizedTarget || !normalizedCurrent) { + return false; + } return normalizedTarget !== normalizedCurrent; } @@ -74,13 +88,19 @@ export function enforceCrossContextPolicy(params: { action: ChannelMessageActionName; args: Record; toolContext?: ChannelThreadingToolContext; - cfg: MoltbotConfig; + cfg: OpenClawConfig; }): void { const currentTarget = params.toolContext?.currentChannelId?.trim(); - if (!currentTarget) return; - if (!CONTEXT_GUARDED_ACTIONS.has(params.action)) return; + if (!currentTarget) { + return; + } + if (!CONTEXT_GUARDED_ACTIONS.has(params.action)) { + return; + } - if (params.cfg.tools?.message?.allowCrossContextSend) return; + if (params.cfg.tools?.message?.allowCrossContextSend) { + return; + } const currentProvider = params.toolContext?.currentChannelProvider; const allowWithinProvider = @@ -97,10 +117,14 @@ export function enforceCrossContextPolicy(params: { return; } - if (allowWithinProvider) return; + if (allowWithinProvider) { + return; + } const target = resolveContextGuardTarget(params.action, params.args); - if (!target) return; + if (!target) { + return; + } if (!isCrossContextTarget({ channel: params.channel, target, toolContext: params.toolContext })) { return; @@ -112,19 +136,27 @@ export function enforceCrossContextPolicy(params: { } export async function buildCrossContextDecoration(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; target: string; toolContext?: ChannelThreadingToolContext; accountId?: string | null; }): Promise { - if (!params.toolContext?.currentChannelId) return null; + if (!params.toolContext?.currentChannelId) { + return null; + } // Skip decoration for direct tool sends (agent composing, not forwarding) - if (params.toolContext.skipCrossContextDecoration) return null; - if (!isCrossContextTarget(params)) return null; + if (params.toolContext.skipCrossContextDecoration) { + return null; + } + if (!isCrossContextTarget(params)) { + return null; + } const markerConfig = params.cfg.tools?.message?.crossContext?.marker; - if (markerConfig?.enabled === false) return null; + if (markerConfig?.enabled === false) { + return null; + } const currentName = (await lookupDirectoryDisplay({ diff --git a/src/infra/outbound/outbound-send-service.ts b/src/infra/outbound/outbound-send-service.ts index 6499eb452..cc9cb9476 100644 --- a/src/infra/outbound/outbound-send-service.ts +++ b/src/infra/outbound/outbound-send-service.ts @@ -1,11 +1,11 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js"; import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; -import { appendAssistantMessageToSessionTranscript } from "../../config/sessions.js"; +import type { OpenClawConfig } from "../../config/config.js"; import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js"; import type { OutboundSendDeps } from "./deliver.js"; import type { MessagePollResult, MessageSendResult } from "./message.js"; +import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js"; +import { appendAssistantMessageToSessionTranscript } from "../../config/sessions.js"; import { sendMessage, sendPoll } from "./message.js"; export type OutboundGatewayContext = { @@ -18,7 +18,7 @@ export type OutboundGatewayContext = { }; export type OutboundSendContext = { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; params: Record; accountId?: string | null; @@ -36,7 +36,9 @@ export type OutboundSendContext = { }; function extractToolPayload(result: AgentToolResult): unknown { - if (result.details !== undefined) return result.details; + if (result.details !== undefined) { + return result.details; + } const textBlock = Array.isArray(result.content) ? result.content.find( (block) => diff --git a/src/infra/outbound/outbound-session.test.ts b/src/infra/outbound/outbound-session.test.ts index 23db09e74..944dbf869 100644 --- a/src/infra/outbound/outbound-session.test.ts +++ b/src/infra/outbound/outbound-session.test.ts @@ -1,9 +1,8 @@ import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { resolveOutboundSessionRoute } from "./outbound-session.js"; -const baseConfig = {} as MoltbotConfig; +const baseConfig = {} as OpenClawConfig; describe("resolveOutboundSessionRoute", () => { it("builds Slack thread session keys", async () => { @@ -36,7 +35,7 @@ describe("resolveOutboundSessionRoute", () => { }); it("treats Telegram usernames as DMs when unresolved", async () => { - const cfg = { session: { dmScope: "per-channel-peer" } } as MoltbotConfig; + const cfg = { session: { dmScope: "per-channel-peer" } } as OpenClawConfig; const route = await resolveOutboundSessionRoute({ cfg, channel: "telegram", @@ -56,7 +55,7 @@ describe("resolveOutboundSessionRoute", () => { alice: ["discord:123"], }, }, - } as MoltbotConfig; + } as OpenClawConfig; const route = await resolveOutboundSessionRoute({ cfg, @@ -81,7 +80,7 @@ describe("resolveOutboundSessionRoute", () => { }); it("treats Zalo Personal DM targets as direct sessions", async () => { - const cfg = { session: { dmScope: "per-channel-peer" } } as MoltbotConfig; + const cfg = { session: { dmScope: "per-channel-peer" } } as OpenClawConfig; const route = await resolveOutboundSessionRoute({ cfg, channel: "zalouser", @@ -102,7 +101,7 @@ describe("resolveOutboundSessionRoute", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const route = await resolveOutboundSessionRoute({ cfg, diff --git a/src/infra/outbound/outbound-session.ts b/src/infra/outbound/outbound-session.ts index 9c12fab96..c31b18aca 100644 --- a/src/infra/outbound/outbound-session.ts +++ b/src/infra/outbound/outbound-session.ts @@ -1,7 +1,8 @@ import type { MsgContext } from "../../auto-reply/templating.js"; -import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { ChannelId } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import type { ResolvedMessagingTarget } from "./target-resolver.js"; +import { getChannelPlugin } from "../../channels/plugins/index.js"; import { recordSessionMetaFromInbound, resolveStorePath } from "../../config/sessions.js"; import { parseDiscordTarget } from "../../discord/targets.js"; import { parseIMessageTarget, normalizeIMessageHandle } from "../../imessage/targets.js"; @@ -11,20 +12,19 @@ import { type RoutePeerKind, } from "../../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../../routing/session-key.js"; -import { resolveSlackAccount } from "../../slack/accounts.js"; -import { createSlackWebClient } from "../../slack/client.js"; -import { normalizeAllowListLower } from "../../slack/monitor/allow-list.js"; import { resolveSignalPeerId, resolveSignalRecipient, resolveSignalSender, } from "../../signal/identity.js"; +import { resolveSlackAccount } from "../../slack/accounts.js"; +import { createSlackWebClient } from "../../slack/client.js"; +import { normalizeAllowListLower } from "../../slack/monitor/allow-list.js"; import { parseSlackTarget } from "../../slack/targets.js"; import { buildTelegramGroupPeerId } from "../../telegram/bot/helpers.js"; import { resolveTelegramTargetChatType } from "../../telegram/inline-buttons.js"; import { parseTelegramTarget } from "../../telegram/targets.js"; import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js"; -import type { ResolvedMessagingTarget } from "./target-resolver.js"; export type OutboundSessionRoute = { sessionKey: string; @@ -37,7 +37,7 @@ export type OutboundSessionRoute = { }; export type ResolveOutboundSessionRouteParams = { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; agentId: string; accountId?: string | null; @@ -53,16 +53,24 @@ const UUID_COMPACT_RE = /^[0-9a-f]{32}$/i; const SLACK_CHANNEL_TYPE_CACHE = new Map(); function looksLikeUuid(value: string): boolean { - if (UUID_RE.test(value) || UUID_COMPACT_RE.test(value)) return true; + if (UUID_RE.test(value) || UUID_COMPACT_RE.test(value)) { + return true; + } const compact = value.replace(/-/g, ""); - if (!/^[0-9a-f]+$/i.test(compact)) return false; + if (!/^[0-9a-f]+$/i.test(compact)) { + return false; + } return /[a-f]/i.test(compact); } function normalizeThreadId(value?: string | number | null): string | undefined { - if (value == null) return undefined; + if (value == null) { + return undefined; + } if (typeof value === "number") { - if (!Number.isFinite(value)) return undefined; + if (!Number.isFinite(value)) { + return undefined; + } return String(Math.trunc(value)); } const trimmed = value.trim(); @@ -73,7 +81,9 @@ function stripProviderPrefix(raw: string, channel: string): string { const trimmed = raw.trim(); const lower = trimmed.toLowerCase(); const prefix = `${channel.toLowerCase()}:`; - if (lower.startsWith(prefix)) return trimmed.slice(prefix.length).trim(); + if (lower.startsWith(prefix)) { + return trimmed.slice(prefix.length).trim(); + } return trimmed; } @@ -86,21 +96,27 @@ function inferPeerKind(params: { resolvedTarget?: ResolvedMessagingTarget; }): RoutePeerKind { const resolvedKind = params.resolvedTarget?.kind; - if (resolvedKind === "user") return "dm"; - if (resolvedKind === "channel") return "channel"; + if (resolvedKind === "user") { + return "dm"; + } + if (resolvedKind === "channel") { + return "channel"; + } if (resolvedKind === "group") { const plugin = getChannelPlugin(params.channel); const chatTypes = plugin?.capabilities?.chatTypes ?? []; const supportsChannel = chatTypes.includes("channel"); const supportsGroup = chatTypes.includes("group"); - if (supportsChannel && !supportsGroup) return "channel"; + if (supportsChannel && !supportsGroup) { + return "channel"; + } return "group"; } return "dm"; } function buildBaseSessionKey(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentId: string; channel: ChannelId; accountId?: string | null; @@ -118,14 +134,18 @@ function buildBaseSessionKey(params: { // Best-effort mpim detection: allowlist/config, then Slack API (if token available). async function resolveSlackChannelType(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; channelId: string; }): Promise<"channel" | "group" | "dm" | "unknown"> { const channelId = params.channelId.trim(); - if (!channelId) return "unknown"; + if (!channelId) { + return "unknown"; + } const cached = SLACK_CHANNEL_TYPE_CACHE.get(`${params.accountId ?? "default"}:${channelId}`); - if (cached) return cached; + if (cached) { + return cached; + } const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId }); const groupChannels = normalizeAllowListLower(account.dm?.groupChannels); @@ -181,7 +201,9 @@ async function resolveSlackSession( params: ResolveOutboundSessionRouteParams, ): Promise { const parsed = parseSlackTarget(params.target, { defaultKind: "channel" }); - if (!parsed) return null; + if (!parsed) { + return null; + } const isDm = parsed.kind === "user"; let peerKind: RoutePeerKind = isDm ? "dm" : "channel"; if (!isDm && /^G/i.test(parsed.id)) { @@ -191,8 +213,12 @@ async function resolveSlackSession( accountId: params.accountId, channelId: parsed.id, }); - if (channelType === "group") peerKind = "group"; - if (channelType === "dm") peerKind = "dm"; + if (channelType === "group") { + peerKind = "group"; + } + if (channelType === "dm") { + peerKind = "dm"; + } } const peer: RoutePeer = { kind: peerKind, @@ -230,7 +256,9 @@ function resolveDiscordSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { const parsed = parseDiscordTarget(params.target, { defaultKind: "channel" }); - if (!parsed) return null; + if (!parsed) { + return null; + } const isDm = parsed.kind === "user"; const peer: RoutePeer = { kind: isDm ? "dm" : "channel", @@ -267,7 +295,9 @@ function resolveTelegramSession( ): OutboundSessionRoute | null { const parsed = parseTelegramTarget(params.target); const chatId = parsed.chatId.trim(); - if (!chatId) return null; + if (!chatId) { + return null; + } const parsedThreadId = parsed.messageThreadId; const fallbackThreadId = normalizeThreadId(params.threadId); const resolvedThreadId = @@ -307,7 +337,9 @@ function resolveWhatsAppSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { const normalized = normalizeWhatsAppTarget(params.target); - if (!normalized) return null; + if (!normalized) { + return null; + } const isGroup = isWhatsAppGroupJid(normalized); const peer: RoutePeer = { kind: isGroup ? "group" : "dm", @@ -337,7 +369,9 @@ function resolveSignalSession( const lowered = stripped.toLowerCase(); if (lowered.startsWith("group:")) { const groupId = stripped.slice("group:".length).trim(); - if (!groupId) return null; + if (!groupId) { + return null; + } const peer: RoutePeer = { kind: "group", id: groupId }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -362,7 +396,9 @@ function resolveSignalSession( } else if (lowered.startsWith("u:")) { recipient = stripped.slice("u:".length).trim(); } - if (!recipient) return null; + if (!recipient) { + return null; + } const uuidCandidate = recipient.toLowerCase().startsWith("uuid:") ? recipient.slice("uuid:".length) @@ -397,7 +433,9 @@ function resolveIMessageSession( const parsed = parseIMessageTarget(params.target); if (parsed.kind === "handle") { const handle = normalizeIMessageHandle(parsed.to); - if (!handle) return null; + if (!handle) { + return null; + } const peer: RoutePeer = { kind: "dm", id: handle }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -422,7 +460,9 @@ function resolveIMessageSession( : parsed.kind === "chat_guid" ? parsed.chatGuid : parsed.chatIdentifier; - if (!peerId) return null; + if (!peerId) { + return null; + } const peer: RoutePeer = { kind: "group", id: peerId }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -454,7 +494,9 @@ function resolveMatrixSession( const isUser = params.resolvedTarget?.kind === "user" || stripped.startsWith("@") || /^user:/i.test(stripped); const rawId = stripKindPrefix(stripped); - if (!rawId) return null; + if (!rawId) { + return null; + } const peer: RoutePeer = { kind: isUser ? "dm" : "channel", id: rawId }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -477,13 +519,17 @@ function resolveMSTeamsSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { let trimmed = params.target.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } trimmed = trimmed.replace(/^(msteams|teams):/i, "").trim(); const lower = trimmed.toLowerCase(); const isUser = lower.startsWith("user:"); const rawId = stripKindPrefix(trimmed); - if (!rawId) return null; + if (!rawId) { + return null; + } const conversationId = rawId.split(";")[0] ?? rawId; const isChannel = !isUser && /@thread\.tacv2/i.test(conversationId); const peer: RoutePeer = { @@ -515,7 +561,9 @@ function resolveMattermostSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { let trimmed = params.target.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } trimmed = trimmed.replace(/^mattermost:/i, "").trim(); const lower = trimmed.toLowerCase(); const isUser = lower.startsWith("user:") || trimmed.startsWith("@"); @@ -523,7 +571,9 @@ function resolveMattermostSession( trimmed = trimmed.slice(1).trim(); } const rawId = stripKindPrefix(trimmed); - if (!rawId) return null; + if (!rawId) { + return null; + } const peer: RoutePeer = { kind: isUser ? "dm" : "channel", id: rawId }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -565,7 +615,9 @@ function resolveBlueBubblesSession( const peerId = isGroup ? rawPeerId.replace(/^(chat_id|chat_guid|chat_identifier):/i, "") : rawPeerId; - if (!peerId) return null; + if (!peerId) { + return null; + } const peer: RoutePeer = { kind: isGroup ? "group" : "dm", id: peerId, @@ -591,10 +643,14 @@ function resolveNextcloudTalkSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { let trimmed = params.target.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } trimmed = trimmed.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").trim(); trimmed = trimmed.replace(/^room:/i, "").trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const peer: RoutePeer = { kind: "group", id: trimmed }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -619,7 +675,9 @@ function resolveZaloSession( const trimmed = stripProviderPrefix(params.target, "zalo") .replace(/^(zl):/i, "") .trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const isGroup = trimmed.toLowerCase().startsWith("group:"); const peerId = stripKindPrefix(trimmed); const peer: RoutePeer = { kind: isGroup ? "group" : "dm", id: peerId }; @@ -646,7 +704,9 @@ function resolveZalouserSession( const trimmed = stripProviderPrefix(params.target, "zalouser") .replace(/^(zlu):/i, "") .trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const isGroup = trimmed.toLowerCase().startsWith("group:"); const peerId = stripKindPrefix(trimmed); // Keep DM vs group aligned with inbound sessions for Zalo Personal. @@ -672,7 +732,9 @@ function resolveNostrSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { const trimmed = stripProviderPrefix(params.target, "nostr").trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const peer: RoutePeer = { kind: "dm", id: trimmed }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -693,7 +755,9 @@ function resolveNostrSession( function normalizeTlonShip(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } return trimmed.startsWith("~") ? trimmed : `~${trimmed}`; } @@ -702,7 +766,9 @@ function resolveTlonSession( ): OutboundSessionRoute | null { let trimmed = stripProviderPrefix(params.target, "tlon"); trimmed = trimmed.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const lower = trimmed.toLowerCase(); let isGroup = lower.startsWith("group:") || lower.startsWith("room:") || lower.startsWith("chat/"); @@ -754,13 +820,17 @@ function resolveFallbackSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { const trimmed = stripProviderPrefix(params.target, params.channel).trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const peerKind = inferPeerKind({ channel: params.channel, resolvedTarget: params.resolvedTarget, }); const peerId = stripKindPrefix(trimmed); - if (!peerId) return null; + if (!peerId) { + return null; + } const peer: RoutePeer = { kind: peerKind, id: peerId }; const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, @@ -786,7 +856,9 @@ export async function resolveOutboundSessionRoute( params: ResolveOutboundSessionRouteParams, ): Promise { const target = params.target.trim(); - if (!target) return null; + if (!target) { + return null; + } switch (params.channel) { case "slack": return await resolveSlackSession({ ...params, target }); @@ -824,7 +896,7 @@ export async function resolveOutboundSessionRoute( } export async function ensureOutboundSessionEntry(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentId: string; channel: ChannelId; accountId?: string | null; diff --git a/src/infra/outbound/payloads.test.ts b/src/infra/outbound/payloads.test.ts index 9165abed9..be3f66daf 100644 --- a/src/infra/outbound/payloads.test.ts +++ b/src/infra/outbound/payloads.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { formatOutboundPayloadLog, normalizeOutboundPayloads, diff --git a/src/infra/outbound/payloads.ts b/src/infra/outbound/payloads.ts index 94eabb2bc..a44fdf2f1 100644 --- a/src/infra/outbound/payloads.ts +++ b/src/infra/outbound/payloads.ts @@ -1,6 +1,6 @@ +import type { ReplyPayload } from "../../auto-reply/types.js"; import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js"; import { isRenderablePayload } from "../../auto-reply/reply/reply-payloads.js"; -import type { ReplyPayload } from "../../auto-reply/types.js"; export type NormalizedOutboundPayload = { text: string; @@ -19,11 +19,17 @@ function mergeMediaUrls(...lists: Array | undefined>): const seen = new Set(); const merged: string[] = []; for (const list of lists) { - if (!list) continue; + if (!list) { + continue; + } for (const entry of list) { const trimmed = entry?.trim(); - if (!trimmed) continue; - if (seen.has(trimmed)) continue; + if (!trimmed) { + continue; + } + if (seen.has(trimmed)) { + continue; + } seen.add(trimmed); merged.push(trimmed); } @@ -52,8 +58,12 @@ export function normalizeReplyPayloadsForDelivery(payloads: ReplyPayload[]): Rep replyToCurrent: payload.replyToCurrent || parsed.replyToCurrent, audioAsVoice: Boolean(payload.audioAsVoice || parsed.audioAsVoice), }; - if (parsed.isSilent && mergedMedia.length === 0) return []; - if (!isRenderablePayload(next)) return []; + if (parsed.isSilent && mergedMedia.length === 0) { + return []; + } + if (!isRenderablePayload(next)) { + return []; + } return [next]; }); } @@ -90,7 +100,11 @@ export function normalizeOutboundPayloadsForJson(payloads: ReplyPayload[]): Outb export function formatOutboundPayloadLog(payload: NormalizedOutboundPayload): string { const lines: string[] = []; - if (payload.text) lines.push(payload.text.trimEnd()); - for (const url of payload.mediaUrls) lines.push(`MEDIA:${url}`); + if (payload.text) { + lines.push(payload.text.trimEnd()); + } + for (const url of payload.mediaUrls) { + lines.push(`MEDIA:${url}`); + } return lines.join("\n"); } diff --git a/src/infra/outbound/target-errors.ts b/src/infra/outbound/target-errors.ts index 76b12e985..0bf589817 100644 --- a/src/infra/outbound/target-errors.ts +++ b/src/infra/outbound/target-errors.ts @@ -23,6 +23,8 @@ export function unknownTargetError(provider: string, raw: string, hint?: string) } function formatTargetHint(hint?: string, withLabel = false): string { - if (!hint) return ""; + if (!hint) { + return ""; + } return withLabel ? ` Hint: ${hint}` : ` ${hint}`; } diff --git a/src/infra/outbound/target-normalization.ts b/src/infra/outbound/target-normalization.ts index dbab5e4e4..507740446 100644 --- a/src/infra/outbound/target-normalization.ts +++ b/src/infra/outbound/target-normalization.ts @@ -1,12 +1,14 @@ -import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { ChannelId } from "../../channels/plugins/types.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; export function normalizeChannelTargetInput(raw: string): string { return raw.trim(); } export function normalizeTargetForProvider(provider: string, raw?: string): string | undefined { - if (!raw) return undefined; + if (!raw) { + return undefined; + } const providerId = normalizeChannelId(provider); const plugin = providerId ? getChannelPlugin(providerId) : undefined; const normalized = diff --git a/src/infra/outbound/target-resolver.test.ts b/src/infra/outbound/target-resolver.test.ts index f4734f544..3b9c37486 100644 --- a/src/infra/outbound/target-resolver.test.ts +++ b/src/infra/outbound/target-resolver.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import type { ChannelDirectoryEntry } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { resetDirectoryCache, resolveMessagingTarget } from "./target-resolver.js"; const mocks = vi.hoisted(() => ({ @@ -16,7 +15,7 @@ vi.mock("../../channels/plugins/index.js", () => ({ })); describe("resolveMessagingTarget (directory fallback)", () => { - const cfg = {} as MoltbotConfig; + const cfg = {} as OpenClawConfig; beforeEach(() => { mocks.listGroups.mockReset(); diff --git a/src/infra/outbound/target-resolver.ts b/src/infra/outbound/target-resolver.ts index 6b2505a79..d2bac1e9d 100644 --- a/src/infra/outbound/target-resolver.ts +++ b/src/infra/outbound/target-resolver.ts @@ -1,18 +1,18 @@ -import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { ChannelDirectoryEntry, ChannelDirectoryEntryKind, ChannelId, } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { getChannelPlugin } from "../../channels/plugins/index.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; import { buildDirectoryCacheKey, DirectoryCache } from "./directory-cache.js"; +import { ambiguousTargetError, unknownTargetError } from "./target-errors.js"; import { buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider, } from "./target-normalization.js"; -import { ambiguousTargetError, unknownTargetError } from "./target-errors.js"; export type TargetResolveKind = ChannelDirectoryEntryKind | "channel"; @@ -30,7 +30,7 @@ export type ResolveMessagingTargetResult = | { ok: false; error: Error; candidates?: ChannelDirectoryEntry[] }; export async function resolveChannelTarget(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; input: string; accountId?: string | null; @@ -51,8 +51,12 @@ export function resetDirectoryCache(params?: { channel?: ChannelId; accountId?: const channelKey = params.channel; const accountKey = params.accountId ?? "default"; directoryCache.clearMatching((key) => { - if (!key.startsWith(`${channelKey}:`)) return false; - if (!params.accountId) return true; + if (!key.startsWith(`${channelKey}:`)) { + return false; + } + if (!params.accountId) { + return true; + } return key.startsWith(`${channelKey}:${accountKey}:`); }); } @@ -91,14 +95,24 @@ export function formatTargetDisplay(params: { (lowered.startsWith("user:") ? "user" : lowered.startsWith("channel:") ? "group" : undefined); if (display) { - if (display.startsWith("#") || display.startsWith("@")) return display; - if (kind === "user") return `@${display}`; - if (kind === "group" || kind === "channel") return `#${display}`; + if (display.startsWith("#") || display.startsWith("@")) { + return display; + } + if (kind === "user") { + return `@${display}`; + } + if (kind === "group" || kind === "channel") { + return `#${display}`; + } return display; } - if (!trimmedTarget) return trimmedTarget; - if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@")) return trimmedTarget; + if (!trimmedTarget) { + return trimmedTarget; + } + if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@")) { + return trimmedTarget; + } const channelPrefix = `${params.channel}:`; const withoutProvider = trimmedTarget.toLowerCase().startsWith(channelPrefix) @@ -116,11 +130,19 @@ export function formatTargetDisplay(params: { } function preserveTargetCase(channel: ChannelId, raw: string, normalized: string): string { - if (channel !== "slack") return normalized; + if (channel !== "slack") { + return normalized; + } const trimmed = raw.trim(); - if (/^channel:/i.test(trimmed) || /^user:/i.test(trimmed)) return trimmed; - if (trimmed.startsWith("#")) return `channel:${trimmed.slice(1).trim()}`; - if (trimmed.startsWith("@")) return `user:${trimmed.slice(1).trim()}`; + if (/^channel:/i.test(trimmed) || /^user:/i.test(trimmed)) { + return trimmed; + } + if (trimmed.startsWith("#")) { + return `channel:${trimmed.slice(1).trim()}`; + } + if (trimmed.startsWith("@")) { + return `user:${trimmed.slice(1).trim()}`; + } return trimmed; } @@ -129,12 +151,20 @@ function detectTargetKind( raw: string, preferred?: TargetResolveKind, ): TargetResolveKind { - if (preferred) return preferred; + if (preferred) { + return preferred; + } const trimmed = raw.trim(); - if (!trimmed) return "group"; + if (!trimmed) { + return "group"; + } - if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) return "user"; - if (trimmed.startsWith("#") || /^channel:/i.test(trimmed)) return "group"; + if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) { + return "user"; + } + if (trimmed.startsWith("#") || /^channel:/i.test(trimmed)) { + return "group"; + } // For some channels (e.g., BlueBubbles/iMessage), bare phone numbers are almost always DM targets. if ((channel === "bluebubbles" || channel === "imessage") && /^\+?\d{6,}$/.test(trimmed)) { @@ -155,7 +185,9 @@ function matchesDirectoryEntry(params: { query: string; }): boolean { const query = normalizeQuery(params.query); - if (!query) return false; + if (!query) { + return false; + } const id = stripTargetPrefixes(normalizeDirectoryEntryId(params.channel, params.entry)); const name = params.entry.name ? stripTargetPrefixes(params.entry.name) : ""; const handle = params.entry.handle ? stripTargetPrefixes(params.entry.handle) : ""; @@ -171,13 +203,17 @@ function resolveMatch(params: { const matches = params.entries.filter((entry) => matchesDirectoryEntry({ channel: params.channel, entry, query: params.query }), ); - if (matches.length === 0) return { kind: "none" as const }; - if (matches.length === 1) return { kind: "single" as const, entry: matches[0] }; + if (matches.length === 0) { + return { kind: "none" as const }; + } + if (matches.length === 1) { + return { kind: "single" as const, entry: matches[0] }; + } return { kind: "ambiguous" as const, entries: matches }; } async function listDirectoryEntries(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; accountId?: string | null; kind: ChannelDirectoryEntryKind; @@ -187,12 +223,16 @@ async function listDirectoryEntries(params: { }): Promise { const plugin = getChannelPlugin(params.channel); const directory = plugin?.directory; - if (!directory) return []; + if (!directory) { + return []; + } const runtime = params.runtime ?? defaultRuntime; const useLive = params.source === "live"; if (params.kind === "user") { const fn = useLive ? (directory.listPeersLive ?? directory.listPeers) : directory.listPeers; - if (!fn) return []; + if (!fn) { + return []; + } return await fn({ cfg: params.cfg, accountId: params.accountId ?? undefined, @@ -202,7 +242,9 @@ async function listDirectoryEntries(params: { }); } const fn = useLive ? (directory.listGroupsLive ?? directory.listGroups) : directory.listGroups; - if (!fn) return []; + if (!fn) { + return []; + } return await fn({ cfg: params.cfg, accountId: params.accountId ?? undefined, @@ -213,7 +255,7 @@ async function listDirectoryEntries(params: { } async function getDirectoryEntries(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; accountId?: string | null; kind: ChannelDirectoryEntryKind; @@ -230,7 +272,9 @@ async function getDirectoryEntries(params: { signature, }); const cached = directoryCache.get(cacheKey, params.cfg); - if (cached) return cached; + if (cached) { + return cached; + } const entries = await listDirectoryEntries({ cfg: params.cfg, channel: params.channel, @@ -269,8 +313,12 @@ function pickAmbiguousMatch( entries: ChannelDirectoryEntry[], mode: ResolveAmbiguousMode, ): ChannelDirectoryEntry | null { - if (entries.length === 0) return null; - if (mode === "first") return entries[0] ?? null; + if (entries.length === 0) { + return null; + } + if (mode === "first") { + return entries[0] ?? null; + } const ranked = entries.map((entry) => ({ entry, rank: typeof entry.rank === "number" ? entry.rank : 0, @@ -281,7 +329,7 @@ function pickAmbiguousMatch( } export async function resolveMessagingTarget(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; input: string; accountId?: string | null; @@ -300,19 +348,33 @@ export async function resolveMessagingTarget(params: { const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw; const looksLikeTargetId = (): boolean => { const trimmed = raw.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } const lookup = plugin?.messaging?.targetResolver?.looksLikeId; - if (lookup) return lookup(trimmed, normalized); - if (/^(channel|group|user):/i.test(trimmed)) return true; - if (/^[@#]/.test(trimmed)) return true; + if (lookup) { + return lookup(trimmed, normalized); + } + if (/^(channel|group|user):/i.test(trimmed)) { + return true; + } + if (/^[@#]/.test(trimmed)) { + return true; + } if (/^\+?\d{6,}$/.test(trimmed)) { // BlueBubbles/iMessage phone numbers should usually resolve via the directory to a DM chat, // otherwise the provider may pick an existing group containing that handle. - if (params.channel === "bluebubbles" || params.channel === "imessage") return false; + if (params.channel === "bluebubbles" || params.channel === "imessage") { + return false; + } + return true; + } + if (trimmed.includes("@thread")) { + return true; + } + if (/^(conversation|user):/i.test(trimmed)) { return true; } - if (trimmed.includes("@thread")) return true; - if (/^(conversation|user):/i.test(trimmed)) return true; return false; }; if (looksLikeTargetId()) { @@ -397,7 +459,7 @@ export async function resolveMessagingTarget(params: { } export async function lookupDirectoryDisplay(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: ChannelId; targetId: string; accountId?: string | null; diff --git a/src/infra/outbound/targets.test.ts b/src/infra/outbound/targets.test.ts index d8b968d06..a8b45af31 100644 --- a/src/infra/outbound/targets.test.ts +++ b/src/infra/outbound/targets.test.ts @@ -1,10 +1,9 @@ import { beforeEach, describe, expect, it } from "vitest"; -import type { MoltbotConfig } from "../../config/config.js"; - -import { setActivePluginRegistry } from "../../plugins/runtime.js"; -import { createTestRegistry } from "../../test-utils/channel-plugins.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { telegramPlugin } from "../../../extensions/telegram/src/channel.js"; import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js"; +import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { resolveOutboundTarget, resolveSessionDeliveryTarget } from "./targets.js"; describe("resolveOutboundTarget", () => { @@ -18,7 +17,7 @@ describe("resolveOutboundTarget", () => { }); it("falls back to whatsapp allowFrom via config", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["+1555"] } }, }; const res = resolveOutboundTarget({ diff --git a/src/infra/outbound/targets.ts b/src/infra/outbound/targets.ts index 8b557c0a6..f2703e2b8 100644 --- a/src/infra/outbound/targets.ts +++ b/src/infra/outbound/targets.ts @@ -1,14 +1,14 @@ -import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; -import { formatCliCommand } from "../../cli/command-format.js"; -import type { ChannelId, ChannelOutboundTargetMode } from "../../channels/plugins/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js"; -import { deliveryContextFromSession } from "../../utils/delivery-context.js"; import type { DeliverableMessageChannel, GatewayMessageChannel, } from "../../utils/message-channel.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; +import { formatCliCommand } from "../../cli/command-format.js"; +import { deliveryContextFromSession } from "../../utils/delivery-context.js"; import { INTERNAL_MESSAGE_CHANNEL, isDeliverableMessageChannel, @@ -119,7 +119,7 @@ export function resolveOutboundTarget(params: { channel: GatewayMessageChannel; to?: string; allowFrom?: string[]; - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; accountId?: string | null; mode?: ChannelOutboundTargetMode; }): OutboundTargetResolution { @@ -127,12 +127,12 @@ export function resolveOutboundTarget(params: { return { ok: false, error: new Error( - `Delivering to WebChat is not supported via \`${formatCliCommand("moltbot agent")}\`; use WhatsApp/Telegram or run with --deliver=false.`, + `Delivering to WebChat is not supported via \`${formatCliCommand("openclaw agent")}\`; use WhatsApp/Telegram or run with --deliver=false.`, ), }; } - const plugin = getChannelPlugin(params.channel as ChannelId); + const plugin = getChannelPlugin(params.channel); if (!plugin) { return { ok: false, @@ -172,7 +172,7 @@ export function resolveOutboundTarget(params: { } export function resolveHeartbeatDeliveryTarget(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; entry?: SessionEntry; heartbeat?: AgentDefaultsConfig["heartbeat"]; }): OutboundTarget { @@ -184,7 +184,9 @@ export function resolveHeartbeatDeliveryTarget(params: { target = rawTarget; } else if (typeof rawTarget === "string") { const normalized = normalizeChannelId(rawTarget); - if (normalized) target = normalized; + if (normalized) { + target = normalized; + } } if (target === "none") { @@ -233,7 +235,7 @@ export function resolveHeartbeatDeliveryTarget(params: { } let reason: string | undefined; - const plugin = getChannelPlugin(resolvedTarget.channel as ChannelId); + const plugin = getChannelPlugin(resolvedTarget.channel); if (plugin?.config.resolveAllowFrom) { const explicit = resolveOutboundTarget({ channel: resolvedTarget.channel, @@ -279,17 +281,21 @@ function resolveHeartbeatSenderId(params: { } if (candidates.length > 0 && allowList.length > 0) { const matched = candidates.find((candidate) => allowList.includes(candidate)); - if (matched) return matched; + if (matched) { + return matched; + } } if (candidates.length > 0 && allowList.length === 0) { return candidates[0]; } - if (allowList.length > 0) return allowList[0]; + if (allowList.length > 0) { + return allowList[0]; + } return candidates[0] ?? "heartbeat"; } export function resolveHeartbeatSenderContext(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; entry?: SessionEntry; delivery: OutboundTarget; }): HeartbeatSenderContext { diff --git a/src/infra/path-env.test.ts b/src/infra/path-env.test.ts index 9c54ff6d5..49d577ce3 100644 --- a/src/infra/path-env.test.ts +++ b/src/infra/path-env.test.ts @@ -1,27 +1,25 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; +import { ensureOpenClawCliOnPath } from "./path-env.js"; -import { ensureMoltbotCliOnPath } from "./path-env.js"; - -describe("ensureMoltbotCliOnPath", () => { - it("prepends the bundled app bin dir when a sibling moltbot exists", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-path-")); +describe("ensureOpenClawCliOnPath", () => { + it("prepends the bundled app bin dir when a sibling openclaw exists", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-path-")); try { const appBinDir = path.join(tmp, "AppBin"); await fs.mkdir(appBinDir, { recursive: true }); - const cliPath = path.join(appBinDir, "moltbot"); + const cliPath = path.join(appBinDir, "openclaw"); await fs.writeFile(cliPath, "#!/bin/sh\necho ok\n", "utf-8"); await fs.chmod(cliPath, 0o755); const originalPath = process.env.PATH; - const originalFlag = process.env.CLAWDBOT_PATH_BOOTSTRAPPED; + const originalFlag = process.env.OPENCLAW_PATH_BOOTSTRAPPED; process.env.PATH = "/usr/bin"; - delete process.env.CLAWDBOT_PATH_BOOTSTRAPPED; + delete process.env.OPENCLAW_PATH_BOOTSTRAPPED; try { - ensureMoltbotCliOnPath({ + ensureOpenClawCliOnPath({ execPath: cliPath, cwd: tmp, homeDir: tmp, @@ -31,8 +29,11 @@ describe("ensureMoltbotCliOnPath", () => { expect(updated.split(path.delimiter)[0]).toBe(appBinDir); } finally { process.env.PATH = originalPath; - if (originalFlag === undefined) delete process.env.CLAWDBOT_PATH_BOOTSTRAPPED; - else process.env.CLAWDBOT_PATH_BOOTSTRAPPED = originalFlag; + if (originalFlag === undefined) { + delete process.env.OPENCLAW_PATH_BOOTSTRAPPED; + } else { + process.env.OPENCLAW_PATH_BOOTSTRAPPED = originalFlag; + } } } finally { await fs.rm(tmp, { recursive: true, force: true }); @@ -41,11 +42,11 @@ describe("ensureMoltbotCliOnPath", () => { it("is idempotent", () => { const originalPath = process.env.PATH; - const originalFlag = process.env.CLAWDBOT_PATH_BOOTSTRAPPED; + const originalFlag = process.env.OPENCLAW_PATH_BOOTSTRAPPED; process.env.PATH = "/bin"; - process.env.CLAWDBOT_PATH_BOOTSTRAPPED = "1"; + process.env.OPENCLAW_PATH_BOOTSTRAPPED = "1"; try { - ensureMoltbotCliOnPath({ + ensureOpenClawCliOnPath({ execPath: "/tmp/does-not-matter", cwd: "/tmp", homeDir: "/tmp", @@ -54,26 +55,29 @@ describe("ensureMoltbotCliOnPath", () => { expect(process.env.PATH).toBe("/bin"); } finally { process.env.PATH = originalPath; - if (originalFlag === undefined) delete process.env.CLAWDBOT_PATH_BOOTSTRAPPED; - else process.env.CLAWDBOT_PATH_BOOTSTRAPPED = originalFlag; + if (originalFlag === undefined) { + delete process.env.OPENCLAW_PATH_BOOTSTRAPPED; + } else { + process.env.OPENCLAW_PATH_BOOTSTRAPPED = originalFlag; + } } }); it("prepends mise shims when available", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-path-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-path-")); const originalPath = process.env.PATH; - const originalFlag = process.env.CLAWDBOT_PATH_BOOTSTRAPPED; + const originalFlag = process.env.OPENCLAW_PATH_BOOTSTRAPPED; const originalMiseDataDir = process.env.MISE_DATA_DIR; try { const appBinDir = path.join(tmp, "AppBin"); await fs.mkdir(appBinDir, { recursive: true }); - const appCli = path.join(appBinDir, "moltbot"); + const appCli = path.join(appBinDir, "openclaw"); await fs.writeFile(appCli, "#!/bin/sh\necho ok\n", "utf-8"); await fs.chmod(appCli, 0o755); const localBinDir = path.join(tmp, "node_modules", ".bin"); await fs.mkdir(localBinDir, { recursive: true }); - const localCli = path.join(localBinDir, "moltbot"); + const localCli = path.join(localBinDir, "openclaw"); await fs.writeFile(localCli, "#!/bin/sh\necho ok\n", "utf-8"); await fs.chmod(localCli, 0o755); @@ -82,9 +86,9 @@ describe("ensureMoltbotCliOnPath", () => { await fs.mkdir(shimsDir, { recursive: true }); process.env.MISE_DATA_DIR = miseDataDir; process.env.PATH = "/usr/bin"; - delete process.env.CLAWDBOT_PATH_BOOTSTRAPPED; + delete process.env.OPENCLAW_PATH_BOOTSTRAPPED; - ensureMoltbotCliOnPath({ + ensureOpenClawCliOnPath({ execPath: appCli, cwd: tmp, homeDir: tmp, @@ -101,18 +105,24 @@ describe("ensureMoltbotCliOnPath", () => { expect(shimsIndex).toBeGreaterThan(localIndex); } finally { process.env.PATH = originalPath; - if (originalFlag === undefined) delete process.env.CLAWDBOT_PATH_BOOTSTRAPPED; - else process.env.CLAWDBOT_PATH_BOOTSTRAPPED = originalFlag; - if (originalMiseDataDir === undefined) delete process.env.MISE_DATA_DIR; - else process.env.MISE_DATA_DIR = originalMiseDataDir; + if (originalFlag === undefined) { + delete process.env.OPENCLAW_PATH_BOOTSTRAPPED; + } else { + process.env.OPENCLAW_PATH_BOOTSTRAPPED = originalFlag; + } + if (originalMiseDataDir === undefined) { + delete process.env.MISE_DATA_DIR; + } else { + process.env.MISE_DATA_DIR = originalMiseDataDir; + } await fs.rm(tmp, { recursive: true, force: true }); } }); it("prepends Linuxbrew dirs when present", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-path-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-path-")); const originalPath = process.env.PATH; - const originalFlag = process.env.CLAWDBOT_PATH_BOOTSTRAPPED; + const originalFlag = process.env.OPENCLAW_PATH_BOOTSTRAPPED; const originalHomebrewPrefix = process.env.HOMEBREW_PREFIX; const originalHomebrewBrewFile = process.env.HOMEBREW_BREW_FILE; const originalXdgBinHome = process.env.XDG_BIN_HOME; @@ -126,12 +136,12 @@ describe("ensureMoltbotCliOnPath", () => { await fs.mkdir(linuxbrewSbin, { recursive: true }); process.env.PATH = "/usr/bin"; - delete process.env.CLAWDBOT_PATH_BOOTSTRAPPED; + delete process.env.OPENCLAW_PATH_BOOTSTRAPPED; delete process.env.HOMEBREW_PREFIX; delete process.env.HOMEBREW_BREW_FILE; delete process.env.XDG_BIN_HOME; - ensureMoltbotCliOnPath({ + ensureOpenClawCliOnPath({ execPath: path.join(execDir, "node"), cwd: tmp, homeDir: tmp, @@ -144,14 +154,26 @@ describe("ensureMoltbotCliOnPath", () => { expect(parts[1]).toBe(linuxbrewSbin); } finally { process.env.PATH = originalPath; - if (originalFlag === undefined) delete process.env.CLAWDBOT_PATH_BOOTSTRAPPED; - else process.env.CLAWDBOT_PATH_BOOTSTRAPPED = originalFlag; - if (originalHomebrewPrefix === undefined) delete process.env.HOMEBREW_PREFIX; - else process.env.HOMEBREW_PREFIX = originalHomebrewPrefix; - if (originalHomebrewBrewFile === undefined) delete process.env.HOMEBREW_BREW_FILE; - else process.env.HOMEBREW_BREW_FILE = originalHomebrewBrewFile; - if (originalXdgBinHome === undefined) delete process.env.XDG_BIN_HOME; - else process.env.XDG_BIN_HOME = originalXdgBinHome; + if (originalFlag === undefined) { + delete process.env.OPENCLAW_PATH_BOOTSTRAPPED; + } else { + process.env.OPENCLAW_PATH_BOOTSTRAPPED = originalFlag; + } + if (originalHomebrewPrefix === undefined) { + delete process.env.HOMEBREW_PREFIX; + } else { + process.env.HOMEBREW_PREFIX = originalHomebrewPrefix; + } + if (originalHomebrewBrewFile === undefined) { + delete process.env.HOMEBREW_BREW_FILE; + } else { + process.env.HOMEBREW_BREW_FILE = originalHomebrewBrewFile; + } + if (originalXdgBinHome === undefined) { + delete process.env.XDG_BIN_HOME; + } else { + process.env.XDG_BIN_HOME = originalXdgBinHome; + } await fs.rm(tmp, { recursive: true, force: true }); } }); diff --git a/src/infra/path-env.ts b/src/infra/path-env.ts index b079b36ec..dc7458789 100644 --- a/src/infra/path-env.ts +++ b/src/infra/path-env.ts @@ -1,11 +1,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { resolveBrewPathDirs } from "./brew.js"; import { isTruthyEnvValue } from "./env.js"; -import { resolveBrewPathDirs } from "./brew.js"; - -type EnsureMoltbotPathOpts = { +type EnsureOpenClawPathOpts = { execPath?: string; cwd?: string; homeDir?: string; @@ -48,7 +47,7 @@ function mergePath(params: { existing: string; prepend: string[] }): string { return merged.join(path.delimiter); } -function candidateBinDirs(opts: EnsureMoltbotPathOpts): string[] { +function candidateBinDirs(opts: EnsureOpenClawPathOpts): string[] { const execPath = opts.execPath ?? process.execPath; const cwd = opts.cwd ?? process.cwd(); const homeDir = opts.homeDir ?? os.homedir(); @@ -56,23 +55,29 @@ function candidateBinDirs(opts: EnsureMoltbotPathOpts): string[] { const candidates: string[] = []; - // Bundled macOS app: `moltbot` lives next to the executable (process.execPath). + // Bundled macOS app: `openclaw` lives next to the executable (process.execPath). try { const execDir = path.dirname(execPath); - const siblingMoltbot = path.join(execDir, "moltbot"); - if (isExecutable(siblingMoltbot)) candidates.push(execDir); + const siblingCli = path.join(execDir, "openclaw"); + if (isExecutable(siblingCli)) { + candidates.push(execDir); + } } catch { // ignore } - // Project-local installs (best effort): if a `node_modules/.bin/moltbot` exists near cwd, + // Project-local installs (best effort): if a `node_modules/.bin/openclaw` exists near cwd, // include it. This helps when running under launchd or other minimal PATH environments. const localBinDir = path.join(cwd, "node_modules", ".bin"); - if (isExecutable(path.join(localBinDir, "moltbot"))) candidates.push(localBinDir); + if (isExecutable(path.join(localBinDir, "openclaw"))) { + candidates.push(localBinDir); + } const miseDataDir = process.env.MISE_DATA_DIR ?? path.join(homeDir, ".local", "share", "mise"); const miseShims = path.join(miseDataDir, "shims"); - if (isDirectory(miseShims)) candidates.push(miseShims); + if (isDirectory(miseShims)) { + candidates.push(miseShims); + } candidates.push(...resolveBrewPathDirs({ homeDir })); @@ -80,7 +85,9 @@ function candidateBinDirs(opts: EnsureMoltbotPathOpts): string[] { if (platform === "darwin") { candidates.push(path.join(homeDir, "Library", "pnpm")); } - if (process.env.XDG_BIN_HOME) candidates.push(process.env.XDG_BIN_HOME); + if (process.env.XDG_BIN_HOME) { + candidates.push(process.env.XDG_BIN_HOME); + } candidates.push(path.join(homeDir, ".local", "bin")); candidates.push(path.join(homeDir, ".local", "share", "pnpm")); candidates.push(path.join(homeDir, ".bun", "bin")); @@ -91,17 +98,23 @@ function candidateBinDirs(opts: EnsureMoltbotPathOpts): string[] { } /** - * Best-effort PATH bootstrap so skills that require the `moltbot` CLI can run + * Best-effort PATH bootstrap so skills that require the `openclaw` CLI can run * under launchd/minimal environments (and inside the macOS app bundle). */ -export function ensureMoltbotCliOnPath(opts: EnsureMoltbotPathOpts = {}) { - if (isTruthyEnvValue(process.env.CLAWDBOT_PATH_BOOTSTRAPPED)) return; - process.env.CLAWDBOT_PATH_BOOTSTRAPPED = "1"; +export function ensureOpenClawCliOnPath(opts: EnsureOpenClawPathOpts = {}) { + if (isTruthyEnvValue(process.env.OPENCLAW_PATH_BOOTSTRAPPED)) { + return; + } + process.env.OPENCLAW_PATH_BOOTSTRAPPED = "1"; const existing = opts.pathEnv ?? process.env.PATH ?? ""; const prepend = candidateBinDirs(opts); - if (prepend.length === 0) return; + if (prepend.length === 0) { + return; + } const merged = mergePath({ existing, prepend }); - if (merged) process.env.PATH = merged; + if (merged) { + process.env.PATH = merged; + } } diff --git a/src/infra/ports-format.ts b/src/infra/ports-format.ts index 5767d6d9b..54fb75b66 100644 --- a/src/infra/ports-format.ts +++ b/src/infra/ports-format.ts @@ -1,27 +1,33 @@ -import { formatCliCommand } from "../cli/command-format.js"; import type { PortListener, PortListenerKind, PortUsage } from "./ports-types.js"; +import { formatCliCommand } from "../cli/command-format.js"; export function classifyPortListener(listener: PortListener, port: number): PortListenerKind { const raw = `${listener.commandLine ?? ""} ${listener.command ?? ""}`.trim().toLowerCase(); - if (raw.includes("moltbot")) return "gateway"; + if (raw.includes("openclaw")) { + return "gateway"; + } if (raw.includes("ssh")) { const portToken = String(port); const tunnelPattern = new RegExp( `-(l|r)\\s*${portToken}\\b|-(l|r)${portToken}\\b|:${portToken}\\b`, ); - if (!raw || tunnelPattern.test(raw)) return "ssh"; + if (!raw || tunnelPattern.test(raw)) { + return "ssh"; + } return "ssh"; } return "unknown"; } export function buildPortHints(listeners: PortListener[], port: number): string[] { - if (listeners.length === 0) return []; + if (listeners.length === 0) { + return []; + } const kinds = new Set(listeners.map((listener) => classifyPortListener(listener, port))); const hints: string[] = []; if (kinds.has("gateway")) { hints.push( - `Gateway already running locally. Stop it (${formatCliCommand("moltbot gateway stop")}) or use a different port.`, + `Gateway already running locally. Stop it (${formatCliCommand("openclaw gateway stop")}) or use a different port.`, ); } if (kinds.has("ssh")) { diff --git a/src/infra/ports-inspect.ts b/src/infra/ports-inspect.ts index 767480ced..970a1c11c 100644 --- a/src/infra/ports-inspect.ts +++ b/src/infra/ports-inspect.ts @@ -1,8 +1,8 @@ import net from "node:net"; -import { runCommandWithTimeout } from "../process/exec.js"; -import { resolveLsofCommand } from "./ports-lsof.js"; -import { buildPortHints } from "./ports-format.js"; import type { PortListener, PortUsage, PortUsageStatus } from "./ports-types.js"; +import { runCommandWithTimeout } from "../process/exec.js"; +import { buildPortHints } from "./ports-format.js"; +import { resolveLsofCommand } from "./ports-lsof.js"; type CommandResult = { stdout: string; @@ -39,7 +39,9 @@ function parseLsofFieldOutput(output: string): PortListener[] { let current: PortListener = {}; for (const line of lines) { if (line.startsWith("p")) { - if (current.pid || current.command) listeners.push(current); + if (current.pid || current.command) { + listeners.push(current); + } const pid = Number.parseInt(line.slice(1), 10); current = Number.isFinite(pid) ? { pid } : {}; } else if (line.startsWith("c")) { @@ -47,23 +49,31 @@ function parseLsofFieldOutput(output: string): PortListener[] { } else if (line.startsWith("n")) { // TCP 127.0.0.1:18789 (LISTEN) // TCP *:18789 (LISTEN) - if (!current.address) current.address = line.slice(1); + if (!current.address) { + current.address = line.slice(1); + } } } - if (current.pid || current.command) listeners.push(current); + if (current.pid || current.command) { + listeners.push(current); + } return listeners; } async function resolveUnixCommandLine(pid: number): Promise { const res = await runCommandSafe(["ps", "-p", String(pid), "-o", "command="]); - if (res.code !== 0) return undefined; + if (res.code !== 0) { + return undefined; + } const line = res.stdout.trim(); return line || undefined; } async function resolveUnixUser(pid: number): Promise { const res = await runCommandSafe(["ps", "-p", String(pid), "-o", "user="]); - if (res.code !== 0) return undefined; + if (res.code !== 0) { + return undefined; + } const line = res.stdout.trim(); return line || undefined; } @@ -78,13 +88,19 @@ async function readUnixListeners( const listeners = parseLsofFieldOutput(res.stdout); await Promise.all( listeners.map(async (listener) => { - if (!listener.pid) return; + if (!listener.pid) { + return; + } const [commandLine, user] = await Promise.all([ resolveUnixCommandLine(listener.pid), resolveUnixUser(listener.pid), ]); - if (commandLine) listener.commandLine = commandLine; - if (user) listener.user = user; + if (commandLine) { + listener.commandLine = commandLine; + } + if (user) { + listener.user = user; + } }), ); return { listeners, detail: res.stdout.trim() || undefined, errors }; @@ -93,9 +109,13 @@ async function readUnixListeners( if (res.code === 1 && !res.error && !stderr) { return { listeners: [], detail: undefined, errors }; } - if (res.error) errors.push(res.error); + if (res.error) { + errors.push(res.error); + } const detail = [stderr, res.stdout.trim()].filter(Boolean).join("\n"); - if (detail) errors.push(detail); + if (detail) { + errors.push(detail); + } return { listeners: [], detail: undefined, errors }; } @@ -104,17 +124,29 @@ function parseNetstatListeners(output: string, port: number): PortListener[] { const portToken = `:${port}`; for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.trim(); - if (!line) continue; - if (!line.toLowerCase().includes("listen")) continue; - if (!line.includes(portToken)) continue; + if (!line) { + continue; + } + if (!line.toLowerCase().includes("listen")) { + continue; + } + if (!line.includes(portToken)) { + continue; + } const parts = line.split(/\s+/); - if (parts.length < 4) continue; + if (parts.length < 4) { + continue; + } const pidRaw = parts.at(-1); const pid = pidRaw ? Number.parseInt(pidRaw, 10) : NaN; const localAddr = parts[1]; const listener: PortListener = {}; - if (Number.isFinite(pid)) listener.pid = pid; - if (localAddr?.includes(portToken)) listener.address = localAddr; + if (Number.isFinite(pid)) { + listener.pid = pid; + } + if (localAddr?.includes(portToken)) { + listener.address = localAddr; + } listeners.push(listener); } return listeners; @@ -122,10 +154,14 @@ function parseNetstatListeners(output: string, port: number): PortListener[] { async function resolveWindowsImageName(pid: number): Promise { const res = await runCommandSafe(["tasklist", "/FI", `PID eq ${pid}`, "/FO", "LIST"]); - if (res.code !== 0) return undefined; + if (res.code !== 0) { + return undefined; + } for (const rawLine of res.stdout.split(/\r?\n/)) { const line = rawLine.trim(); - if (!line.toLowerCase().startsWith("image name:")) continue; + if (!line.toLowerCase().startsWith("image name:")) { + continue; + } const value = line.slice("image name:".length).trim(); return value || undefined; } @@ -142,10 +178,14 @@ async function resolveWindowsCommandLine(pid: number): Promise { - if (!listener.pid) return; + if (!listener.pid) { + return; + } const [imageName, commandLine] = await Promise.all([ resolveWindowsImageName(listener.pid), resolveWindowsCommandLine(listener.pid), ]); - if (imageName) listener.command = imageName; - if (commandLine) listener.commandLine = commandLine; + if (imageName) { + listener.command = imageName; + } + if (commandLine) { + listener.commandLine = commandLine; + } }), ); return { listeners, detail: res.stdout.trim() || undefined, errors }; @@ -191,7 +241,9 @@ async function tryListenOnHost(port: number, host: string): Promise { let sawUnknown = false; for (const host of hosts) { const result = await tryListenOnHost(port, host); - if (result === "busy") return "busy"; - if (result === "unknown") sawUnknown = true; + if (result === "busy") { + return "busy"; + } + if (result === "unknown") { + sawUnknown = true; + } } return sawUnknown ? "unknown" : "free"; } diff --git a/src/infra/ports-lsof.ts b/src/infra/ports-lsof.ts index 4b5f01a6a..1409103e3 100644 --- a/src/infra/ports-lsof.ts +++ b/src/infra/ports-lsof.ts @@ -17,7 +17,9 @@ async function canExecute(path: string): Promise { export async function resolveLsofCommand(): Promise { for (const candidate of LSOF_CANDIDATES) { - if (await canExecute(candidate)) return candidate; + if (await canExecute(candidate)) { + return candidate; + } } return "lsof"; } diff --git a/src/infra/ports.test.ts b/src/infra/ports.test.ts index abb28ea53..96a9294a4 100644 --- a/src/infra/ports.test.ts +++ b/src/infra/ports.test.ts @@ -1,6 +1,5 @@ import net from "node:net"; import { describe, expect, it, vi } from "vitest"; - import { buildPortHints, classifyPortListener, @@ -37,7 +36,7 @@ describe("ports helpers", () => { expect( classifyPortListener( { - commandLine: "node /Users/me/Projects/moltbot/dist/entry.js gateway", + commandLine: "node /Users/me/Projects/openclaw/dist/entry.js gateway", }, 18789, ), diff --git a/src/infra/ports.ts b/src/infra/ports.ts index 027082432..cdbc395fe 100644 --- a/src/infra/ports.ts +++ b/src/infra/ports.ts @@ -1,11 +1,11 @@ import net from "node:net"; +import type { RuntimeEnv } from "../runtime.js"; +import type { PortListener, PortListenerKind, PortUsage, PortUsageStatus } from "./ports-types.js"; import { danger, info, shouldLogVerbose, warn } from "../globals.js"; import { logDebug } from "../logger.js"; -import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { formatPortDiagnostics } from "./ports-format.js"; import { inspectPortUsage } from "./ports-inspect.js"; -import type { PortListener, PortListenerKind, PortUsage, PortUsageStatus } from "./ports-types.js"; class PortInUseError extends Error { port: number; @@ -25,7 +25,9 @@ function isErrno(err: unknown): err is NodeJS.ErrnoException { export async function describePortOwner(port: number): Promise { const diagnostics = await inspectPortUsage(port); - if (diagnostics.listeners.length === 0) return undefined; + if (diagnostics.listeners.length === 0) { + return undefined; + } return formatPortDiagnostics(diagnostics).join("\n"); } @@ -63,10 +65,10 @@ export async function handlePortError( if (details) { runtime.error(info("Port listener details:")); runtime.error(details); - if (/moltbot|src\/index\.ts|dist\/index\.js/.test(details)) { + if (/openclaw|src\/index\.ts|dist\/index\.js/.test(details)) { runtime.error( warn( - "It looks like another moltbot instance is already running. Stop it or pick a different port.", + "It looks like another OpenClaw instance is already running. Stop it or pick a different port.", ), ); } @@ -80,8 +82,12 @@ export async function handlePortError( if (shouldLogVerbose()) { const stdout = (err as { stdout?: string })?.stdout; const stderr = (err as { stderr?: string })?.stderr; - if (stdout?.trim()) logDebug(`stdout: ${stdout.trim()}`); - if (stderr?.trim()) logDebug(`stderr: ${stderr.trim()}`); + if (stdout?.trim()) { + logDebug(`stdout: ${stdout.trim()}`); + } + if (stderr?.trim()) { + logDebug(`stderr: ${stderr.trim()}`); + } } return runtime.exit(1); } diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index 90d73bb59..6be3753d8 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - +import type { UsageProviderId } from "./provider-usage.types.js"; import { ensureAuthProfileStore, listProfilesForProvider, @@ -11,7 +11,6 @@ import { import { getCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import { loadConfig } from "../config/config.js"; -import type { UsageProviderId } from "./provider-usage.types.js"; export type ProviderAuth = { provider: UsageProviderId; @@ -20,7 +19,9 @@ export type ProviderAuth = { }; function parseGoogleToken(apiKey: string): { token: string } | null { - if (!apiKey) return null; + if (!apiKey) { + return null; + } try { const parsed = JSON.parse(apiKey) as { token?: unknown }; if (parsed && typeof parsed.token === "string") { @@ -34,14 +35,20 @@ function parseGoogleToken(apiKey: string): { token: string } | null { function resolveZaiApiKey(): string | undefined { const envDirect = process.env.ZAI_API_KEY?.trim() || process.env.Z_AI_API_KEY?.trim(); - if (envDirect) return envDirect; + if (envDirect) { + return envDirect; + } const envResolved = resolveEnvApiKey("zai"); - if (envResolved?.apiKey) return envResolved.apiKey; + if (envResolved?.apiKey) { + return envResolved.apiKey; + } const cfg = loadConfig(); const key = getCustomProviderApiKey(cfg, "zai") || getCustomProviderApiKey(cfg, "z-ai"); - if (key) return key; + if (key) { + return key; + } const store = ensureAuthProfileStore(); const apiProfile = [ @@ -57,7 +64,9 @@ function resolveZaiApiKey(): string | undefined { try { const authPath = path.join(os.homedir(), ".pi", "agent", "auth.json"); - if (!fs.existsSync(authPath)) return undefined; + if (!fs.existsSync(authPath)) { + return undefined; + } const data = JSON.parse(fs.readFileSync(authPath, "utf-8")) as Record< string, { access?: string } @@ -71,21 +80,64 @@ function resolveZaiApiKey(): string | undefined { function resolveMinimaxApiKey(): string | undefined { const envDirect = process.env.MINIMAX_CODE_PLAN_KEY?.trim() || process.env.MINIMAX_API_KEY?.trim(); - if (envDirect) return envDirect; + if (envDirect) { + return envDirect; + } const envResolved = resolveEnvApiKey("minimax"); - if (envResolved?.apiKey) return envResolved.apiKey; + if (envResolved?.apiKey) { + return envResolved.apiKey; + } const cfg = loadConfig(); const key = getCustomProviderApiKey(cfg, "minimax"); - if (key) return key; + if (key) { + return key; + } const store = ensureAuthProfileStore(); const apiProfile = listProfilesForProvider(store, "minimax").find((id) => { const cred = store.profiles[id]; return cred?.type === "api_key" || cred?.type === "token"; }); - if (!apiProfile) return undefined; + if (!apiProfile) { + return undefined; + } + const cred = store.profiles[apiProfile]; + if (cred?.type === "api_key" && cred.key?.trim()) { + return cred.key.trim(); + } + if (cred?.type === "token" && cred.token?.trim()) { + return cred.token.trim(); + } + return undefined; +} + +function resolveXiaomiApiKey(): string | undefined { + const envDirect = process.env.XIAOMI_API_KEY?.trim(); + if (envDirect) { + return envDirect; + } + + const envResolved = resolveEnvApiKey("xiaomi"); + if (envResolved?.apiKey) { + return envResolved.apiKey; + } + + const cfg = loadConfig(); + const key = getCustomProviderApiKey(cfg, "xiaomi"); + if (key) { + return key; + } + + const store = ensureAuthProfileStore(); + const apiProfile = listProfilesForProvider(store, "xiaomi").find((id) => { + const cred = store.profiles[id]; + return cred?.type === "api_key" || cred?.type === "token"; + }); + if (!apiProfile) { + return undefined; + } const cred = store.profiles[apiProfile]; if (cred?.type === "api_key" && cred.key?.trim()) { return cred.key.trim(); @@ -113,12 +165,16 @@ async function resolveOAuthToken(params: { const candidates = order; const deduped: string[] = []; for (const entry of candidates) { - if (!deduped.includes(entry)) deduped.push(entry); + if (!deduped.includes(entry)) { + deduped.push(entry); + } } for (const profileId of deduped) { const cred = store.profiles[profileId]; - if (!cred || (cred.type !== "oauth" && cred.type !== "token")) continue; + if (!cred || (cred.type !== "oauth" && cred.type !== "token")) { + continue; + } try { const resolved = await resolveApiKeyForProfile({ // Usage snapshots should work even if config profile metadata is stale. @@ -128,7 +184,9 @@ async function resolveOAuthToken(params: { profileId, agentDir: params.agentDir, }); - if (!resolved?.apiKey) continue; + if (!resolved?.apiKey) { + continue; + } let token = resolved.apiKey; if (params.provider === "google-gemini-cli" || params.provider === "google-antigravity") { const parsed = parseGoogleToken(resolved.apiKey); @@ -168,7 +226,9 @@ function resolveOAuthProviders(agentDir?: string): UsageProviderId[] { }; return providers.filter((provider) => { const profiles = listProfilesForProvider(store, provider).filter(isOAuthLikeCredential); - if (profiles.length > 0) return true; + if (profiles.length > 0) { + return true; + } const normalized = normalizeProviderId(provider); const configuredProfiles = Object.entries(cfg.auth?.profiles ?? {}) .filter(([, profile]) => normalizeProviderId(profile.provider) === normalized) @@ -183,7 +243,9 @@ export async function resolveProviderAuths(params: { auth?: ProviderAuth[]; agentDir?: string; }): Promise { - if (params.auth) return params.auth; + if (params.auth) { + return params.auth; + } const oauthProviders = resolveOAuthProviders(params.agentDir); const auths: ProviderAuth[] = []; @@ -191,21 +253,36 @@ export async function resolveProviderAuths(params: { for (const provider of params.providers) { if (provider === "zai") { const apiKey = resolveZaiApiKey(); - if (apiKey) auths.push({ provider, token: apiKey }); + if (apiKey) { + auths.push({ provider, token: apiKey }); + } continue; } if (provider === "minimax") { const apiKey = resolveMinimaxApiKey(); - if (apiKey) auths.push({ provider, token: apiKey }); + if (apiKey) { + auths.push({ provider, token: apiKey }); + } + continue; + } + if (provider === "xiaomi") { + const apiKey = resolveXiaomiApiKey(); + if (apiKey) { + auths.push({ provider, token: apiKey }); + } continue; } - if (!oauthProviders.includes(provider)) continue; + if (!oauthProviders.includes(provider)) { + continue; + } const auth = await resolveOAuthToken({ provider, agentDir: params.agentDir, }); - if (auth) auths.push(auth); + if (auth) { + auths.push(auth); + } } return auths; diff --git a/src/infra/provider-usage.fetch.antigravity.ts b/src/infra/provider-usage.fetch.antigravity.ts index b40b6d91e..e739458c9 100644 --- a/src/infra/provider-usage.fetch.antigravity.ts +++ b/src/infra/provider-usage.fetch.antigravity.ts @@ -1,7 +1,7 @@ +import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; import { logDebug } from "../logger.js"; import { fetchJson } from "./provider-usage.fetch.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; -import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; type LoadCodeAssistResponse = { availablePromptCredits?: number | string; @@ -46,19 +46,27 @@ const METADATA = { }; function parseNumber(value: number | string | undefined): number | undefined { - if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } if (typeof value === "string") { const parsed = Number.parseFloat(value); - if (Number.isFinite(parsed)) return parsed; + if (Number.isFinite(parsed)) { + return parsed; + } } return undefined; } function parseEpochMs(isoString: string | undefined): number | undefined { - if (!isoString?.trim()) return undefined; + if (!isoString?.trim()) { + return undefined; + } try { const ms = Date.parse(isoString); - if (Number.isFinite(ms)) return ms; + if (Number.isFinite(ms)) { + return ms; + } } catch { // ignore parse errors } @@ -69,7 +77,9 @@ async function parseErrorMessage(res: Response): Promise { try { const data = (await res.json()) as { error?: { message?: string } }; const message = data?.error?.message?.trim(); - if (message) return message; + if (message) { + return message; + } } catch { // ignore parse errors } @@ -79,36 +89,52 @@ async function parseErrorMessage(res: Response): Promise { function extractCredits(data: LoadCodeAssistResponse): CreditsInfo | undefined { const available = parseNumber(data.availablePromptCredits); const monthly = parseNumber(data.planInfo?.monthlyPromptCredits); - if (available === undefined || monthly === undefined || monthly <= 0) return undefined; + if (available === undefined || monthly === undefined || monthly <= 0) { + return undefined; + } return { available, monthly }; } function extractPlanInfo(data: LoadCodeAssistResponse): string | undefined { const tierName = data.currentTier?.name?.trim(); - if (tierName) return tierName; + if (tierName) { + return tierName; + } const planType = data.planType?.trim(); - if (planType) return planType; + if (planType) { + return planType; + } return undefined; } function extractProjectId(data: LoadCodeAssistResponse): string | undefined { const project = data.cloudaicompanionProject; - if (!project) return undefined; - if (typeof project === "string") return project.trim() ? project : undefined; + if (!project) { + return undefined; + } + if (typeof project === "string") { + return project.trim() ? project : undefined; + } const projectId = typeof project.id === "string" ? project.id.trim() : undefined; return projectId || undefined; } function extractModelQuotas(data: FetchAvailableModelsResponse): Map { const result = new Map(); - if (!data.models || typeof data.models !== "object") return result; + if (!data.models || typeof data.models !== "object") { + return result; + } for (const [modelId, modelInfo] of Object.entries(data.models)) { const quotaInfo = modelInfo.quotaInfo; - if (!quotaInfo) continue; + if (!quotaInfo) { + continue; + } const remainingFraction = parseNumber(quotaInfo.remainingFraction); - if (remainingFraction === undefined) continue; + if (remainingFraction === undefined) { + continue; + } const resetTime = parseEpochMs(quotaInfo.resetTime); result.set(modelId, { remainingFraction, resetTime }); @@ -145,7 +171,9 @@ function buildUsageWindows(opts: { const usedPercent = clampPercent((1 - quota.remainingFraction) * 100); const window: UsageWindow = { label: modelId, usedPercent }; - if (quota.resetTime) window.resetAt = quota.resetTime; + if (quota.resetTime) { + window.resetAt = quota.resetTime; + } modelWindows.push(window); } @@ -251,7 +279,9 @@ export async function fetchAntigravityUsage( } } } catch { - if (!lastError) lastError = "Network error"; + if (!lastError) { + lastError = "Network error"; + } } // Build windows from available data diff --git a/src/infra/provider-usage.fetch.claude.ts b/src/infra/provider-usage.fetch.claude.ts index a0f6a241a..e0d0b67e4 100644 --- a/src/infra/provider-usage.fetch.claude.ts +++ b/src/infra/provider-usage.fetch.claude.ts @@ -1,6 +1,6 @@ +import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; import { fetchJson } from "./provider-usage.fetch.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; -import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; type ClaudeUsageResponse = { five_hour?: { utilization?: number; resets_at?: string }; @@ -19,10 +19,14 @@ type ClaudeWebUsageResponse = ClaudeUsageResponse; function resolveClaudeWebSessionKey(): string | undefined { const direct = process.env.CLAUDE_AI_SESSION_KEY?.trim() ?? process.env.CLAUDE_WEB_SESSION_KEY?.trim(); - if (direct?.startsWith("sk-ant-")) return direct; + if (direct?.startsWith("sk-ant-")) { + return direct; + } const cookieHeader = process.env.CLAUDE_WEB_COOKIE?.trim(); - if (!cookieHeader) return undefined; + if (!cookieHeader) { + return undefined; + } const stripped = cookieHeader.replace(/^cookie:\\s*/i, ""); const match = stripped.match(/(?:^|;\\s*)sessionKey=([^;\\s]+)/i); const value = match?.[1]?.trim(); @@ -45,11 +49,15 @@ async function fetchClaudeWebUsage( timeoutMs, fetchFn, ); - if (!orgRes.ok) return null; + if (!orgRes.ok) { + return null; + } const orgs = (await orgRes.json()) as ClaudeWebOrganizationsResponse; const orgId = orgs?.[0]?.uuid?.trim(); - if (!orgId) return null; + if (!orgId) { + return null; + } const usageRes = await fetchJson( `https://claude.ai/api/organizations/${orgId}/usage`, @@ -57,7 +65,9 @@ async function fetchClaudeWebUsage( timeoutMs, fetchFn, ); - if (!usageRes.ok) return null; + if (!usageRes.ok) { + return null; + } const data = (await usageRes.json()) as ClaudeWebUsageResponse; const windows: UsageWindow[] = []; @@ -86,7 +96,9 @@ async function fetchClaudeWebUsage( }); } - if (windows.length === 0) return null; + if (windows.length === 0) { + return null; + } return { provider: "anthropic", displayName: PROVIDER_LABELS.anthropic, @@ -104,7 +116,7 @@ export async function fetchClaudeUsage( { headers: { Authorization: `Bearer ${token}`, - "User-Agent": "moltbot", + "User-Agent": "openclaw", Accept: "application/json", "anthropic-version": "2023-06-01", "anthropic-beta": "oauth-2025-04-20", @@ -121,7 +133,9 @@ export async function fetchClaudeUsage( error?: { message?: unknown } | null; }; const raw = data?.error?.message; - if (typeof raw === "string" && raw.trim()) message = raw.trim(); + if (typeof raw === "string" && raw.trim()) { + message = raw.trim(); + } } catch { // ignore parse errors } @@ -133,7 +147,9 @@ export async function fetchClaudeUsage( const sessionKey = resolveClaudeWebSessionKey(); if (sessionKey) { const web = await fetchClaudeWebUsage(sessionKey, timeoutMs, fetchFn); - if (web) return web; + if (web) { + return web; + } } } diff --git a/src/infra/provider-usage.fetch.codex.ts b/src/infra/provider-usage.fetch.codex.ts index 4a3a1cf4e..fa433586a 100644 --- a/src/infra/provider-usage.fetch.codex.ts +++ b/src/infra/provider-usage.fetch.codex.ts @@ -1,6 +1,6 @@ +import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; import { fetchJson } from "./provider-usage.fetch.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; -import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; type CodexUsageResponse = { rate_limit?: { @@ -30,7 +30,9 @@ export async function fetchCodexUsage( "User-Agent": "CodexBar", Accept: "application/json", }; - if (accountId) headers["ChatGPT-Account-Id"] = accountId; + if (accountId) { + headers["ChatGPT-Account-Id"] = accountId; + } const res = await fetchJson( "https://chatgpt.com/backend-api/wham/usage", diff --git a/src/infra/provider-usage.fetch.copilot.ts b/src/infra/provider-usage.fetch.copilot.ts index 3782982aa..bcdd9a431 100644 --- a/src/infra/provider-usage.fetch.copilot.ts +++ b/src/infra/provider-usage.fetch.copilot.ts @@ -1,6 +1,6 @@ +import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; import { fetchJson } from "./provider-usage.fetch.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; -import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; type CopilotUsageResponse = { quota_snapshots?: { diff --git a/src/infra/provider-usage.fetch.gemini.ts b/src/infra/provider-usage.fetch.gemini.ts index 5c6fe244d..7ec96651d 100644 --- a/src/infra/provider-usage.fetch.gemini.ts +++ b/src/infra/provider-usage.fetch.gemini.ts @@ -1,10 +1,10 @@ -import { fetchJson } from "./provider-usage.fetch.shared.js"; -import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; import type { ProviderUsageSnapshot, UsageProviderId, UsageWindow, } from "./provider-usage.types.js"; +import { fetchJson } from "./provider-usage.fetch.shared.js"; +import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; type GeminiUsageResponse = { buckets?: Array<{ modelId?: string; remainingFraction?: number }>; @@ -45,7 +45,9 @@ export async function fetchGeminiUsage( for (const bucket of data.buckets || []) { const model = bucket.modelId || "unknown"; const frac = bucket.remainingFraction ?? 1; - if (!quotas[model] || frac < quotas[model]) quotas[model] = frac; + if (!quotas[model] || frac < quotas[model]) { + quotas[model] = frac; + } } const windows: UsageWindow[] = []; @@ -58,24 +60,30 @@ export async function fetchGeminiUsage( const lower = model.toLowerCase(); if (lower.includes("pro")) { hasPro = true; - if (frac < proMin) proMin = frac; + if (frac < proMin) { + proMin = frac; + } } if (lower.includes("flash")) { hasFlash = true; - if (frac < flashMin) flashMin = frac; + if (frac < flashMin) { + flashMin = frac; + } } } - if (hasPro) + if (hasPro) { windows.push({ label: "Pro", usedPercent: clampPercent((1 - proMin) * 100), }); - if (hasFlash) + } + if (hasFlash) { windows.push({ label: "Flash", usedPercent: clampPercent((1 - flashMin) * 100), }); + } return { provider, displayName: PROVIDER_LABELS[provider], windows }; } diff --git a/src/infra/provider-usage.fetch.minimax.ts b/src/infra/provider-usage.fetch.minimax.ts index 1cc2cea51..0ff4c680e 100644 --- a/src/infra/provider-usage.fetch.minimax.ts +++ b/src/infra/provider-usage.fetch.minimax.ts @@ -1,6 +1,6 @@ +import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; import { fetchJson } from "./provider-usage.fetch.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; -import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; type MinimaxBaseResp = { status_code?: number; @@ -155,10 +155,14 @@ function isRecord(value: unknown): value is Record { function pickNumber(record: Record, keys: readonly string[]): number | undefined { for (const key of keys) { const value = record[key]; - if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } if (typeof value === "string") { const parsed = Number.parseFloat(value); - if (Number.isFinite(parsed)) return parsed; + if (Number.isFinite(parsed)) { + return parsed; + } } } return undefined; @@ -167,19 +171,25 @@ function pickNumber(record: Record, keys: readonly string[]): n function pickString(record: Record, keys: readonly string[]): string | undefined { for (const key of keys) { const value = record[key]; - if (typeof value === "string" && value.trim()) return value.trim(); + if (typeof value === "string" && value.trim()) { + return value.trim(); + } } return undefined; } function parseEpoch(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) { - if (value < 1e12) return Math.floor(value * 1000); + if (value < 1e12) { + return Math.floor(value * 1000); + } return Math.floor(value); } if (typeof value === "string" && value.trim()) { const parsed = Date.parse(value); - if (Number.isFinite(parsed)) return parsed; + if (Number.isFinite(parsed)) { + return parsed; + } } return undefined; } @@ -190,11 +200,21 @@ function hasAny(record: Record, keys: readonly string[]): boole function scoreUsageRecord(record: Record): number { let score = 0; - if (hasAny(record, PERCENT_KEYS)) score += 4; - if (hasAny(record, TOTAL_KEYS)) score += 3; - if (hasAny(record, USED_KEYS) || hasAny(record, REMAINING_KEYS)) score += 2; - if (hasAny(record, RESET_KEYS)) score += 1; - if (hasAny(record, PLAN_KEYS)) score += 1; + if (hasAny(record, PERCENT_KEYS)) { + score += 4; + } + if (hasAny(record, TOTAL_KEYS)) { + score += 3; + } + if (hasAny(record, USED_KEYS) || hasAny(record, REMAINING_KEYS)) { + score += 2; + } + if (hasAny(record, RESET_KEYS)) { + score += 1; + } + if (hasAny(record, PLAN_KEYS)) { + score += 1; + } return score; } @@ -208,15 +228,21 @@ function collectUsageCandidates(root: Record): Record 0) candidates.push({ record: value, score, depth }); + if (score > 0) { + candidates.push({ record: value, score, depth }); + } if (depth < MAX_SCAN_DEPTH) { for (const nested of Object.values(value)) { if (isRecord(nested) || Array.isArray(nested)) { @@ -242,9 +268,13 @@ function collectUsageCandidates(root: Record): Record): string { const hours = pickNumber(payload, WINDOW_HOUR_KEYS); - if (hours && Number.isFinite(hours)) return `${hours}h`; + if (hours && Number.isFinite(hours)) { + return `${hours}h`; + } const minutes = pickNumber(payload, WINDOW_MINUTE_KEYS); - if (minutes && Number.isFinite(minutes)) return `${minutes}m`; + if (minutes && Number.isFinite(minutes)) { + return `${minutes}m`; + } return "5h"; } @@ -289,7 +319,7 @@ export async function fetchMinimaxUsage( headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", - "MM-API-Source": "Moltbot", + "MM-API-Source": "OpenClaw", }, }, timeoutMs, @@ -315,7 +345,7 @@ export async function fetchMinimaxUsage( }; } - const baseResp = isRecord(data.base_resp) ? (data.base_resp as MinimaxBaseResp) : undefined; + const baseResp = isRecord(data.base_resp) ? data.base_resp : undefined; if (baseResp && typeof baseResp.status_code === "number" && baseResp.status_code !== 0) { return { provider: "minimax", diff --git a/src/infra/provider-usage.fetch.zai.ts b/src/infra/provider-usage.fetch.zai.ts index 03237f279..1a8fc2ea8 100644 --- a/src/infra/provider-usage.fetch.zai.ts +++ b/src/infra/provider-usage.fetch.zai.ts @@ -1,6 +1,6 @@ +import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; import { fetchJson } from "./provider-usage.fetch.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; -import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; type ZaiUsageResponse = { success?: boolean; @@ -63,9 +63,13 @@ export async function fetchZaiUsage( const percent = clampPercent(limit.percentage || 0); const nextReset = limit.nextResetTime ? new Date(limit.nextResetTime).getTime() : undefined; let windowLabel = "Limit"; - if (limit.unit === 1) windowLabel = `${limit.number}d`; - else if (limit.unit === 3) windowLabel = `${limit.number}h`; - else if (limit.unit === 5) windowLabel = `${limit.number}m`; + if (limit.unit === 1) { + windowLabel = `${limit.number}d`; + } else if (limit.unit === 3) { + windowLabel = `${limit.number}h`; + } else if (limit.unit === 5) { + windowLabel = `${limit.number}m`; + } if (limit.type === "TOKENS_LIMIT") { windows.push({ diff --git a/src/infra/provider-usage.format.ts b/src/infra/provider-usage.format.ts index d10879008..7733d8121 100644 --- a/src/infra/provider-usage.format.ts +++ b/src/infra/provider-usage.format.ts @@ -1,21 +1,31 @@ -import { clampPercent } from "./provider-usage.shared.js"; import type { ProviderUsageSnapshot, UsageSummary, UsageWindow } from "./provider-usage.types.js"; +import { clampPercent } from "./provider-usage.shared.js"; function formatResetRemaining(targetMs?: number, now?: number): string | null { - if (!targetMs) return null; + if (!targetMs) { + return null; + } const base = now ?? Date.now(); const diffMs = targetMs - base; - if (diffMs <= 0) return "now"; + if (diffMs <= 0) { + return "now"; + } const diffMins = Math.floor(diffMs / 60000); - if (diffMins < 60) return `${diffMins}m`; + if (diffMins < 60) { + return `${diffMins}m`; + } const hours = Math.floor(diffMins / 60); const mins = diffMins % 60; - if (hours < 24) return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; + if (hours < 24) { + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; + } const days = Math.floor(hours / 24); - if (days < 7) return `${days}d ${hours % 24}h`; + if (days < 7) { + return `${days}d ${hours % 24}h`; + } return new Intl.DateTimeFormat("en-US", { month: "short", @@ -24,7 +34,9 @@ function formatResetRemaining(targetMs?: number, now?: number): string | null { } function pickPrimaryWindow(windows: UsageWindow[]): UsageWindow | undefined { - if (windows.length === 0) return undefined; + if (windows.length === 0) { + return undefined; + } return windows.reduce((best, next) => (next.usedPercent > best.usedPercent ? next : best)); } @@ -39,8 +51,12 @@ export function formatUsageWindowSummary( snapshot: ProviderUsageSnapshot, opts?: { now?: number; maxWindows?: number; includeResets?: boolean }, ): string | null { - if (snapshot.error) return null; - if (snapshot.windows.length === 0) return null; + if (snapshot.error) { + return null; + } + if (snapshot.windows.length === 0) { + return null; + } const now = opts?.now ?? Date.now(); const maxWindows = typeof opts?.maxWindows === "number" && opts.maxWindows > 0 @@ -64,17 +80,23 @@ export function formatUsageSummaryLine( const providers = summary.providers .filter((entry) => entry.windows.length > 0 && !entry.error) .slice(0, opts?.maxProviders ?? summary.providers.length); - if (providers.length === 0) return null; + if (providers.length === 0) { + return null; + } const parts = providers .map((entry) => { const window = pickPrimaryWindow(entry.windows); - if (!window) return null; + if (!window) { + return null; + } return `${entry.displayName} ${formatWindowShort(window, opts?.now)}`; }) .filter(Boolean) as string[]; - if (parts.length === 0) return null; + if (parts.length === 0) { + return null; + } return `📊 Usage: ${parts.join(" · ")}`; } diff --git a/src/infra/provider-usage.load.ts b/src/infra/provider-usage.load.ts index 39a97a86c..ea3a5b434 100644 --- a/src/infra/provider-usage.load.ts +++ b/src/infra/provider-usage.load.ts @@ -1,3 +1,9 @@ +import type { + ProviderUsageSnapshot, + UsageProviderId, + UsageSummary, +} from "./provider-usage.types.js"; +import { resolveFetch } from "./fetch.js"; import { type ProviderAuth, resolveProviderAuths } from "./provider-usage.auth.js"; import { fetchAntigravityUsage, @@ -15,12 +21,6 @@ import { usageProviders, withTimeout, } from "./provider-usage.shared.js"; -import type { - ProviderUsageSnapshot, - UsageProviderId, - UsageSummary, -} from "./provider-usage.types.js"; -import { resolveFetch } from "./fetch.js"; type UsageSummaryOptions = { now?: number; @@ -66,6 +66,12 @@ export async function loadProviderUsageSummary( return await fetchCodexUsage(auth.token, auth.accountId, timeoutMs, fetchFn); case "minimax": return await fetchMinimaxUsage(auth.token, timeoutMs, fetchFn); + case "xiaomi": + return { + provider: "xiaomi", + displayName: PROVIDER_LABELS.xiaomi, + windows: [], + }; case "zai": return await fetchZaiUsage(auth.token, timeoutMs, fetchFn); default: @@ -89,8 +95,12 @@ export async function loadProviderUsageSummary( const snapshots = await Promise.all(tasks); const providers = snapshots.filter((entry) => { - if (entry.windows.length > 0) return true; - if (!entry.error) return true; + if (entry.windows.length > 0) { + return true; + } + if (!entry.error) { + return true; + } return !ignoredErrors.has(entry.error); }); diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 6c8c1d9bb..2f66a7403 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -1,5 +1,5 @@ -import { normalizeProviderId } from "../agents/model-selection.js"; import type { UsageProviderId } from "./provider-usage.types.js"; +import { normalizeProviderId } from "../agents/model-selection.js"; export const DEFAULT_TIMEOUT_MS = 5000; @@ -10,6 +10,7 @@ export const PROVIDER_LABELS: Record = { "google-antigravity": "Antigravity", minimax: "MiniMax", "openai-codex": "Codex", + xiaomi: "Xiaomi", zai: "z.ai", }; @@ -20,11 +21,14 @@ export const usageProviders: UsageProviderId[] = [ "google-antigravity", "minimax", "openai-codex", + "xiaomi", "zai", ]; export function resolveUsageProviderId(provider?: string | null): UsageProviderId | undefined { - if (!provider) return undefined; + if (!provider) { + return undefined; + } const normalized = normalizeProviderId(provider); return usageProviders.includes(normalized as UsageProviderId) ? (normalized as UsageProviderId) @@ -52,6 +56,8 @@ export const withTimeout = async (work: Promise, ms: number, fallback: T): }), ]); } finally { - if (timeout) clearTimeout(timeout); + if (timeout) { + clearTimeout(timeout); + } } }; diff --git a/src/infra/provider-usage.test.ts b/src/infra/provider-usage.test.ts index 077e70918..43e543a86 100644 --- a/src/infra/provider-usage.test.ts +++ b/src/infra/provider-usage.test.ts @@ -253,7 +253,7 @@ describe("provider usage loading", () => { await withTempHome( async (tempHome) => { const agentDir = path.join( - process.env.CLAWDBOT_STATE_DIR ?? path.join(tempHome, ".clawdbot"), + process.env.OPENCLAW_STATE_DIR ?? path.join(tempHome, ".openclaw"), "agents", "main", "agent", @@ -328,9 +328,9 @@ describe("provider usage loading", () => { }, { env: { - CLAWDBOT_STATE_DIR: (home) => path.join(home, ".clawdbot"), + OPENCLAW_STATE_DIR: (home) => path.join(home, ".openclaw"), }, - prefix: "moltbot-provider-usage-", + prefix: "openclaw-provider-usage-", }, ); }); @@ -383,8 +383,11 @@ describe("provider usage loading", () => { expect(claude?.windows.some((w) => w.label === "5h")).toBe(true); expect(claude?.windows.some((w) => w.label === "Week")).toBe(true); } finally { - if (cookieSnapshot === undefined) delete process.env.CLAUDE_AI_SESSION_KEY; - else process.env.CLAUDE_AI_SESSION_KEY = cookieSnapshot; + if (cookieSnapshot === undefined) { + delete process.env.CLAUDE_AI_SESSION_KEY; + } else { + process.env.CLAUDE_AI_SESSION_KEY = cookieSnapshot; + } } }); }); diff --git a/src/infra/provider-usage.types.ts b/src/infra/provider-usage.types.ts index cef446ceb..0a4637a7d 100644 --- a/src/infra/provider-usage.types.ts +++ b/src/infra/provider-usage.types.ts @@ -24,4 +24,5 @@ export type UsageProviderId = | "google-antigravity" | "minimax" | "openai-codex" + | "xiaomi" | "zai"; diff --git a/src/infra/restart-sentinel.test.ts b/src/infra/restart-sentinel.test.ts index cc4934774..638d389f5 100644 --- a/src/infra/restart-sentinel.test.ts +++ b/src/infra/restart-sentinel.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; - import { consumeRestartSentinel, readRestartSentinel, @@ -16,14 +15,17 @@ describe("restart sentinel", () => { let tempDir: string; beforeEach(async () => { - prevStateDir = process.env.CLAWDBOT_STATE_DIR; - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-sentinel-")); - process.env.CLAWDBOT_STATE_DIR = tempDir; + prevStateDir = process.env.OPENCLAW_STATE_DIR; + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sentinel-")); + process.env.OPENCLAW_STATE_DIR = tempDir; }); afterEach(async () => { - if (prevStateDir) process.env.CLAWDBOT_STATE_DIR = prevStateDir; - else delete process.env.CLAWDBOT_STATE_DIR; + if (prevStateDir) { + process.env.OPENCLAW_STATE_DIR = prevStateDir; + } else { + delete process.env.OPENCLAW_STATE_DIR; + } await fs.rm(tempDir, { recursive: true, force: true }); }); diff --git a/src/infra/restart-sentinel.ts b/src/infra/restart-sentinel.ts index 06ea96426..1f3b13094 100644 --- a/src/infra/restart-sentinel.ts +++ b/src/infra/restart-sentinel.ts @@ -1,6 +1,5 @@ import fs from "node:fs/promises"; import path from "node:path"; - import { formatCliCommand } from "../cli/command-format.js"; import { resolveStateDir } from "../config/paths.js"; @@ -56,7 +55,7 @@ const SENTINEL_FILENAME = "restart-sentinel.json"; export function formatDoctorNonInteractiveHint( env: Record = process.env as Record, ): string { - return `Run: ${formatCliCommand("moltbot doctor --non-interactive", env)}`; + return `Run: ${formatCliCommand("openclaw doctor --non-interactive", env)}`; } export function resolveRestartSentinelPath(env: NodeJS.ProcessEnv = process.env): string { @@ -102,7 +101,9 @@ export async function consumeRestartSentinel( ): Promise { const filePath = resolveRestartSentinelPath(env); const parsed = await readRestartSentinel(env); - if (!parsed) return null; + if (!parsed) { + return null; + } await fs.unlink(filePath).catch(() => {}); return parsed; } @@ -119,8 +120,12 @@ export function summarizeRestartSentinel(payload: RestartSentinelPayload): strin } export function trimLogTail(input?: string | null, maxChars = 8000) { - if (!input) return null; + if (!input) { + return null; + } const text = input.trimEnd(); - if (text.length <= maxChars) return text; + if (text.length <= maxChars) { + return text; + } return `…${text.slice(text.length - maxChars)}`; } diff --git a/src/infra/restart.test.ts b/src/infra/restart.test.ts index 010f71239..d9d09696e 100644 --- a/src/infra/restart.test.ts +++ b/src/infra/restart.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { __testing, consumeGatewaySigusr1RestartAuthorization, diff --git a/src/infra/restart.ts b/src/infra/restart.ts index 130029e53..d671c112b 100644 --- a/src/infra/restart.ts +++ b/src/infra/restart.ts @@ -19,8 +19,12 @@ let sigusr1AuthorizedUntil = 0; let sigusr1ExternalAllowed = false; function resetSigusr1AuthorizationIfExpired(now = Date.now()) { - if (sigusr1AuthorizedCount <= 0) return; - if (now <= sigusr1AuthorizedUntil) return; + if (sigusr1AuthorizedCount <= 0) { + return; + } + if (now <= sigusr1AuthorizedUntil) { + return; + } sigusr1AuthorizedCount = 0; sigusr1AuthorizedUntil = 0; } @@ -44,7 +48,9 @@ export function authorizeGatewaySigusr1Restart(delayMs = 0) { export function consumeGatewaySigusr1RestartAuthorization(): boolean { resetSigusr1AuthorizationIfExpired(); - if (sigusr1AuthorizedCount <= 0) return false; + if (sigusr1AuthorizedCount <= 0) { + return false; + } sigusr1AuthorizedCount -= 1; if (sigusr1AuthorizedCount <= 0) { sigusr1AuthorizedUntil = 0; @@ -63,8 +69,12 @@ function formatSpawnDetail(result: { return text.replace(/\s+/g, " ").trim(); }; if (result.error) { - if (result.error instanceof Error) return result.error.message; - if (typeof result.error === "string") return result.error; + if (result.error instanceof Error) { + return result.error.message; + } + if (typeof result.error === "string") { + return result.error; + } try { return JSON.stringify(result.error); } catch { @@ -72,10 +82,16 @@ function formatSpawnDetail(result: { } } const stderr = clean(result.stderr); - if (stderr) return stderr; + if (stderr) { + return stderr; + } const stdout = clean(result.stdout); - if (stdout) return stdout; - if (typeof result.status === "number") return `exit ${result.status}`; + if (stdout) { + return stdout; + } + if (typeof result.status === "number") { + return `exit ${result.status}`; + } return "unknown error"; } @@ -87,7 +103,7 @@ function normalizeSystemdUnit(raw?: string, profile?: string): string { return unit.endsWith(".service") ? unit : `${unit}.service`; } -export function triggerMoltbotRestart(): RestartAttempt { +export function triggerOpenClawRestart(): RestartAttempt { if (process.env.VITEST || process.env.NODE_ENV === "test") { return { ok: true, method: "supervisor", detail: "test mode" }; } @@ -95,8 +111,8 @@ export function triggerMoltbotRestart(): RestartAttempt { if (process.platform !== "darwin") { if (process.platform === "linux") { const unit = normalizeSystemdUnit( - process.env.CLAWDBOT_SYSTEMD_UNIT, - process.env.CLAWDBOT_PROFILE, + process.env.OPENCLAW_SYSTEMD_UNIT, + process.env.OPENCLAW_PROFILE, ); const userArgs = ["--user", "restart", unit]; tried.push(`systemctl ${userArgs.join(" ")}`); @@ -130,8 +146,8 @@ export function triggerMoltbotRestart(): RestartAttempt { } const label = - process.env.CLAWDBOT_LAUNCHD_LABEL || - resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE); + process.env.OPENCLAW_LAUNCHD_LABEL || + resolveGatewayLaunchAgentLabel(process.env.OPENCLAW_PROFILE); const uid = typeof process.getuid === "function" ? process.getuid() : undefined; const target = uid !== undefined ? `gui/${uid}/${label}` : label; const args = ["kickstart", "-k", target]; diff --git a/src/infra/retry-policy.test.ts b/src/infra/retry-policy.test.ts index 02aedb087..00962367e 100644 --- a/src/infra/retry-policy.test.ts +++ b/src/infra/retry-policy.test.ts @@ -1,5 +1,4 @@ import { afterEach, describe, expect, it, vi } from "vitest"; - import { createTelegramRetryRunner } from "./retry-policy.js"; describe("createTelegramRetryRunner", () => { diff --git a/src/infra/retry-policy.ts b/src/infra/retry-policy.ts index 6d647aa5e..d0a232179 100644 --- a/src/infra/retry-policy.ts +++ b/src/infra/retry-policy.ts @@ -1,5 +1,4 @@ import { RateLimitError } from "@buape/carbon"; - import { formatErrorMessage } from "./errors.js"; import { type RetryConfig, resolveRetryConfig, retryAsync } from "./retry.js"; @@ -22,7 +21,9 @@ export const TELEGRAM_RETRY_DEFAULTS = { const TELEGRAM_RETRY_RE = /429|timeout|connect|reset|closed|unavailable|temporarily/i; function getTelegramRetryAfterMs(err: unknown): number | undefined { - if (!err || typeof err !== "object") return undefined; + if (!err || typeof err !== "object") { + return undefined; + } const candidate = "parameters" in err && err.parameters && typeof err.parameters === "object" ? (err.parameters as { retry_after?: unknown }).retry_after diff --git a/src/infra/retry.test.ts b/src/infra/retry.test.ts index e5f450447..ed4b43fea 100644 --- a/src/infra/retry.test.ts +++ b/src/infra/retry.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { retryAsync } from "./retry.js"; describe("retryAsync", () => { diff --git a/src/infra/retry.ts b/src/infra/retry.ts index 7918cf577..dff51fd49 100644 --- a/src/infra/retry.ts +++ b/src/infra/retry.ts @@ -34,7 +34,9 @@ const asFiniteNumber = (value: unknown): number | undefined => const clampNumber = (value: unknown, fallback: number, min?: number, max?: number) => { const next = asFiniteNumber(value); - if (next === undefined) return fallback; + if (next === undefined) { + return fallback; + } const floor = typeof min === "number" ? min : Number.NEGATIVE_INFINITY; const ceiling = typeof max === "number" ? max : Number.POSITIVE_INFINITY; return Math.min(Math.max(next, floor), ceiling); @@ -58,7 +60,9 @@ export function resolveRetryConfig( } function applyJitter(delayMs: number, jitter: number): number { - if (jitter <= 0) return delayMs; + if (jitter <= 0) { + return delayMs; + } const offset = (Math.random() * 2 - 1) * jitter; return Math.max(0, Math.round(delayMs * (1 + offset))); } @@ -76,7 +80,9 @@ export async function retryAsync( return await fn(); } catch (err) { lastErr = err; - if (i === attempts - 1) break; + if (i === attempts - 1) { + break; + } const delay = initialDelayMs * 2 ** i; await sleep(delay); } @@ -102,7 +108,9 @@ export async function retryAsync( return await fn(); } catch (err) { lastErr = err; - if (attempt >= maxAttempts || !shouldRetry(err, attempt)) break; + if (attempt >= maxAttempts || !shouldRetry(err, attempt)) { + break; + } const retryAfterMs = options.retryAfterMs?.(err); const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs); diff --git a/src/infra/runtime-guard.test.ts b/src/infra/runtime-guard.test.ts index 07bd429d1..1e3d4ef22 100644 --- a/src/infra/runtime-guard.test.ts +++ b/src/infra/runtime-guard.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { assertSupportedRuntime, detectRuntime, diff --git a/src/infra/runtime-guard.ts b/src/infra/runtime-guard.ts index 7f0b330fe..c15668ebf 100644 --- a/src/infra/runtime-guard.ts +++ b/src/infra/runtime-guard.ts @@ -1,5 +1,4 @@ import process from "node:process"; - import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; export type RuntimeKind = "node" | "unknown"; @@ -22,9 +21,13 @@ export type RuntimeDetails = { const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/; export function parseSemver(version: string | null): Semver | null { - if (!version) return null; + if (!version) { + return null; + } const match = version.match(SEMVER_RE); - if (!match) return null; + if (!match) { + return null; + } const [, major, minor, patch] = match; return { major: Number.parseInt(major, 10), @@ -34,9 +37,15 @@ export function parseSemver(version: string | null): Semver | null { } export function isAtLeast(version: Semver | null, minimum: Semver): boolean { - if (!version) return false; - if (version.major !== minimum.major) return version.major > minimum.major; - if (version.minor !== minimum.minor) return version.minor > minimum.minor; + if (!version) { + return false; + } + if (version.major !== minimum.major) { + return version.major > minimum.major; + } + if (version.minor !== minimum.minor) { + return version.minor > minimum.minor; + } return version.patch >= minimum.patch; } @@ -54,7 +63,9 @@ export function detectRuntime(): RuntimeDetails { export function runtimeSatisfies(details: RuntimeDetails): boolean { const parsed = parseSemver(details.version); - if (details.kind === "node") return isAtLeast(parsed, MIN_NODE); + if (details.kind === "node") { + return isAtLeast(parsed, MIN_NODE); + } return false; } @@ -66,7 +77,9 @@ export function assertSupportedRuntime( runtime: RuntimeEnv = defaultRuntime, details: RuntimeDetails = detectRuntime(), ): void { - if (runtimeSatisfies(details)) return; + if (runtimeSatisfies(details)) { + return; + } const versionLabel = details.version ?? "unknown"; const runtimeLabel = @@ -75,11 +88,11 @@ export function assertSupportedRuntime( runtime.error( [ - "moltbot requires Node >=22.0.0.", + "openclaw requires Node >=22.0.0.", `Detected: ${runtimeLabel} (exec: ${execLabel}).`, `PATH searched: ${details.pathEnv}`, "Install Node: https://nodejs.org/en/download", - "Upgrade Node and re-run moltbot.", + "Upgrade Node and re-run openclaw.", ].join("\n"), ); runtime.exit(1); diff --git a/src/infra/session-cost-usage.test.ts b/src/infra/session-cost-usage.test.ts index 79b99b76d..bb598bcb7 100644 --- a/src/infra/session-cost-usage.test.ts +++ b/src/infra/session-cost-usage.test.ts @@ -1,15 +1,13 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { loadCostUsageSummary, loadSessionCostSummary } from "./session-cost-usage.js"; describe("session cost usage", () => { it("aggregates daily totals with log cost and pricing fallback", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-cost-")); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cost-")); const sessionsDir = path.join(root, "agents", "main", "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); const sessionFile = path.join(sessionsDir, "sess-1.jsonl"); @@ -92,23 +90,26 @@ describe("session cost usage", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; - const originalState = process.env.CLAWDBOT_STATE_DIR; - process.env.CLAWDBOT_STATE_DIR = root; + const originalState = process.env.OPENCLAW_STATE_DIR; + process.env.OPENCLAW_STATE_DIR = root; try { const summary = await loadCostUsageSummary({ days: 30, config }); expect(summary.daily.length).toBe(1); expect(summary.totals.totalTokens).toBe(50); expect(summary.totals.totalCost).toBeCloseTo(0.03003, 5); } finally { - if (originalState === undefined) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = originalState; + if (originalState === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = originalState; + } } }); it("summarizes a single session file", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-cost-session-")); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cost-session-")); const sessionFile = path.join(root, "session.jsonl"); const now = new Date(); diff --git a/src/infra/session-cost-usage.ts b/src/infra/session-cost-usage.ts index 6d8578743..3e592825a 100644 --- a/src/infra/session-cost-usage.ts +++ b/src/infra/session-cost-usage.ts @@ -1,11 +1,10 @@ import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; - import type { NormalizedUsage, UsageLike } from "../agents/usage.js"; -import { normalizeUsage } from "../agents/usage.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions/types.js"; +import { normalizeUsage } from "../agents/usage.js"; import { resolveSessionFilePath, resolveSessionTranscriptsDirForAgent, @@ -58,18 +57,28 @@ const emptyTotals = (): CostUsageTotals => ({ }); const toFiniteNumber = (value: unknown): number | undefined => { - if (typeof value !== "number") return undefined; - if (!Number.isFinite(value)) return undefined; + if (typeof value !== "number") { + return undefined; + } + if (!Number.isFinite(value)) { + return undefined; + } return value; }; const extractCostTotal = (usageRaw?: UsageLike | null): number | undefined => { - if (!usageRaw || typeof usageRaw !== "object") return undefined; + if (!usageRaw || typeof usageRaw !== "object") { + return undefined; + } const record = usageRaw as Record; const cost = record.cost as Record | undefined; const total = toFiniteNumber(cost?.total); - if (total === undefined) return undefined; - if (total < 0) return undefined; + if (total === undefined) { + return undefined; + } + if (total < 0) { + return undefined; + } return total; }; @@ -77,13 +86,17 @@ const parseTimestamp = (entry: Record): Date | undefined => { const raw = entry.timestamp; if (typeof raw === "string") { const parsed = new Date(raw); - if (!Number.isNaN(parsed.valueOf())) return parsed; + if (!Number.isNaN(parsed.valueOf())) { + return parsed; + } } const message = entry.message as Record | undefined; const messageTimestamp = toFiniteNumber(message?.timestamp); if (messageTimestamp !== undefined) { const parsed = new Date(messageTimestamp); - if (!Number.isNaN(parsed.valueOf())) return parsed; + if (!Number.isNaN(parsed.valueOf())) { + return parsed; + } } return undefined; }; @@ -91,12 +104,16 @@ const parseTimestamp = (entry: Record): Date | undefined => { const parseUsageEntry = (entry: Record): ParsedUsageEntry | null => { const message = entry.message as Record | undefined; const role = message?.role; - if (role !== "assistant") return null; + if (role !== "assistant") { + return null; + } const usageRaw = (message?.usage as UsageLike | undefined) ?? (entry.usage as UsageLike | undefined); const usage = normalizeUsage(usageRaw); - if (!usage) return null; + if (!usage) { + return null; + } const provider = (typeof message?.provider === "string" ? message?.provider : undefined) ?? @@ -138,7 +155,7 @@ const applyCostTotal = (totals: CostUsageTotals, costTotal: number | undefined) async function scanUsageFile(params: { filePath: string; - config?: MoltbotConfig; + config?: OpenClawConfig; onEntry: (entry: ParsedUsageEntry) => void; }): Promise { const fileStream = fs.createReadStream(params.filePath, { encoding: "utf-8" }); @@ -146,11 +163,15 @@ async function scanUsageFile(params: { for await (const line of rl) { const trimmed = line.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } try { const parsed = JSON.parse(trimmed) as Record; const entry = parseUsageEntry(parsed); - if (!entry) continue; + if (!entry) { + continue; + } if (entry.costTotal === undefined) { const cost = resolveModelCostConfig({ @@ -170,7 +191,7 @@ async function scanUsageFile(params: { export async function loadCostUsageSummary(params?: { days?: number; - config?: MoltbotConfig; + config?: OpenClawConfig; agentId?: string; }): Promise { const days = Math.max(1, Math.floor(params?.days ?? 30)); @@ -191,8 +212,12 @@ export async function loadCostUsageSummary(params?: { .map(async (entry) => { const filePath = path.join(sessionsDir, entry.name); const stats = await fs.promises.stat(filePath).catch(() => null); - if (!stats) return null; - if (stats.mtimeMs < sinceTime) return null; + if (!stats) { + return null; + } + if (stats.mtimeMs < sinceTime) { + return null; + } return filePath; }), ) @@ -204,7 +229,9 @@ export async function loadCostUsageSummary(params?: { config: params?.config, onEntry: (entry) => { const ts = entry.timestamp?.getTime(); - if (!ts || ts < sinceTime) return; + if (!ts || ts < sinceTime) { + return; + } const dayKey = formatDayKey(entry.timestamp ?? now); const bucket = dailyMap.get(dayKey) ?? emptyTotals(); applyUsageTotals(bucket, entry.usage); @@ -218,8 +245,8 @@ export async function loadCostUsageSummary(params?: { } const daily = Array.from(dailyMap.entries()) - .map(([date, bucket]) => ({ date, ...bucket })) - .sort((a, b) => a.date.localeCompare(b.date)); + .map(([date, bucket]) => Object.assign({ date }, bucket)) + .toSorted((a, b) => a.date.localeCompare(b.date)); return { updatedAt: Date.now(), @@ -233,12 +260,14 @@ export async function loadSessionCostSummary(params: { sessionId?: string; sessionEntry?: SessionEntry; sessionFile?: string; - config?: MoltbotConfig; + config?: OpenClawConfig; }): Promise { const sessionFile = params.sessionFile ?? (params.sessionId ? resolveSessionFilePath(params.sessionId, params.sessionEntry) : undefined); - if (!sessionFile || !fs.existsSync(sessionFile)) return null; + if (!sessionFile || !fs.existsSync(sessionFile)) { + return null; + } const totals = emptyTotals(); let lastActivity: number | undefined; diff --git a/src/infra/shell-env.path.test.ts b/src/infra/shell-env.path.test.ts index 0e20e3b1b..1ae19f0be 100644 --- a/src/infra/shell-env.path.test.ts +++ b/src/infra/shell-env.path.test.ts @@ -1,12 +1,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; - import { getShellPathFromLoginShell, resetShellPathCacheForTests } from "./shell-env.js"; describe("getShellPathFromLoginShell", () => { afterEach(() => resetShellPathCacheForTests()); it("returns PATH from login shell env", () => { - if (process.platform === "win32") return; + if (process.platform === "win32") { + return; + } const exec = vi .fn() .mockReturnValue(Buffer.from("PATH=/custom/bin\0HOME=/home/user\0", "utf-8")); @@ -15,7 +16,9 @@ describe("getShellPathFromLoginShell", () => { }); it("caches the value", () => { - if (process.platform === "win32") return; + if (process.platform === "win32") { + return; + } const exec = vi.fn().mockReturnValue(Buffer.from("PATH=/custom/bin\0", "utf-8")); const env = { SHELL: "/bin/sh" } as NodeJS.ProcessEnv; expect(getShellPathFromLoginShell({ env, exec })).toBe("/custom/bin"); @@ -24,7 +27,9 @@ describe("getShellPathFromLoginShell", () => { }); it("returns null on exec failure", () => { - if (process.platform === "win32") return; + if (process.platform === "win32") { + return; + } const exec = vi.fn(() => { throw new Error("boom"); }); diff --git a/src/infra/shell-env.test.ts b/src/infra/shell-env.test.ts index 8c3d3f018..c2391fb96 100644 --- a/src/infra/shell-env.test.ts +++ b/src/infra/shell-env.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { loadShellEnvFallback, resolveShellEnvFallbackTimeoutMs, @@ -9,16 +8,16 @@ import { describe("shell env fallback", () => { it("is disabled by default", () => { expect(shouldEnableShellEnvFallback({} as NodeJS.ProcessEnv)).toBe(false); - expect(shouldEnableShellEnvFallback({ CLAWDBOT_LOAD_SHELL_ENV: "0" })).toBe(false); - expect(shouldEnableShellEnvFallback({ CLAWDBOT_LOAD_SHELL_ENV: "1" })).toBe(true); + expect(shouldEnableShellEnvFallback({ OPENCLAW_LOAD_SHELL_ENV: "0" })).toBe(false); + expect(shouldEnableShellEnvFallback({ OPENCLAW_LOAD_SHELL_ENV: "1" })).toBe(true); }); it("resolves timeout from env with default fallback", () => { expect(resolveShellEnvFallbackTimeoutMs({} as NodeJS.ProcessEnv)).toBe(15000); - expect(resolveShellEnvFallbackTimeoutMs({ CLAWDBOT_SHELL_ENV_TIMEOUT_MS: "42" })).toBe(42); + expect(resolveShellEnvFallbackTimeoutMs({ OPENCLAW_SHELL_ENV_TIMEOUT_MS: "42" })).toBe(42); expect( resolveShellEnvFallbackTimeoutMs({ - CLAWDBOT_SHELL_ENV_TIMEOUT_MS: "nope", + OPENCLAW_SHELL_ENV_TIMEOUT_MS: "nope", }), ).toBe(15000); }); diff --git a/src/infra/shell-env.ts b/src/infra/shell-env.ts index 11c7759aa..7082db2ca 100644 --- a/src/infra/shell-env.ts +++ b/src/infra/shell-env.ts @@ -1,5 +1,4 @@ import { execFileSync } from "node:child_process"; - import { isTruthyEnvValue } from "./env.js"; const DEFAULT_TIMEOUT_MS = 15_000; @@ -16,12 +15,18 @@ function parseShellEnv(stdout: Buffer): Map { const shellEnv = new Map(); const parts = stdout.toString("utf8").split("\0"); for (const part of parts) { - if (!part) continue; + if (!part) { + continue; + } const eq = part.indexOf("="); - if (eq <= 0) continue; + if (eq <= 0) { + continue; + } const key = part.slice(0, eq); const value = part.slice(eq + 1); - if (!key) continue; + if (!key) { + continue; + } shellEnv.set(key, value); } return shellEnv; @@ -74,7 +79,7 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - logger.warn(`[moltbot] shell env fallback failed: ${msg}`); + logger.warn(`[openclaw] shell env fallback failed: ${msg}`); lastAppliedKeys = []; return { ok: false, error: msg, applied: [] }; } @@ -83,9 +88,13 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal const applied: string[] = []; for (const key of opts.expectedKeys) { - if (opts.env[key]?.trim()) continue; + if (opts.env[key]?.trim()) { + continue; + } const value = shellEnv.get(key); - if (!value?.trim()) continue; + if (!value?.trim()) { + continue; + } opts.env[key] = value; applied.push(key); } @@ -95,18 +104,22 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal } export function shouldEnableShellEnvFallback(env: NodeJS.ProcessEnv): boolean { - return isTruthyEnvValue(env.CLAWDBOT_LOAD_SHELL_ENV); + return isTruthyEnvValue(env.OPENCLAW_LOAD_SHELL_ENV); } export function shouldDeferShellEnvFallback(env: NodeJS.ProcessEnv): boolean { - return isTruthyEnvValue(env.CLAWDBOT_DEFER_SHELL_ENV_FALLBACK); + return isTruthyEnvValue(env.OPENCLAW_DEFER_SHELL_ENV_FALLBACK); } export function resolveShellEnvFallbackTimeoutMs(env: NodeJS.ProcessEnv): number { - const raw = env.CLAWDBOT_SHELL_ENV_TIMEOUT_MS?.trim(); - if (!raw) return DEFAULT_TIMEOUT_MS; + const raw = env.OPENCLAW_SHELL_ENV_TIMEOUT_MS?.trim(); + if (!raw) { + return DEFAULT_TIMEOUT_MS; + } const parsed = Number.parseInt(raw, 10); - if (!Number.isFinite(parsed)) return DEFAULT_TIMEOUT_MS; + if (!Number.isFinite(parsed)) { + return DEFAULT_TIMEOUT_MS; + } return Math.max(0, parsed); } @@ -115,7 +128,9 @@ export function getShellPathFromLoginShell(opts: { timeoutMs?: number; exec?: typeof execFileSync; }): string | null { - if (cachedShellPath !== undefined) return cachedShellPath; + if (cachedShellPath !== undefined) { + return cachedShellPath; + } if (process.platform === "win32") { cachedShellPath = null; return cachedShellPath; diff --git a/src/infra/skills-remote.ts b/src/infra/skills-remote.ts index 00329af6b..5854810d3 100644 --- a/src/infra/skills-remote.ts +++ b/src/infra/skills-remote.ts @@ -1,11 +1,11 @@ import type { SkillEligibilityContext, SkillEntry } from "../agents/skills.js"; -import { loadWorkspaceSkillEntries } from "../agents/skills.js"; -import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; -import type { MoltbotConfig } from "../config/config.js"; -import { listNodePairing, updatePairedNodeMetadata } from "./node-pairing.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; -import { bumpSkillsSnapshotVersion } from "../agents/skills/refresh.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { NodeRegistry } from "../gateway/node-registry.js"; +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { loadWorkspaceSkillEntries } from "../agents/skills.js"; +import { bumpSkillsSnapshotVersion } from "../agents/skills/refresh.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; +import { listNodePairing, updatePairedNodeMetadata } from "./node-pairing.js"; type RemoteNodeRecord = { nodeId: string; @@ -30,9 +30,15 @@ function describeNode(nodeId: string): string { } function extractErrorMessage(err: unknown): string | undefined { - if (!err) return undefined; - if (typeof err === "string") return err; - if (err instanceof Error) return err.message; + if (!err) { + return undefined; + } + if (typeof err === "string") { + return err; + } + if (err instanceof Error) { + return err.message; + } if (typeof err === "object" && "message" in err && typeof err.message === "string") { return err.message; } @@ -75,9 +81,15 @@ function isMacPlatform(platform?: string, deviceFamily?: string): boolean { const familyNorm = String(deviceFamily ?? "") .trim() .toLowerCase(); - if (platformNorm.includes("mac")) return true; - if (platformNorm.includes("darwin")) return true; - if (familyNorm === "mac") return true; + if (platformNorm.includes("mac")) { + return true; + } + if (platformNorm.includes("darwin")) { + return true; + } + if (familyNorm === "mac") { + return true; + } return false; } @@ -156,7 +168,7 @@ export function recordRemoteNodeBins(nodeId: string, bins: string[]) { upsertNode({ nodeId, bins }); } -function listWorkspaceDirs(cfg: MoltbotConfig): string[] { +function listWorkspaceDirs(cfg: OpenClawConfig): string[] { const dirs = new Set(); const list = cfg.agents?.list; if (Array.isArray(list)) { @@ -174,14 +186,20 @@ function collectRequiredBins(entries: SkillEntry[], targetPlatform: string): str const bins = new Set(); for (const entry of entries) { const os = entry.metadata?.os ?? []; - if (os.length > 0 && !os.includes(targetPlatform)) continue; + if (os.length > 0 && !os.includes(targetPlatform)) { + continue; + } const required = entry.metadata?.requires?.bins ?? []; const anyBins = entry.metadata?.requires?.anyBins ?? []; for (const bin of required) { - if (bin.trim()) bins.add(bin.trim()); + if (bin.trim()) { + bins.add(bin.trim()); + } } for (const bin of anyBins) { - if (bin.trim()) bins.add(bin.trim()); + if (bin.trim()) { + bins.add(bin.trim()); + } } } return [...bins]; @@ -193,7 +211,9 @@ function buildBinProbeScript(bins: string[]): string { } function parseBinProbePayload(payloadJSON: string | null | undefined, payload?: unknown): string[] { - if (!payloadJSON && !payload) return []; + if (!payloadJSON && !payload) { + return []; + } try { const parsed = payloadJSON ? (JSON.parse(payloadJSON) as { stdout?: unknown; bins?: unknown }) @@ -214,10 +234,16 @@ function parseBinProbePayload(payloadJSON: string | null | undefined, payload?: } function areBinSetsEqual(a: Set | undefined, b: Set): boolean { - if (!a) return false; - if (a.size !== b.size) return false; + if (!a) { + return false; + } + if (a.size !== b.size) { + return false; + } for (const bin of b) { - if (!a.has(bin)) return false; + if (!a.has(bin)) { + return false; + } } return true; } @@ -227,14 +253,20 @@ export async function refreshRemoteNodeBins(params: { platform?: string; deviceFamily?: string; commands?: string[]; - cfg: MoltbotConfig; + cfg: OpenClawConfig; timeoutMs?: number; }) { - if (!remoteRegistry) return; - if (!isMacPlatform(params.platform, params.deviceFamily)) return; + if (!remoteRegistry) { + return; + } + if (!isMacPlatform(params.platform, params.deviceFamily)) { + return; + } const canWhich = supportsSystemWhich(params.commands); const canRun = supportsSystemRun(params.commands); - if (!canWhich && !canRun) return; + if (!canWhich && !canRun) { + return; + } const workspaceDirs = listWorkspaceDirs(params.cfg); const requiredBins = new Set(); @@ -244,7 +276,9 @@ export async function refreshRemoteNodeBins(params: { requiredBins.add(bin); } } - if (requiredBins.size === 0) return; + if (requiredBins.size === 0) { + return; + } try { const binsList = [...requiredBins]; @@ -274,7 +308,9 @@ export async function refreshRemoteNodeBins(params: { const nextBins = new Set(bins); const hasChanged = !areBinSetsEqual(existingBins, nextBins); recordRemoteNodeBins(params.nodeId, bins); - if (!hasChanged) return; + if (!hasChanged) { + return; + } await updatePairedNodeMetadata(params.nodeId, { bins }); bumpSkillsSnapshotVersion({ reason: "remote-node" }); } catch (err) { @@ -286,10 +322,14 @@ export function getRemoteSkillEligibility(): SkillEligibilityContext["remote"] | const macNodes = [...remoteNodes.values()].filter( (node) => isMacPlatform(node.platform, node.deviceFamily) && supportsSystemRun(node.commands), ); - if (macNodes.length === 0) return undefined; + if (macNodes.length === 0) { + return undefined; + } const bins = new Set(); for (const node of macNodes) { - for (const bin of node.bins) bins.add(bin); + for (const bin of node.bins) { + bins.add(bin); + } } const labels = macNodes.map((node) => node.displayName ?? node.nodeId).filter(Boolean); const note = @@ -304,8 +344,10 @@ export function getRemoteSkillEligibility(): SkillEligibilityContext["remote"] | }; } -export async function refreshRemoteBinsForConnectedNodes(cfg: MoltbotConfig) { - if (!remoteRegistry) return; +export async function refreshRemoteBinsForConnectedNodes(cfg: OpenClawConfig) { + if (!remoteRegistry) { + return; + } const connected = remoteRegistry.listConnected(); for (const node of connected) { await refreshRemoteNodeBins({ diff --git a/src/infra/ssh-config.test.ts b/src/infra/ssh-config.test.ts index 8f3248e0c..48a8bf310 100644 --- a/src/infra/ssh-config.test.ts +++ b/src/infra/ssh-config.test.ts @@ -54,6 +54,8 @@ describe("ssh-config", () => { expect(config?.host).toBe("peters-mac-studio-1.sheep-coho.ts.net"); expect(config?.port).toBe(2222); expect(config?.identityFiles).toEqual(["/tmp/id_ed25519"]); + const args = spawnMock.mock.calls[0]?.[1] as string[] | undefined; + expect(args?.slice(-2)).toEqual(["--", "me@alias"]); }); it("returns null when ssh -G fails", async () => { diff --git a/src/infra/ssh-config.ts b/src/infra/ssh-config.ts index 037405e8c..fe3c26f01 100644 --- a/src/infra/ssh-config.ts +++ b/src/infra/ssh-config.ts @@ -1,5 +1,4 @@ import { spawn } from "node:child_process"; - import type { SshParsedTarget } from "./ssh-tunnel.js"; export type SshResolvedConfig = { @@ -10,9 +9,13 @@ export type SshResolvedConfig = { }; function parsePort(value: string | undefined): number | undefined { - if (!value) return undefined; + if (!value) { + return undefined; + } const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed) || parsed <= 0) return undefined; + if (!Number.isFinite(parsed) || parsed <= 0) { + return undefined; + } return parsed; } @@ -21,10 +24,14 @@ export function parseSshConfigOutput(output: string): SshResolvedConfig { const lines = output.split("\n"); for (const raw of lines) { const line = raw.trim(); - if (!line) continue; + if (!line) { + continue; + } const [key, ...rest] = line.split(/\s+/); const value = rest.join(" ").trim(); - if (!key || !value) continue; + if (!key || !value) { + continue; + } switch (key) { case "user": result.user = value; @@ -36,7 +43,9 @@ export function parseSshConfigOutput(output: string): SshResolvedConfig { result.port = parsePort(value); break; case "identityfile": - if (value !== "none") result.identityFiles.push(value); + if (value !== "none") { + result.identityFiles.push(value); + } break; default: break; @@ -58,7 +67,8 @@ export async function resolveSshConfig( args.push("-i", opts.identity.trim()); } const userHost = target.user ? `${target.user}@${target.host}` : target.host; - args.push(userHost); + // Use "--" so userHost can't be parsed as an ssh option. + args.push("--", userHost); return await new Promise((resolve) => { const child = spawn(sshPath, args, { diff --git a/src/infra/ssh-tunnel.test.ts b/src/infra/ssh-tunnel.test.ts new file mode 100644 index 000000000..10aeb21a3 --- /dev/null +++ b/src/infra/ssh-tunnel.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { parseSshTarget } from "./ssh-tunnel.js"; + +describe("parseSshTarget", () => { + it("parses user@host:port targets", () => { + expect(parseSshTarget("me@example.com:2222")).toEqual({ + user: "me", + host: "example.com", + port: 2222, + }); + }); + + it("parses host-only targets with default port", () => { + expect(parseSshTarget("example.com")).toEqual({ + user: undefined, + host: "example.com", + port: 22, + }); + }); + + it("rejects hostnames that start with '-'", () => { + expect(parseSshTarget("-V")).toBeNull(); + expect(parseSshTarget("me@-badhost")).toBeNull(); + expect(parseSshTarget("-oProxyCommand=echo")).toBeNull(); + }); +}); diff --git a/src/infra/ssh-tunnel.ts b/src/infra/ssh-tunnel.ts index 8b3c7693b..a86169c8b 100644 --- a/src/infra/ssh-tunnel.ts +++ b/src/infra/ssh-tunnel.ts @@ -1,6 +1,5 @@ import { spawn } from "node:child_process"; import net from "node:net"; - import { ensurePortAvailable } from "./ports.js"; export type SshParsedTarget = { @@ -24,7 +23,9 @@ function isErrno(err: unknown): err is NodeJS.ErrnoException { export function parseSshTarget(raw: string): SshParsedTarget | null { const trimmed = raw.trim().replace(/^ssh\s+/, ""); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const [userPart, hostPart] = trimmed.includes("@") ? ((): [string | undefined, string] => { @@ -40,11 +41,23 @@ export function parseSshTarget(raw: string): SshParsedTarget | null { const host = hostPart.slice(0, colonIdx).trim(); const portRaw = hostPart.slice(colonIdx + 1).trim(); const port = Number.parseInt(portRaw, 10); - if (!host || !Number.isFinite(port) || port <= 0) return null; + if (!host || !Number.isFinite(port) || port <= 0) { + return null; + } + // Security: Reject hostnames starting with '-' to prevent argument injection + if (host.startsWith("-")) { + return null; + } return { user: userPart, host, port }; } - if (!hostPart) return null; + if (!hostPart) { + return null; + } + // Security: Reject hostnames starting with '-' to prevent argument injection + if (hostPart.startsWith("-")) { + return null; + } return { user: userPart, host: hostPart, port: 22 }; } @@ -82,7 +95,9 @@ async function canConnectLocal(port: number): Promise { async function waitForLocalListener(port: number, timeoutMs: number): Promise { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { - if (await canConnectLocal(port)) return; + if (await canConnectLocal(port)) { + return; + } await new Promise((r) => setTimeout(r, 50)); } throw new Error(`ssh tunnel did not start listening on localhost:${port}`); @@ -96,7 +111,9 @@ export async function startSshPortForward(opts: { timeoutMs: number; }): Promise { const parsed = parseSshTarget(opts.target); - if (!parsed) throw new Error(`invalid SSH target: ${opts.target}`); + if (!parsed) { + throw new Error(`invalid SSH target: ${opts.target}`); + } let localPort = opts.localPortPreferred; try { @@ -134,7 +151,8 @@ export async function startSshPortForward(opts: { if (opts.identity?.trim()) { args.push("-i", opts.identity.trim()); } - args.push(userHost); + // Security: Use '--' to prevent userHost from being interpreted as an option + args.push("--", userHost); const stderr: string[] = []; const child = spawn("/usr/bin/ssh", args, { @@ -150,7 +168,9 @@ export async function startSshPortForward(opts: { }); const stop = async () => { - if (child.killed) return; + if (child.killed) { + return; + } child.kill("SIGTERM"); await new Promise((resolve) => { const t = setTimeout(() => { @@ -179,7 +199,7 @@ export async function startSshPortForward(opts: { } catch (err) { await stop(); const suffix = stderr.length > 0 ? `\n${stderr.join("\n")}` : ""; - throw new Error(`${err instanceof Error ? err.message : String(err)}${suffix}`); + throw new Error(`${err instanceof Error ? err.message : String(err)}${suffix}`, { cause: err }); } return { diff --git a/src/infra/state-migrations.fs.test.ts b/src/infra/state-migrations.fs.test.ts index 95604ebe5..0fab21597 100644 --- a/src/infra/state-migrations.fs.test.ts +++ b/src/infra/state-migrations.fs.test.ts @@ -2,12 +2,11 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; - import { readSessionStoreJson5 } from "./state-migrations.fs.js"; describe("state migrations fs", () => { it("treats array session stores as invalid", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-store-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, "[]", "utf-8"); diff --git a/src/infra/state-migrations.fs.ts b/src/infra/state-migrations.fs.ts index 298fed1bd..1f105d8cd 100644 --- a/src/infra/state-migrations.fs.ts +++ b/src/infra/state-migrations.fs.ts @@ -1,6 +1,5 @@ -import fs from "node:fs"; - import JSON5 from "json5"; +import fs from "node:fs"; export type SessionEntryLike = { sessionId?: string; @@ -36,8 +35,12 @@ export function fileExists(p: string): boolean { } export function isLegacyWhatsAppAuthFile(name: string): boolean { - if (name === "creds.json" || name === "creds.json.bak") return true; - if (!name.endsWith(".json")) return false; + if (name === "creds.json" || name === "creds.json.bak") { + return true; + } + if (!name.endsWith(".json")) { + return false; + } return /^(app-state-sync|session|sender-key|pre-key)-/.test(name); } diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index f5e50740e..39e601fd3 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -1,18 +1,18 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - +import type { OpenClawConfig } from "../config/config.js"; +import type { SessionEntry } from "../config/sessions.js"; +import type { SessionScope } from "../config/sessions/types.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import type { MoltbotConfig } from "../config/config.js"; import { - resolveLegacyStateDir, + resolveLegacyStateDirs, resolveNewStateDir, resolveOAuthDir, resolveStateDir, } from "../config/paths.js"; -import type { SessionEntry } from "../config/sessions.js"; -import type { SessionScope } from "../config/sessions/types.js"; import { saveSessionStore } from "../config/sessions.js"; +import { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { buildAgentMainSessionKey, @@ -20,7 +20,6 @@ import { DEFAULT_MAIN_KEY, normalizeAgentId, } from "../routing/session-key.js"; -import { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js"; import { ensureDir, existsDir, @@ -72,13 +71,23 @@ function isSurfaceGroupKey(key: string): boolean { function isLegacyGroupKey(key: string): boolean { const trimmed = key.trim(); - if (!trimmed) return false; - if (trimmed.startsWith("group:")) return true; + if (!trimmed) { + return false; + } + if (trimmed.startsWith("group:")) { + return true; + } const lower = trimmed.toLowerCase(); - if (!lower.includes("@g.us")) return false; + if (!lower.includes("@g.us")) { + return false; + } // Legacy WhatsApp group keys: bare JID or "whatsapp:" without explicit ":group:" kind. - if (!trimmed.includes(":")) return true; - if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:")) return true; + if (!trimmed.includes(":")) { + return true; + } + if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:")) { + return true; + } return false; } @@ -90,24 +99,34 @@ function canonicalizeSessionKeyForAgent(params: { }): string { const agentId = normalizeAgentId(params.agentId); const raw = params.key.trim(); - if (!raw) return raw; - if (raw.toLowerCase() === "global" || raw.toLowerCase() === "unknown") return raw.toLowerCase(); + if (!raw) { + return raw; + } + if (raw.toLowerCase() === "global" || raw.toLowerCase() === "unknown") { + return raw.toLowerCase(); + } const canonicalMain = canonicalizeMainSessionAlias({ cfg: { session: { scope: params.scope, mainKey: params.mainKey } }, agentId, sessionKey: raw, }); - if (canonicalMain !== raw) return canonicalMain.toLowerCase(); + if (canonicalMain !== raw) { + return canonicalMain.toLowerCase(); + } - if (raw.toLowerCase().startsWith("agent:")) return raw.toLowerCase(); + if (raw.toLowerCase().startsWith("agent:")) { + return raw.toLowerCase(); + } if (raw.toLowerCase().startsWith("subagent:")) { const rest = raw.slice("subagent:".length); return `agent:${agentId}:subagent:${rest}`.toLowerCase(); } if (raw.startsWith("group:")) { const id = raw.slice("group:".length).trim(); - if (!id) return raw; + if (!id) { + return raw; + } const channel = id.toLowerCase().includes("@g.us") ? "whatsapp" : "unknown"; return `agent:${agentId}:${channel}:group:${id}`.toLowerCase(); } @@ -133,13 +152,25 @@ function pickLatestLegacyDirectEntry( let best: SessionEntryLike | null = null; let bestUpdated = -1; for (const [key, entry] of Object.entries(store)) { - if (!entry || typeof entry !== "object") continue; + if (!entry || typeof entry !== "object") { + continue; + } const normalized = key.trim(); - if (!normalized) continue; - if (normalized === "global") continue; - if (normalized.startsWith("agent:")) continue; - if (normalized.toLowerCase().startsWith("subagent:")) continue; - if (isLegacyGroupKey(normalized) || isSurfaceGroupKey(normalized)) continue; + if (!normalized) { + continue; + } + if (normalized === "global") { + continue; + } + if (normalized.startsWith("agent:")) { + continue; + } + if (normalized.toLowerCase().startsWith("subagent:")) { + continue; + } + if (isLegacyGroupKey(normalized) || isSurfaceGroupKey(normalized)) { + continue; + } const updatedAt = typeof entry.updatedAt === "number" ? entry.updatedAt : 0; if (updatedAt > bestUpdated) { bestUpdated = updatedAt; @@ -151,7 +182,9 @@ function pickLatestLegacyDirectEntry( function normalizeSessionEntry(entry: SessionEntryLike): SessionEntry | null { const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : null; - if (!sessionId) return null; + if (!sessionId) { + return null; + } const updatedAt = typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt @@ -176,11 +209,17 @@ function mergeSessionEntry(params: { incoming: SessionEntryLike; preferIncomingOnTie?: boolean; }): SessionEntryLike { - if (!params.existing) return params.incoming; + if (!params.existing) { + return params.incoming; + } const existingUpdated = resolveUpdatedAt(params.existing); const incomingUpdated = resolveUpdatedAt(params.incoming); - if (incomingUpdated > existingUpdated) return params.incoming; - if (incomingUpdated < existingUpdated) return params.existing; + if (incomingUpdated > existingUpdated) { + return params.incoming; + } + if (incomingUpdated < existingUpdated) { + return params.existing; + } return params.preferIncomingOnTie ? params.incoming : params.existing; } @@ -195,7 +234,9 @@ function canonicalizeSessionStore(params: { const legacyKeys: string[] = []; for (const [key, entry] of Object.entries(params.store)) { - if (!entry || typeof entry !== "object") continue; + if (!entry || typeof entry !== "object") { + continue; + } const canonicalKey = canonicalizeSessionKeyForAgent({ key, agentId: params.agentId, @@ -203,7 +244,9 @@ function canonicalizeSessionStore(params: { scope: params.scope, }); const isCanonical = canonicalKey === key; - if (!isCanonical) legacyKeys.push(key); + if (!isCanonical) { + legacyKeys.push(key); + } const existing = canonical[canonicalKey]; if (!existing) { canonical[canonicalKey] = entry; @@ -219,8 +262,12 @@ function canonicalizeSessionStore(params: { meta.set(canonicalKey, { isCanonical, updatedAt: incomingUpdated }); continue; } - if (incomingUpdated < existingUpdated) continue; - if (existingMeta?.isCanonical && !isCanonical) continue; + if (incomingUpdated < existingUpdated) { + continue; + } + if (existingMeta?.isCanonical && !isCanonical) { + continue; + } if (!existingMeta?.isCanonical && isCanonical) { canonical[canonicalKey] = entry; meta.set(canonicalKey, { isCanonical, updatedAt: incomingUpdated }); @@ -245,19 +292,27 @@ function listLegacySessionKeys(params: { mainKey: params.mainKey, scope: params.scope, }); - if (canonical !== key) legacy.push(key); + if (canonical !== key) { + legacy.push(key); + } } return legacy; } function emptyDirOrMissing(dir: string): boolean { - if (!existsDir(dir)) return true; + if (!existsDir(dir)) { + return true; + } return safeReadDir(dir).length === 0; } function removeDirIfEmpty(dir: string) { - if (!existsDir(dir)) return; - if (!emptyDirOrMissing(dir)) return; + if (!existsDir(dir)) { + return; + } + if (!emptyDirOrMissing(dir)) { + return; + } try { fs.rmdirSync(dir); } catch { @@ -316,19 +371,26 @@ export async function autoMigrateLegacyStateDir(params: { autoMigrateStateDirChecked = true; const env = params.env ?? process.env; - if (env.MOLTBOT_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim()) { + if (env.OPENCLAW_STATE_DIR?.trim()) { return { migrated: false, skipped: true, changes: [], warnings: [] }; } const homedir = params.homedir ?? os.homedir; - const legacyDir = resolveLegacyStateDir(homedir); const targetDir = resolveNewStateDir(homedir); + const legacyDirs = resolveLegacyStateDirs(homedir); + let legacyDir = legacyDirs.find((dir) => { + try { + return fs.existsSync(dir); + } catch { + return false; + } + }); const warnings: string[] = []; const changes: string[] = []; let legacyStat: fs.Stats | null = null; try { - legacyStat = fs.lstatSync(legacyDir); + legacyStat = legacyDir ? fs.lstatSync(legacyDir) : null; } catch { legacyStat = null; } @@ -340,13 +402,42 @@ export async function autoMigrateLegacyStateDir(params: { return { migrated: false, skipped: false, changes, warnings }; } - if (legacyStat.isSymbolicLink()) { - const legacyTarget = resolveSymlinkTarget(legacyDir); - if (legacyTarget && path.resolve(legacyTarget) === path.resolve(targetDir)) { + let symlinkDepth = 0; + while (legacyStat.isSymbolicLink()) { + const legacyTarget = legacyDir ? resolveSymlinkTarget(legacyDir) : null; + if (!legacyTarget) { + warnings.push( + `Legacy state dir is a symlink (${legacyDir ?? "unknown"}); could not resolve target.`, + ); return { migrated: false, skipped: false, changes, warnings }; } + if (path.resolve(legacyTarget) === path.resolve(targetDir)) { + return { migrated: false, skipped: false, changes, warnings }; + } + if (legacyDirs.some((dir) => path.resolve(dir) === path.resolve(legacyTarget))) { + legacyDir = legacyTarget; + try { + legacyStat = fs.lstatSync(legacyDir); + } catch { + legacyStat = null; + } + if (!legacyStat) { + warnings.push(`Legacy state dir missing after symlink resolution: ${legacyDir}`); + return { migrated: false, skipped: false, changes, warnings }; + } + if (!legacyStat.isDirectory() && !legacyStat.isSymbolicLink()) { + warnings.push(`Legacy state path is not a directory: ${legacyDir}`); + return { migrated: false, skipped: false, changes, warnings }; + } + symlinkDepth += 1; + if (symlinkDepth > 2) { + warnings.push(`Legacy state dir symlink chain too deep: ${legacyDir}`); + return { migrated: false, skipped: false, changes, warnings }; + } + continue; + } warnings.push( - `Legacy state dir is a symlink (${legacyDir} → ${legacyTarget ?? "unknown"}); skipping auto-migration.`, + `Legacy state dir is a symlink (${legacyDir ?? "unknown"} → ${legacyTarget}); skipping auto-migration.`, ); return { migrated: false, skipped: false, changes, warnings }; } @@ -359,18 +450,29 @@ export async function autoMigrateLegacyStateDir(params: { } try { + if (!legacyDir) { + throw new Error("Legacy state dir not found"); + } fs.renameSync(legacyDir, targetDir); } catch (err) { - warnings.push(`Failed to move legacy state dir (${legacyDir} → ${targetDir}): ${String(err)}`); + warnings.push( + `Failed to move legacy state dir (${legacyDir ?? "unknown"} → ${targetDir}): ${String(err)}`, + ); return { migrated: false, skipped: false, changes, warnings }; } try { + if (!legacyDir) { + throw new Error("Legacy state dir not found"); + } fs.symlinkSync(targetDir, legacyDir, "dir"); changes.push(formatStateDirMigration(legacyDir, targetDir)); } catch (err) { try { if (process.platform === "win32") { + if (!legacyDir) { + throw new Error("Legacy state dir not found", { cause: err }); + } fs.symlinkSync(targetDir, legacyDir, "junction"); changes.push(formatStateDirMigration(legacyDir, targetDir)); } else { @@ -378,6 +480,10 @@ export async function autoMigrateLegacyStateDir(params: { } } catch (fallbackErr) { try { + if (!legacyDir) { + // oxlint-disable-next-line preserve-caught-error + throw new Error("Legacy state dir not found", { cause: fallbackErr }); + } fs.renameSync(targetDir, legacyDir); warnings.push( `State dir migration rolled back (failed to link legacy path): ${String(fallbackErr)}`, @@ -385,12 +491,12 @@ export async function autoMigrateLegacyStateDir(params: { return { migrated: false, skipped: false, changes: [], warnings }; } catch (rollbackErr) { warnings.push( - `State dir moved but failed to link legacy path (${legacyDir} → ${targetDir}): ${String(fallbackErr)}`, + `State dir moved but failed to link legacy path (${legacyDir ?? "unknown"} → ${targetDir}): ${String(fallbackErr)}`, ); warnings.push( - `Rollback failed; set MOLTBOT_STATE_DIR=${targetDir} to avoid split state: ${String(rollbackErr)}`, + `Rollback failed; set OPENCLAW_STATE_DIR=${targetDir} to avoid split state: ${String(rollbackErr)}`, ); - changes.push(`State dir: ${legacyDir} → ${targetDir}`); + changes.push(`State dir: ${legacyDir ?? "unknown"} → ${targetDir}`); } } } @@ -399,7 +505,7 @@ export async function autoMigrateLegacyStateDir(params: { } export async function detectLegacyStateMigrations(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; homedir?: () => string; }): Promise { @@ -494,7 +600,9 @@ async function migrateLegacySessions( ): Promise<{ changes: string[]; warnings: string[] }> { const changes: string[] = []; const warnings: string[] = []; - if (!detected.sessions.hasLegacy) return { changes, warnings }; + if (!detected.sessions.hasLegacy) { + return { changes, warnings }; + } ensureDir(detected.sessions.targetDir); @@ -554,7 +662,9 @@ async function migrateLegacySessions( const normalized: Record = {}; for (const [key, entry] of Object.entries(merged)) { const normalizedEntry = normalizeSessionEntry(entry); - if (!normalizedEntry) continue; + if (!normalizedEntry) { + continue; + } normalized[key] = normalizedEntry; } await saveSessionStore(detected.sessions.targetStorePath, normalized); @@ -566,11 +676,17 @@ async function migrateLegacySessions( const entries = safeReadDir(detected.sessions.legacyDir); for (const entry of entries) { - if (!entry.isFile()) continue; - if (entry.name === "sessions.json") continue; + if (!entry.isFile()) { + continue; + } + if (entry.name === "sessions.json") { + continue; + } const from = path.join(detected.sessions.legacyDir, entry.name); const to = path.join(detected.sessions.targetDir, entry.name); - if (fileExists(to)) continue; + if (fileExists(to)) { + continue; + } try { fs.renameSync(from, to); changes.push(`Moved ${entry.name} → agents/${detected.targetAgentId}/sessions`); @@ -610,7 +726,9 @@ export async function migrateLegacyAgentDir( ): Promise<{ changes: string[]; warnings: string[] }> { const changes: string[] = []; const warnings: string[] = []; - if (!detected.agentDir.hasLegacy) return { changes, warnings }; + if (!detected.agentDir.hasLegacy) { + return { changes, warnings }; + } ensureDir(detected.agentDir.targetDir); @@ -618,7 +736,9 @@ export async function migrateLegacyAgentDir( for (const entry of entries) { const from = path.join(detected.agentDir.legacyDir, entry.name); const to = path.join(detected.agentDir.targetDir, entry.name); - if (fs.existsSync(to)) continue; + if (fs.existsSync(to)) { + continue; + } try { fs.renameSync(from, to); changes.push(`Moved agent file ${entry.name} → agents/${detected.targetAgentId}/agent`); @@ -651,18 +771,28 @@ async function migrateLegacyWhatsAppAuth( ): Promise<{ changes: string[]; warnings: string[] }> { const changes: string[] = []; const warnings: string[] = []; - if (!detected.whatsappAuth.hasLegacy) return { changes, warnings }; + if (!detected.whatsappAuth.hasLegacy) { + return { changes, warnings }; + } ensureDir(detected.whatsappAuth.targetDir); const entries = safeReadDir(detected.whatsappAuth.legacyDir); for (const entry of entries) { - if (!entry.isFile()) continue; - if (entry.name === "oauth.json") continue; - if (!isLegacyWhatsAppAuthFile(entry.name)) continue; + if (!entry.isFile()) { + continue; + } + if (entry.name === "oauth.json") { + continue; + } + if (!isLegacyWhatsAppAuthFile(entry.name)) { + continue; + } const from = path.join(detected.whatsappAuth.legacyDir, entry.name); const to = path.join(detected.whatsappAuth.targetDir, entry.name); - if (fileExists(to)) continue; + if (fileExists(to)) { + continue; + } try { fs.renameSync(from, to); changes.push(`Moved WhatsApp auth ${entry.name} → whatsapp/default`); @@ -690,7 +820,7 @@ export async function runLegacyStateMigrations(params: { } export async function autoMigrateLegacyAgentDir(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; homedir?: () => string; log?: MigrationLogger; @@ -705,7 +835,7 @@ export async function autoMigrateLegacyAgentDir(params: { } export async function autoMigrateLegacyState(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; homedir?: () => string; log?: MigrationLogger; @@ -727,7 +857,7 @@ export async function autoMigrateLegacyState(params: { homedir: params.homedir, log: params.log, }); - if (env.CLAWDBOT_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim()) { + if (env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim()) { return { migrated: stateDirResult.migrated, skipped: true, diff --git a/src/infra/system-events.test.ts b/src/infra/system-events.test.ts index dc5cea250..03a39cd9e 100644 --- a/src/infra/system-events.test.ts +++ b/src/infra/system-events.test.ts @@ -1,11 +1,10 @@ import { beforeEach, describe, expect, it } from "vitest"; - +import type { OpenClawConfig } from "../config/config.js"; import { prependSystemEvents } from "../auto-reply/reply/session-updates.js"; -import type { MoltbotConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { enqueueSystemEvent, peekSystemEvents, resetSystemEventsForTest } from "./system-events.js"; -const cfg = {} as unknown as MoltbotConfig; +const cfg = {} as unknown as OpenClawConfig; const mainKey = resolveMainSessionKey(cfg); describe("system events (session routing)", () => { diff --git a/src/infra/system-events.ts b/src/infra/system-events.ts index b2842142d..866dcb162 100644 --- a/src/infra/system-events.ts +++ b/src/infra/system-events.ts @@ -28,9 +28,13 @@ function requireSessionKey(key?: string | null): string { } function normalizeContextKey(key?: string | null): string | null { - if (!key) return null; + if (!key) { + return null; + } const trimmed = key.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } return trimmed.toLowerCase(); } @@ -58,18 +62,26 @@ export function enqueueSystemEvent(text: string, options: SystemEventOptions) { return created; })(); const cleaned = text.trim(); - if (!cleaned) return; + if (!cleaned) { + return; + } entry.lastContextKey = normalizeContextKey(options?.contextKey); - if (entry.lastText === cleaned) return; // skip consecutive duplicates + if (entry.lastText === cleaned) { + return; + } // skip consecutive duplicates entry.lastText = cleaned; entry.queue.push({ text: cleaned, ts: Date.now() }); - if (entry.queue.length > MAX_EVENTS) entry.queue.shift(); + if (entry.queue.length > MAX_EVENTS) { + entry.queue.shift(); + } } export function drainSystemEventEntries(sessionKey: string): SystemEvent[] { const key = requireSessionKey(sessionKey); const entry = queues.get(key); - if (!entry || entry.queue.length === 0) return []; + if (!entry || entry.queue.length === 0) { + return []; + } const out = entry.queue.slice(); entry.queue.length = 0; entry.lastText = null; diff --git a/src/infra/system-presence.test.ts b/src/infra/system-presence.test.ts index 16e03a06a..982c7d611 100644 --- a/src/infra/system-presence.test.ts +++ b/src/infra/system-presence.test.ts @@ -8,7 +8,7 @@ describe("system-presence", () => { const instanceIdLower = instanceIdUpper.toLowerCase(); upsertPresence(instanceIdUpper, { - host: "moltbot", + host: "openclaw", mode: "ui", instanceId: instanceIdUpper, reason: "connect", @@ -39,7 +39,7 @@ describe("system-presence", () => { upsertPresence(deviceId, { deviceId, - host: "moltbot", + host: "openclaw", roles: ["operator"], scopes: ["operator.admin"], reason: "connect", diff --git a/src/infra/system-presence.ts b/src/infra/system-presence.ts index 0e5d453ac..c78f5ccc1 100644 --- a/src/infra/system-presence.ts +++ b/src/infra/system-presence.ts @@ -32,9 +32,13 @@ const TTL_MS = 5 * 60 * 1000; // 5 minutes const MAX_ENTRIES = 200; function normalizePresenceKey(key: string | undefined): string | undefined { - if (!key) return undefined; + if (!key) { + return undefined; + } const trimmed = key.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } return trimmed.toLowerCase(); } @@ -45,11 +49,15 @@ function resolvePrimaryIPv4(): string | undefined { for (const name of names) { const list = nets[name]; const entry = list?.find((n) => n.family === "IPv4" && !n.internal); - if (entry?.address) return entry.address; + if (entry?.address) { + return entry.address; + } } for (const list of Object.values(nets)) { const entry = list?.find((n) => n.family === "IPv4" && !n.internal); - if (entry?.address) return entry.address; + if (entry?.address) { + return entry.address; + } } return undefined; }; @@ -59,7 +67,7 @@ function resolvePrimaryIPv4(): string | undefined { function initSelfPresence() { const host = os.hostname(); const ip = resolvePrimaryIPv4() ?? undefined; - const version = process.env.CLAWDBOT_VERSION ?? process.env.npm_package_version ?? "unknown"; + const version = process.env.OPENCLAW_VERSION ?? process.env.npm_package_version ?? "unknown"; const modelIdentifier = (() => { const p = os.platform(); if (p === "darwin") { @@ -81,15 +89,25 @@ function initSelfPresence() { const platform = (() => { const p = os.platform(); const rel = os.release(); - if (p === "darwin") return `macos ${macOSVersion()}`; - if (p === "win32") return `windows ${rel}`; + if (p === "darwin") { + return `macos ${macOSVersion()}`; + } + if (p === "win32") { + return `windows ${rel}`; + } return `${p} ${rel}`; })(); const deviceFamily = (() => { const p = os.platform(); - if (p === "darwin") return "Mac"; - if (p === "win32") return "Windows"; - if (p === "linux") return "Linux"; + if (p === "darwin") { + return "Mac"; + } + if (p === "win32") { + return "Windows"; + } + if (p === "linux") { + return "Linux"; + } return p; })(); const text = `Gateway: ${host}${ip ? ` (${ip})` : ""} · app ${version} · mode gateway · reason self`; @@ -175,10 +193,14 @@ type SystemPresencePayload = { function mergeStringList(...values: Array): string[] | undefined { const out = new Set(); for (const list of values) { - if (!Array.isArray(list)) continue; + if (!Array.isArray(list)) { + continue; + } for (const item of list) { const trimmed = String(item).trim(); - if (trimmed) out.add(trimmed); + if (trimmed) { + out.add(trimmed); + } } } return out.size > 0 ? [...out] : undefined; @@ -266,16 +288,18 @@ export function listSystemPresence(): SystemPresence[] { // prune expired const now = Date.now(); for (const [k, v] of entries) { - if (now - v.ts > TTL_MS) entries.delete(k); + if (now - v.ts > TTL_MS) { + entries.delete(k); + } } // enforce max size (LRU by ts) if (entries.size > MAX_ENTRIES) { - const sorted = [...entries.entries()].sort((a, b) => a[1].ts - b[1].ts); + const sorted = [...entries.entries()].toSorted((a, b) => a[1].ts - b[1].ts); const toDrop = entries.size - MAX_ENTRIES; for (let i = 0; i < toDrop; i++) { entries.delete(sorted[i][0]); } } touchSelfPresence(); - return [...entries.values()].sort((a, b) => b.ts - a.ts); + return [...entries.values()].toSorted((a, b) => b.ts - a.ts); } diff --git a/src/infra/tailnet.test.ts b/src/infra/tailnet.test.ts index 1068d1098..15c18368f 100644 --- a/src/infra/tailnet.test.ts +++ b/src/infra/tailnet.test.ts @@ -1,7 +1,5 @@ import os from "node:os"; - import { describe, expect, it, vi } from "vitest"; - import { listTailnetAddresses } from "./tailnet.js"; describe("tailnet address detection", () => { diff --git a/src/infra/tailnet.ts b/src/infra/tailnet.ts index f5900a24b..ed666b868 100644 --- a/src/infra/tailnet.ts +++ b/src/infra/tailnet.ts @@ -7,9 +7,13 @@ export type TailnetAddresses = { function isTailnetIPv4(address: string): boolean { const parts = address.split("."); - if (parts.length !== 4) return false; + if (parts.length !== 4) { + return false; + } const octets = parts.map((p) => Number.parseInt(p, 10)); - if (octets.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) return false; + if (octets.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) { + return false; + } // Tailscale IPv4 range: 100.64.0.0/10 // https://tailscale.com/kb/1015/100.x-addresses @@ -30,13 +34,23 @@ export function listTailnetAddresses(): TailnetAddresses { const ifaces = os.networkInterfaces(); for (const entries of Object.values(ifaces)) { - if (!entries) continue; + if (!entries) { + continue; + } for (const e of entries) { - if (!e || e.internal) continue; + if (!e || e.internal) { + continue; + } const address = e.address?.trim(); - if (!address) continue; - if (isTailnetIPv4(address)) ipv4.push(address); - if (isTailnetIPv6(address)) ipv6.push(address); + if (!address) { + continue; + } + if (isTailnetIPv4(address)) { + ipv4.push(address); + } + if (isTailnetIPv6(address)) { + ipv6.push(address); + } } } diff --git a/src/infra/tailscale.test.ts b/src/infra/tailscale.test.ts index cc31c3ca9..0e30c1f72 100644 --- a/src/infra/tailscale.test.ts +++ b/src/infra/tailscale.test.ts @@ -1,5 +1,4 @@ import { afterEach, describe, expect, it, vi } from "vitest"; - import * as tailscale from "./tailscale.js"; const { diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index 47df93a82..bf74306bf 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -1,10 +1,10 @@ import { existsSync } from "node:fs"; +import { formatCliCommand } from "../cli/command-format.js"; import { promptYesNo } from "../cli/prompt.js"; import { danger, info, logVerbose, shouldLogVerbose, warn } from "../globals.js"; import { runExec } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; -import { formatCliCommand } from "../cli/command-format.js"; import { ensureBinary } from "./binaries.js"; function parsePossiblyNoisyJsonObject(stdout: string): Record { @@ -29,7 +29,9 @@ function parsePossiblyNoisyJsonObject(stdout: string): Record { export async function findTailscaleBinary(): Promise { // Helper to check if a binary exists and is executable const checkBinary = async (path: string): Promise => { - if (!path || !existsSync(path)) return false; + if (!path || !existsSync(path)) { + return false; + } try { // Use Promise.race with runExec to implement timeout await Promise.race([ @@ -109,7 +111,9 @@ export async function getTailnetHostname(exec: typeof runExec = runExec, detecte let lastError: unknown; for (const candidate of candidates) { - if (candidate.startsWith("/") && !existsSync(candidate)) continue; + if (candidate.startsWith("/") && !existsSync(candidate)) { + continue; + } try { const { stdout } = await exec(candidate, ["status", "--json"], { timeoutMs: 5000, @@ -120,12 +124,16 @@ export async function getTailnetHostname(exec: typeof runExec = runExec, detecte typeof parsed.Self === "object" && parsed.Self !== null ? (parsed.Self as Record) : undefined; - const dns = typeof self?.DNSName === "string" ? (self.DNSName as string) : undefined; + const dns = typeof self?.DNSName === "string" ? self.DNSName : undefined; const ips = Array.isArray(self?.TailscaleIPs) ? ((parsed.Self as { TailscaleIPs?: string[] }).TailscaleIPs ?? []) : []; - if (dns && dns.length > 0) return dns.replace(/\.$/, ""); - if (ips.length > 0) return ips[0]; + if (dns && dns.length > 0) { + return dns.replace(/\.$/, ""); + } + if (ips.length > 0) { + return ips[0]; + } throw new Error("Could not determine Tailscale DNS or IP"); } catch (err) { lastError = err; @@ -142,7 +150,9 @@ export async function getTailnetHostname(exec: typeof runExec = runExec, detecte let cachedTailscaleBinary: string | null = null; export async function getTailscaleBinary(): Promise { - if (cachedTailscaleBinary) return cachedTailscaleBinary; + if (cachedTailscaleBinary) { + return cachedTailscaleBinary; + } cachedTailscaleBinary = await findTailscaleBinary(); return cachedTailscaleBinary ?? "tailscale"; } @@ -169,7 +179,9 @@ export async function ensureGoInstalled( () => true, () => false, ); - if (hasGo) return; + if (hasGo) { + return; + } const install = await prompt( "Go is not installed. Install via Homebrew (brew install go)?", true, @@ -192,7 +204,9 @@ export async function ensureTailscaledInstalled( () => true, () => false, ); - if (hasTailscaled) return; + if (hasTailscaled) { + return; + } const install = await prompt( "tailscaled not found. Install via Homebrew (tailscale package)?", @@ -236,7 +250,9 @@ function extractExecErrorText(err: unknown) { function isPermissionDeniedError(err: unknown): boolean { const { stdout, stderr, message, code } = extractExecErrorText(err); - if (code.toUpperCase() === "EACCES") return true; + if (code.toUpperCase() === "EACCES") { + return true; + } const combined = `${stdout}\n${stderr}\n${message}`.toLowerCase(); return ( combined.includes("permission denied") || @@ -270,7 +286,9 @@ async function execWithSudoFallback( } catch (sudoErr) { const { stderr, message } = extractExecErrorText(sudoErr); const detail = (stderr || message).trim(); - if (detail) logVerbose(`Sudo retry failed: ${detail}`); + if (detail) { + logVerbose(`Sudo retry failed: ${detail}`); + } throw err; } } @@ -300,7 +318,9 @@ export async function ensureFunnel( ), ); const proceed = await prompt("Attempt local setup with user-space tailscaled?", true); - if (!proceed) runtime.exit(1); + if (!proceed) { + runtime.exit(1); + } await ensureBinary("brew", exec, runtime); await ensureGoInstalled(exec, prompt, runtime); await ensureTailscaledInstalled(exec, prompt, runtime); @@ -317,7 +337,9 @@ export async function ensureFunnel( timeoutMs: 15_000, }, ); - if (stdout.trim()) console.log(stdout.trim()); + if (stdout.trim()) { + console.log(stdout.trim()); + } } catch (err) { const errOutput = err as { stdout?: unknown; stderr?: unknown }; const stdout = typeof errOutput.stdout === "string" ? errOutput.stdout : ""; @@ -345,7 +367,7 @@ export async function ensureFunnel( runtime.error("Failed to enable Tailscale Funnel. Is it allowed on your tailnet?"); runtime.error( info( - `Tip: Funnel is optional for Moltbot. You can keep running the web gateway without it: \`${formatCliCommand("moltbot gateway")}\``, + `Tip: Funnel is optional for OpenClaw. You can keep running the web gateway without it: \`${formatCliCommand("openclaw gateway")}\``, ), ); if (shouldLogVerbose()) { @@ -411,7 +433,9 @@ function parseWhoisIdentity(payload: Record): TailscaleWhoisIde getString(userProfile?.login) ?? getString(payload.LoginName) ?? getString(payload.login); - if (!login) return null; + if (!login) { + return null; + } const name = getString(userProfile?.DisplayName) ?? getString(userProfile?.Name) ?? @@ -423,7 +447,9 @@ function parseWhoisIdentity(payload: Record): TailscaleWhoisIde function readCachedWhois(ip: string, now: number): TailscaleWhoisIdentity | null | undefined { const cached = whoisCache.get(ip); - if (!cached) return undefined; + if (!cached) { + return undefined; + } if (cached.expiresAt <= now) { whoisCache.delete(ip); return undefined; @@ -441,10 +467,14 @@ export async function readTailscaleWhoisIdentity( opts?: { timeoutMs?: number; cacheTtlMs?: number; errorTtlMs?: number }, ): Promise { const normalized = ip.trim(); - if (!normalized) return null; + if (!normalized) { + return null; + } const now = Date.now(); const cached = readCachedWhois(normalized, now); - if (cached !== undefined) return cached; + if (cached !== undefined) { + return cached; + } const cacheTtlMs = opts?.cacheTtlMs ?? 60_000; const errorTtlMs = opts?.errorTtlMs ?? 5_000; diff --git a/src/infra/tls/fingerprint.test.ts b/src/infra/tls/fingerprint.test.ts index f1412d747..7e0f99ec6 100644 --- a/src/infra/tls/fingerprint.test.ts +++ b/src/infra/tls/fingerprint.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { normalizeFingerprint } from "./fingerprint.js"; describe("normalizeFingerprint", () => { diff --git a/src/infra/tls/gateway.ts b/src/infra/tls/gateway.ts index 8c9d7c640..5e5d4eca7 100644 --- a/src/infra/tls/gateway.ts +++ b/src/infra/tls/gateway.ts @@ -4,7 +4,6 @@ import fs from "node:fs/promises"; import path from "node:path"; import tls from "node:tls"; import { promisify } from "node:util"; - import type { GatewayTlsConfig } from "../../config/types.gateway.js"; import { CONFIG_DIR, ensureDir, resolveUserPath, shortenHomeInString } from "../../utils.js"; import { normalizeFingerprint } from "./fingerprint.js"; @@ -56,7 +55,7 @@ async function generateSelfSignedCert(params: { "-out", params.certPath, "-subj", - "/CN=moltbot-gateway", + "/CN=openclaw-gateway", ]); await fs.chmod(params.keyPath, 0o600).catch(() => {}); await fs.chmod(params.certPath, 0o600).catch(() => {}); @@ -69,7 +68,9 @@ export async function loadGatewayTlsRuntime( cfg: GatewayTlsConfig | undefined, log?: { info?: (msg: string) => void; warn?: (msg: string) => void }, ): Promise { - if (!cfg || cfg.enabled !== true) return { enabled: false, required: false }; + if (!cfg || cfg.enabled !== true) { + return { enabled: false, required: false }; + } const autoGenerate = cfg.autoGenerate !== false; const baseDir = path.join(CONFIG_DIR, "gateway", "tls"); diff --git a/src/infra/transport-ready.test.ts b/src/infra/transport-ready.test.ts index 79c42e22d..3768908e0 100644 --- a/src/infra/transport-ready.test.ts +++ b/src/infra/transport-ready.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { waitForTransportReady } from "./transport-ready.js"; describe("waitForTransportReady", () => { @@ -15,7 +14,9 @@ describe("waitForTransportReady", () => { runtime, check: async () => { attempts += 1; - if (attempts > 4) return { ok: true }; + if (attempts > 4) { + return { ok: true }; + } return { ok: false, error: "not ready" }; }, }); diff --git a/src/infra/transport-ready.ts b/src/infra/transport-ready.ts index ec514d2e5..6c1225079 100644 --- a/src/infra/transport-ready.ts +++ b/src/infra/transport-ready.ts @@ -1,5 +1,5 @@ -import { danger } from "../globals.js"; import type { RuntimeEnv } from "../runtime.js"; +import { danger } from "../globals.js"; import { sleepWithAbort } from "./backoff.js"; export type TransportReadyResult = { @@ -29,13 +29,19 @@ export async function waitForTransportReady(params: WaitForTransportReadyParams) let lastError: string | null = null; while (true) { - if (params.abortSignal?.aborted) return; + if (params.abortSignal?.aborted) { + return; + } const res = await params.check(); - if (res.ok) return; + if (res.ok) { + return; + } lastError = res.error ?? null; const now = Date.now(); - if (now >= deadline) break; + if (now >= deadline) { + break; + } if (now >= nextLogAt) { const elapsedMs = now - started; params.runtime.error?.( @@ -47,7 +53,9 @@ export async function waitForTransportReady(params: WaitForTransportReadyParams) try { await sleepWithAbort(pollIntervalMs, params.abortSignal); } catch (err) { - if (params.abortSignal?.aborted) return; + if (params.abortSignal?.aborted) { + return; + } throw err; } } diff --git a/src/infra/unhandled-rejections.fatal-detection.test.ts b/src/infra/unhandled-rejections.fatal-detection.test.ts index 7944a1e73..76cc22568 100644 --- a/src/infra/unhandled-rejections.fatal-detection.test.ts +++ b/src/infra/unhandled-rejections.fatal-detection.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest"; import process from "node:process"; - +import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest"; import { installUnhandledRejectionHandler } from "./unhandled-rejections.js"; describe("installUnhandledRejectionHandler - fatal detection", () => { @@ -47,7 +46,7 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { expect(exitCalls).toEqual([1]); expect(consoleErrorSpy).toHaveBeenCalledWith( - "[moltbot] FATAL unhandled rejection:", + "[openclaw] FATAL unhandled rejection:", expect.stringContaining("Out of memory"), ); }); @@ -83,7 +82,7 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { expect(exitCalls).toEqual([1]); expect(consoleErrorSpy).toHaveBeenCalledWith( - "[moltbot] CONFIGURATION ERROR - requires fix:", + "[openclaw] CONFIGURATION ERROR - requires fix:", expect.stringContaining("Invalid config"), ); }); @@ -109,7 +108,7 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalledWith( - "[moltbot] Non-fatal unhandled rejection (continuing):", + "[openclaw] Non-fatal unhandled rejection (continuing):", expect.stringContaining("fetch failed"), ); }); @@ -132,7 +131,7 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { expect(exitCalls).toEqual([1]); expect(consoleErrorSpy).toHaveBeenCalledWith( - "[moltbot] Unhandled promise rejection:", + "[openclaw] Unhandled promise rejection:", expect.stringContaining("Something went wrong"), ); }); diff --git a/src/infra/unhandled-rejections.test.ts b/src/infra/unhandled-rejections.test.ts index 1ec144ba1..150e644ea 100644 --- a/src/infra/unhandled-rejections.test.ts +++ b/src/infra/unhandled-rejections.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { isAbortError, isTransientNetworkError } from "./unhandled-rejections.js"; describe("isAbortError", () => { diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index d186c6a78..c2e8d935c 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -1,5 +1,4 @@ import process from "node:process"; - import { extractErrorCode, formatUncaughtError } from "./errors.js"; type UnhandledRejectionHandler = (reason: unknown) => boolean; @@ -37,13 +36,17 @@ const TRANSIENT_NETWORK_CODES = new Set([ ]); function getErrorCause(err: unknown): unknown { - if (!err || typeof err !== "object") return undefined; + if (!err || typeof err !== "object") { + return undefined; + } return (err as { cause?: unknown }).cause; } function extractErrorCodeWithCause(err: unknown): string | undefined { const direct = extractErrorCode(err); - if (direct) return direct; + if (direct) { + return direct; + } return extractErrorCode(getErrorCause(err)); } @@ -52,12 +55,18 @@ function extractErrorCodeWithCause(err: unknown): string | undefined { * These are typically intentional cancellations (e.g., during shutdown) and shouldn't crash. */ export function isAbortError(err: unknown): boolean { - if (!err || typeof err !== "object") return false; + if (!err || typeof err !== "object") { + return false; + } const name = "name" in err ? String(err.name) : ""; - if (name === "AbortError") return true; + if (name === "AbortError") { + return true; + } // Check for "This operation was aborted" message from Node's undici const message = "message" in err && typeof err.message === "string" ? err.message : ""; - if (message === "This operation was aborted") return true; + if (message === "This operation was aborted") { + return true; + } return false; } @@ -76,15 +85,21 @@ function isConfigError(err: unknown): boolean { * These are typically temporary connectivity issues that will resolve on their own. */ export function isTransientNetworkError(err: unknown): boolean { - if (!err) return false; + if (!err) { + return false; + } const code = extractErrorCodeWithCause(err); - if (code && TRANSIENT_NETWORK_CODES.has(code)) return true; + if (code && TRANSIENT_NETWORK_CODES.has(code)) { + return true; + } // "fetch failed" TypeError from undici (Node's native fetch) if (err instanceof TypeError && err.message === "fetch failed") { const cause = getErrorCause(err); - if (cause) return isTransientNetworkError(cause); + if (cause) { + return isTransientNetworkError(cause); + } return true; } @@ -112,10 +127,12 @@ export function registerUnhandledRejectionHandler(handler: UnhandledRejectionHan export function isUnhandledRejectionHandled(reason: unknown): boolean { for (const handler of handlers) { try { - if (handler(reason)) return true; + if (handler(reason)) { + return true; + } } catch (err) { console.error( - "[moltbot] Unhandled rejection handler failed:", + "[openclaw] Unhandled rejection handler failed:", err instanceof Error ? (err.stack ?? err.message) : err, ); } @@ -125,36 +142,38 @@ export function isUnhandledRejectionHandled(reason: unknown): boolean { export function installUnhandledRejectionHandler(): void { process.on("unhandledRejection", (reason, _promise) => { - if (isUnhandledRejectionHandled(reason)) return; + if (isUnhandledRejectionHandled(reason)) { + return; + } // AbortError is typically an intentional cancellation (e.g., during shutdown) // Log it but don't crash - these are expected during graceful shutdown if (isAbortError(reason)) { - console.warn("[moltbot] Suppressed AbortError:", formatUncaughtError(reason)); + console.warn("[openclaw] Suppressed AbortError:", formatUncaughtError(reason)); return; } if (isFatalError(reason)) { - console.error("[moltbot] FATAL unhandled rejection:", formatUncaughtError(reason)); + console.error("[openclaw] FATAL unhandled rejection:", formatUncaughtError(reason)); process.exit(1); return; } if (isConfigError(reason)) { - console.error("[moltbot] CONFIGURATION ERROR - requires fix:", formatUncaughtError(reason)); + console.error("[openclaw] CONFIGURATION ERROR - requires fix:", formatUncaughtError(reason)); process.exit(1); return; } if (isTransientNetworkError(reason)) { console.warn( - "[moltbot] Non-fatal unhandled rejection (continuing):", + "[openclaw] Non-fatal unhandled rejection (continuing):", formatUncaughtError(reason), ); return; } - console.error("[moltbot] Unhandled promise rejection:", formatUncaughtError(reason)); + console.error("[openclaw] Unhandled promise rejection:", formatUncaughtError(reason)); process.exit(1); }); } diff --git a/src/infra/update-channels.ts b/src/infra/update-channels.ts index bb40295d5..f363d943c 100644 --- a/src/infra/update-channels.ts +++ b/src/infra/update-channels.ts @@ -6,15 +6,23 @@ export const DEFAULT_GIT_CHANNEL: UpdateChannel = "dev"; export const DEV_BRANCH = "main"; export function normalizeUpdateChannel(value?: string | null): UpdateChannel | null { - if (!value) return null; + if (!value) { + return null; + } const normalized = value.trim().toLowerCase(); - if (normalized === "stable" || normalized === "beta" || normalized === "dev") return normalized; + if (normalized === "stable" || normalized === "beta" || normalized === "dev") { + return normalized; + } return null; } export function channelToNpmTag(channel: UpdateChannel): string { - if (channel === "beta") return "beta"; - if (channel === "dev") return "dev"; + if (channel === "beta") { + return "beta"; + } + if (channel === "dev") { + return "dev"; + } return "latest"; } @@ -60,7 +68,9 @@ export function formatUpdateChannelLabel(params: { gitTag?: string | null; gitBranch?: string | null; }): string { - if (params.source === "config") return `${params.channel} (config)`; + if (params.source === "config") { + return `${params.channel} (config)`; + } if (params.source === "git-tag") { return params.gitTag ? `${params.channel} (${params.gitTag})` : `${params.channel} (tag)`; } diff --git a/src/infra/update-check.test.ts b/src/infra/update-check.test.ts index a259c07c5..faa3482ef 100644 --- a/src/infra/update-check.test.ts +++ b/src/infra/update-check.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { resolveNpmChannelTag } from "./update-check.js"; describe("resolveNpmChannelTag", () => { diff --git a/src/infra/update-check.ts b/src/infra/update-check.ts index 8e10d412b..c4be8d5da 100644 --- a/src/infra/update-check.ts +++ b/src/infra/update-check.ts @@ -1,6 +1,5 @@ import fs from "node:fs/promises"; import path from "node:path"; - import { runCommandWithTimeout } from "../process/exec.js"; import { parseSemver } from "./runtime-guard.js"; import { channelToNpmTag, type UpdateChannel } from "./update-channels.js"; @@ -62,15 +61,23 @@ async function detectPackageManager(root: string): Promise { const raw = await fs.readFile(path.join(root, "package.json"), "utf-8"); const parsed = JSON.parse(raw) as { packageManager?: string }; const pm = parsed?.packageManager?.split("@")[0]?.trim(); - if (pm === "pnpm" || pm === "bun" || pm === "npm") return pm; + if (pm === "pnpm" || pm === "bun" || pm === "npm") { + return pm; + } } catch { // ignore } const files = await fs.readdir(root).catch((): string[] => []); - if (files.includes("pnpm-lock.yaml")) return "pnpm"; - if (files.includes("bun.lockb")) return "bun"; - if (files.includes("package-lock.json")) return "npm"; + if (files.includes("pnpm-lock.yaml")) { + return "pnpm"; + } + if (files.includes("bun.lockb")) { + return "bun"; + } + if (files.includes("package-lock.json")) { + return "npm"; + } return "unknown"; } @@ -78,7 +85,9 @@ async function detectGitRoot(root: string): Promise { const res = await runCommandWithTimeout(["git", "-C", root, "rev-parse", "--show-toplevel"], { timeoutMs: 4000, }).catch(() => null); - if (!res || res.code !== 0) return null; + if (!res || res.code !== 0) { + return null; + } const top = res.stdout.trim(); return top ? path.resolve(top) : null; } @@ -151,10 +160,14 @@ export async function checkGitUpdateStatus(params: { const parseCounts = (raw: string): { ahead: number; behind: number } | null => { const parts = raw.trim().split(/\s+/); - if (parts.length < 2) return null; + if (parts.length < 2) { + return null; + } const ahead = Number.parseInt(parts[0] ?? "", 10); const behind = Number.parseInt(parts[1] ?? "", 10); - if (!Number.isFinite(ahead) || !Number.isFinite(behind)) return null; + if (!Number.isFinite(ahead) || !Number.isFinite(behind)) { + return null; + } return { ahead, behind }; }; const parsed = counts && counts.code === 0 ? parseCounts(counts.stdout) : null; @@ -303,7 +316,7 @@ export async function fetchNpmTagVersion(params: { const tag = params.tag; try { const res = await fetchWithTimeout( - `https://registry.npmjs.org/moltbot/${encodeURIComponent(tag)}`, + `https://registry.npmjs.org/openclaw/${encodeURIComponent(tag)}`, timeoutMs, ); if (!res.ok) { @@ -344,10 +357,18 @@ export async function resolveNpmChannelTag(params: { export function compareSemverStrings(a: string | null, b: string | null): number | null { const pa = parseSemver(a); const pb = parseSemver(b); - if (!pa || !pb) return null; - if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1; - if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1; - if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1; + if (!pa || !pb) { + return null; + } + if (pa.major !== pb.major) { + return pa.major < pb.major ? -1 : 1; + } + if (pa.minor !== pb.minor) { + return pa.minor < pb.minor ? -1 : 1; + } + if (pa.patch !== pb.patch) { + return pa.patch < pb.patch ? -1 : 1; + } return 0; } diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index 7050ad0eb..940e44445 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -9,6 +9,9 @@ export type CommandRunner = ( options: { timeoutMs: number; cwd?: string; env?: NodeJS.ProcessEnv }, ) => Promise<{ stdout: string; stderr: string; code: number | null }>; +const PRIMARY_PACKAGE_NAME = "openclaw"; +const ALL_PACKAGE_NAMES = [PRIMARY_PACKAGE_NAME] as const; + async function pathExists(targetPath: string): Promise { try { await fs.access(targetPath); @@ -36,10 +39,14 @@ export async function resolveGlobalRoot( runCommand: CommandRunner, timeoutMs: number, ): Promise { - if (manager === "bun") return resolveBunGlobalRoot(); + if (manager === "bun") { + return resolveBunGlobalRoot(); + } const argv = manager === "pnpm" ? ["pnpm", "root", "-g"] : ["npm", "root", "-g"]; const res = await runCommand(argv, { timeoutMs }).catch(() => null); - if (!res || res.code !== 0) return null; + if (!res || res.code !== 0) { + return null; + } const root = res.stdout.trim(); return root || null; } @@ -50,8 +57,10 @@ export async function resolveGlobalPackageRoot( timeoutMs: number, ): Promise { const root = await resolveGlobalRoot(manager, runCommand, timeoutMs); - if (!root) return null; - return path.join(root, "moltbot"); + if (!root) { + return null; + } + return path.join(root, PRIMARY_PACKAGE_NAME); } export async function detectGlobalInstallManagerForRoot( @@ -71,18 +80,30 @@ export async function detectGlobalInstallManagerForRoot( for (const { manager, argv } of candidates) { const res = await runCommand(argv, { timeoutMs }).catch(() => null); - if (!res || res.code !== 0) continue; + if (!res || res.code !== 0) { + continue; + } const globalRoot = res.stdout.trim(); - if (!globalRoot) continue; + if (!globalRoot) { + continue; + } const globalReal = await tryRealpath(globalRoot); - const expected = path.join(globalReal, "moltbot"); - if (path.resolve(expected) === path.resolve(pkgReal)) return manager; + for (const name of ALL_PACKAGE_NAMES) { + const expected = path.join(globalReal, name); + if (path.resolve(expected) === path.resolve(pkgReal)) { + return manager; + } + } } const bunGlobalRoot = resolveBunGlobalRoot(); const bunGlobalReal = await tryRealpath(bunGlobalRoot); - const bunExpected = path.join(bunGlobalReal, "moltbot"); - if (path.resolve(bunExpected) === path.resolve(pkgReal)) return "bun"; + for (const name of ALL_PACKAGE_NAMES) { + const bunExpected = path.join(bunGlobalReal, name); + if (path.resolve(bunExpected) === path.resolve(pkgReal)) { + return "bun"; + } + } return null; } @@ -93,17 +114,31 @@ export async function detectGlobalInstallManagerByPresence( ): Promise { for (const manager of ["npm", "pnpm"] as const) { const root = await resolveGlobalRoot(manager, runCommand, timeoutMs); - if (!root) continue; - if (await pathExists(path.join(root, "moltbot"))) return manager; + if (!root) { + continue; + } + for (const name of ALL_PACKAGE_NAMES) { + if (await pathExists(path.join(root, name))) { + return manager; + } + } } const bunRoot = resolveBunGlobalRoot(); - if (await pathExists(path.join(bunRoot, "moltbot"))) return "bun"; + for (const name of ALL_PACKAGE_NAMES) { + if (await pathExists(path.join(bunRoot, name))) { + return "bun"; + } + } return null; } export function globalInstallArgs(manager: GlobalInstallManager, spec: string): string[] { - if (manager === "pnpm") return ["pnpm", "add", "-g", spec]; - if (manager === "bun") return ["bun", "add", "-g", spec]; + if (manager === "pnpm") { + return ["pnpm", "add", "-g", spec]; + } + if (manager === "bun") { + return ["bun", "add", "-g", spec]; + } return ["npm", "i", "-g", spec]; } diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index 6a49a85c0..fea01b88f 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { runGatewayUpdate } from "./update-runner.js"; type CommandResult = { stdout?: string; stderr?: string; code?: number }; @@ -26,7 +25,7 @@ describe("runGatewayUpdate", () => { let tempDir: string; beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-update-")); + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-")); }); afterEach(async () => { @@ -37,7 +36,7 @@ describe("runGatewayUpdate", () => { await fs.mkdir(path.join(tempDir, ".git")); await fs.writeFile( path.join(tempDir, "package.json"), - JSON.stringify({ name: "moltbot", version: "1.0.0" }), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), "utf-8", ); const { runner, calls } = createRunner({ @@ -62,7 +61,7 @@ describe("runGatewayUpdate", () => { await fs.mkdir(path.join(tempDir, ".git")); await fs.writeFile( path.join(tempDir, "package.json"), - JSON.stringify({ name: "moltbot", version: "1.0.0" }), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), "utf-8", ); const { runner, calls } = createRunner({ @@ -95,7 +94,7 @@ describe("runGatewayUpdate", () => { await fs.mkdir(path.join(tempDir, ".git")); await fs.writeFile( path.join(tempDir, "package.json"), - JSON.stringify({ name: "moltbot", version: "1.0.0", packageManager: "pnpm@8.0.0" }), + JSON.stringify({ name: "openclaw", version: "1.0.0", packageManager: "pnpm@8.0.0" }), "utf-8", ); const stableTag = "v1.0.1-1"; @@ -113,7 +112,7 @@ describe("runGatewayUpdate", () => { "pnpm build": { stdout: "" }, "pnpm ui:build": { stdout: "" }, [`git -C ${tempDir} checkout -- dist/control-ui/`]: { stdout: "" }, - "pnpm moltbot doctor --non-interactive": { stdout: "" }, + "pnpm openclaw doctor --non-interactive": { stdout: "" }, }); const result = await runGatewayUpdate({ @@ -131,7 +130,7 @@ describe("runGatewayUpdate", () => { it("skips update when no git root", async () => { await fs.writeFile( path.join(tempDir, "package.json"), - JSON.stringify({ name: "moltbot", packageManager: "pnpm@8.0.0" }), + JSON.stringify({ name: "openclaw", packageManager: "pnpm@8.0.0" }), "utf-8", ); await fs.writeFile(path.join(tempDir, "pnpm-lock.yaml"), "", "utf-8"); @@ -155,11 +154,11 @@ describe("runGatewayUpdate", () => { it("updates global npm installs when detected", async () => { const nodeModules = path.join(tempDir, "node_modules"); - const pkgRoot = path.join(nodeModules, "moltbot"); + const pkgRoot = path.join(nodeModules, "openclaw"); await fs.mkdir(pkgRoot, { recursive: true }); await fs.writeFile( path.join(pkgRoot, "package.json"), - JSON.stringify({ name: "moltbot", version: "1.0.0" }), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), "utf-8", ); @@ -173,10 +172,10 @@ describe("runGatewayUpdate", () => { if (key === "npm root -g") { return { stdout: nodeModules, stderr: "", code: 0 }; } - if (key === "npm i -g moltbot@latest") { + if (key === "npm i -g openclaw@latest") { await fs.writeFile( path.join(pkgRoot, "package.json"), - JSON.stringify({ name: "moltbot", version: "2.0.0" }), + JSON.stringify({ name: "openclaw", version: "2.0.0" }), "utf-8", ); return { stdout: "ok", stderr: "", code: 0 }; @@ -197,16 +196,16 @@ describe("runGatewayUpdate", () => { expect(result.mode).toBe("npm"); expect(result.before?.version).toBe("1.0.0"); expect(result.after?.version).toBe("2.0.0"); - expect(calls.some((call) => call === "npm i -g moltbot@latest")).toBe(true); + expect(calls.some((call) => call === "npm i -g openclaw@latest")).toBe(true); }); it("updates global npm installs with tag override", async () => { const nodeModules = path.join(tempDir, "node_modules"); - const pkgRoot = path.join(nodeModules, "moltbot"); + const pkgRoot = path.join(nodeModules, "openclaw"); await fs.mkdir(pkgRoot, { recursive: true }); await fs.writeFile( path.join(pkgRoot, "package.json"), - JSON.stringify({ name: "moltbot", version: "1.0.0" }), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), "utf-8", ); @@ -220,10 +219,10 @@ describe("runGatewayUpdate", () => { if (key === "npm root -g") { return { stdout: nodeModules, stderr: "", code: 0 }; } - if (key === "npm i -g moltbot@beta") { + if (key === "npm i -g openclaw@beta") { await fs.writeFile( path.join(pkgRoot, "package.json"), - JSON.stringify({ name: "moltbot", version: "2.0.0" }), + JSON.stringify({ name: "openclaw", version: "2.0.0" }), "utf-8", ); return { stdout: "ok", stderr: "", code: 0 }; @@ -245,7 +244,7 @@ describe("runGatewayUpdate", () => { expect(result.mode).toBe("npm"); expect(result.before?.version).toBe("1.0.0"); expect(result.after?.version).toBe("2.0.0"); - expect(calls.some((call) => call === "npm i -g moltbot@beta")).toBe(true); + expect(calls.some((call) => call === "npm i -g openclaw@beta")).toBe(true); }); it("updates global bun installs when detected", async () => { @@ -255,11 +254,11 @@ describe("runGatewayUpdate", () => { try { const bunGlobalRoot = path.join(bunInstall, "install", "global", "node_modules"); - const pkgRoot = path.join(bunGlobalRoot, "moltbot"); + const pkgRoot = path.join(bunGlobalRoot, "openclaw"); await fs.mkdir(pkgRoot, { recursive: true }); await fs.writeFile( path.join(pkgRoot, "package.json"), - JSON.stringify({ name: "moltbot", version: "1.0.0" }), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), "utf-8", ); @@ -276,10 +275,10 @@ describe("runGatewayUpdate", () => { if (key === "pnpm root -g") { return { stdout: "", stderr: "", code: 1 }; } - if (key === "bun add -g moltbot@latest") { + if (key === "bun add -g openclaw@latest") { await fs.writeFile( path.join(pkgRoot, "package.json"), - JSON.stringify({ name: "moltbot", version: "2.0.0" }), + JSON.stringify({ name: "openclaw", version: "2.0.0" }), "utf-8", ); return { stdout: "ok", stderr: "", code: 0 }; @@ -297,14 +296,17 @@ describe("runGatewayUpdate", () => { expect(result.mode).toBe("bun"); expect(result.before?.version).toBe("1.0.0"); expect(result.after?.version).toBe("2.0.0"); - expect(calls.some((call) => call === "bun add -g moltbot@latest")).toBe(true); + expect(calls.some((call) => call === "bun add -g openclaw@latest")).toBe(true); } finally { - if (oldBunInstall === undefined) delete process.env.BUN_INSTALL; - else process.env.BUN_INSTALL = oldBunInstall; + if (oldBunInstall === undefined) { + delete process.env.BUN_INSTALL; + } else { + process.env.BUN_INSTALL = oldBunInstall; + } } }); - it("rejects git roots that are not a moltbot checkout", async () => { + it("rejects git roots that are not a openclaw checkout", async () => { await fs.mkdir(path.join(tempDir, ".git")); const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(tempDir); const { runner, calls } = createRunner({ @@ -320,7 +322,7 @@ describe("runGatewayUpdate", () => { cwdSpy.mockRestore(); expect(result.status).toBe("error"); - expect(result.reason).toBe("not-moltbot-root"); + expect(result.reason).toBe("not-openclaw-root"); expect(calls.some((call) => call.includes("status --porcelain"))).toBe(false); }); }); diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 0735edb39..2ca0fcbad 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -1,12 +1,11 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { type CommandOptions, runCommandWithTimeout } from "../process/exec.js"; -import { compareSemverStrings } from "./update-check.js"; -import { DEV_BRANCH, isBetaTag, isStableTag, type UpdateChannel } from "./update-channels.js"; -import { detectGlobalInstallManagerForRoot, globalInstallArgs } from "./update-global.js"; import { trimLogTail } from "./restart-sentinel.js"; +import { DEV_BRANCH, isBetaTag, isStableTag, type UpdateChannel } from "./update-channels.js"; +import { compareSemverStrings } from "./update-check.js"; +import { detectGlobalInstallManagerForRoot, globalInstallArgs } from "./update-global.js"; export type UpdateStepResult = { name: string; @@ -66,13 +65,17 @@ const DEFAULT_TIMEOUT_MS = 20 * 60_000; const MAX_LOG_CHARS = 8000; const PREFLIGHT_MAX_COMMITS = 10; const START_DIRS = ["cwd", "argv1", "process"]; -const DEFAULT_PACKAGE_NAME = "moltbot"; -const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME, "moltbot"]); +const DEFAULT_PACKAGE_NAME = "openclaw"; +const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]); function normalizeDir(value?: string | null) { - if (!value) return null; + if (!value) { + return null; + } const trimmed = value.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } return path.resolve(trimmed); } @@ -80,8 +83,12 @@ function resolveNodeModulesBinPackageRoot(argv1: string): string | null { const normalized = path.resolve(argv1); const parts = normalized.split(path.sep); const binIndex = parts.lastIndexOf(".bin"); - if (binIndex <= 0) return null; - if (parts[binIndex - 1] !== "node_modules") return null; + if (binIndex <= 0) { + return null; + } + if (parts[binIndex - 1] !== "node_modules") { + return null; + } const binName = path.basename(normalized); const nodeModulesDir = parts.slice(0, binIndex).join(path.sep); return path.join(nodeModulesDir, binName); @@ -90,15 +97,21 @@ function resolveNodeModulesBinPackageRoot(argv1: string): string | null { function buildStartDirs(opts: UpdateRunnerOptions): string[] { const dirs: string[] = []; const cwd = normalizeDir(opts.cwd); - if (cwd) dirs.push(cwd); + if (cwd) { + dirs.push(cwd); + } const argv1 = normalizeDir(opts.argv1); if (argv1) { dirs.push(path.dirname(argv1)); const packageRoot = resolveNodeModulesBinPackageRoot(argv1); - if (packageRoot) dirs.push(packageRoot); + if (packageRoot) { + dirs.push(packageRoot); + } } const proc = normalizeDir(process.cwd()); - if (proc) dirs.push(proc); + if (proc) { + dirs.push(proc); + } return Array.from(new Set(dirs)); } @@ -131,7 +144,9 @@ async function readBranchName( const res = await runCommand(["git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD"], { timeoutMs, }).catch(() => null); - if (!res || res.code !== 0) return null; + if (!res || res.code !== 0) { + return null; + } const branch = res.stdout.trim(); return branch || null; } @@ -145,7 +160,9 @@ async function listGitTags( const res = await runCommand(["git", "-C", root, "tag", "--list", pattern, "--sort=-v:refname"], { timeoutMs, }).catch(() => null); - if (!res || res.code !== 0) return []; + if (!res || res.code !== 0) { + return []; + } return res.stdout .split("\n") .map((line) => line.trim()) @@ -162,10 +179,16 @@ async function resolveChannelTag( if (channel === "beta") { const betaTag = tags.find((tag) => isBetaTag(tag)) ?? null; const stableTag = tags.find((tag) => isStableTag(tag)) ?? null; - if (!betaTag) return stableTag; - if (!stableTag) return betaTag; + if (!betaTag) { + return stableTag; + } + if (!stableTag) { + return betaTag; + } const cmp = compareSemverStrings(betaTag, stableTag); - if (cmp != null && cmp < 0) return stableTag; + if (cmp != null && cmp < 0) { + return stableTag; + } return betaTag; } return tags.find((tag) => isStableTag(tag)) ?? null; @@ -182,7 +205,9 @@ async function resolveGitRoot( }); if (res.code === 0) { const root = res.stdout.trim(); - if (root) return root; + if (root) { + return root; + } } } return null; @@ -197,12 +222,16 @@ async function findPackageRoot(candidates: string[]) { const raw = await fs.readFile(pkgPath, "utf-8"); const parsed = JSON.parse(raw) as { name?: string }; const name = parsed?.name?.trim(); - if (name && CORE_PACKAGE_NAMES.has(name)) return current; + if (name && CORE_PACKAGE_NAMES.has(name)) { + return current; + } } catch { // ignore } const parent = path.dirname(current); - if (parent === current) break; + if (parent === current) { + break; + } current = parent; } } @@ -214,15 +243,23 @@ async function detectPackageManager(root: string) { const raw = await fs.readFile(path.join(root, "package.json"), "utf-8"); const parsed = JSON.parse(raw) as { packageManager?: string }; const pm = parsed?.packageManager?.split("@")[0]?.trim(); - if (pm === "pnpm" || pm === "bun" || pm === "npm") return pm; + if (pm === "pnpm" || pm === "bun" || pm === "npm") { + return pm; + } } catch { // ignore } const files = await fs.readdir(root).catch((): string[] => []); - if (files.includes("pnpm-lock.yaml")) return "pnpm"; - if (files.includes("bun.lockb")) return "bun"; - if (files.includes("package-lock.json")) return "npm"; + if (files.includes("pnpm-lock.yaml")) { + return "pnpm"; + } + if (files.includes("bun.lockb")) { + return "bun"; + } + if (files.includes("package-lock.json")) { + return "npm"; + } return "npm"; } @@ -276,22 +313,36 @@ async function runStep(opts: RunStepOptions): Promise { } function managerScriptArgs(manager: "pnpm" | "bun" | "npm", script: string, args: string[] = []) { - if (manager === "pnpm") return ["pnpm", script, ...args]; - if (manager === "bun") return ["bun", "run", script, ...args]; - if (args.length > 0) return ["npm", "run", script, "--", ...args]; + if (manager === "pnpm") { + return ["pnpm", script, ...args]; + } + if (manager === "bun") { + return ["bun", "run", script, ...args]; + } + if (args.length > 0) { + return ["npm", "run", script, "--", ...args]; + } return ["npm", "run", script]; } function managerInstallArgs(manager: "pnpm" | "bun" | "npm") { - if (manager === "pnpm") return ["pnpm", "install"]; - if (manager === "bun") return ["bun", "install"]; + if (manager === "pnpm") { + return ["pnpm", "install"]; + } + if (manager === "bun") { + return ["bun", "install"]; + } return ["npm", "install"]; } function normalizeTag(tag?: string) { const trimmed = tag?.trim(); - if (!trimmed) return "latest"; - if (trimmed.startsWith("moltbot@")) return trimmed.slice("moltbot@".length); + if (!trimmed) { + return "latest"; + } + if (trimmed.startsWith("openclaw@")) { + return trimmed.slice("openclaw@".length); + } if (trimmed.startsWith(`${DEFAULT_PACKAGE_NAME}@`)) { return trimmed.slice(`${DEFAULT_PACKAGE_NAME}@`.length); } @@ -347,7 +398,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< status: "error", mode: "unknown", root: gitRoot, - reason: "not-moltbot-root", + reason: "not-openclaw-root", steps: [], durationMs: Date.now() - startedAt, }; @@ -502,7 +553,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< } const manager = await detectPackageManager(gitRoot); - const preflightRoot = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-update-preflight-")); + const preflightRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-preflight-")); const worktreeDir = path.join(preflightRoot, "worktree"); const worktreeStep = await runStep( step( @@ -537,25 +588,33 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< ), ); steps.push(checkoutStep); - if (checkoutStep.exitCode !== 0) continue; + if (checkoutStep.exitCode !== 0) { + continue; + } const depsStep = await runStep( step(`preflight deps install (${shortSha})`, managerInstallArgs(manager), worktreeDir), ); steps.push(depsStep); - if (depsStep.exitCode !== 0) continue; + if (depsStep.exitCode !== 0) { + continue; + } const lintStep = await runStep( step(`preflight lint (${shortSha})`, managerScriptArgs(manager, "lint"), worktreeDir), ); steps.push(lintStep); - if (lintStep.exitCode !== 0) continue; + if (lintStep.exitCode !== 0) { + continue; + } const buildStep = await runStep( step(`preflight build (${shortSha})`, managerScriptArgs(manager, "build"), worktreeDir), ); steps.push(buildStep); - if (buildStep.exitCode !== 0) continue; + if (buildStep.exitCode !== 0) { + continue; + } selectedSha = sha; break; @@ -689,10 +748,10 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< const doctorStep = await runStep( step( - "moltbot doctor", - managerScriptArgs(manager, "moltbot", ["doctor", "--non-interactive"]), + "openclaw doctor", + managerScriptArgs(manager, "openclaw", ["doctor", "--non-interactive"]), gitRoot, - { CLAWDBOT_UPDATE_IN_PROGRESS: "1" }, + { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, ), ); steps.push(doctorStep); diff --git a/src/infra/update-startup.test.ts b/src/infra/update-startup.test.ts index 9a792b190..1d0aafd26 100644 --- a/src/infra/update-startup.test.ts +++ b/src/infra/update-startup.test.ts @@ -2,11 +2,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import type { UpdateCheckResult } from "./update-check.js"; -vi.mock("./moltbot-root.js", () => ({ - resolveMoltbotPackageRoot: vi.fn(), +vi.mock("./openclaw-root.js", () => ({ + resolveOpenClawPackageRoot: vi.fn(), })); vi.mock("./update-check.js", async () => { @@ -30,8 +29,8 @@ describe("update-startup", () => { beforeEach(async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-17T10:00:00Z")); - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-update-check-")); - process.env.CLAWDBOT_STATE_DIR = tempDir; + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-check-")); + process.env.OPENCLAW_STATE_DIR = tempDir; delete process.env.VITEST; process.env.NODE_ENV = "test"; }); @@ -43,13 +42,13 @@ describe("update-startup", () => { }); it("logs update hint for npm installs when newer tag exists", async () => { - const { resolveMoltbotPackageRoot } = await import("./moltbot-root.js"); + const { resolveOpenClawPackageRoot } = await import("./openclaw-root.js"); const { checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js"); const { runGatewayUpdateCheck } = await import("./update-startup.js"); - vi.mocked(resolveMoltbotPackageRoot).mockResolvedValue("/opt/moltbot"); + vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw"); vi.mocked(checkUpdateStatus).mockResolvedValue({ - root: "/opt/moltbot", + root: "/opt/openclaw", installKind: "package", packageManager: "npm", } satisfies UpdateCheckResult); @@ -77,13 +76,13 @@ describe("update-startup", () => { }); it("uses latest when beta tag is older than release", async () => { - const { resolveMoltbotPackageRoot } = await import("./moltbot-root.js"); + const { resolveOpenClawPackageRoot } = await import("./openclaw-root.js"); const { checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js"); const { runGatewayUpdateCheck } = await import("./update-startup.js"); - vi.mocked(resolveMoltbotPackageRoot).mockResolvedValue("/opt/moltbot"); + vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw"); vi.mocked(checkUpdateStatus).mockResolvedValue({ - root: "/opt/moltbot", + root: "/opt/openclaw", installKind: "package", packageManager: "npm", } satisfies UpdateCheckResult); diff --git a/src/infra/update-startup.ts b/src/infra/update-startup.ts index 07fc0293f..4f9e7e42d 100644 --- a/src/infra/update-startup.ts +++ b/src/infra/update-startup.ts @@ -1,13 +1,12 @@ import fs from "node:fs/promises"; import path from "node:path"; - import type { loadConfig } from "../config/config.js"; -import { resolveStateDir } from "../config/paths.js"; -import { resolveMoltbotPackageRoot } from "./moltbot-root.js"; -import { compareSemverStrings, resolveNpmChannelTag, checkUpdateStatus } from "./update-check.js"; -import { normalizeUpdateChannel, DEFAULT_PACKAGE_CHANNEL } from "./update-channels.js"; -import { VERSION } from "../version.js"; import { formatCliCommand } from "../cli/command-format.js"; +import { resolveStateDir } from "../config/paths.js"; +import { VERSION } from "../version.js"; +import { resolveOpenClawPackageRoot } from "./openclaw-root.js"; +import { normalizeUpdateChannel, DEFAULT_PACKAGE_CHANNEL } from "./update-channels.js"; +import { compareSemverStrings, resolveNpmChannelTag, checkUpdateStatus } from "./update-check.js"; type UpdateCheckState = { lastCheckedAt?: string; @@ -19,8 +18,12 @@ const UPDATE_CHECK_FILENAME = "update-check.json"; const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; function shouldSkipCheck(allowInTests: boolean): boolean { - if (allowInTests) return false; - if (process.env.VITEST || process.env.NODE_ENV === "test") return true; + if (allowInTests) { + return false; + } + if (process.env.VITEST || process.env.NODE_ENV === "test") { + return true; + } return false; } @@ -45,19 +48,27 @@ export async function runGatewayUpdateCheck(params: { isNixMode: boolean; allowInTests?: boolean; }): Promise { - if (shouldSkipCheck(Boolean(params.allowInTests))) return; - if (params.isNixMode) return; - if (params.cfg.update?.checkOnStart === false) return; + if (shouldSkipCheck(Boolean(params.allowInTests))) { + return; + } + if (params.isNixMode) { + return; + } + if (params.cfg.update?.checkOnStart === false) { + return; + } const statePath = path.join(resolveStateDir(), UPDATE_CHECK_FILENAME); const state = await readState(statePath); const now = Date.now(); const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : null; if (lastCheckedAt && Number.isFinite(lastCheckedAt)) { - if (now - lastCheckedAt < UPDATE_CHECK_INTERVAL_MS) return; + if (now - lastCheckedAt < UPDATE_CHECK_INTERVAL_MS) { + return; + } } - const root = await resolveMoltbotPackageRoot({ + const root = await resolveOpenClawPackageRoot({ moduleUrl: import.meta.url, argv1: process.argv[1], cwd: process.cwd(), @@ -93,7 +104,7 @@ export async function runGatewayUpdateCheck(params: { state.lastNotifiedVersion !== resolved.version || state.lastNotifiedTag !== tag; if (shouldNotify) { params.log.info( - `update available (${tag}): v${resolved.version} (current v${VERSION}). Run: ${formatCliCommand("moltbot update")}`, + `update available (${tag}): v${resolved.version} (current v${VERSION}). Run: ${formatCliCommand("openclaw update")}`, ); nextState.lastNotifiedVersion = resolved.version; nextState.lastNotifiedTag = tag; diff --git a/src/infra/voicewake.test.ts b/src/infra/voicewake.test.ts index 308a3dc9b..55665b7ea 100644 --- a/src/infra/voicewake.test.ts +++ b/src/infra/voicewake.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { defaultVoiceWakeTriggers, loadVoiceWakeConfig, @@ -12,14 +10,14 @@ import { describe("voicewake store", () => { it("returns defaults when missing", async () => { - const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-voicewake-")); + const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-")); const cfg = await loadVoiceWakeConfig(baseDir); expect(cfg.triggers).toEqual(defaultVoiceWakeTriggers()); expect(cfg.updatedAtMs).toBe(0); }); it("sanitizes and persists triggers", async () => { - const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-voicewake-")); + const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-")); const saved = await setVoiceWakeTriggers([" hi ", "", " there "], baseDir); expect(saved.triggers).toEqual(["hi", "there"]); expect(saved.updatedAtMs).toBeGreaterThan(0); @@ -30,7 +28,7 @@ describe("voicewake store", () => { }); it("falls back to defaults when triggers empty", async () => { - const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-voicewake-")); + const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-")); const saved = await setVoiceWakeTriggers(["", " "], baseDir); expect(saved.triggers).toEqual(defaultVoiceWakeTriggers()); }); diff --git a/src/infra/voicewake.ts b/src/infra/voicewake.ts index 204d7e379..9d0867a0a 100644 --- a/src/infra/voicewake.ts +++ b/src/infra/voicewake.ts @@ -8,7 +8,7 @@ export type VoiceWakeConfig = { updatedAtMs: number; }; -const DEFAULT_TRIGGERS = ["clawd", "claude", "computer"]; +const DEFAULT_TRIGGERS = ["openclaw", "claude", "computer"]; function resolvePath(baseDir?: string) { const root = baseDir ?? resolveStateDir(); diff --git a/src/infra/warnings.ts b/src/infra/warnings.ts index 5fe7a9955..91a98b0f3 100644 --- a/src/infra/warnings.ts +++ b/src/infra/warnings.ts @@ -1,4 +1,4 @@ -const warningFilterKey = Symbol.for("moltbot.warning-filter"); +const warningFilterKey = Symbol.for("openclaw.warning-filter"); type Warning = Error & { code?: string; @@ -26,11 +26,15 @@ export function installProcessWarningFilter(): void { const globalState = globalThis as typeof globalThis & { [warningFilterKey]?: { installed: boolean }; }; - if (globalState[warningFilterKey]?.installed) return; + if (globalState[warningFilterKey]?.installed) { + return; + } globalState[warningFilterKey] = { installed: true }; process.on("warning", (warning: Warning) => { - if (shouldIgnoreWarning(warning)) return; + if (shouldIgnoreWarning(warning)) { + return; + } process.stderr.write(`${warning.stack ?? warning.toString()}\n`); }); } diff --git a/src/infra/widearea-dns.test.ts b/src/infra/widearea-dns.test.ts index d702d8072..409c5cc42 100644 --- a/src/infra/widearea-dns.test.ts +++ b/src/infra/widearea-dns.test.ts @@ -1,37 +1,38 @@ import { describe, expect, it } from "vitest"; - -import { renderWideAreaGatewayZoneText, WIDE_AREA_DISCOVERY_DOMAIN } from "./widearea-dns.js"; +import { renderWideAreaGatewayZoneText } from "./widearea-dns.js"; describe("wide-area DNS-SD zone rendering", () => { - it("renders a moltbot.internal zone with gateway PTR/SRV/TXT records", () => { + it("renders a zone with gateway PTR/SRV/TXT records", () => { const txt = renderWideAreaGatewayZoneText({ + domain: "openclaw.internal.", serial: 2025121701, gatewayPort: 18789, - displayName: "Mac Studio (Moltbot)", + displayName: "Mac Studio (OpenClaw)", tailnetIPv4: "100.123.224.76", tailnetIPv6: "fd7a:115c:a1e0::8801:e04c", hostLabel: "studio-london", instanceLabel: "studio-london", sshPort: 22, - cliPath: "/opt/homebrew/bin/moltbot", + cliPath: "/opt/homebrew/bin/openclaw", }); - expect(txt).toContain(`$ORIGIN ${WIDE_AREA_DISCOVERY_DOMAIN}`); + expect(txt).toContain(`$ORIGIN openclaw.internal.`); expect(txt).toContain(`studio-london IN A 100.123.224.76`); expect(txt).toContain(`studio-london IN AAAA fd7a:115c:a1e0::8801:e04c`); - expect(txt).toContain(`_moltbot-gw._tcp IN PTR studio-london._moltbot-gw._tcp`); - expect(txt).toContain(`studio-london._moltbot-gw._tcp IN SRV 0 0 18789 studio-london`); - expect(txt).toContain(`displayName=Mac Studio (Moltbot)`); + expect(txt).toContain(`_openclaw-gw._tcp IN PTR studio-london._openclaw-gw._tcp`); + expect(txt).toContain(`studio-london._openclaw-gw._tcp IN SRV 0 0 18789 studio-london`); + expect(txt).toContain(`displayName=Mac Studio (OpenClaw)`); expect(txt).toContain(`gatewayPort=18789`); expect(txt).toContain(`sshPort=22`); - expect(txt).toContain(`cliPath=/opt/homebrew/bin/moltbot`); + expect(txt).toContain(`cliPath=/opt/homebrew/bin/openclaw`); }); it("includes tailnetDns when provided", () => { const txt = renderWideAreaGatewayZoneText({ + domain: "openclaw.internal.", serial: 2025121701, gatewayPort: 18789, - displayName: "Mac Studio (Moltbot)", + displayName: "Mac Studio (OpenClaw)", tailnetIPv4: "100.123.224.76", tailnetDns: "peters-mac-studio-1.sheep-coho.ts.net", hostLabel: "studio-london", diff --git a/src/infra/widearea-dns.ts b/src/infra/widearea-dns.ts index c796275dd..40cf936b7 100644 --- a/src/infra/widearea-dns.ts +++ b/src/infra/widearea-dns.ts @@ -1,14 +1,31 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - import { CONFIG_DIR, ensureDir } from "../utils.js"; -export const WIDE_AREA_DISCOVERY_DOMAIN = "moltbot.internal."; -export const WIDE_AREA_ZONE_FILENAME = "moltbot.internal.db"; +export function normalizeWideAreaDomain(raw?: string | null): string | null { + const trimmed = raw?.trim(); + if (!trimmed) { + return null; + } + return trimmed.endsWith(".") ? trimmed : `${trimmed}.`; +} -export function getWideAreaZonePath(): string { - return path.join(CONFIG_DIR, "dns", WIDE_AREA_ZONE_FILENAME); +export function resolveWideAreaDiscoveryDomain(params?: { + env?: NodeJS.ProcessEnv; + configDomain?: string | null; +}): string | null { + const env = params?.env ?? process.env; + const candidate = params?.configDomain ?? env.OPENCLAW_WIDE_AREA_DOMAIN ?? null; + return normalizeWideAreaDomain(candidate); +} + +function zoneFilenameForDomain(domain: string): string { + return `${domain.replace(/\.$/, "")}.db`; +} + +export function getWideAreaZonePath(domain: string): string { + return path.join(CONFIG_DIR, "dns", zoneFilenameForDomain(domain)); } function dnsLabel(raw: string, fallback: string): string { @@ -37,21 +54,27 @@ function formatYyyyMmDd(date: Date): string { function nextSerial(existingSerial: number | null, now: Date): number { const today = formatYyyyMmDd(now); const base = Number.parseInt(`${today}01`, 10); - if (!existingSerial || !Number.isFinite(existingSerial)) return base; + if (!existingSerial || !Number.isFinite(existingSerial)) { + return base; + } const existing = String(existingSerial); - if (existing.startsWith(today)) return existingSerial + 1; + if (existing.startsWith(today)) { + return existingSerial + 1; + } return base; } function extractSerial(zoneText: string): number | null { const match = zoneText.match(/^\s*@\s+IN\s+SOA\s+\S+\s+\S+\s+(\d+)\s+/m); - if (!match) return null; + if (!match) { + return null; + } const parsed = Number.parseInt(match[1], 10); return Number.isFinite(parsed) ? parsed : null; } function extractContentHash(zoneText: string): string | null { - const match = zoneText.match(/^\s*;\s*moltbot-content-hash:\s*(\S+)\s*$/m); + const match = zoneText.match(/^\s*;\s*openclaw-content-hash:\s*(\S+)\s*$/m); return match?.[1] ?? null; } @@ -66,6 +89,7 @@ function computeContentHash(body: string): string { } export type WideAreaGatewayZoneOpts = { + domain: string; gatewayPort: number; displayName: string; tailnetIPv4: string; @@ -80,9 +104,10 @@ export type WideAreaGatewayZoneOpts = { }; function renderZone(opts: WideAreaGatewayZoneOpts & { serial: number }): string { - const hostname = os.hostname().split(".")[0] ?? "moltbot"; - const hostLabel = dnsLabel(opts.hostLabel ?? hostname, "moltbot"); - const instanceLabel = dnsLabel(opts.instanceLabel ?? `${hostname}-gateway`, "moltbot-gw"); + const hostname = os.hostname().split(".")[0] ?? "openclaw"; + const hostLabel = dnsLabel(opts.hostLabel ?? hostname, "openclaw"); + const instanceLabel = dnsLabel(opts.instanceLabel ?? `${hostname}-gateway`, "openclaw-gw"); + const domain = normalizeWideAreaDomain(opts.domain) ?? "local."; const txt = [ `displayName=${opts.displayName.trim() || hostname}`, @@ -108,7 +133,7 @@ function renderZone(opts: WideAreaGatewayZoneOpts & { serial: number }): string const records: string[] = []; - records.push(`$ORIGIN ${WIDE_AREA_DISCOVERY_DOMAIN}`); + records.push(`$ORIGIN ${domain}`); records.push(`$TTL 60`); const soaLine = `@ IN SOA ns1 hostmaster ${opts.serial} 7200 3600 1209600 60`; records.push(soaLine); @@ -119,9 +144,9 @@ function renderZone(opts: WideAreaGatewayZoneOpts & { serial: number }): string records.push(`${hostLabel} IN AAAA ${opts.tailnetIPv6}`); } - records.push(`_moltbot-gw._tcp IN PTR ${instanceLabel}._moltbot-gw._tcp`); - records.push(`${instanceLabel}._moltbot-gw._tcp IN SRV 0 0 ${opts.gatewayPort} ${hostLabel}`); - records.push(`${instanceLabel}._moltbot-gw._tcp IN TXT ${txt.map(txtQuote).join(" ")}`); + records.push(`_openclaw-gw._tcp IN PTR ${instanceLabel}._openclaw-gw._tcp`); + records.push(`${instanceLabel}._openclaw-gw._tcp IN SRV 0 0 ${opts.gatewayPort} ${hostLabel}`); + records.push(`${instanceLabel}._openclaw-gw._tcp IN TXT ${txt.map(txtQuote).join(" ")}`); const contentBody = `${records.join("\n")}\n`; const hashBody = `${records @@ -131,7 +156,7 @@ function renderZone(opts: WideAreaGatewayZoneOpts & { serial: number }): string .join("\n")}\n`; const contentHash = computeContentHash(hashBody); - return `; moltbot-content-hash: ${contentHash}\n${contentBody}`; + return `; openclaw-content-hash: ${contentHash}\n${contentBody}`; } export function renderWideAreaGatewayZoneText( @@ -143,7 +168,11 @@ export function renderWideAreaGatewayZoneText( export async function writeWideAreaGatewayZone( opts: WideAreaGatewayZoneOpts, ): Promise<{ zonePath: string; changed: boolean }> { - const zonePath = getWideAreaZonePath(); + const domain = normalizeWideAreaDomain(opts.domain); + if (!domain) { + throw new Error("wide-area discovery domain is required"); + } + const zonePath = getWideAreaZonePath(domain); await ensureDir(path.dirname(zonePath)); const existing = (() => { diff --git a/src/infra/ws.ts b/src/infra/ws.ts index 99d753780..585e181bc 100644 --- a/src/infra/ws.ts +++ b/src/infra/ws.ts @@ -1,14 +1,19 @@ -import { Buffer } from "node:buffer"; - import type WebSocket from "ws"; +import { Buffer } from "node:buffer"; export function rawDataToString( data: WebSocket.RawData, encoding: BufferEncoding = "utf8", ): string { - if (typeof data === "string") return data; - if (Buffer.isBuffer(data)) return data.toString(encoding); - if (Array.isArray(data)) return Buffer.concat(data).toString(encoding); + if (typeof data === "string") { + return data; + } + if (Buffer.isBuffer(data)) { + return data.toString(encoding); + } + if (Array.isArray(data)) { + return Buffer.concat(data).toString(encoding); + } if (data instanceof ArrayBuffer) { return Buffer.from(data).toString(encoding); } diff --git a/src/infra/wsl.ts b/src/infra/wsl.ts index 3de3d9ec9..df52ab934 100644 --- a/src/infra/wsl.ts +++ b/src/infra/wsl.ts @@ -10,7 +10,9 @@ export function isWSLEnv(): boolean { } export async function isWSL(): Promise { - if (wslCached !== null) return wslCached; + if (wslCached !== null) { + return wslCached; + } if (isWSLEnv()) { wslCached = true; return wslCached; diff --git a/src/line/accounts.test.ts b/src/line/accounts.test.ts index 4b2188d69..3330d0523 100644 --- a/src/line/accounts.test.ts +++ b/src/line/accounts.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveLineAccount, listLineAccountIds, @@ -6,7 +7,6 @@ import { normalizeAccountId, DEFAULT_ACCOUNT_ID, } from "./accounts.js"; -import type { MoltbotConfig } from "../config/config.js"; describe("LINE accounts", () => { const originalEnv = { ...process.env }; @@ -23,7 +23,7 @@ describe("LINE accounts", () => { describe("resolveLineAccount", () => { it("resolves account from config", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { line: { enabled: true, @@ -48,7 +48,7 @@ describe("LINE accounts", () => { process.env.LINE_CHANNEL_ACCESS_TOKEN = "env-token"; process.env.LINE_CHANNEL_SECRET = "env-secret"; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { line: { enabled: true, @@ -64,7 +64,7 @@ describe("LINE accounts", () => { }); it("resolves named account", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { line: { enabled: true, @@ -90,7 +90,7 @@ describe("LINE accounts", () => { }); it("returns empty token when not configured", () => { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const account = resolveLineAccount({ cfg }); @@ -102,7 +102,7 @@ describe("LINE accounts", () => { describe("listLineAccountIds", () => { it("returns default account when configured at base level", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { line: { channelAccessToken: "test-token", @@ -116,7 +116,7 @@ describe("LINE accounts", () => { }); it("returns named accounts", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { line: { accounts: { @@ -135,7 +135,7 @@ describe("LINE accounts", () => { it("returns default from env", () => { process.env.LINE_CHANNEL_ACCESS_TOKEN = "env-token"; - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const ids = listLineAccountIds(cfg); @@ -145,7 +145,7 @@ describe("LINE accounts", () => { describe("resolveDefaultLineAccountId", () => { it("returns default when configured", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { line: { channelAccessToken: "test-token", @@ -159,7 +159,7 @@ describe("LINE accounts", () => { }); it("returns first named account when default not configured", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { line: { accounts: { diff --git a/src/line/accounts.ts b/src/line/accounts.ts index dc08a2695..f8b94e2ae 100644 --- a/src/line/accounts.ts +++ b/src/line/accounts.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { LineConfig, LineAccountConfig, @@ -10,7 +10,9 @@ import type { export const DEFAULT_ACCOUNT_ID = "default"; function readFileIfExists(filePath: string | undefined): string | undefined { - if (!filePath) return undefined; + if (!filePath) { + return undefined; + } try { return fs.readFileSync(filePath, "utf-8").trim(); } catch { @@ -95,7 +97,7 @@ function resolveSecret(params: { } export function resolveLineAccount(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string; }): ResolvedLineAccount { const { cfg, accountId = DEFAULT_ACCOUNT_ID } = params; @@ -138,7 +140,7 @@ export function resolveLineAccount(params: { }; } -export function listLineAccountIds(cfg: MoltbotConfig): string[] { +export function listLineAccountIds(cfg: OpenClawConfig): string[] { const lineConfig = cfg.channels?.line as LineConfig | undefined; const accounts = lineConfig?.accounts; const ids = new Set(); @@ -162,7 +164,7 @@ export function listLineAccountIds(cfg: MoltbotConfig): string[] { return Array.from(ids); } -export function resolveDefaultLineAccountId(cfg: MoltbotConfig): string { +export function resolveDefaultLineAccountId(cfg: OpenClawConfig): string { const ids = listLineAccountIds(cfg); if (ids.includes(DEFAULT_ACCOUNT_ID)) { return DEFAULT_ACCOUNT_ID; diff --git a/src/line/auto-reply-delivery.test.ts b/src/line/auto-reply-delivery.test.ts index 48a7bf724..1acab3a8a 100644 --- a/src/line/auto-reply-delivery.test.ts +++ b/src/line/auto-reply-delivery.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { deliverLineAutoReply } from "./auto-reply-delivery.js"; import { sendLineReplyChunks } from "./reply-chunks.js"; diff --git a/src/line/auto-reply-delivery.ts b/src/line/auto-reply-delivery.ts index ad4573ca1..c303382f9 100644 --- a/src/line/auto-reply-delivery.ts +++ b/src/line/auto-reply-delivery.ts @@ -2,8 +2,8 @@ import type { messagingApi } from "@line/bot-sdk"; import type { ReplyPayload } from "../auto-reply/types.js"; import type { FlexContainer } from "./flex-templates.js"; import type { ProcessedLineMessage } from "./markdown-to-line.js"; -import type { LineChannelData, LineTemplateMessagePayload } from "./types.js"; import type { LineReplyMessage, SendLineReplyChunksParams } from "./reply-chunks.js"; +import type { LineChannelData, LineTemplateMessagePayload } from "./types.js"; export type LineAutoReplyDeps = { buildTemplateMessageFromPayload: ( @@ -59,7 +59,9 @@ export async function deliverLineAutoReply(params: { let replyTokenUsed = params.replyTokenUsed; const pushLineMessages = async (messages: messagingApi.Message[]): Promise => { - if (messages.length === 0) return; + if (messages.length === 0) { + return; + } for (let i = 0; i < messages.length; i += 5) { await deps.pushMessagesLine(to, messages.slice(i, i + 5), { accountId, @@ -71,7 +73,9 @@ export async function deliverLineAutoReply(params: { messages: messagingApi.Message[], allowReplyToken: boolean, ): Promise => { - if (messages.length === 0) return; + if (messages.length === 0) { + return; + } let remaining = messages; if (allowReplyToken && replyToken && !replyTokenUsed) { @@ -121,9 +125,7 @@ export async function deliverLineAutoReply(params: { : { text: "", flexMessages: [] }; for (const flexMsg of processed.flexMessages) { - richMessages.push( - deps.createFlexMessage(flexMsg.altText.slice(0, 400), flexMsg.contents as FlexContainer), - ); + richMessages.push(deps.createFlexMessage(flexMsg.altText.slice(0, 400), flexMsg.contents)); } const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : []; diff --git a/src/line/bot-access.ts b/src/line/bot-access.ts index 2df9502fe..449882661 100644 --- a/src/line/bot-access.ts +++ b/src/line/bot-access.ts @@ -6,8 +6,12 @@ export type NormalizedAllowFrom = { function normalizeAllowEntry(value: string | number): string { const trimmed = String(value).trim(); - if (!trimmed) return ""; - if (trimmed === "*") return "*"; + if (!trimmed) { + return ""; + } + if (trimmed === "*") { + return "*"; + } return trimmed.replace(/^line:(?:user:)?/i, ""); } @@ -31,7 +35,9 @@ export const normalizeAllowFromWithStore = (params: { export const firstDefined = (...values: Array) => { for (const value of values) { - if (typeof value !== "undefined") return value; + if (typeof value !== "undefined") { + return value; + } } return undefined; }; @@ -41,8 +47,14 @@ export const isSenderAllowed = (params: { senderId?: string; }): boolean => { const { allow, senderId } = params; - if (!allow.hasEntries) return false; - if (allow.hasWildcard) return true; - if (!senderId) return false; + if (!allow.hasEntries) { + return false; + } + if (allow.hasWildcard) { + return true; + } + if (!senderId) { + return false; + } return allow.entries.includes(senderId); }; diff --git a/src/line/bot-handlers.test.ts b/src/line/bot-handlers.test.ts index 00f0082ed..695c318c2 100644 --- a/src/line/bot-handlers.test.ts +++ b/src/line/bot-handlers.test.ts @@ -1,5 +1,5 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { MessageEvent } from "@line/bot-sdk"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { buildLineMessageContextMock, buildLinePostbackContextMock } = vi.hoisted(() => ({ buildLineMessageContextMock: vi.fn(async () => ({ diff --git a/src/line/bot-handlers.ts b/src/line/bot-handlers.ts index 912cc315c..757c8c180 100644 --- a/src/line/bot-handlers.ts +++ b/src/line/bot-handlers.ts @@ -8,7 +8,9 @@ import type { PostbackEvent, EventSource, } from "@line/bot-sdk"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { LineGroupConfig, ResolvedLineAccount } from "./types.js"; import { danger, logVerbose } from "../globals.js"; import { resolvePairingIdLabel } from "../pairing/pairing-labels.js"; import { buildPairingReply } from "../pairing/pairing-messages.js"; @@ -16,16 +18,14 @@ import { readChannelAllowFromStore, upsertChannelPairingRequest, } from "../pairing/pairing-store.js"; -import type { RuntimeEnv } from "../runtime.js"; +import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; import { buildLineMessageContext, buildLinePostbackContext, type LineInboundContext, } from "./bot-message-context.js"; -import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; import { downloadLineMedia } from "./download.js"; import { pushMessageLine, replyMessageLine } from "./send.js"; -import type { LineGroupConfig, ResolvedLineAccount } from "./types.js"; interface MediaRef { path: string; @@ -33,7 +33,7 @@ interface MediaRef { } export interface LineHandlerContext { - cfg: MoltbotConfig; + cfg: OpenClawConfig; account: ResolvedLineAccount; runtime: RuntimeEnv; mediaMaxBytes: number; @@ -87,7 +87,9 @@ async function sendLinePairingReply(params: { channel: "line", id: senderId, }); - if (!created) return; + if (!created) { + return; + } logVerbose(`line pairing request sender=${senderId}`); const idLabel = (() => { try { @@ -219,7 +221,9 @@ async function handleMessageEvent(event: MessageEvent, context: LineHandlerConte const { cfg, account, runtime, mediaMaxBytes, processMessage } = context; const message = event.message; - if (!(await shouldProcessLineEvent(event, context))) return; + if (!(await shouldProcessLineEvent(event, context))) { + return; + } // Download media if applicable const allMedia: MediaRef[] = []; @@ -290,14 +294,18 @@ async function handlePostbackEvent( const data = event.postback.data; logVerbose(`line: received postback: ${data}`); - if (!(await shouldProcessLineEvent(event, context))) return; + if (!(await shouldProcessLineEvent(event, context))) { + return; + } const postbackContext = await buildLinePostbackContext({ event, cfg: context.cfg, account: context.account, }); - if (!postbackContext) return; + if (!postbackContext) { + return; + } await context.processMessage(postbackContext); } diff --git a/src/line/bot-message-context.test.ts b/src/line/bot-message-context.test.ts index 01cd0035b..b75300dc0 100644 --- a/src/line/bot-message-context.test.ts +++ b/src/line/bot-message-context.test.ts @@ -1,16 +1,16 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { MessageEvent, PostbackEvent } from "@line/bot-sdk"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { MessageEvent, PostbackEvent } from "@line/bot-sdk"; -import type { MoltbotConfig } from "../config/config.js"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import type { ResolvedLineAccount } from "./types.js"; import { buildLineMessageContext, buildLinePostbackContext } from "./bot-message-context.js"; describe("buildLineMessageContext", () => { let tmpDir: string; let storePath: string; - let cfg: MoltbotConfig; + let cfg: OpenClawConfig; const account: ResolvedLineAccount = { accountId: "default", enabled: true, @@ -21,7 +21,7 @@ describe("buildLineMessageContext", () => { }; beforeEach(async () => { - tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-line-context-")); + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-line-context-")); storePath = path.join(tmpDir, "sessions.json"); cfg = { session: { store: storePath } }; }); diff --git a/src/line/bot-message-context.ts b/src/line/bot-message-context.ts index eb483ea94..f11729ade 100644 --- a/src/line/bot-message-context.ts +++ b/src/line/bot-message-context.ts @@ -1,15 +1,9 @@ -import type { - MessageEvent, - TextEventMessage, - StickerEventMessage, - LocationEventMessage, - EventSource, - PostbackEvent, -} from "@line/bot-sdk"; +import type { MessageEvent, StickerEventMessage, EventSource, PostbackEvent } from "@line/bot-sdk"; +import type { OpenClawConfig } from "../config/config.js"; +import type { ResolvedLineAccount } from "./types.js"; import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js"; import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; import { formatLocationText, toLocationContext } from "../channels/location.js"; -import type { MoltbotConfig } from "../config/config.js"; import { readSessionUpdatedAt, recordSessionMetaFromInbound, @@ -19,7 +13,6 @@ import { import { logVerbose, shouldLogVerbose } from "../globals.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; import { resolveAgentRoute } from "../routing/resolve-route.js"; -import type { ResolvedLineAccount } from "./types.js"; interface MediaRef { path: string; @@ -29,7 +22,7 @@ interface MediaRef { interface BuildLineMessageContextParams { event: MessageEvent; allMedia: MediaRef[]; - cfg: MoltbotConfig; + cfg: OpenClawConfig; account: ResolvedLineAccount; } @@ -102,10 +95,10 @@ function describeStickerKeywords(sticker: StickerEventMessage): string { function extractMessageText(message: MessageEvent["message"]): string { if (message.type === "text") { - return (message as TextEventMessage).text; + return message.text; } if (message.type === "location") { - const loc = message as LocationEventMessage; + const loc = message; return ( formatLocationText({ latitude: loc.latitude, @@ -116,7 +109,7 @@ function extractMessageText(message: MessageEvent["message"]): string { ); } if (message.type === "sticker") { - const sticker = message as StickerEventMessage; + const sticker = message; const packageName = STICKER_PACKAGES[sticker.packageId] ?? "sticker"; const keywords = describeStickerKeywords(sticker); @@ -222,7 +215,7 @@ export async function buildLineMessageContext(params: BuildLineMessageContextPar // Build location context if applicable let locationContext: ReturnType | undefined; if (message.type === "location") { - const loc = message as LocationEventMessage; + const loc = message; locationContext = toLocationContext({ latitude: loc.latitude, longitude: loc.longitude, @@ -315,7 +308,7 @@ export async function buildLineMessageContext(params: BuildLineMessageContextPar export async function buildLinePostbackContext(params: { event: PostbackEvent; - cfg: MoltbotConfig; + cfg: OpenClawConfig; account: ResolvedLineAccount; }) { const { event, cfg, account } = params; @@ -342,7 +335,9 @@ export async function buildLinePostbackContext(params: { const timestamp = event.timestamp; const rawData = event.postback?.data?.trim() ?? ""; - if (!rawData) return null; + if (!rawData) { + return null; + } let rawBody = rawData; if (rawData.includes("line.action=")) { const params = new URLSearchParams(rawData); diff --git a/src/line/bot.ts b/src/line/bot.ts index a22604932..b78a667e1 100644 --- a/src/line/bot.ts +++ b/src/line/bot.ts @@ -1,20 +1,21 @@ import type { WebhookRequestBody } from "@line/bot-sdk"; -import type { MoltbotConfig } from "../config/config.js"; +import type { Request, Response, NextFunction } from "express"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { LineInboundContext } from "./bot-message-context.js"; +import type { ResolvedLineAccount } from "./types.js"; import { loadConfig } from "../config/config.js"; import { logVerbose } from "../globals.js"; -import type { RuntimeEnv } from "../runtime.js"; import { resolveLineAccount } from "./accounts.js"; import { handleLineWebhookEvents } from "./bot-handlers.js"; -import type { LineInboundContext } from "./bot-message-context.js"; import { startLineWebhook } from "./webhook.js"; -import type { ResolvedLineAccount } from "./types.js"; export interface LineBotOptions { channelAccessToken: string; channelSecret: string; accountId?: string; runtime?: RuntimeEnv; - config?: MoltbotConfig; + config?: OpenClawConfig; mediaMaxMb?: number; onMessage?: (ctx: LineInboundContext) => Promise; } @@ -71,7 +72,7 @@ export function createLineWebhookCallback( bot: LineBot, channelSecret: string, path = "/line/webhook", -) { +): { path: string; handler: (req: Request, res: Response, _next: NextFunction) => Promise } { const { handler } = startLineWebhook({ channelSecret, onEvents: bot.handleWebhook, diff --git a/src/line/download.ts b/src/line/download.ts index e48cb0e71..9219025cc 100644 --- a/src/line/download.ts +++ b/src/line/download.ts @@ -1,7 +1,7 @@ -import fs from "node:fs"; -import path from "node:path"; -import os from "node:os"; import { messagingApi } from "@line/bot-sdk"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { logVerbose } from "../globals.js"; interface DownloadResult { diff --git a/src/line/flex-templates.ts b/src/line/flex-templates.ts index e0fe7e693..7b8c9f0d3 100644 --- a/src/line/flex-templates.ts +++ b/src/line/flex-templates.ts @@ -252,7 +252,7 @@ export function createImageCard( }; if (body && bubble.body) { - (bubble.body as FlexBox).contents.push({ + bubble.body.contents.push({ type: "text", text: body, size: "md", @@ -852,8 +852,12 @@ export function createAgendaCard(params: { // Secondary info line const secondaryParts: string[] = []; - if (event.location) secondaryParts.push(event.location); - if (event.calendar) secondaryParts.push(event.calendar); + if (event.location) { + secondaryParts.push(event.location); + } + if (event.calendar) { + secondaryParts.push(event.calendar); + } if (secondaryParts.length > 0) { detailContents.push({ diff --git a/src/line/http-registry.ts b/src/line/http-registry.ts index 1d971e752..fcf6d3e98 100644 --- a/src/line/http-registry.ts +++ b/src/line/http-registry.ts @@ -16,7 +16,9 @@ const lineHttpRoutes = new Map(); export function normalizeLineWebhookPath(path?: string | null): string { const trimmed = path?.trim(); - if (!trimmed) return "/line/webhook"; + if (!trimmed) { + return "/line/webhook"; + } return trimmed.startsWith("/") ? trimmed : `/${trimmed}`; } @@ -39,7 +41,9 @@ export async function handleLineHttpRequest( ): Promise { const url = new URL(req.url ?? "/", "http://localhost"); const handler = lineHttpRoutes.get(url.pathname); - if (!handler) return false; + if (!handler) { + return false; + } await handler(req, res); return true; } diff --git a/src/line/markdown-to-line.ts b/src/line/markdown-to-line.ts index 21253c36a..ef41a3846 100644 --- a/src/line/markdown-to-line.ts +++ b/src/line/markdown-to-line.ts @@ -80,8 +80,12 @@ function parseTableRow(row: string): string[] { .map((cell) => cell.trim()) .filter((cell, index, arr) => { // Filter out empty cells at start/end (from leading/trailing pipes) - if (index === 0 && cell === "") return false; - if (index === arr.length - 1 && cell === "") return false; + if (index === 0 && cell === "") { + return false; + } + if (index === arr.length - 1 && cell === "") { + return false; + } return true; }); } @@ -94,7 +98,9 @@ export function convertTableToFlexBubble(table: MarkdownTable): FlexBubble { value: string | undefined, ): { text: string; bold: boolean; hasMarkup: boolean } => { const raw = value?.trim() ?? ""; - if (!raw) return { text: "-", bold: false, hasMarkup: false }; + if (!raw) { + return { text: "-", bold: false, hasMarkup: false }; + } let hasMarkup = false; const stripped = raw.replace(/\*\*(.+?)\*\*/g, (_, inner) => { @@ -417,17 +423,29 @@ export function processLineMessage(text: string): ProcessedLineMessage { export function hasMarkdownToConvert(text: string): boolean { // Check for tables MARKDOWN_TABLE_REGEX.lastIndex = 0; - if (MARKDOWN_TABLE_REGEX.test(text)) return true; + if (MARKDOWN_TABLE_REGEX.test(text)) { + return true; + } // Check for code blocks MARKDOWN_CODE_BLOCK_REGEX.lastIndex = 0; - if (MARKDOWN_CODE_BLOCK_REGEX.test(text)) return true; + if (MARKDOWN_CODE_BLOCK_REGEX.test(text)) { + return true; + } // Check for other markdown patterns - if (/\*\*[^*]+\*\*/.test(text)) return true; // bold - if (/~~[^~]+~~/.test(text)) return true; // strikethrough - if (/^#{1,6}\s+/m.test(text)) return true; // headers - if (/^>\s+/m.test(text)) return true; // blockquotes + if (/\*\*[^*]+\*\*/.test(text)) { + return true; + } // bold + if (/~~[^~]+~~/.test(text)) { + return true; + } // strikethrough + if (/^#{1,6}\s+/m.test(text)) { + return true; + } // headers + if (/^>\s+/m.test(text)) { + return true; + } // blockquotes return false; } diff --git a/src/line/monitor.ts b/src/line/monitor.ts index 764dbf130..8880e4a77 100644 --- a/src/line/monitor.ts +++ b/src/line/monitor.ts @@ -1,12 +1,18 @@ import type { WebhookRequestBody } from "@line/bot-sdk"; import type { IncomingMessage, ServerResponse } from "node:http"; -import type { MoltbotConfig } from "../config/config.js"; -import { danger, logVerbose } from "../globals.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; -import { createLineBot } from "./bot.js"; -import { validateLineSignature } from "./signature.js"; +import type { LineChannelData, ResolvedLineAccount } from "./types.js"; +import { resolveEffectiveMessagesConfig } from "../agents/identity.js"; +import { chunkMarkdownText } from "../auto-reply/chunk.js"; +import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; +import { danger, logVerbose } from "../globals.js"; import { normalizePluginHttpPath } from "../plugins/http-path.js"; import { registerPluginHttpRoute } from "../plugins/http-registry.js"; +import { deliverLineAutoReply } from "./auto-reply-delivery.js"; +import { createLineBot } from "./bot.js"; +import { processLineMessage } from "./markdown-to-line.js"; +import { sendLineReplyChunks } from "./reply-chunks.js"; import { replyMessageLine, showLoadingAnimation, @@ -20,20 +26,14 @@ import { createImageMessage, createLocationMessage, } from "./send.js"; +import { validateLineSignature } from "./signature.js"; import { buildTemplateMessageFromPayload } from "./template-messages.js"; -import type { LineChannelData, ResolvedLineAccount } from "./types.js"; -import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; -import { resolveEffectiveMessagesConfig } from "../agents/identity.js"; -import { chunkMarkdownText } from "../auto-reply/chunk.js"; -import { processLineMessage } from "./markdown-to-line.js"; -import { sendLineReplyChunks } from "./reply-chunks.js"; -import { deliverLineAutoReply } from "./auto-reply-delivery.js"; export interface MonitorLineProviderOptions { channelAccessToken: string; channelSecret: string; accountId?: string; - config: MoltbotConfig; + config: OpenClawConfig; runtime: RuntimeEnv; abortSignal?: AbortSignal; webhookUrl?: string; @@ -105,7 +105,9 @@ function startLineLoadingKeepalive(params: { let stopped = false; const trigger = () => { - if (stopped) return; + if (stopped) { + return; + } void showLoadingAnimation(params.userId, { accountId: params.accountId, loadingSeconds, @@ -116,7 +118,9 @@ function startLineLoadingKeepalive(params: { const timer = setInterval(trigger, intervalMs); return () => { - if (stopped) return; + if (stopped) { + return; + } stopped = true; clearInterval(timer); }; @@ -154,7 +158,9 @@ export async function monitorLineProvider( runtime, config, onMessage: async (ctx) => { - if (!ctx) return; + if (!ctx) { + return; + } const { ctxPayload, replyToken, route } = ctx; diff --git a/src/line/probe.test.ts b/src/line/probe.test.ts index e76715b7f..688c754b1 100644 --- a/src/line/probe.test.ts +++ b/src/line/probe.test.ts @@ -37,9 +37,9 @@ describe("probeLineBot", () => { it("returns bot info when available", async () => { getBotInfoMock.mockResolvedValue({ - displayName: "Moltbot", + displayName: "OpenClaw", userId: "U123", - basicId: "@moltbot", + basicId: "@openclaw", pictureUrl: "https://example.com/bot.png", }); diff --git a/src/line/probe.ts b/src/line/probe.ts index d538d4271..d5f7755cd 100644 --- a/src/line/probe.ts +++ b/src/line/probe.ts @@ -32,12 +32,16 @@ export async function probeLineBot( } function withTimeout(promise: Promise, timeoutMs: number): Promise { - if (!timeoutMs || timeoutMs <= 0) return promise; + if (!timeoutMs || timeoutMs <= 0) { + return promise; + } let timer: NodeJS.Timeout | null = null; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("timeout")), timeoutMs); }); return Promise.race([promise, timeout]).finally(() => { - if (timer) clearTimeout(timer); + if (timer) { + clearTimeout(timer); + } }); } diff --git a/src/line/reply-chunks.ts b/src/line/reply-chunks.ts index e4d5c4b9d..466a1be02 100644 --- a/src/line/reply-chunks.ts +++ b/src/line/reply-chunks.ts @@ -48,7 +48,7 @@ export async function sendLineReplyChunks( if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) { const lastIndex = replyMessages.length - 1; replyMessages[lastIndex] = params.createTextMessageWithQuickReplies( - replyBatch[lastIndex]!, + replyBatch[lastIndex], params.quickReplies!, ); } @@ -63,12 +63,12 @@ export async function sendLineReplyChunks( if (isLastChunk && hasQuickReplies) { await params.pushTextMessageWithQuickReplies( params.to, - remaining[i]!, + remaining[i], params.quickReplies!, { accountId: params.accountId }, ); } else { - await params.pushMessageLine(params.to, remaining[i]!, { + await params.pushMessageLine(params.to, remaining[i], { accountId: params.accountId, }); } @@ -86,12 +86,12 @@ export async function sendLineReplyChunks( if (isLastChunk && hasQuickReplies) { await params.pushTextMessageWithQuickReplies( params.to, - params.chunks[i]!, + params.chunks[i], params.quickReplies!, { accountId: params.accountId }, ); } else { - await params.pushMessageLine(params.to, params.chunks[i]!, { + await params.pushMessageLine(params.to, params.chunks[i], { accountId: params.accountId, }); } diff --git a/src/line/rich-menu.ts b/src/line/rich-menu.ts index 6149405a9..670ac9b76 100644 --- a/src/line/rich-menu.ts +++ b/src/line/rich-menu.ts @@ -42,7 +42,9 @@ function resolveToken( explicit: string | undefined, params: { accountId: string; channelAccessToken: string }, ): string { - if (explicit?.trim()) return explicit.trim(); + if (explicit?.trim()) { + return explicit.trim(); + } if (!params.channelAccessToken) { throw new Error( `LINE channel access token missing for account "${params.accountId}" (set channels.line.channelAccessToken or LINE_CHANNEL_ACCESS_TOKEN).`, diff --git a/src/line/send.ts b/src/line/send.ts index 68be26a29..874a7ea41 100644 --- a/src/line/send.ts +++ b/src/line/send.ts @@ -1,9 +1,9 @@ import { messagingApi } from "@line/bot-sdk"; +import type { LineSendResult } from "./types.js"; import { loadConfig } from "../config/config.js"; import { logVerbose } from "../globals.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; import { resolveLineAccount } from "./accounts.js"; -import type { LineSendResult } from "./types.js"; // Use the messaging API types directly type Message = messagingApi.Message; @@ -35,7 +35,9 @@ function resolveToken( explicit: string | undefined, params: { accountId: string; channelAccessToken: string }, ): string { - if (explicit?.trim()) return explicit.trim(); + if (explicit?.trim()) { + return explicit.trim(); + } if (!params.channelAccessToken) { throw new Error( `LINE channel access token missing for account "${params.accountId}" (set channels.line.channelAccessToken or LINE_CHANNEL_ACCESS_TOKEN).`, @@ -46,7 +48,9 @@ function resolveToken( function normalizeTarget(to: string): string { const trimmed = to.trim(); - if (!trimmed) throw new Error("Recipient is required for LINE sends"); + if (!trimmed) { + throw new Error("Recipient is required for LINE sends"); + } // Strip internal prefixes let normalized = trimmed @@ -55,7 +59,9 @@ function normalizeTarget(to: string): string { .replace(/^line:user:/i, "") .replace(/^line:/i, ""); - if (!normalized) throw new Error("Recipient is required for LINE sends"); + if (!normalized) { + throw new Error("Recipient is required for LINE sends"); + } return normalized; } @@ -91,7 +97,9 @@ export function createLocationMessage(location: { } function logLineHttpError(err: unknown, context: string): void { - if (!err || typeof err !== "object") return; + if (!err || typeof err !== "object") { + return; + } const { status, statusText, body } = err as { status?: number; statusText?: string; diff --git a/src/line/webhook.ts b/src/line/webhook.ts index 9986617f9..b2e9806fa 100644 --- a/src/line/webhook.ts +++ b/src/line/webhook.ts @@ -1,7 +1,7 @@ -import type { Request, Response, NextFunction } from "express"; import type { WebhookRequestBody } from "@line/bot-sdk"; -import { logVerbose, danger } from "../globals.js"; +import type { Request, Response, NextFunction } from "express"; import type { RuntimeEnv } from "../runtime.js"; +import { logVerbose, danger } from "../globals.js"; import { validateLineSignature } from "./signature.js"; export interface LineWebhookOptions { @@ -14,7 +14,9 @@ function readRawBody(req: Request): string | null { const rawBody = (req as { rawBody?: string | Buffer }).rawBody ?? (typeof req.body === "string" || Buffer.isBuffer(req.body) ? req.body : null); - if (!rawBody) return null; + if (!rawBody) { + return null; + } return Buffer.isBuffer(rawBody) ? rawBody.toString("utf-8") : rawBody; } @@ -29,7 +31,9 @@ function parseWebhookBody(req: Request, rawBody: string): WebhookRequestBody | n } } -export function createLineWebhookMiddleware(options: LineWebhookOptions) { +export function createLineWebhookMiddleware( + options: LineWebhookOptions, +): (req: Request, res: Response, _next: NextFunction) => Promise { const { channelSecret, onEvents, runtime } = options; return async (req: Request, res: Response, _next: NextFunction): Promise => { @@ -85,7 +89,10 @@ export interface StartLineWebhookOptions { path?: string; } -export function startLineWebhook(options: StartLineWebhookOptions) { +export function startLineWebhook(options: StartLineWebhookOptions): { + path: string; + handler: (req: Request, res: Response, _next: NextFunction) => Promise; +} { const path = options.path ?? "/line/webhook"; const middleware = createLineWebhookMiddleware({ channelSecret: options.channelSecret, diff --git a/src/link-understanding/apply.ts b/src/link-understanding/apply.ts index 1bc18a66d..f2bd97981 100644 --- a/src/link-understanding/apply.ts +++ b/src/link-understanding/apply.ts @@ -1,5 +1,5 @@ -import type { MoltbotConfig } from "../config/config.js"; import type { MsgContext } from "../auto-reply/templating.js"; +import type { OpenClawConfig } from "../config/config.js"; import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; import { formatLinkUnderstandingBody } from "./format.js"; import { runLinkUnderstanding } from "./runner.js"; @@ -11,7 +11,7 @@ export type ApplyLinkUnderstandingResult = { export async function applyLinkUnderstanding(params: { ctx: MsgContext; - cfg: MoltbotConfig; + cfg: OpenClawConfig; }): Promise { const result = await runLinkUnderstanding({ cfg: params.cfg, diff --git a/src/link-understanding/detect.test.ts b/src/link-understanding/detect.test.ts index 07545f403..f65280b8b 100644 --- a/src/link-understanding/detect.test.ts +++ b/src/link-understanding/detect.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { extractLinksFromMessage } from "./detect.js"; describe("extractLinksFromMessage", () => { diff --git a/src/link-understanding/detect.ts b/src/link-understanding/detect.ts index 9edecde63..79899f94b 100644 --- a/src/link-understanding/detect.ts +++ b/src/link-understanding/detect.ts @@ -18,8 +18,12 @@ function resolveMaxLinks(value?: number): number { function isAllowedUrl(raw: string): boolean { try { const parsed = new URL(raw); - if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false; - if (parsed.hostname === "127.0.0.1") return false; + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return false; + } + if (parsed.hostname === "127.0.0.1") { + return false; + } return true; } catch { return false; @@ -28,7 +32,9 @@ function isAllowedUrl(raw: string): boolean { export function extractLinksFromMessage(message: string, opts?: { maxLinks?: number }): string[] { const source = message?.trim(); - if (!source) return []; + if (!source) { + return []; + } const maxLinks = resolveMaxLinks(opts?.maxLinks); const sanitized = stripMarkdownLinks(source); @@ -37,12 +43,20 @@ export function extractLinksFromMessage(message: string, opts?: { maxLinks?: num for (const match of sanitized.matchAll(BARE_LINK_RE)) { const raw = match[0]?.trim(); - if (!raw) continue; - if (!isAllowedUrl(raw)) continue; - if (seen.has(raw)) continue; + if (!raw) { + continue; + } + if (!isAllowedUrl(raw)) { + continue; + } + if (seen.has(raw)) { + continue; + } seen.add(raw); results.push(raw); - if (results.length >= maxLinks) break; + if (results.length >= maxLinks) { + break; + } } return results; diff --git a/src/link-understanding/format.ts b/src/link-understanding/format.ts index b28d16a1a..a81a86bf3 100644 --- a/src/link-understanding/format.ts +++ b/src/link-understanding/format.ts @@ -5,6 +5,8 @@ export function formatLinkUnderstandingBody(params: { body?: string; outputs: st } const base = (params.body ?? "").trim(); - if (!base) return outputs.join("\n"); + if (!base) { + return outputs.join("\n"); + } return `${base}\n\n${outputs.join("\n")}`; } diff --git a/src/link-understanding/runner.ts b/src/link-understanding/runner.ts index bfd2e7286..f77f0f85c 100644 --- a/src/link-understanding/runner.ts +++ b/src/link-understanding/runner.ts @@ -1,15 +1,15 @@ -import type { MoltbotConfig } from "../config/config.js"; import type { MsgContext } from "../auto-reply/templating.js"; -import { applyTemplate } from "../auto-reply/templating.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { LinkModelConfig, LinkToolsConfig } from "../config/types.tools.js"; +import { applyTemplate } from "../auto-reply/templating.js"; import { logVerbose, shouldLogVerbose } from "../globals.js"; -import { runExec } from "../process/exec.js"; import { CLI_OUTPUT_MAX_BUFFER } from "../media-understanding/defaults.js"; import { resolveTimeoutMs } from "../media-understanding/resolve.js"; import { normalizeMediaUnderstandingChatType, resolveMediaUnderstandingScope, } from "../media-understanding/scope.js"; +import { runExec } from "../process/exec.js"; import { DEFAULT_LINK_TIMEOUT_SECONDS } from "./defaults.js"; import { extractLinksFromMessage } from "./detect.js"; @@ -44,9 +44,13 @@ async function runCliEntry(params: { url: string; config?: LinkToolsConfig; }): Promise { - if ((params.entry.type ?? "cli") !== "cli") return null; + if ((params.entry.type ?? "cli") !== "cli") { + return null; + } const command = params.entry.command.trim(); - if (!command) return null; + if (!command) { + return null; + } const args = params.entry.args ?? []; const timeoutMs = resolveTimeoutMsFromConfig({ config: params.config, entry: params.entry }); const templCtx = { @@ -84,7 +88,9 @@ async function runLinkEntries(params: { url: params.url, config: params.config, }); - if (output) return output; + if (output) { + return output; + } } catch (err) { lastError = err; if (shouldLogVerbose()) { @@ -99,12 +105,14 @@ async function runLinkEntries(params: { } export async function runLinkUnderstanding(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; ctx: MsgContext; message?: string; }): Promise { const config = params.cfg.tools?.links; - if (!config || config.enabled === false) return { urls: [], outputs: [] }; + if (!config || config.enabled === false) { + return { urls: [], outputs: [] }; + } const scopeDecision = resolveScopeDecision({ config, ctx: params.ctx }); if (scopeDecision === "deny") { @@ -116,10 +124,14 @@ export async function runLinkUnderstanding(params: { const message = params.message ?? params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body; const links = extractLinksFromMessage(message ?? "", { maxLinks: config?.maxLinks }); - if (links.length === 0) return { urls: [], outputs: [] }; + if (links.length === 0) { + return { urls: [], outputs: [] }; + } const entries = config?.models ?? []; - if (entries.length === 0) return { urls: links, outputs: [] }; + if (entries.length === 0) { + return { urls: links, outputs: [] }; + } const outputs: string[] = []; for (const url of links) { @@ -129,7 +141,9 @@ export async function runLinkUnderstanding(params: { url, config, }); - if (output) outputs.push(output); + if (output) { + outputs.push(output); + } } return { urls: links, outputs }; diff --git a/src/logger.test.ts b/src/logger.test.ts index 1853d42b9..9f87d4b37 100644 --- a/src/logger.test.ts +++ b/src/logger.test.ts @@ -2,13 +2,11 @@ import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - import { afterEach, describe, expect, it, vi } from "vitest"; - +import type { RuntimeEnv } from "./runtime.js"; import { setVerbose } from "./globals.js"; import { logDebug, logError, logInfo, logSuccess, logWarn } from "./logger.js"; import { DEFAULT_LOG_DIR, resetLogger, setLoggerOverride } from "./logging.js"; -import type { RuntimeEnv } from "./runtime.js"; describe("logger helpers", () => { afterEach(() => { @@ -71,10 +69,10 @@ describe("logger helpers", () => { resetLogger(); setLoggerOverride({}); // force defaults regardless of user config const today = localDateString(new Date()); - const todayPath = path.join(DEFAULT_LOG_DIR, `moltbot-${today}.log`); + const todayPath = path.join(DEFAULT_LOG_DIR, `openclaw-${today}.log`); // create an old file to be pruned - const oldPath = path.join(DEFAULT_LOG_DIR, "moltbot-2000-01-01.log"); + const oldPath = path.join(DEFAULT_LOG_DIR, "openclaw-2000-01-01.log"); fs.mkdirSync(DEFAULT_LOG_DIR, { recursive: true }); fs.writeFileSync(oldPath, "old"); fs.utimesSync(oldPath, new Date(0), new Date(0)); @@ -91,7 +89,7 @@ describe("logger helpers", () => { }); function pathForTest() { - const file = path.join(os.tmpdir(), `moltbot-log-${crypto.randomUUID()}.log`); + const file = path.join(os.tmpdir(), `openclaw-log-${crypto.randomUUID()}.log`); fs.mkdirSync(path.dirname(file), { recursive: true }); return file; } diff --git a/src/logger.ts b/src/logger.ts index 6015cf344..4ae1cb20d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -7,7 +7,9 @@ const subsystemPrefixRe = /^([a-z][a-z0-9-]{1,20}):\s+(.*)$/i; function splitSubsystem(message: string) { const match = message.match(subsystemPrefixRe); - if (!match) return null; + if (!match) { + return null; + } const [, subsystem, rest] = match; return { subsystem, rest }; } diff --git a/src/logging.ts b/src/logging.ts index ed7a11167..5706787cd 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,3 +1,7 @@ +import type { ConsoleLoggerSettings, ConsoleStyle } from "./logging/console.js"; +import type { LogLevel } from "./logging/levels.js"; +import type { LoggerResolvedSettings, LoggerSettings, PinoLikeLogger } from "./logging/logger.js"; +import type { SubsystemLogger } from "./logging/subsystem.js"; import { enableConsoleCapture, getConsoleSettings, @@ -7,9 +11,7 @@ import { setConsoleTimestampPrefix, shouldLogSubsystemToConsole, } from "./logging/console.js"; -import type { ConsoleLoggerSettings, ConsoleStyle } from "./logging/console.js"; import { ALLOWED_LOG_LEVELS, levelToMinLevel, normalizeLogLevel } from "./logging/levels.js"; -import type { LogLevel } from "./logging/levels.js"; import { DEFAULT_LOG_DIR, DEFAULT_LOG_FILE, @@ -21,14 +23,12 @@ import { setLoggerOverride, toPinoLikeLogger, } from "./logging/logger.js"; -import type { LoggerResolvedSettings, LoggerSettings, PinoLikeLogger } from "./logging/logger.js"; import { createSubsystemLogger, createSubsystemRuntime, runtimeForLogger, stripRedundantSubsystemPrefixForConsole, } from "./logging/subsystem.js"; -import type { SubsystemLogger } from "./logging/subsystem.js"; export { enableConsoleCapture, diff --git a/src/logging/config.ts b/src/logging/config.ts index 50e8f30d6..a42145347 100644 --- a/src/logging/config.ts +++ b/src/logging/config.ts @@ -1,20 +1,22 @@ -import fs from "node:fs"; - import json5 from "json5"; - +import fs from "node:fs"; +import type { OpenClawConfig } from "../config/types.js"; import { resolveConfigPath } from "../config/paths.js"; -import type { MoltbotConfig } from "../config/types.js"; -type LoggingConfig = MoltbotConfig["logging"]; +type LoggingConfig = OpenClawConfig["logging"]; export function readLoggingConfig(): LoggingConfig | undefined { const configPath = resolveConfigPath(); try { - if (!fs.existsSync(configPath)) return undefined; + if (!fs.existsSync(configPath)) { + return undefined; + } const raw = fs.readFileSync(configPath, "utf-8"); - const parsed = json5.parse(raw) as Record; + const parsed = json5.parse(raw); const logging = parsed?.logging; - if (!logging || typeof logging !== "object" || Array.isArray(logging)) return undefined; + if (!logging || typeof logging !== "object" || Array.isArray(logging)) { + return undefined; + } return logging as LoggingConfig; } catch { return undefined; diff --git a/src/logging/console-capture.test.ts b/src/logging/console-capture.test.ts index 25add68b5..638332ddf 100644 --- a/src/logging/console-capture.test.ts +++ b/src/logging/console-capture.test.ts @@ -1,9 +1,7 @@ import crypto from "node:crypto"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { enableConsoleCapture, resetLogger, @@ -126,7 +124,7 @@ describe("enableConsoleCapture", () => { }); function tempLogPath() { - return path.join(os.tmpdir(), `moltbot-log-${crypto.randomUUID()}.log`); + return path.join(os.tmpdir(), `openclaw-log-${crypto.randomUUID()}.log`); } function eioError() { diff --git a/src/logging/console-prefix.test.ts b/src/logging/console-prefix.test.ts index 2fd3bf091..3bc3b13df 100644 --- a/src/logging/console-prefix.test.ts +++ b/src/logging/console-prefix.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { stripRedundantSubsystemPrefixForConsole } from "../logging.js"; describe("stripRedundantSubsystemPrefixForConsole", () => { diff --git a/src/logging/console.ts b/src/logging/console.ts index 0a1218494..986bf89ac 100644 --- a/src/logging/console.ts +++ b/src/logging/console.ts @@ -1,12 +1,11 @@ import { createRequire } from "node:module"; import util from "node:util"; - -import type { MoltbotConfig } from "../config/types.js"; +import type { OpenClawConfig } from "../config/types.js"; import { isVerbose } from "../globals.js"; import { stripAnsi } from "../terminal/ansi.js"; +import { readLoggingConfig } from "./config.js"; import { type LogLevel, normalizeLogLevel } from "./levels.js"; import { getLogger, type LoggerSettings } from "./logger.js"; -import { readLoggingConfig } from "./config.js"; import { loggingState } from "./state.js"; export type ConsoleStyle = "pretty" | "compact" | "json"; @@ -19,7 +18,9 @@ export type ConsoleLoggerSettings = ConsoleSettings; const requireConfig = createRequire(import.meta.url); function normalizeConsoleLevel(level?: string): LogLevel { - if (isVerbose()) return "debug"; + if (isVerbose()) { + return "debug"; + } return normalizeLogLevel(level, "info"); } @@ -27,12 +28,14 @@ function normalizeConsoleStyle(style?: string): ConsoleStyle { if (style === "compact" || style === "json" || style === "pretty") { return style; } - if (!process.stdout.isTTY) return "compact"; + if (!process.stdout.isTTY) { + return "compact"; + } return "pretty"; } function resolveConsoleSettings(): ConsoleSettings { - let cfg: MoltbotConfig["logging"] | undefined = + let cfg: OpenClawConfig["logging"] | undefined = (loggingState.overrideSettings as LoggerSettings | null) ?? readLoggingConfig(); if (!cfg) { if (loggingState.resolvingConsoleSettings) { @@ -41,7 +44,7 @@ function resolveConsoleSettings(): ConsoleSettings { loggingState.resolvingConsoleSettings = true; try { const loaded = requireConfig("../config/config.js") as { - loadConfig?: () => MoltbotConfig; + loadConfig?: () => OpenClawConfig; }; cfg = loaded.loadConfig?.().logging; } catch { @@ -57,7 +60,9 @@ function resolveConsoleSettings(): ConsoleSettings { } function consoleSettingsChanged(a: ConsoleSettings | null, b: ConsoleSettings) { - if (!a) return true; + if (!a) { + return true; + } return a.level !== b.level || a.style !== b.style; } @@ -110,7 +115,9 @@ const SUPPRESSED_CONSOLE_PREFIXES = [ ] as const; function shouldSuppressConsoleMessage(message: string): boolean { - if (isVerbose()) return false; + if (isVerbose()) { + return false; + } if (SUPPRESSED_CONSOLE_PREFIXES.some((prefix) => message.startsWith(prefix))) { return true; } @@ -130,7 +137,9 @@ function isEpipeError(err: unknown): boolean { function formatConsoleTimestamp(style: ConsoleStyle): string { const now = new Date().toISOString(); - if (style === "pretty") return now.slice(11, 19); + if (style === "pretty") { + return now.slice(11, 19); + } return now; } @@ -140,7 +149,9 @@ function hasTimestampPrefix(value: string): boolean { function isJsonPayload(value: string): boolean { const trimmed = value.trim(); - if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return false; + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) { + return false; + } try { JSON.parse(trimmed); return true; @@ -154,7 +165,9 @@ function isJsonPayload(value: string): boolean { * This keeps user-facing output unchanged but guarantees every console call is captured in log files. */ export function enableConsoleCapture(): void { - if (loggingState.consolePatched) return; + if (loggingState.consolePatched) { + return; + } loggingState.consolePatched = true; let logger: ReturnType | null = null; @@ -184,7 +197,9 @@ export function enableConsoleCapture(): void { (level: LogLevel, orig: (...args: unknown[]) => void) => (...args: unknown[]) => { const formatted = util.format(...args); - if (shouldSuppressConsoleMessage(formatted)) return; + if (shouldSuppressConsoleMessage(formatted)) { + return; + } const trimmed = stripAnsi(formatted).trimStart(); const shouldPrefixTimestamp = loggingState.consoleTimestampPrefix && @@ -219,7 +234,9 @@ export function enableConsoleCapture(): void { const line = timestamp ? `${timestamp} ${formatted}` : formatted; process.stderr.write(`${line}\n`); } catch (err) { - if (isEpipeError(err)) return; + if (isEpipeError(err)) { + return; + } throw err; } } else { @@ -238,7 +255,9 @@ export function enableConsoleCapture(): void { } orig.call(console, timestamp, ...args); } catch (err) { - if (isEpipeError(err)) return; + if (isEpipeError(err)) { + return; + } throw err; } } diff --git a/src/logging/diagnostic.ts b/src/logging/diagnostic.ts index ff51dd4bf..24dfc8961 100644 --- a/src/logging/diagnostic.ts +++ b/src/logging/diagnostic.ts @@ -41,8 +41,12 @@ function getSessionState(ref: SessionRef): SessionState { const key = resolveSessionKey(ref); const existing = sessionStates.get(key); if (existing) { - if (ref.sessionId) existing.sessionId = ref.sessionId; - if (ref.sessionKey) existing.sessionKey = ref.sessionKey; + if (ref.sessionId) { + existing.sessionId = ref.sessionId; + } + if (ref.sessionKey) { + existing.sessionKey = ref.sessionKey; + } return existing; } const created: SessionState = { @@ -201,7 +205,9 @@ export function logSessionStateChange( const prevState = state.state; state.state = params.state; state.lastActivity = Date.now(); - if (params.state === "idle") state.queueDepth = Math.max(0, state.queueDepth - 1); + if (params.state === "idle") { + state.queueDepth = Math.max(0, state.queueDepth - 1); + } if (!isProbeSession) { diag.debug( `session state: sessionId=${state.sessionId ?? "unknown"} sessionKey=${ @@ -292,7 +298,9 @@ export function logActiveRuns() { let heartbeatInterval: NodeJS.Timeout | null = null; export function startDiagnosticHeartbeat() { - if (heartbeatInterval) return; + if (heartbeatInterval) { + return; + } heartbeatInterval = setInterval(() => { const now = Date.now(); const activeCount = Array.from(sessionStates.values()).filter( @@ -311,8 +319,12 @@ export function startDiagnosticHeartbeat() { activeCount > 0 || waitingCount > 0 || totalQueued > 0; - if (!hasActivity) return; - if (now - lastActivityAt > 120_000 && activeCount === 0 && waitingCount === 0) return; + if (!hasActivity) { + return; + } + if (now - lastActivityAt > 120_000 && activeCount === 0 && waitingCount === 0) { + return; + } diag.debug( `heartbeat: webhooks=${webhookStats.received}/${webhookStats.processed}/${webhookStats.errors} active=${activeCount} waiting=${waitingCount} queued=${totalQueued}`, diff --git a/src/logging/logger.ts b/src/logging/logger.ts index ae36a75e1..819a14a8a 100644 --- a/src/logging/logger.ts +++ b/src/logging/logger.ts @@ -1,21 +1,19 @@ -import { createRequire } from "node:module"; import fs from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; - import { Logger as TsLogger } from "tslog"; - -import type { MoltbotConfig } from "../config/types.js"; +import type { OpenClawConfig } from "../config/types.js"; import type { ConsoleStyle } from "./console.js"; -import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js"; import { readLoggingConfig } from "./config.js"; +import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js"; import { loggingState } from "./state.js"; // Pin to /tmp so mac Debug UI and docs match; os.tmpdir() can be a per-user // randomized path on macOS which made the “Open log” button a no-op. -export const DEFAULT_LOG_DIR = "/tmp/moltbot"; -export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "moltbot.log"); // legacy single-file path +export const DEFAULT_LOG_DIR = "/tmp/openclaw"; +export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "openclaw.log"); // legacy single-file path -const LOG_PREFIX = "moltbot"; +const LOG_PREFIX = "openclaw"; const LOG_SUFFIX = ".log"; const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h @@ -42,7 +40,9 @@ const externalTransports = new Set(); function attachExternalTransport(logger: TsLogger, transport: LogTransport): void { logger.attachTransport((logObj: LogObj) => { - if (!externalTransports.has(transport)) return; + if (!externalTransports.has(transport)) { + return; + } try { transport(logObj as LogTransportRecord); } catch { @@ -52,12 +52,12 @@ function attachExternalTransport(logger: TsLogger, transport: LogTranspo } function resolveSettings(): ResolvedSettings { - let cfg: MoltbotConfig["logging"] | undefined = + let cfg: OpenClawConfig["logging"] | undefined = (loggingState.overrideSettings as LoggerSettings | null) ?? readLoggingConfig(); if (!cfg) { try { const loaded = requireConfig("../config/config.js") as { - loadConfig?: () => MoltbotConfig; + loadConfig?: () => OpenClawConfig; }; cfg = loaded.loadConfig?.().logging; } catch { @@ -70,14 +70,20 @@ function resolveSettings(): ResolvedSettings { } function settingsChanged(a: ResolvedSettings | null, b: ResolvedSettings) { - if (!a) return true; + if (!a) { + return true; + } return a.level !== b.level || a.file !== b.file; } export function isFileLogLevelEnabled(level: LogLevel): boolean { const settings = (loggingState.cachedSettings as ResolvedSettings | null) ?? resolveSettings(); - if (!loggingState.cachedSettings) loggingState.cachedSettings = settings; - if (settings.level === "silent") return false; + if (!loggingState.cachedSettings) { + loggingState.cachedSettings = settings; + } + if (settings.level === "silent") { + return false; + } return levelToMinLevel(level) <= levelToMinLevel(settings.level); } @@ -88,7 +94,7 @@ function buildLogger(settings: ResolvedSettings): TsLogger { pruneOldRollingLogs(path.dirname(settings.file)); } const logger = new TsLogger({ - name: "moltbot", + name: "openclaw", minLevel: levelToMinLevel(settings.level), type: "hidden", // no ansi formatting }); @@ -223,8 +229,12 @@ function pruneOldRollingLogs(dir: string): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); const cutoff = Date.now() - MAX_LOG_AGE_MS; for (const entry of entries) { - if (!entry.isFile()) continue; - if (!entry.name.startsWith(`${LOG_PREFIX}-`) || !entry.name.endsWith(LOG_SUFFIX)) continue; + if (!entry.isFile()) { + continue; + } + if (!entry.name.startsWith(`${LOG_PREFIX}-`) || !entry.name.endsWith(LOG_SUFFIX)) { + continue; + } const fullPath = path.join(dir, entry.name); try { const stat = fs.statSync(fullPath); diff --git a/src/logging/parse-log-line.test.ts b/src/logging/parse-log-line.test.ts index 272ce176e..cf1fb6058 100644 --- a/src/logging/parse-log-line.test.ts +++ b/src/logging/parse-log-line.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { parseLogLine } from "./parse-log-line.js"; describe("parseLogLine", () => { diff --git a/src/logging/parse-log-line.ts b/src/logging/parse-log-line.ts index 1554fe7a2..97623efa8 100644 --- a/src/logging/parse-log-line.ts +++ b/src/logging/parse-log-line.ts @@ -10,7 +10,9 @@ export type ParsedLogLine = { function extractMessage(value: Record): string { const parts: string[] = []; for (const key of Object.keys(value)) { - if (!/^\d+$/.test(key)) continue; + if (!/^\d+$/.test(key)) { + continue; + } const item = value[key]; if (typeof item === "string") { parts.push(item); @@ -22,7 +24,9 @@ function extractMessage(value: Record): string { } function parseMetaName(raw?: unknown): { subsystem?: string; module?: string } { - if (typeof raw !== "string") return {}; + if (typeof raw !== "string") { + return {}; + } try { const parsed = JSON.parse(raw) as Record; return { diff --git a/src/logging/redact.test.ts b/src/logging/redact.test.ts index 5b3ecbfb8..3e8b754dd 100644 --- a/src/logging/redact.test.ts +++ b/src/logging/redact.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { getDefaultRedactPatterns, redactSensitiveText } from "./redact.js"; const defaults = getDefaultRedactPatterns(); diff --git a/src/logging/redact.ts b/src/logging/redact.ts index f2be8339e..f79bed7e0 100644 --- a/src/logging/redact.ts +++ b/src/logging/redact.ts @@ -1,6 +1,5 @@ import { createRequire } from "node:module"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; const requireConfig = createRequire(import.meta.url); @@ -46,7 +45,9 @@ function normalizeMode(value?: string): RedactSensitiveMode { } function parsePattern(raw: string): RegExp | null { - if (!raw.trim()) return null; + if (!raw.trim()) { + return null; + } const match = raw.match(/^\/(.+)\/([gimsuy]*)$/); try { if (match) { @@ -65,7 +66,9 @@ function resolvePatterns(value?: string[]): RegExp[] { } function maskToken(token: string): string { - if (token.length < DEFAULT_REDACT_MIN_LENGTH) return "***"; + if (token.length < DEFAULT_REDACT_MIN_LENGTH) { + return "***"; + } const start = token.slice(0, DEFAULT_REDACT_KEEP_START); const end = token.slice(-DEFAULT_REDACT_KEEP_END); return `${start}…${end}`; @@ -73,16 +76,22 @@ function maskToken(token: string): string { function redactPemBlock(block: string): string { const lines = block.split(/\r?\n/).filter(Boolean); - if (lines.length < 2) return "***"; + if (lines.length < 2) { + return "***"; + } return `${lines[0]}\n…redacted…\n${lines[lines.length - 1]}`; } function redactMatch(match: string, groups: string[]): string { - if (match.includes("PRIVATE KEY-----")) return redactPemBlock(match); + if (match.includes("PRIVATE KEY-----")) { + return redactPemBlock(match); + } const token = groups.filter((value) => typeof value === "string" && value.length > 0).at(-1) ?? match; const masked = maskToken(token); - if (token === match) return masked; + if (token === match) { + return masked; + } return match.replace(token, masked); } @@ -97,10 +106,10 @@ function redactText(text: string, patterns: RegExp[]): string { } function resolveConfigRedaction(): RedactOptions { - let cfg: MoltbotConfig["logging"] | undefined; + let cfg: OpenClawConfig["logging"] | undefined; try { const loaded = requireConfig("../config/config.js") as { - loadConfig?: () => MoltbotConfig; + loadConfig?: () => OpenClawConfig; }; cfg = loaded.loadConfig?.().logging; } catch { @@ -113,17 +122,25 @@ function resolveConfigRedaction(): RedactOptions { } export function redactSensitiveText(text: string, options?: RedactOptions): string { - if (!text) return text; + if (!text) { + return text; + } const resolved = options ?? resolveConfigRedaction(); - if (normalizeMode(resolved.mode) === "off") return text; + if (normalizeMode(resolved.mode) === "off") { + return text; + } const patterns = resolvePatterns(resolved.patterns); - if (!patterns.length) return text; + if (!patterns.length) { + return text; + } return redactText(text, patterns); } export function redactToolDetail(detail: string): string { const resolved = resolveConfigRedaction(); - if (normalizeMode(resolved.mode) !== "tools") return detail; + if (normalizeMode(resolved.mode) !== "tools") { + return detail; + } return redactSensitiveText(detail, resolved); } diff --git a/src/logging/subsystem.ts b/src/logging/subsystem.ts index a156fd8f3..a1ec00abc 100644 --- a/src/logging/subsystem.ts +++ b/src/logging/subsystem.ts @@ -1,14 +1,13 @@ -import { Chalk } from "chalk"; import type { Logger as TsLogger } from "tslog"; - +import { Chalk } from "chalk"; import { CHAT_CHANNEL_ORDER } from "../channels/registry.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { getConsoleSettings, shouldLogSubsystemToConsole } from "./console.js"; import { isVerbose } from "../globals.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { clearActiveProgressLine } from "../terminal/progress-line.js"; +import { getConsoleSettings, shouldLogSubsystemToConsole } from "./console.js"; import { type LogLevel, levelToMinLevel } from "./levels.js"; import { getChildLogger } from "./logger.js"; import { loggingState } from "./state.js"; -import { clearActiveProgressLine } from "../terminal/progress-line.js"; type LogObj = { date?: Date } & Record; @@ -25,7 +24,9 @@ export type SubsystemLogger = { }; function shouldLogToConsole(level: LogLevel, settings: { level: LogLevel }): boolean { - if (settings.level === "silent") return false; + if (settings.level === "silent") { + return false; + } const current = levelToMinLevel(level); const min = levelToMinLevel(settings.level); return current <= min; @@ -35,7 +36,9 @@ type ChalkInstance = InstanceType; function isRichConsoleEnv(): boolean { const term = (process.env.TERM ?? "").toLowerCase(); - if (process.env.COLORTERM || process.env.TERM_PROGRAM) return true; + if (process.env.COLORTERM || process.env.TERM_PROGRAM) { + return true; + } return term.length > 0 && term !== "dumb"; } @@ -44,7 +47,9 @@ function getColorForConsole(): ChalkInstance { typeof process.env.FORCE_COLOR === "string" && process.env.FORCE_COLOR.trim().length > 0 && process.env.FORCE_COLOR.trim() !== "0"; - if (process.env.NO_COLOR && !hasForceColor) return new Chalk({ level: 0 }); + if (process.env.NO_COLOR && !hasForceColor) { + return new Chalk({ level: 0 }); + } const hasTty = Boolean(process.stdout.isTTY || process.stderr.isTTY); return hasTty || isRichConsoleEnv() ? new Chalk({ level: 1 }) : new Chalk({ level: 0 }); } @@ -59,7 +64,9 @@ const CHANNEL_SUBSYSTEM_PREFIXES = new Set(CHAT_CHANNEL_ORDER); function pickSubsystemColor(color: ChalkInstance, subsystem: string): ChalkInstance { const override = SUBSYSTEM_COLOR_OVERRIDES[subsystem]; - if (override) return color[override]; + if (override) { + return color[override]; + } let hash = 0; for (let i = 0; i < subsystem.length; i += 1) { hash = (hash * 31 + subsystem.charCodeAt(i)) | 0; @@ -78,7 +85,9 @@ function formatSubsystemForConsole(subsystem: string): string { ) { parts.shift(); } - if (parts.length === 0) return original; + if (parts.length === 0) { + return original; + } if (CHANNEL_SUBSYSTEM_PREFIXES.has(parts[0])) { return parts[0]; } @@ -92,7 +101,9 @@ export function stripRedundantSubsystemPrefixForConsole( message: string, displaySubsystem: string, ): string { - if (!displaySubsystem) return message; + if (!displaySubsystem) { + return message; + } // Common duplication: "[discord] discord: ..." (when a message manually includes the subsystem tag). if (message.startsWith("[")) { @@ -101,22 +112,34 @@ export function stripRedundantSubsystemPrefixForConsole( const bracketTag = message.slice(1, closeIdx); if (bracketTag.toLowerCase() === displaySubsystem.toLowerCase()) { let i = closeIdx + 1; - while (message[i] === " ") i += 1; + while (message[i] === " ") { + i += 1; + } return message.slice(i); } } } const prefix = message.slice(0, displaySubsystem.length); - if (prefix.toLowerCase() !== displaySubsystem.toLowerCase()) return message; + if (prefix.toLowerCase() !== displaySubsystem.toLowerCase()) { + return message; + } const next = message.slice(displaySubsystem.length, displaySubsystem.length + 1); - if (next !== ":" && next !== " ") return message; + if (next !== ":" && next !== " ") { + return message; + } let i = displaySubsystem.length; - while (message[i] === " ") i += 1; - if (message[i] === ":") i += 1; - while (message[i] === " ") i += 1; + while (message[i] === " ") { + i += 1; + } + if (message[i] === ":") { + i += 1; + } + while (message[i] === " ") { + i += 1; + } return message.slice(i); } @@ -186,12 +209,16 @@ function logToFile( message: string, meta?: Record, ) { - if (level === "silent") return; - const safeLevel = level as Exclude; - const method = (fileLogger as unknown as Record)[safeLevel] as unknown as + if (level === "silent") { + return; + } + const safeLevel = level; + const method = (fileLogger as unknown as Record)[safeLevel] as | ((...args: unknown[]) => void) | undefined; - if (typeof method !== "function") return; + if (typeof method !== "function") { + return; + } if (meta && Object.keys(meta).length > 0) { method.call(fileLogger, meta, message); } else { @@ -202,7 +229,9 @@ function logToFile( export function createSubsystemLogger(subsystem: string): SubsystemLogger { let fileLogger: TsLogger | null = null; const getFileLogger = () => { - if (!fileLogger) fileLogger = getChildLogger({ subsystem }); + if (!fileLogger) { + fileLogger = getChildLogger({ subsystem }); + } return fileLogger; }; const emit = (level: LogLevel, message: string, meta?: Record) => { @@ -219,8 +248,12 @@ export function createSubsystemLogger(subsystem: string): SubsystemLogger { fileMeta = Object.keys(rest).length > 0 ? rest : undefined; } logToFile(getFileLogger(), level, message, fileMeta); - if (!shouldLogToConsole(level, { level: consoleSettings.level })) return; - if (!shouldLogSubsystemToConsole(subsystem)) return; + if (!shouldLogToConsole(level, { level: consoleSettings.level })) { + return; + } + if (!shouldLogSubsystemToConsole(subsystem)) { + return; + } const consoleMessage = consoleMessageOverride ?? message; if ( !isVerbose() && diff --git a/src/macos/gateway-daemon.ts b/src/macos/gateway-daemon.ts index 686f705c2..eb02c0606 100644 --- a/src/macos/gateway-daemon.ts +++ b/src/macos/gateway-daemon.ts @@ -2,16 +2,18 @@ import process from "node:process"; import type { GatewayLockHandle } from "../infra/gateway-lock.js"; -declare const __CLAWDBOT_VERSION__: string; +declare const __OPENCLAW_VERSION__: string | undefined; const BUNDLED_VERSION = - (typeof __CLAWDBOT_VERSION__ === "string" && __CLAWDBOT_VERSION__) || - process.env.CLAWDBOT_BUNDLED_VERSION || + (typeof __OPENCLAW_VERSION__ === "string" && __OPENCLAW_VERSION__) || + process.env.OPENCLAW_BUNDLED_VERSION || "0.0.0"; function argValue(args: string[], flag: string): string | undefined { const idx = args.indexOf(flag); - if (idx < 0) return undefined; + if (idx < 0) { + return undefined; + } const value = args[idx + 1]; return value && !value.startsWith("-") ? value : undefined; } @@ -26,7 +28,7 @@ type GatewayWsLogStyle = "auto" | "full" | "compact"; async function main() { if (hasFlag(args, "--version") || hasFlag(args, "-v")) { - // Match `moltbot --version` behavior for Swift env/version checks. + // Match `openclaw --version` behavior for Swift env/version checks. // Keep output a single line. console.log(BUNDLED_VERSION); process.exit(0); @@ -65,9 +67,7 @@ async function main() { setConsoleTimestampPrefix(true); setVerbose(hasFlag(args, "--verbose")); - const wsLogRaw = (hasFlag(args, "--compact") ? "compact" : argValue(args, "--ws-log")) as - | string - | undefined; + const wsLogRaw = hasFlag(args, "--compact") ? "compact" : argValue(args, "--ws-log"); const wsLogStyle: GatewayWsLogStyle = wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto"; setGatewayWsLogStyle(wsLogStyle); @@ -75,6 +75,7 @@ async function main() { const cfg = loadConfig(); const portRaw = argValue(args, "--port") ?? + process.env.OPENCLAW_GATEWAY_PORT ?? process.env.CLAWDBOT_GATEWAY_PORT ?? (typeof cfg.gateway?.port === "number" ? String(cfg.gateway.port) : "") ?? "18789"; @@ -86,6 +87,7 @@ async function main() { const bindRaw = argValue(args, "--bind") ?? + process.env.OPENCLAW_GATEWAY_BIND ?? process.env.CLAWDBOT_GATEWAY_BIND ?? cfg.gateway?.bind ?? "loopback"; @@ -103,7 +105,9 @@ async function main() { } const token = argValue(args, "--token"); - if (token) process.env.CLAWDBOT_GATEWAY_TOKEN = token; + if (token) { + process.env.OPENCLAW_GATEWAY_TOKEN = token; + } let server: Awaited> | null = null; let lock: GatewayLockHandle | null = null; @@ -143,7 +147,9 @@ async function main() { } catch (err) { defaultRuntime.error(`gateway: shutdown error: ${String(err)}`); } finally { - if (forceExitTimer) clearTimeout(forceExitTimer); + if (forceExitTimer) { + clearTimeout(forceExitTimer); + } server = null; if (isRestart) { shuttingDown = false; @@ -211,7 +217,7 @@ async function main() { void main().catch((err) => { console.error( - "[moltbot] Gateway daemon failed:", + "[openclaw] Gateway daemon failed:", err instanceof Error ? (err.stack ?? err.message) : err, ); process.exit(1); diff --git a/src/macos/relay-smoke.test.ts b/src/macos/relay-smoke.test.ts index 9d546a053..bbd75c571 100644 --- a/src/macos/relay-smoke.test.ts +++ b/src/macos/relay-smoke.test.ts @@ -15,8 +15,8 @@ describe("parseRelaySmokeTest", () => { }); it("parses env var smoke mode only when no args", () => { - expect(parseRelaySmokeTest([], { CLAWDBOT_SMOKE_QR: "1" })).toBe("qr"); - expect(parseRelaySmokeTest(["send"], { CLAWDBOT_SMOKE_QR: "1" })).toBe(null); + expect(parseRelaySmokeTest([], { OPENCLAW_SMOKE_QR: "1" })).toBe("qr"); + expect(parseRelaySmokeTest(["send"], { OPENCLAW_SMOKE_QR: "1" })).toBe(null); }); it("rejects unknown smoke values", () => { diff --git a/src/macos/relay-smoke.ts b/src/macos/relay-smoke.ts index 7d228cbf2..3dac20158 100644 --- a/src/macos/relay-smoke.ts +++ b/src/macos/relay-smoke.ts @@ -7,15 +7,19 @@ export function parseRelaySmokeTest(args: string[], env: NodeJS.ProcessEnv): Rel if (!value || value.startsWith("-")) { throw new Error("Missing value for --smoke (expected: qr)"); } - if (value === "qr") return "qr"; + if (value === "qr") { + return "qr"; + } throw new Error(`Unknown smoke test: ${value}`); } - if (args.includes("--smoke-qr")) return "qr"; + if (args.includes("--smoke-qr")) { + return "qr"; + } // Back-compat: only run env-based smoke mode when no CLI args are present, // to avoid surprising early-exit when users set env vars globally. - if (args.length === 0 && (env.CLAWDBOT_SMOKE_QR === "1" || env.CLAWDBOT_SMOKE === "qr")) { + if (args.length === 0 && (env.OPENCLAW_SMOKE_QR === "1" || env.OPENCLAW_SMOKE === "qr")) { return "qr"; } diff --git a/src/macos/relay.ts b/src/macos/relay.ts index ec580760b..c39a4f02a 100644 --- a/src/macos/relay.ts +++ b/src/macos/relay.ts @@ -1,11 +1,11 @@ #!/usr/bin/env node import process from "node:process"; -declare const __CLAWDBOT_VERSION__: string | undefined; +declare const __OPENCLAW_VERSION__: string | undefined; const BUNDLED_VERSION = - (typeof __CLAWDBOT_VERSION__ === "string" && __CLAWDBOT_VERSION__) || - process.env.CLAWDBOT_BUNDLED_VERSION || + (typeof __OPENCLAW_VERSION__ === "string" && __OPENCLAW_VERSION__) || + process.env.OPENCLAW_BUNDLED_VERSION || "0.0.0"; function hasFlag(args: string[], flag: string): boolean { @@ -15,7 +15,9 @@ function hasFlag(args: string[], flag: string): boolean { async function patchBunLongForProtobuf(): Promise { // Bun ships a global `Long` that protobufjs detects, but it is not long.js and // misses critical APIs (fromBits, ...). Baileys WAProto expects long.js. - if (typeof process.versions.bun !== "string") return; + if (typeof process.versions.bun !== "string") { + return; + } const mod = await import("long"); const Long = (mod as unknown as { default?: unknown }).default ?? mod; (globalThis as unknown as { Long?: unknown }).Long = Long; @@ -47,8 +49,8 @@ async function main() { const { loadDotEnv } = await import("../infra/dotenv.js"); loadDotEnv({ quiet: true }); - const { ensureMoltbotCliOnPath } = await import("../infra/path-env.js"); - ensureMoltbotCliOnPath(); + const { ensureOpenClawCliOnPath } = await import("../infra/path-env.js"); + ensureOpenClawCliOnPath(); const { enableConsoleCapture } = await import("../logging.js"); enableConsoleCapture(); @@ -64,7 +66,7 @@ async function main() { installUnhandledRejectionHandler(); process.on("uncaughtException", (error) => { - console.error("[moltbot] Uncaught exception:", formatUncaughtError(error)); + console.error("[openclaw] Uncaught exception:", formatUncaughtError(error)); process.exit(1); }); @@ -72,6 +74,9 @@ async function main() { } void main().catch((err) => { - console.error("[moltbot] Relay failed:", err instanceof Error ? (err.stack ?? err.message) : err); + console.error( + "[openclaw] Relay failed:", + err instanceof Error ? (err.stack ?? err.message) : err, + ); process.exit(1); }); diff --git a/src/markdown/fences.ts b/src/markdown/fences.ts index 3a11bab3d..d3cbbced1 100644 --- a/src/markdown/fences.ts +++ b/src/markdown/fences.ts @@ -53,7 +53,9 @@ export function parseFenceSpans(buffer: string): FenceSpan[] { } } - if (nextNewline === -1) break; + if (nextNewline === -1) { + break; + } offset = nextNewline + 1; } diff --git a/src/markdown/frontmatter.test.ts b/src/markdown/frontmatter.test.ts index d7c5d1a50..dfc822c86 100644 --- a/src/markdown/frontmatter.test.ts +++ b/src/markdown/frontmatter.test.ts @@ -1,6 +1,5 @@ import JSON5 from "json5"; import { describe, expect, it } from "vitest"; - import { parseFrontmatterBlock } from "./frontmatter.js"; describe("parseFrontmatterBlock", () => { @@ -22,7 +21,7 @@ description: | name: session-memory metadata: { - "moltbot": + "openclaw": { "emoji": "disk", "events": ["command:new"], @@ -33,18 +32,18 @@ metadata: const result = parseFrontmatterBlock(content); expect(result.metadata).toBeDefined(); - const parsed = JSON5.parse(result.metadata ?? "") as { moltbot?: { emoji?: string } }; - expect(parsed.moltbot?.emoji).toBe("disk"); + const parsed = JSON5.parse(result.metadata ?? ""); + expect(parsed.openclaw?.emoji).toBe("disk"); }); it("preserves inline JSON values", () => { const content = `--- name: inline-json -metadata: {"moltbot": {"events": ["test"]}} +metadata: {"openclaw": {"events": ["test"]}} --- `; const result = parseFrontmatterBlock(content); - expect(result.metadata).toBe('{"moltbot": {"events": ["test"]}}'); + expect(result.metadata).toBe('{"openclaw": {"events": ["test"]}}'); }); it("stringifies YAML objects and arrays", () => { @@ -56,7 +55,7 @@ tags: - alpha - beta metadata: - moltbot: + openclaw: events: - command:new --- @@ -65,8 +64,8 @@ metadata: expect(result.enabled).toBe("true"); expect(result.retries).toBe("3"); expect(JSON.parse(result.tags ?? "[]")).toEqual(["alpha", "beta"]); - const parsed = JSON5.parse(result.metadata ?? "") as { moltbot?: { events?: string[] } }; - expect(parsed.moltbot?.events).toEqual(["command:new"]); + const parsed = JSON5.parse(result.metadata ?? ""); + expect(parsed.openclaw?.events).toEqual(["command:new"]); }); it("returns empty when frontmatter is missing", () => { diff --git a/src/markdown/frontmatter.ts b/src/markdown/frontmatter.ts index 1afde6c3d..0994a7687 100644 --- a/src/markdown/frontmatter.ts +++ b/src/markdown/frontmatter.ts @@ -13,9 +13,15 @@ function stripQuotes(value: string): string { } function coerceFrontmatterValue(value: unknown): string | undefined { - if (value === null || value === undefined) return undefined; - if (typeof value === "string") return value.trim(); - if (typeof value === "number" || typeof value === "boolean") return String(value); + if (value === null || value === undefined) { + return undefined; + } + if (typeof value === "string") { + return value.trim(); + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } if (typeof value === "object") { try { return JSON.stringify(value); @@ -29,13 +35,19 @@ function coerceFrontmatterValue(value: unknown): string | undefined { function parseYamlFrontmatter(block: string): ParsedFrontmatter | null { try { const parsed = YAML.parse(block) as unknown; - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return null; + } const result: ParsedFrontmatter = {}; for (const [rawKey, value] of Object.entries(parsed as Record)) { const key = rawKey.trim(); - if (!key) continue; + if (!key) { + continue; + } const coerced = coerceFrontmatterValue(value); - if (coerced === undefined) continue; + if (coerced === undefined) { + continue; + } result[key] = coerced; } return result; @@ -50,7 +62,9 @@ function extractMultiLineValue( ): { value: string; linesConsumed: number } { const startLine = lines[startIndex]; const match = startLine.match(/^([\w-]+):\s*(.*)$/); - if (!match) return { value: "", linesConsumed: 1 }; + if (!match) { + return { value: "", linesConsumed: 1 }; + } const inlineValue = match[2].trim(); if (inlineValue) { @@ -118,14 +132,20 @@ function parseLineFrontmatter(block: string): ParsedFrontmatter { export function parseFrontmatterBlock(content: string): ParsedFrontmatter { const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - if (!normalized.startsWith("---")) return {}; + if (!normalized.startsWith("---")) { + return {}; + } const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) return {}; + if (endIndex === -1) { + return {}; + } const block = normalized.slice(4, endIndex); const lineParsed = parseLineFrontmatter(block); const yamlParsed = parseYamlFrontmatter(block); - if (yamlParsed === null) return lineParsed; + if (yamlParsed === null) { + return lineParsed; + } const merged: ParsedFrontmatter = { ...yamlParsed }; for (const [key, value] of Object.entries(lineParsed)) { diff --git a/src/markdown/ir.ts b/src/markdown/ir.ts index 186abeda0..2fd3a5a0c 100644 --- a/src/markdown/ir.ts +++ b/src/markdown/ir.ts @@ -1,7 +1,6 @@ import MarkdownIt from "markdown-it"; - -import { chunkText } from "../auto-reply/chunk.js"; import type { MarkdownTableMode } from "../config/types.base.js"; +import { chunkText } from "../auto-reply/chunk.js"; type ListState = { type: "bullet" | "ordered"; @@ -112,10 +111,14 @@ function createMarkdownIt(options: MarkdownParseOptions): MarkdownIt { } function getAttr(token: MarkdownToken, name: string): string | null { - if (token.attrGet) return token.attrGet(name); + if (token.attrGet) { + return token.attrGet(name); + } if (token.attrs) { for (const [key, value] of token.attrs) { - if (key === name) return value; + if (key === name) { + return value; + } } } return null; @@ -187,7 +190,9 @@ function resolveRenderTarget(state: RenderState): RenderTarget { } function appendText(state: RenderState, value: string) { - if (!value) return; + if (!value) { + return; + } const target = resolveRenderTarget(state); target.text += value; } @@ -213,15 +218,21 @@ function closeStyle(state: RenderState, style: MarkdownStyle) { } function appendParagraphSeparator(state: RenderState) { - if (state.env.listStack.length > 0) return; - if (state.table) return; // Don't add paragraph separators inside tables + if (state.env.listStack.length > 0) { + return; + } + if (state.table) { + return; + } // Don't add paragraph separators inside tables state.text += "\n\n"; } function appendListPrefix(state: RenderState) { const stack = state.env.listStack; const top = stack[stack.length - 1]; - if (!top) return; + if (!top) { + return; + } top.index += 1; const indent = " ".repeat(Math.max(0, stack.length - 1)); const prefix = top.type === "ordered" ? `${top.index}. ` : "• "; @@ -229,7 +240,9 @@ function appendListPrefix(state: RenderState) { } function renderInlineCode(state: RenderState, content: string) { - if (!content) return; + if (!content) { + return; + } const target = resolveRenderTarget(state); const start = target.text.length; target.text += content; @@ -238,7 +251,9 @@ function renderInlineCode(state: RenderState, content: string) { function renderCodeBlock(state: RenderState, content: string) { let code = content ?? ""; - if (!code.endsWith("\n")) code = `${code}\n`; + if (!code.endsWith("\n")) { + code = `${code}\n`; + } const target = resolveRenderTarget(state); const start = target.text.length; target.text += code; @@ -251,9 +266,13 @@ function renderCodeBlock(state: RenderState, content: string) { function handleLinkClose(state: RenderState) { const target = resolveRenderTarget(state); const link = target.linkStack.pop(); - if (!link?.href) return; + if (!link?.href) { + return; + } const href = link.href.trim(); - if (!href) return; + if (!href) { + return; + } const start = link.labelStart; const end = target.text.length; if (end <= start) { @@ -286,9 +305,15 @@ function trimCell(cell: TableCell): TableCell { const text = cell.text; let start = 0; let end = text.length; - while (start < end && /\s/.test(text[start] ?? "")) start += 1; - while (end > start && /\s/.test(text[end - 1] ?? "")) end -= 1; - if (start === 0 && end === text.length) return cell; + while (start < end && /\s/.test(text[start] ?? "")) { + start += 1; + } + while (end > start && /\s/.test(text[end - 1] ?? "")) { + end -= 1; + } + if (start === 0 && end === text.length) { + return cell; + } const trimmedText = text.slice(start, end); const trimmedLength = trimmedText.length; const trimmedStyles: MarkdownStyleSpan[] = []; @@ -311,7 +336,9 @@ function trimCell(cell: TableCell): TableCell { } function appendCell(state: RenderState, cell: TableCell) { - if (!cell.text) return; + if (!cell.text) { + return; + } const start = state.text.length; state.text += cell.text; for (const span of cell.styles) { @@ -331,12 +358,16 @@ function appendCell(state: RenderState, cell: TableCell) { } function renderTableAsBullets(state: RenderState) { - if (!state.table) return; + if (!state.table) { + return; + } const headers = state.table.headers.map(trimCell); const rows = state.table.rows.map((row) => row.map(trimCell)); // If no headers or rows, skip - if (headers.length === 0 && rows.length === 0) return; + if (headers.length === 0 && rows.length === 0) { + return; + } // Determine if first column should be used as row labels // (common pattern: first column is category/feature name) @@ -345,7 +376,9 @@ function renderTableAsBullets(state: RenderState) { if (useFirstColAsLabel) { // Format: each row becomes a section with header as row[0], then key:value pairs for (const row of rows) { - if (row.length === 0) continue; + if (row.length === 0) { + continue; + } const rowLabel = row[0]; if (rowLabel?.text) { @@ -362,7 +395,9 @@ function renderTableAsBullets(state: RenderState) { for (let i = 1; i < row.length; i++) { const header = headers[i]; const value = row[i]; - if (!value?.text) continue; + if (!value?.text) { + continue; + } state.text += "• "; if (header?.text) { appendCell(state, header); @@ -381,7 +416,9 @@ function renderTableAsBullets(state: RenderState) { for (let i = 0; i < row.length; i++) { const header = headers[i]; const value = row[i]; - if (!value?.text) continue; + if (!value?.text) { + continue; + } state.text += "• "; if (header?.text) { appendCell(state, header); @@ -396,23 +433,31 @@ function renderTableAsBullets(state: RenderState) { } function renderTableAsCode(state: RenderState) { - if (!state.table) return; + if (!state.table) { + return; + } const headers = state.table.headers.map(trimCell); const rows = state.table.rows.map((row) => row.map(trimCell)); const columnCount = Math.max(headers.length, ...rows.map((row) => row.length)); - if (columnCount === 0) return; + if (columnCount === 0) { + return; + } const widths = Array.from({ length: columnCount }, () => 0); const updateWidths = (cells: TableCell[]) => { for (let i = 0; i < columnCount; i += 1) { const cell = cells[i]; const width = cell?.text.length ?? 0; - if (widths[i] < width) widths[i] = width; + if (widths[i] < width) { + widths[i] = width; + } } }; updateWidths(headers); - for (const row of rows) updateWidths(row); + for (const row of rows) { + updateWidths(row); + } const codeStart = state.text.length; @@ -421,9 +466,13 @@ function renderTableAsCode(state: RenderState) { for (let i = 0; i < columnCount; i += 1) { state.text += " "; const cell = cells[i]; - if (cell) appendCell(state, cell); + if (cell) { + appendCell(state, cell); + } const pad = widths[i] - (cell?.text.length ?? 0); - if (pad > 0) state.text += " ".repeat(pad); + if (pad > 0) { + state.text += " ".repeat(pad); + } state.text += " |"; } state.text += "\n"; @@ -457,7 +506,9 @@ function renderTokens(tokens: MarkdownToken[], state: RenderState): void { for (const token of tokens) { switch (token.type) { case "inline": - if (token.children) renderTokens(token.children, state); + if (token.children) { + renderTokens(token.children, state); + } break; case "text": appendText(state, token.content ?? ""); @@ -484,10 +535,14 @@ function renderTokens(tokens: MarkdownToken[], state: RenderState): void { renderInlineCode(state, token.content ?? ""); break; case "spoiler_open": - if (state.enableSpoilers) openStyle(state, "spoiler"); + if (state.enableSpoilers) { + openStyle(state, "spoiler"); + } break; case "spoiler_close": - if (state.enableSpoilers) closeStyle(state, "spoiler"); + if (state.enableSpoilers) { + closeStyle(state, "spoiler"); + } break; case "link_open": { const href = getAttr(token, "href") ?? ""; @@ -509,14 +564,20 @@ function renderTokens(tokens: MarkdownToken[], state: RenderState): void { appendParagraphSeparator(state); break; case "heading_open": - if (state.headingStyle === "bold") openStyle(state, "bold"); + if (state.headingStyle === "bold") { + openStyle(state, "bold"); + } break; case "heading_close": - if (state.headingStyle === "bold") closeStyle(state, "bold"); + if (state.headingStyle === "bold") { + closeStyle(state, "bold"); + } appendParagraphSeparator(state); break; case "blockquote_open": - if (state.blockquotePrefix) state.text += state.blockquotePrefix; + if (state.blockquotePrefix) { + state.text += state.blockquotePrefix; + } break; case "blockquote_close": state.text += "\n"; @@ -613,7 +674,9 @@ function renderTokens(tokens: MarkdownToken[], state: RenderState): void { state.text += "\n"; break; default: - if (token.children) renderTokens(token.children, state); + if (token.children) { + renderTokens(token.children, state); + } break; } } @@ -639,7 +702,9 @@ function clampStyleSpans(spans: MarkdownStyleSpan[], maxLength: number): Markdow for (const span of spans) { const start = Math.max(0, Math.min(span.start, maxLength)); const end = Math.max(start, Math.min(span.end, maxLength)); - if (end > start) clamped.push({ start, end, style: span.style }); + if (end > start) { + clamped.push({ start, end, style: span.style }); + } } return clamped; } @@ -649,15 +714,21 @@ function clampLinkSpans(spans: MarkdownLinkSpan[], maxLength: number): MarkdownL for (const span of spans) { const start = Math.max(0, Math.min(span.start, maxLength)); const end = Math.max(start, Math.min(span.end, maxLength)); - if (end > start) clamped.push({ start, end, href: span.href }); + if (end > start) { + clamped.push({ start, end, href: span.href }); + } } return clamped; } function mergeStyleSpans(spans: MarkdownStyleSpan[]): MarkdownStyleSpan[] { - const sorted = [...spans].sort((a, b) => { - if (a.start !== b.start) return a.start - b.start; - if (a.end !== b.end) return a.end - b.end; + const sorted = [...spans].toSorted((a, b) => { + if (a.start !== b.start) { + return a.start - b.start; + } + if (a.end !== b.end) { + return a.end - b.end; + } return a.style.localeCompare(b.style); }); @@ -678,7 +749,9 @@ function sliceStyleSpans( start: number, end: number, ): MarkdownStyleSpan[] { - if (spans.length === 0) return []; + if (spans.length === 0) { + return []; + } const sliced: MarkdownStyleSpan[] = []; for (const span of spans) { const sliceStart = Math.max(span.start, start); @@ -695,7 +768,9 @@ function sliceStyleSpans( } function sliceLinkSpans(spans: MarkdownLinkSpan[], start: number, end: number): MarkdownLinkSpan[] { - if (spans.length === 0) return []; + if (spans.length === 0) { + return []; + } const sliced: MarkdownLinkSpan[] = []; for (const span of spans) { const sliceStart = Math.max(span.start, start); @@ -750,8 +825,12 @@ export function markdownToIRWithMeta( const trimmedLength = trimmedText.length; let codeBlockEnd = 0; for (const span of state.styles) { - if (span.style !== "code_block") continue; - if (span.end > codeBlockEnd) codeBlockEnd = span.end; + if (span.style !== "code_block") { + continue; + } + if (span.end > codeBlockEnd) { + codeBlockEnd = span.end; + } } const finalLength = Math.max(trimmedLength, codeBlockEnd); const finalText = @@ -768,15 +847,21 @@ export function markdownToIRWithMeta( } export function chunkMarkdownIR(ir: MarkdownIR, limit: number): MarkdownIR[] { - if (!ir.text) return []; - if (limit <= 0 || ir.text.length <= limit) return [ir]; + if (!ir.text) { + return []; + } + if (limit <= 0 || ir.text.length <= limit) { + return [ir]; + } const chunks = chunkText(ir.text, limit); const results: MarkdownIR[] = []; let cursor = 0; chunks.forEach((chunk, index) => { - if (!chunk) return; + if (!chunk) { + return; + } if (index > 0) { while (cursor < ir.text.length && /\s/.test(ir.text[cursor] ?? "")) { cursor += 1; diff --git a/src/markdown/render.ts b/src/markdown/render.ts index 502ab69ef..fb55ee847 100644 --- a/src/markdown/render.ts +++ b/src/markdown/render.ts @@ -34,16 +34,22 @@ const STYLE_RANK = new Map( ); function sortStyleSpans(spans: MarkdownStyleSpan[]): MarkdownStyleSpan[] { - return [...spans].sort((a, b) => { - if (a.start !== b.start) return a.start - b.start; - if (a.end !== b.end) return b.end - a.end; + return [...spans].toSorted((a, b) => { + if (a.start !== b.start) { + return a.start - b.start; + } + if (a.end !== b.end) { + return b.end - a.end; + } return (STYLE_RANK.get(a.style) ?? 0) - (STYLE_RANK.get(b.style) ?? 0); }); } export function renderMarkdownWithMarkers(ir: MarkdownIR, options: RenderOptions): string { const text = ir.text ?? ""; - if (!text) return ""; + if (!text) { + return ""; + } const styleMarkers = options.styleMarkers; const styled = sortStyleSpans(ir.styles.filter((span) => Boolean(styleMarkers[span.style]))); @@ -54,78 +60,132 @@ export function renderMarkdownWithMarkers(ir: MarkdownIR, options: RenderOptions const startsAt = new Map(); for (const span of styled) { - if (span.start === span.end) continue; + if (span.start === span.end) { + continue; + } boundaries.add(span.start); boundaries.add(span.end); const bucket = startsAt.get(span.start); - if (bucket) bucket.push(span); - else startsAt.set(span.start, [span]); + if (bucket) { + bucket.push(span); + } else { + startsAt.set(span.start, [span]); + } } for (const spans of startsAt.values()) { spans.sort((a, b) => { - if (a.end !== b.end) return b.end - a.end; + if (a.end !== b.end) { + return b.end - a.end; + } return (STYLE_RANK.get(a.style) ?? 0) - (STYLE_RANK.get(b.style) ?? 0); }); } const linkStarts = new Map(); - const linkEnds = new Map(); if (options.buildLink) { for (const link of ir.links) { - if (link.start === link.end) continue; + if (link.start === link.end) { + continue; + } const rendered = options.buildLink(link, text); - if (!rendered) continue; + if (!rendered) { + continue; + } boundaries.add(rendered.start); boundaries.add(rendered.end); const openBucket = linkStarts.get(rendered.start); - if (openBucket) openBucket.push(rendered); - else linkStarts.set(rendered.start, [rendered]); - const closeBucket = linkEnds.get(rendered.end); - if (closeBucket) closeBucket.push(rendered); - else linkEnds.set(rendered.end, [rendered]); + if (openBucket) { + openBucket.push(rendered); + } else { + linkStarts.set(rendered.start, [rendered]); + } } } - const points = [...boundaries].sort((a, b) => a - b); - const stack: MarkdownStyleSpan[] = []; + const points = [...boundaries].toSorted((a, b) => a - b); + // Unified stack for both styles and links, tracking close string and end position + const stack: { close: string; end: number }[] = []; + type OpeningItem = + | { end: number; open: string; close: string; kind: "link"; index: number } + | { + end: number; + open: string; + close: string; + kind: "style"; + style: MarkdownStyle; + index: number; + }; let out = ""; for (let i = 0; i < points.length; i += 1) { const pos = points[i]; + // Close ALL elements (styles and links) in LIFO order at this position while (stack.length && stack[stack.length - 1]?.end === pos) { - const span = stack.pop(); - if (!span) break; - const marker = styleMarkers[span.style]; - if (marker) out += marker.close; - } - - const closingLinks = linkEnds.get(pos); - if (closingLinks && closingLinks.length > 0) { - for (const link of closingLinks) { - out += link.close; + const item = stack.pop(); + if (item) { + out += item.close; } } + const openingItems: OpeningItem[] = []; + const openingLinks = linkStarts.get(pos); if (openingLinks && openingLinks.length > 0) { - for (const link of openingLinks) { - out += link.open; + for (const [index, link] of openingLinks.entries()) { + openingItems.push({ + end: link.end, + open: link.open, + close: link.close, + kind: "link", + index, + }); } } const openingStyles = startsAt.get(pos); if (openingStyles) { - for (const span of openingStyles) { + for (const [index, span] of openingStyles.entries()) { const marker = styleMarkers[span.style]; - if (!marker) continue; - stack.push(span); - out += marker.open; + if (!marker) { + continue; + } + openingItems.push({ + end: span.end, + open: marker.open, + close: marker.close, + kind: "style", + style: span.style, + index, + }); + } + } + + if (openingItems.length > 0) { + openingItems.sort((a, b) => { + if (a.end !== b.end) { + return b.end - a.end; + } + if (a.kind !== b.kind) { + return a.kind === "link" ? -1 : 1; + } + if (a.kind === "style" && b.kind === "style") { + return (STYLE_RANK.get(a.style) ?? 0) - (STYLE_RANK.get(b.style) ?? 0); + } + return a.index - b.index; + }); + + // Open outer spans first (larger end) so LIFO closes stay valid for same-start overlaps. + for (const item of openingItems) { + out += item.open; + stack.push({ close: item.close, end: item.end }); } } const next = points[i + 1]; - if (next === undefined) break; + if (next === undefined) { + break; + } if (next > pos) { out += options.escapeText(text.slice(pos, next)); } diff --git a/src/markdown/tables.ts b/src/markdown/tables.ts index 9ae2b750e..ac83b05c5 100644 --- a/src/markdown/tables.ts +++ b/src/markdown/tables.ts @@ -11,7 +11,9 @@ const MARKDOWN_STYLE_MARKERS = { } as const; export function convertMarkdownTables(markdown: string, mode: MarkdownTableMode): string { - if (!markdown || mode === "off") return markdown; + if (!markdown || mode === "off") { + return markdown; + } const { ir, hasTables } = markdownToIRWithMeta(markdown, { linkify: false, autolink: false, @@ -19,15 +21,21 @@ export function convertMarkdownTables(markdown: string, mode: MarkdownTableMode) blockquotePrefix: "", tableMode: mode, }); - if (!hasTables) return markdown; + if (!hasTables) { + return markdown; + } return renderMarkdownWithMarkers(ir, { styleMarkers: MARKDOWN_STYLE_MARKERS, escapeText: (text) => text, buildLink: (link, text) => { const href = link.href.trim(); - if (!href) return null; + if (!href) { + return null; + } const label = text.slice(link.start, link.end); - if (!label) return null; + if (!label) { + return null; + } return { start: link.start, end: link.end, open: "[", close: `](${href})` }; }, }); diff --git a/src/media-understanding/apply.test.ts b/src/media-understanding/apply.test.ts index 8bebb8e20..238293d5e 100644 --- a/src/media-understanding/apply.test.ts +++ b/src/media-understanding/apply.test.ts @@ -1,11 +1,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { beforeEach, describe, expect, it, vi } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; import type { MsgContext } from "../auto-reply/templating.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveApiKeyForProvider } from "../agents/model-auth.js"; import { fetchRemoteMedia } from "../media/fetch.js"; @@ -16,7 +14,9 @@ vi.mock("../agents/model-auth.js", () => ({ mode: "api-key", })), requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => { - if (auth?.apiKey) return auth.apiKey; + if (auth?.apiKey) { + return auth.apiKey; + } throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`); }, })); @@ -41,7 +41,7 @@ describe("applyMediaUnderstanding", () => { mockedResolveApiKey.mockClear(); mockedFetchRemoteMedia.mockReset(); mockedFetchRemoteMedia.mockResolvedValue({ - buffer: Buffer.from("audio-bytes"), + buffer: Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), contentType: "audio/ogg", fileName: "note.ogg", }); @@ -49,16 +49,16 @@ describe("applyMediaUnderstanding", () => { it("sets Transcript and replaces Body when audio transcription succeeds", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const audioPath = path.join(dir, "note.ogg"); - await fs.writeFile(audioPath, "hello"); + await fs.writeFile(audioPath, Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8])); const ctx: MsgContext = { Body: "", MediaPath: audioPath, MediaType: "audio/ogg", }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { @@ -92,16 +92,16 @@ describe("applyMediaUnderstanding", () => { it("keeps caption for command parsing when audio has user text", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const audioPath = path.join(dir, "note.ogg"); - await fs.writeFile(audioPath, "hello"); + await fs.writeFile(audioPath, Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8])); const ctx: MsgContext = { Body: " /capture status", MediaPath: audioPath, MediaType: "audio/ogg", }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { @@ -140,7 +140,7 @@ describe("applyMediaUnderstanding", () => { MediaType: "audio/ogg", ChatType: "dm", }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { @@ -174,9 +174,9 @@ describe("applyMediaUnderstanding", () => { it("skips audio transcription when attachment exceeds maxBytes", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const audioPath = path.join(dir, "large.wav"); - await fs.writeFile(audioPath, "0123456789"); + await fs.writeFile(audioPath, Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); const ctx: MsgContext = { Body: "", @@ -184,7 +184,7 @@ describe("applyMediaUnderstanding", () => { MediaType: "audio/wav", }; const transcribeAudio = vi.fn(async () => ({ text: "should-not-run" })); - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { @@ -209,16 +209,16 @@ describe("applyMediaUnderstanding", () => { it("falls back to CLI model when provider fails", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const audioPath = path.join(dir, "note.ogg"); - await fs.writeFile(audioPath, "hello"); + await fs.writeFile(audioPath, Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8])); const ctx: MsgContext = { Body: "", MediaPath: audioPath, MediaType: "audio/ogg", }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { @@ -262,7 +262,7 @@ describe("applyMediaUnderstanding", () => { it("uses CLI image understanding and preserves caption for commands", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const imagePath = path.join(dir, "photo.jpg"); await fs.writeFile(imagePath, "image-bytes"); @@ -271,7 +271,7 @@ describe("applyMediaUnderstanding", () => { MediaPath: imagePath, MediaType: "image/jpeg", }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { image: { @@ -309,7 +309,7 @@ describe("applyMediaUnderstanding", () => { it("uses shared media models list when capability config is missing", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const imagePath = path.join(dir, "shared.jpg"); await fs.writeFile(imagePath, "image-bytes"); @@ -318,7 +318,7 @@ describe("applyMediaUnderstanding", () => { MediaPath: imagePath, MediaType: "image/jpeg", }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { models: [ @@ -350,16 +350,16 @@ describe("applyMediaUnderstanding", () => { it("uses active model when enabled and models are missing", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const audioPath = path.join(dir, "fallback.ogg"); - await fs.writeFile(audioPath, "hello"); + await fs.writeFile(audioPath, Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6])); const ctx: MsgContext = { Body: "", MediaPath: audioPath, MediaType: "audio/ogg", }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { @@ -387,18 +387,18 @@ describe("applyMediaUnderstanding", () => { it("handles multiple audio attachments when attachment mode is all", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const audioPathA = path.join(dir, "note-a.ogg"); const audioPathB = path.join(dir, "note-b.ogg"); - await fs.writeFile(audioPathA, "hello"); - await fs.writeFile(audioPathB, "world"); + await fs.writeFile(audioPathA, Buffer.from([200, 201, 202, 203, 204, 205, 206, 207, 208])); + await fs.writeFile(audioPathB, Buffer.from([200, 201, 202, 203, 204, 205, 206, 207, 208])); const ctx: MsgContext = { Body: "", MediaPaths: [audioPathA, audioPathB], MediaTypes: ["audio/ogg", "audio/ogg"], }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { @@ -430,12 +430,12 @@ describe("applyMediaUnderstanding", () => { it("orders mixed media outputs as image, audio, video", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); const imagePath = path.join(dir, "photo.jpg"); const audioPath = path.join(dir, "note.ogg"); const videoPath = path.join(dir, "clip.mp4"); await fs.writeFile(imagePath, "image-bytes"); - await fs.writeFile(audioPath, "audio-bytes"); + await fs.writeFile(audioPath, Buffer.from([200, 201, 202, 203, 204, 205, 206, 207, 208])); await fs.writeFile(videoPath, "video-bytes"); const ctx: MsgContext = { @@ -443,7 +443,7 @@ describe("applyMediaUnderstanding", () => { MediaPaths: [imagePath, audioPath, videoPath], MediaTypes: ["image/jpeg", "audio/ogg", "video/mp4"], }; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { image: { enabled: true, models: [{ provider: "openai", model: "gpt-5.2" }] }, @@ -487,4 +487,187 @@ describe("applyMediaUnderstanding", () => { expect(ctx.CommandBody).toBe("audio ok"); expect(ctx.BodyForCommands).toBe("audio ok"); }); + + it("treats text-like audio attachments as CSV (comma wins over tabs)", async () => { + const { applyMediaUnderstanding } = await loadApply(); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); + const csvPath = path.join(dir, "data.mp3"); + const csvText = '"a","b"\t"c"\n"1","2"\t"3"'; + const csvBuffer = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(csvText, "utf16le")]); + await fs.writeFile(csvPath, csvBuffer); + + const ctx: MsgContext = { + Body: "", + MediaPath: csvPath, + MediaType: "audio/mpeg", + }; + const cfg: OpenClawConfig = { + tools: { + media: { + audio: { enabled: false }, + image: { enabled: false }, + video: { enabled: false }, + }, + }, + }; + + const result = await applyMediaUnderstanding({ ctx, cfg }); + + expect(result.appliedFile).toBe(true); + expect(ctx.Body).toContain(''); + expect(ctx.Body).toContain('"a","b"\t"c"'); + }); + + it("infers TSV when tabs are present without commas", async () => { + const { applyMediaUnderstanding } = await loadApply(); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); + const tsvPath = path.join(dir, "report.mp3"); + const tsvText = "a\tb\tc\n1\t2\t3"; + await fs.writeFile(tsvPath, tsvText); + + const ctx: MsgContext = { + Body: "", + MediaPath: tsvPath, + MediaType: "audio/mpeg", + }; + const cfg: OpenClawConfig = { + tools: { + media: { + audio: { enabled: false }, + image: { enabled: false }, + video: { enabled: false }, + }, + }, + }; + + const result = await applyMediaUnderstanding({ ctx, cfg }); + + expect(result.appliedFile).toBe(true); + expect(ctx.Body).toContain(''); + expect(ctx.Body).toContain("a\tb\tc"); + }); + + it("escapes XML special characters in filenames to prevent injection", async () => { + const { applyMediaUnderstanding } = await loadApply(); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); + // Use & in filename — valid on all platforms (including Windows, which + // forbids < and > in NTFS filenames) and still requires XML escaping. + // Note: The sanitizeFilename in store.ts would strip most dangerous chars, + // but we test that even if some slip through, they get escaped in output + const filePath = path.join(dir, "file&test.txt"); + await fs.writeFile(filePath, "safe content"); + + const ctx: MsgContext = { + Body: "", + MediaPath: filePath, + MediaType: "text/plain", + }; + const cfg: OpenClawConfig = { + tools: { + media: { + audio: { enabled: false }, + image: { enabled: false }, + video: { enabled: false }, + }, + }, + }; + + const result = await applyMediaUnderstanding({ ctx, cfg }); + + expect(result.appliedFile).toBe(true); + // Verify XML special chars are escaped in the output + expect(ctx.Body).toContain("&"); + // The name attribute should contain the escaped form, not a raw unescaped & + expect(ctx.Body).toMatch(/name="file&test\.txt"/); + }); + + it("normalizes MIME types to prevent attribute injection", async () => { + const { applyMediaUnderstanding } = await loadApply(); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); + const filePath = path.join(dir, "data.txt"); + await fs.writeFile(filePath, "test content"); + + const ctx: MsgContext = { + Body: "", + MediaPath: filePath, + // Attempt to inject via MIME type with quotes - normalization should strip this + MediaType: 'text/plain" onclick="alert(1)', + }; + const cfg: OpenClawConfig = { + tools: { + media: { + audio: { enabled: false }, + image: { enabled: false }, + video: { enabled: false }, + }, + }, + }; + + const result = await applyMediaUnderstanding({ ctx, cfg }); + + expect(result.appliedFile).toBe(true); + // MIME normalization strips everything after first ; or " - verify injection is blocked + expect(ctx.Body).not.toContain("onclick="); + expect(ctx.Body).not.toContain("alert(1)"); + // Verify the MIME type is normalized to just "text/plain" + expect(ctx.Body).toContain('mime="text/plain"'); + }); + + it("handles path traversal attempts in filenames safely", async () => { + const { applyMediaUnderstanding } = await loadApply(); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); + // Even if a file somehow got a path-like name, it should be handled safely + const filePath = path.join(dir, "normal.txt"); + await fs.writeFile(filePath, "legitimate content"); + + const ctx: MsgContext = { + Body: "", + MediaPath: filePath, + MediaType: "text/plain", + }; + const cfg: OpenClawConfig = { + tools: { + media: { + audio: { enabled: false }, + image: { enabled: false }, + video: { enabled: false }, + }, + }, + }; + + const result = await applyMediaUnderstanding({ ctx, cfg }); + + expect(result.appliedFile).toBe(true); + // Verify the file was processed and output contains expected structure + expect(ctx.Body).toContain(' { + const { applyMediaUnderstanding } = await loadApply(); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-")); + const filePath = path.join(dir, "文档.txt"); + await fs.writeFile(filePath, "中文内容"); + + const ctx: MsgContext = { + Body: "", + MediaPath: filePath, + MediaType: "text/plain", + }; + const cfg: OpenClawConfig = { + tools: { + media: { + audio: { enabled: false }, + image: { enabled: false }, + video: { enabled: false }, + }, + }, + }; + + const result = await applyMediaUnderstanding({ ctx, cfg }); + + expect(result.appliedFile).toBe(true); + expect(ctx.Body).toContain("中文内容"); + }); }); diff --git a/src/media-understanding/apply.ts b/src/media-understanding/apply.ts index dab640789..000439a0d 100644 --- a/src/media-understanding/apply.ts +++ b/src/media-understanding/apply.ts @@ -1,18 +1,34 @@ -import type { MoltbotConfig } from "../config/config.js"; +import path from "node:path"; import type { MsgContext } from "../auto-reply/templating.js"; -import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; -import { - extractMediaUserText, - formatAudioTranscripts, - formatMediaUnderstandingBody, -} from "./format.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { MediaUnderstandingCapability, MediaUnderstandingDecision, MediaUnderstandingOutput, MediaUnderstandingProvider, } from "./types.js"; +import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; +import { logVerbose, shouldLogVerbose } from "../globals.js"; +import { + DEFAULT_INPUT_FILE_MAX_BYTES, + DEFAULT_INPUT_FILE_MAX_CHARS, + DEFAULT_INPUT_FILE_MIMES, + DEFAULT_INPUT_MAX_REDIRECTS, + DEFAULT_INPUT_PDF_MAX_PAGES, + DEFAULT_INPUT_PDF_MAX_PIXELS, + DEFAULT_INPUT_PDF_MIN_TEXT_CHARS, + DEFAULT_INPUT_TIMEOUT_MS, + extractFileContentFromSource, + normalizeMimeList, + normalizeMimeType, +} from "../media/input-files.js"; +import { resolveAttachmentKind } from "./attachments.js"; import { runWithConcurrency } from "./concurrency.js"; +import { + extractMediaUserText, + formatAudioTranscripts, + formatMediaUnderstandingBody, +} from "./format.js"; import { resolveConcurrency } from "./resolve.js"; import { type ActiveMediaModel, @@ -28,13 +44,297 @@ export type ApplyMediaUnderstandingResult = { appliedImage: boolean; appliedAudio: boolean; appliedVideo: boolean; + appliedFile: boolean; }; const CAPABILITY_ORDER: MediaUnderstandingCapability[] = ["image", "audio", "video"]; +const EXTRA_TEXT_MIMES = [ + "application/xml", + "text/xml", + "application/x-yaml", + "text/yaml", + "application/yaml", + "application/javascript", + "text/javascript", + "text/tab-separated-values", +]; +const TEXT_EXT_MIME = new Map([ + [".csv", "text/csv"], + [".tsv", "text/tab-separated-values"], + [".txt", "text/plain"], + [".md", "text/markdown"], + [".log", "text/plain"], + [".ini", "text/plain"], + [".cfg", "text/plain"], + [".conf", "text/plain"], + [".env", "text/plain"], + [".json", "application/json"], + [".yaml", "text/yaml"], + [".yml", "text/yaml"], + [".xml", "application/xml"], +]); + +const XML_ESCAPE_MAP: Record = { + "<": "<", + ">": ">", + "&": "&", + '"': """, + "'": "'", +}; + +/** + * Escapes special XML characters in attribute values to prevent injection. + */ +function xmlEscapeAttr(value: string): string { + return value.replace(/[<>&"']/g, (char) => XML_ESCAPE_MAP[char] ?? char); +} + +function resolveFileLimits(cfg: OpenClawConfig) { + const files = cfg.gateway?.http?.endpoints?.responses?.files; + return { + allowUrl: files?.allowUrl ?? true, + allowedMimes: normalizeMimeList(files?.allowedMimes, DEFAULT_INPUT_FILE_MIMES), + maxBytes: files?.maxBytes ?? DEFAULT_INPUT_FILE_MAX_BYTES, + maxChars: files?.maxChars ?? DEFAULT_INPUT_FILE_MAX_CHARS, + maxRedirects: files?.maxRedirects ?? DEFAULT_INPUT_MAX_REDIRECTS, + timeoutMs: files?.timeoutMs ?? DEFAULT_INPUT_TIMEOUT_MS, + pdf: { + maxPages: files?.pdf?.maxPages ?? DEFAULT_INPUT_PDF_MAX_PAGES, + maxPixels: files?.pdf?.maxPixels ?? DEFAULT_INPUT_PDF_MAX_PIXELS, + minTextChars: files?.pdf?.minTextChars ?? DEFAULT_INPUT_PDF_MIN_TEXT_CHARS, + }, + }; +} + +function appendFileBlocks(body: string | undefined, blocks: string[]): string { + if (!blocks || blocks.length === 0) { + return body ?? ""; + } + const base = typeof body === "string" ? body.trim() : ""; + const suffix = blocks.join("\n\n").trim(); + if (!base) { + return suffix; + } + return `${base}\n\n${suffix}`.trim(); +} + +function resolveUtf16Charset(buffer?: Buffer): "utf-16le" | "utf-16be" | undefined { + if (!buffer || buffer.length < 2) { + return undefined; + } + const b0 = buffer[0]; + const b1 = buffer[1]; + if (b0 === 0xff && b1 === 0xfe) { + return "utf-16le"; + } + if (b0 === 0xfe && b1 === 0xff) { + return "utf-16be"; + } + const sampleLen = Math.min(buffer.length, 2048); + let zeroCount = 0; + for (let i = 0; i < sampleLen; i += 1) { + if (buffer[i] === 0) { + zeroCount += 1; + } + } + if (zeroCount / sampleLen > 0.2) { + return "utf-16le"; + } + return undefined; +} + +function looksLikeUtf8Text(buffer?: Buffer): boolean { + if (!buffer || buffer.length === 0) { + return false; + } + const sampleLen = Math.min(buffer.length, 4096); + let printable = 0; + let other = 0; + for (let i = 0; i < sampleLen; i += 1) { + const byte = buffer[i]; + if (byte === 0) { + other += 1; + continue; + } + if (byte === 9 || byte === 10 || byte === 13 || (byte >= 32 && byte <= 126)) { + printable += 1; + } else { + other += 1; + } + } + const total = printable + other; + if (total === 0) { + return false; + } + return printable / total > 0.85; +} + +function decodeTextSample(buffer?: Buffer): string { + if (!buffer || buffer.length === 0) { + return ""; + } + const sample = buffer.subarray(0, Math.min(buffer.length, 8192)); + const utf16Charset = resolveUtf16Charset(sample); + if (utf16Charset === "utf-16be") { + const swapped = Buffer.alloc(sample.length); + for (let i = 0; i + 1 < sample.length; i += 2) { + swapped[i] = sample[i + 1]; + swapped[i + 1] = sample[i]; + } + return new TextDecoder("utf-16le").decode(swapped); + } + if (utf16Charset === "utf-16le") { + return new TextDecoder("utf-16le").decode(sample); + } + return new TextDecoder("utf-8").decode(sample); +} + +function guessDelimitedMime(text: string): string | undefined { + if (!text) { + return undefined; + } + const line = text.split(/\r?\n/)[0] ?? ""; + const tabs = (line.match(/\t/g) ?? []).length; + const commas = (line.match(/,/g) ?? []).length; + if (commas > 0) { + return "text/csv"; + } + if (tabs > 0) { + return "text/tab-separated-values"; + } + return undefined; +} + +function resolveTextMimeFromName(name?: string): string | undefined { + if (!name) { + return undefined; + } + const ext = path.extname(name).toLowerCase(); + return TEXT_EXT_MIME.get(ext); +} + +async function extractFileBlocks(params: { + attachments: ReturnType; + cache: ReturnType; + limits: ReturnType; +}): Promise { + const { attachments, cache, limits } = params; + if (!attachments || attachments.length === 0) { + return []; + } + const blocks: string[] = []; + for (const attachment of attachments) { + if (!attachment) { + continue; + } + const forcedTextMime = resolveTextMimeFromName(attachment.path ?? attachment.url ?? ""); + const kind = forcedTextMime ? "document" : resolveAttachmentKind(attachment); + if (!forcedTextMime && (kind === "image" || kind === "video")) { + continue; + } + if (!limits.allowUrl && attachment.url && !attachment.path) { + if (shouldLogVerbose()) { + logVerbose(`media: file attachment skipped (url disabled) index=${attachment.index}`); + } + continue; + } + let bufferResult: Awaited>; + try { + bufferResult = await cache.getBuffer({ + attachmentIndex: attachment.index, + maxBytes: limits.maxBytes, + timeoutMs: limits.timeoutMs, + }); + } catch (err) { + if (shouldLogVerbose()) { + logVerbose(`media: file attachment skipped (buffer): ${String(err)}`); + } + continue; + } + const nameHint = bufferResult?.fileName ?? attachment.path ?? attachment.url; + const forcedTextMimeResolved = forcedTextMime ?? resolveTextMimeFromName(nameHint ?? ""); + const utf16Charset = resolveUtf16Charset(bufferResult?.buffer); + const textSample = decodeTextSample(bufferResult?.buffer); + const textLike = Boolean(utf16Charset) || looksLikeUtf8Text(bufferResult?.buffer); + if (!forcedTextMimeResolved && kind === "audio" && !textLike) { + continue; + } + const guessedDelimited = textLike ? guessDelimitedMime(textSample) : undefined; + const textHint = + forcedTextMimeResolved ?? guessedDelimited ?? (textLike ? "text/plain" : undefined); + const rawMime = bufferResult?.mime ?? attachment.mime; + const mimeType = textHint ?? normalizeMimeType(rawMime); + // Log when MIME type is overridden from non-text to text for auditability + if (textHint && rawMime && !rawMime.startsWith("text/")) { + logVerbose( + `media: MIME override from "${rawMime}" to "${textHint}" for index=${attachment.index}`, + ); + } + if (!mimeType) { + if (shouldLogVerbose()) { + logVerbose(`media: file attachment skipped (unknown mime) index=${attachment.index}`); + } + continue; + } + const allowedMimes = new Set(limits.allowedMimes); + for (const extra of EXTRA_TEXT_MIMES) { + allowedMimes.add(extra); + } + if (mimeType.startsWith("text/")) { + allowedMimes.add(mimeType); + } + if (!allowedMimes.has(mimeType)) { + if (shouldLogVerbose()) { + logVerbose( + `media: file attachment skipped (unsupported mime ${mimeType}) index=${attachment.index}`, + ); + } + continue; + } + let extracted: Awaited>; + try { + const mediaType = utf16Charset ? `${mimeType}; charset=${utf16Charset}` : mimeType; + extracted = await extractFileContentFromSource({ + source: { + type: "base64", + data: bufferResult.buffer.toString("base64"), + mediaType, + filename: bufferResult.fileName, + }, + limits: { + ...limits, + allowedMimes, + }, + }); + } catch (err) { + if (shouldLogVerbose()) { + logVerbose(`media: file attachment skipped (extract): ${String(err)}`); + } + continue; + } + const text = extracted?.text?.trim() ?? ""; + let blockText = text; + if (!blockText) { + if (extracted?.images && extracted.images.length > 0) { + blockText = "[PDF content rendered to images; images not forwarded to model]"; + } else { + blockText = "[No extractable text]"; + } + } + const safeName = (bufferResult.fileName ?? `file-${attachment.index + 1}`) + .replace(/[\r\n\t]+/g, " ") + .trim(); + // Escape XML special characters in attributes to prevent injection + blocks.push( + `\n${blockText}\n`, + ); + } + return blocks; +} export async function applyMediaUnderstanding(params: { ctx: MsgContext; - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentDir?: string; providers?: Record; activeModel?: ActiveMediaModel; @@ -51,6 +351,12 @@ export async function applyMediaUnderstanding(params: { const cache = createMediaAttachmentCache(attachments); try { + const fileBlocks = await extractFileBlocks({ + attachments, + cache, + limits: resolveFileLimits(cfg), + }); + const tasks = CAPABILITY_ORDER.map((capability) => async () => { const config = cfg.tools?.media?.[capability]; return await runCapability({ @@ -70,7 +376,9 @@ export async function applyMediaUnderstanding(params: { const outputs: MediaUnderstandingOutput[] = []; const decisions: MediaUnderstandingDecision[] = []; for (const entry of results) { - if (!entry) continue; + if (!entry) { + continue; + } for (const output of entry.outputs) { outputs.push(output); } @@ -99,7 +407,15 @@ export async function applyMediaUnderstanding(params: { ctx.RawBody = originalUserText; } ctx.MediaUnderstanding = [...(ctx.MediaUnderstanding ?? []), ...outputs]; - finalizeInboundContext(ctx, { forceBodyForAgent: true, forceBodyForCommands: true }); + } + if (fileBlocks.length > 0) { + ctx.Body = appendFileBlocks(ctx.Body, fileBlocks); + } + if (outputs.length > 0 || fileBlocks.length > 0) { + finalizeInboundContext(ctx, { + forceBodyForAgent: true, + forceBodyForCommands: outputs.length > 0, + }); } return { @@ -108,6 +424,7 @@ export async function applyMediaUnderstanding(params: { appliedImage: outputs.some((output) => output.kind === "image.description"), appliedAudio: outputs.some((output) => output.kind === "audio.transcription"), appliedVideo: outputs.some((output) => output.kind === "video.description"), + appliedFile: fileBlocks.length > 0, }; } finally { await cache.cleanup(); diff --git a/src/media-understanding/attachments.ts b/src/media-understanding/attachments.ts index fe9b1b793..97b3b5ac5 100644 --- a/src/media-understanding/attachments.ts +++ b/src/media-understanding/attachments.ts @@ -3,15 +3,14 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; - import type { MsgContext } from "../auto-reply/templating.js"; import type { MediaUnderstandingAttachmentsConfig } from "../config/types.tools.js"; +import type { MediaAttachment, MediaUnderstandingCapability } from "./types.js"; +import { logVerbose, shouldLogVerbose } from "../globals.js"; import { fetchRemoteMedia, MediaFetchError } from "../media/fetch.js"; import { detectMime, getFileExtension, isAudioFileName, kindFromMime } from "../media/mime.js"; -import { logVerbose, shouldLogVerbose } from "../globals.js"; -import { fetchWithTimeout } from "./providers/shared.js"; -import type { MediaAttachment, MediaUnderstandingCapability } from "./types.js"; import { MediaUnderstandingSkipError } from "./errors.js"; +import { fetchWithTimeout } from "./providers/shared.js"; type MediaBufferResult = { buffer: Buffer; @@ -40,7 +39,9 @@ const DEFAULT_MAX_ATTACHMENTS = 1; function normalizeAttachmentPath(raw?: string | null): string | undefined { const value = raw?.trim(); - if (!value) return undefined; + if (!value) { + return undefined; + } if (value.startsWith("file://")) { try { return fileURLToPath(value); @@ -58,7 +59,9 @@ export function normalizeAttachments(ctx: MsgContext): MediaAttachment[] { const resolveMime = (count: number, index: number) => { const typeHint = typesFromArray?.[index]; const trimmed = typeof typeHint === "string" ? typeHint.trim() : ""; - if (trimmed) return trimmed; + if (trimmed) { + return trimmed; + } return count === 1 ? ctx.MediaType : undefined; }; @@ -89,7 +92,9 @@ export function normalizeAttachments(ctx: MsgContext): MediaAttachment[] { const pathValue = ctx.MediaPath?.trim(); const url = ctx.MediaUrl?.trim(); - if (!pathValue && !url) return []; + if (!pathValue && !url) { + return []; + } return [ { path: pathValue || undefined, @@ -104,12 +109,20 @@ export function resolveAttachmentKind( attachment: MediaAttachment, ): "image" | "audio" | "video" | "document" | "unknown" { const kind = kindFromMime(attachment.mime); - if (kind === "image" || kind === "audio" || kind === "video") return kind; + if (kind === "image" || kind === "audio" || kind === "video") { + return kind; + } const ext = getFileExtension(attachment.path ?? attachment.url); - if (!ext) return "unknown"; - if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext)) return "video"; - if (isAudioFileName(attachment.path ?? attachment.url)) return "audio"; + if (!ext) { + return "unknown"; + } + if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext)) { + return "video"; + } + if (isAudioFileName(attachment.path ?? attachment.url)) { + return "audio"; + } if ([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif"].includes(ext)) { return "image"; } @@ -129,14 +142,22 @@ export function isImageAttachment(attachment: MediaAttachment): boolean { } function isAbortError(err: unknown): boolean { - if (!err) return false; - if (err instanceof Error && err.name === "AbortError") return true; + if (!err) { + return false; + } + if (err instanceof Error && err.name === "AbortError") { + return true; + } return false; } function resolveRequestUrl(input: RequestInfo | URL): string { - if (typeof input === "string") return input; - if (input instanceof URL) return input.toString(); + if (typeof input === "string") { + return input; + } + if (input instanceof URL) { + return input.toString(); + } return input.url; } @@ -144,8 +165,12 @@ function orderAttachments( attachments: MediaAttachment[], prefer?: MediaUnderstandingAttachmentsConfig["prefer"], ): MediaAttachment[] { - if (!prefer || prefer === "first") return attachments; - if (prefer === "last") return [...attachments].reverse(); + if (!prefer || prefer === "first") { + return attachments; + } + if (prefer === "last") { + return [...attachments].toReversed(); + } if (prefer === "path") { const withPath = attachments.filter((item) => item.path); const withoutPath = attachments.filter((item) => !item.path); @@ -166,11 +191,17 @@ export function selectAttachments(params: { }): MediaAttachment[] { const { capability, attachments, policy } = params; const matches = attachments.filter((item) => { - if (capability === "image") return isImageAttachment(item); - if (capability === "audio") return isAudioAttachment(item); + if (capability === "image") { + return isImageAttachment(item); + } + if (capability === "audio") { + return isAudioAttachment(item); + } return isVideoAttachment(item); }); - if (matches.length === 0) return []; + if (matches.length === 0) { + return []; + } const ordered = orderAttachments(matches, policy?.prefer); const mode = policy?.mode ?? "first"; @@ -326,7 +357,7 @@ export class MediaAttachmentCache { timeoutMs: params.timeoutMs, }); const extension = path.extname(bufferResult.fileName || "") || ""; - const tmpPath = path.join(os.tmpdir(), `moltbot-media-${crypto.randomUUID()}${extension}`); + const tmpPath = path.join(os.tmpdir(), `openclaw-media-${crypto.randomUUID()}${extension}`); await fs.writeFile(tmpPath, bufferResult.buffer); entry.tempPath = tmpPath; entry.tempCleanup = async () => { @@ -367,13 +398,19 @@ export class MediaAttachmentCache { private resolveLocalPath(attachment: MediaAttachment): string | undefined { const rawPath = normalizeAttachmentPath(attachment.path); - if (!rawPath) return undefined; + if (!rawPath) { + return undefined; + } return path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath); } private async ensureLocalStat(entry: AttachmentCacheEntry): Promise { - if (!entry.resolvedPath) return undefined; - if (entry.statSize !== undefined) return entry.statSize; + if (!entry.resolvedPath) { + return undefined; + } + if (entry.statSize !== undefined) { + return entry.statSize; + } try { const stat = await fs.stat(entry.resolvedPath); if (!stat.isFile()) { diff --git a/src/media-understanding/concurrency.ts b/src/media-understanding/concurrency.ts index 8ccba85f4..b70be5afe 100644 --- a/src/media-understanding/concurrency.ts +++ b/src/media-understanding/concurrency.ts @@ -4,7 +4,9 @@ export async function runWithConcurrency( tasks: Array<() => Promise>, limit: number, ): Promise { - if (tasks.length === 0) return []; + if (tasks.length === 0) { + return []; + } const resolvedLimit = Math.max(1, Math.min(limit, tasks.length)); const results: T[] = Array.from({ length: tasks.length }); let next = 0; @@ -13,7 +15,9 @@ export async function runWithConcurrency( while (true) { const index = next; next += 1; - if (index >= tasks.length) return; + if (index >= tasks.length) { + return; + } try { results[index] = await tasks[index](); } catch (err) { diff --git a/src/media-understanding/format.ts b/src/media-understanding/format.ts index c0dd77020..b0542d165 100644 --- a/src/media-understanding/format.ts +++ b/src/media-understanding/format.ts @@ -5,8 +5,12 @@ const MEDIA_PLACEHOLDER_TOKEN_RE = /^]+>(\s*\([^)]*\))?\s*/i; export function extractMediaUserText(body?: string): string | undefined { const trimmed = body?.trim() ?? ""; - if (!trimmed) return undefined; - if (MEDIA_PLACEHOLDER_RE.test(trimmed)) return undefined; + if (!trimmed) { + return undefined; + } + if (MEDIA_PLACEHOLDER_RE.test(trimmed)) { + return undefined; + } const cleaned = trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim(); return cleaned || undefined; } @@ -87,6 +91,8 @@ export function formatMediaUnderstandingBody(params: { } export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string { - if (outputs.length === 1) return outputs[0].text; + if (outputs.length === 1) { + return outputs[0].text; + } return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n"); } diff --git a/src/media-understanding/providers/deepgram/audio.live.test.ts b/src/media-understanding/providers/deepgram/audio.live.test.ts index ad8bc020e..75a4cb872 100644 --- a/src/media-understanding/providers/deepgram/audio.live.test.ts +++ b/src/media-understanding/providers/deepgram/audio.live.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; import { isTruthyEnvValue } from "../../../infra/env.js"; - import { transcribeDeepgramAudio } from "./audio.js"; const DEEPGRAM_KEY = process.env.DEEPGRAM_API_KEY ?? ""; @@ -12,7 +11,7 @@ const SAMPLE_URL = const LIVE = isTruthyEnvValue(process.env.DEEPGRAM_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE) || - isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST); + isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST); const describeLive = LIVE && DEEPGRAM_KEY ? describe : describe.skip; diff --git a/src/media-understanding/providers/deepgram/audio.test.ts b/src/media-understanding/providers/deepgram/audio.test.ts index 17af8443c..98c268e12 100644 --- a/src/media-understanding/providers/deepgram/audio.test.ts +++ b/src/media-understanding/providers/deepgram/audio.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "vitest"; - import { transcribeDeepgramAudio } from "./audio.js"; const resolveRequestUrl = (input: RequestInfo | URL) => { - if (typeof input === "string") return input; - if (input instanceof URL) return input.toString(); + if (typeof input === "string") { + return input; + } + if (input instanceof URL) { + return input.toString(); + } return input.url; }; diff --git a/src/media-understanding/providers/deepgram/audio.ts b/src/media-understanding/providers/deepgram/audio.ts index 1bc9bc782..8d0f53f36 100644 --- a/src/media-understanding/providers/deepgram/audio.ts +++ b/src/media-understanding/providers/deepgram/audio.ts @@ -28,10 +28,14 @@ export async function transcribeDeepgramAudio( const url = new URL(`${baseUrl}/listen`); url.searchParams.set("model", model); - if (params.language?.trim()) url.searchParams.set("language", params.language.trim()); + if (params.language?.trim()) { + url.searchParams.set("language", params.language.trim()); + } if (params.query) { for (const [key, value] of Object.entries(params.query)) { - if (value === undefined) continue; + if (value === undefined) { + continue; + } url.searchParams.set(key, String(value)); } } diff --git a/src/media-understanding/providers/google/audio.ts b/src/media-understanding/providers/google/audio.ts index 52a7136d2..b1058b9f2 100644 --- a/src/media-understanding/providers/google/audio.ts +++ b/src/media-understanding/providers/google/audio.ts @@ -8,7 +8,9 @@ const DEFAULT_GOOGLE_AUDIO_PROMPT = "Transcribe the audio."; function resolveModel(model?: string): string { const trimmed = model?.trim(); - if (!trimmed) return DEFAULT_GOOGLE_AUDIO_MODEL; + if (!trimmed) { + return DEFAULT_GOOGLE_AUDIO_MODEL; + } return normalizeGoogleModelId(trimmed); } diff --git a/src/media-understanding/providers/google/video.test.ts b/src/media-understanding/providers/google/video.test.ts index f94778cdd..4d05a8e04 100644 --- a/src/media-understanding/providers/google/video.test.ts +++ b/src/media-understanding/providers/google/video.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "vitest"; - import { describeGeminiVideo } from "./video.js"; const resolveRequestUrl = (input: RequestInfo | URL) => { - if (typeof input === "string") return input; - if (input instanceof URL) return input.toString(); + if (typeof input === "string") { + return input; + } + if (input instanceof URL) { + return input.toString(); + } return input.url; }; diff --git a/src/media-understanding/providers/google/video.ts b/src/media-understanding/providers/google/video.ts index 0f483b280..6b8ba2b15 100644 --- a/src/media-understanding/providers/google/video.ts +++ b/src/media-understanding/providers/google/video.ts @@ -8,7 +8,9 @@ const DEFAULT_GOOGLE_VIDEO_PROMPT = "Describe the video."; function resolveModel(model?: string): string { const trimmed = model?.trim(); - if (!trimmed) return DEFAULT_GOOGLE_VIDEO_MODEL; + if (!trimmed) { + return DEFAULT_GOOGLE_VIDEO_MODEL; + } return normalizeGoogleModelId(trimmed); } diff --git a/src/media-understanding/providers/image.ts b/src/media-understanding/providers/image.ts index 0f5ea1756..371f7dc47 100644 --- a/src/media-understanding/providers/image.ts +++ b/src/media-understanding/providers/image.ts @@ -1,17 +1,16 @@ -import type { Api, AssistantMessage, Context, Model } from "@mariozechner/pi-ai"; +import type { Api, Context, Model } from "@mariozechner/pi-ai"; import { complete } from "@mariozechner/pi-ai"; -import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent"; - -import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js"; -import { ensureMoltbotModelsJson } from "../../agents/models-config.js"; -import { minimaxUnderstandImage } from "../../agents/minimax-vlm.js"; -import { coerceImageAssistantText } from "../../agents/tools/image-tool.helpers.js"; import type { ImageDescriptionRequest, ImageDescriptionResult } from "../types.js"; +import { minimaxUnderstandImage } from "../../agents/minimax-vlm.js"; +import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js"; +import { ensureOpenClawModelsJson } from "../../agents/models-config.js"; +import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js"; +import { coerceImageAssistantText } from "../../agents/tools/image-tool.helpers.js"; export async function describeImageWithModel( params: ImageDescriptionRequest, ): Promise { - await ensureMoltbotModelsJson(params.cfg, params.agentDir); + await ensureOpenClawModelsJson(params.cfg, params.agentDir); const authStorage = discoverAuthStorage(params.agentDir); const modelRegistry = discoverModels(authStorage, params.agentDir); const model = modelRegistry.find(params.provider, params.model) as Model | null; @@ -54,10 +53,10 @@ export async function describeImageWithModel( }, ], }; - const message = (await complete(model, context, { + const message = await complete(model, context, { apiKey, maxTokens: params.maxTokens ?? 512, - })) as AssistantMessage; + }); const text = coerceImageAssistantText({ message, provider: model.provider, diff --git a/src/media-understanding/providers/index.ts b/src/media-understanding/providers/index.ts index a20ba92fb..5fc5bd02e 100644 --- a/src/media-understanding/providers/index.ts +++ b/src/media-understanding/providers/index.ts @@ -1,5 +1,5 @@ -import { normalizeProviderId } from "../../agents/model-selection.js"; import type { MediaUnderstandingProvider } from "../types.js"; +import { normalizeProviderId } from "../../agents/model-selection.js"; import { anthropicProvider } from "./anthropic/index.js"; import { deepgramProvider } from "./deepgram/index.js"; import { googleProvider } from "./google/index.js"; @@ -18,7 +18,9 @@ const PROVIDERS: MediaUnderstandingProvider[] = [ export function normalizeMediaProviderId(id: string): string { const normalized = normalizeProviderId(id); - if (normalized === "gemini") return "google"; + if (normalized === "gemini") { + return "google"; + } return normalized; } diff --git a/src/media-understanding/providers/openai/audio.test.ts b/src/media-understanding/providers/openai/audio.test.ts index 323c394ae..43c6be6fa 100644 --- a/src/media-understanding/providers/openai/audio.test.ts +++ b/src/media-understanding/providers/openai/audio.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "vitest"; - import { transcribeOpenAiCompatibleAudio } from "./audio.js"; const resolveRequestUrl = (input: RequestInfo | URL) => { - if (typeof input === "string") return input; - if (input instanceof URL) return input.toString(); + if (typeof input === "string") { + return input; + } + if (input instanceof URL) { + return input.toString(); + } return input.url; }; diff --git a/src/media-understanding/providers/openai/audio.ts b/src/media-understanding/providers/openai/audio.ts index acfe595a9..35eab2f1e 100644 --- a/src/media-understanding/providers/openai/audio.ts +++ b/src/media-understanding/providers/openai/audio.ts @@ -1,5 +1,4 @@ import path from "node:path"; - import type { AudioTranscriptionRequest, AudioTranscriptionResult } from "../../types.js"; import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js"; @@ -27,8 +26,12 @@ export async function transcribeOpenAiCompatibleAudio( }); form.append("file", blob, fileName); form.append("model", model); - if (params.language?.trim()) form.append("language", params.language.trim()); - if (params.prompt?.trim()) form.append("prompt", params.prompt.trim()); + if (params.language?.trim()) { + form.append("language", params.language.trim()); + } + if (params.prompt?.trim()) { + form.append("prompt", params.prompt.trim()); + } const headers = new Headers(params.headers); if (!headers.has("authorization")) { diff --git a/src/media-understanding/providers/shared.ts b/src/media-understanding/providers/shared.ts index 9dd36992e..fa0249df2 100644 --- a/src/media-understanding/providers/shared.ts +++ b/src/media-understanding/providers/shared.ts @@ -24,8 +24,12 @@ export async function readErrorResponse(res: Response): Promise { it("uses provider capabilities for shared entries without explicit caps", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { models: [{ provider: "openai", model: "gpt-5.2" }], @@ -34,7 +33,7 @@ describe("resolveModelEntries", () => { }); it("keeps per-capability entries even without explicit caps", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { image: { @@ -54,7 +53,7 @@ describe("resolveModelEntries", () => { }); it("skips shared CLI entries without capabilities", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { models: [{ type: "cli", command: "gemini", args: ["--file", "{{MediaPath}}"] }], @@ -73,7 +72,7 @@ describe("resolveModelEntries", () => { describe("resolveEntriesWithActiveFallback", () => { it("uses active model when enabled and no models are configured", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true }, @@ -93,7 +92,7 @@ describe("resolveEntriesWithActiveFallback", () => { }); it("ignores active model when configured entries exist", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, models: [{ provider: "openai", model: "whisper-1" }] }, @@ -113,7 +112,7 @@ describe("resolveEntriesWithActiveFallback", () => { }); it("skips active model when provider lacks capability", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { media: { video: { enabled: true }, diff --git a/src/media-understanding/resolve.ts b/src/media-understanding/resolve.ts index 97a355bbc..0a05ad9ea 100644 --- a/src/media-understanding/resolve.ts +++ b/src/media-understanding/resolve.ts @@ -1,10 +1,11 @@ -import type { MoltbotConfig } from "../config/config.js"; import type { MsgContext } from "../auto-reply/templating.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { MediaUnderstandingConfig, MediaUnderstandingModelConfig, MediaUnderstandingScopeConfig, } from "../config/types.tools.js"; +import type { MediaUnderstandingCapability } from "./types.js"; import { logVerbose, shouldLogVerbose } from "../globals.js"; import { DEFAULT_MAX_BYTES, @@ -14,7 +15,6 @@ import { } from "./defaults.js"; import { normalizeMediaProviderId } from "./providers/index.js"; import { normalizeMediaUnderstandingChatType, resolveMediaUnderstandingScope } from "./scope.js"; -import type { MediaUnderstandingCapability } from "./types.js"; export function resolveTimeoutMs(seconds: number | undefined, fallbackSeconds: number): number { const value = typeof seconds === "number" && Number.isFinite(seconds) ? seconds : fallbackSeconds; @@ -27,39 +27,45 @@ export function resolvePrompt( maxChars?: number, ): string { const base = prompt?.trim() || DEFAULT_PROMPT[capability]; - if (!maxChars || capability === "audio") return base; + if (!maxChars || capability === "audio") { + return base; + } return `${base} Respond in at most ${maxChars} characters.`; } export function resolveMaxChars(params: { capability: MediaUnderstandingCapability; entry: MediaUnderstandingModelConfig; - cfg: MoltbotConfig; + cfg: OpenClawConfig; config?: MediaUnderstandingConfig; }): number | undefined { const { capability, entry, cfg } = params; const configured = entry.maxChars ?? params.config?.maxChars ?? cfg.tools?.media?.[capability]?.maxChars; - if (typeof configured === "number") return configured; + if (typeof configured === "number") { + return configured; + } return DEFAULT_MAX_CHARS_BY_CAPABILITY[capability]; } export function resolveMaxBytes(params: { capability: MediaUnderstandingCapability; entry: MediaUnderstandingModelConfig; - cfg: MoltbotConfig; + cfg: OpenClawConfig; config?: MediaUnderstandingConfig; }): number { const configured = params.entry.maxBytes ?? params.config?.maxBytes ?? params.cfg.tools?.media?.[params.capability]?.maxBytes; - if (typeof configured === "number") return configured; + if (typeof configured === "number") { + return configured; + } return DEFAULT_MAX_BYTES[params.capability]; } export function resolveCapabilityConfig( - cfg: MoltbotConfig, + cfg: OpenClawConfig, capability: MediaUnderstandingCapability, ): MediaUnderstandingConfig | undefined { return cfg.tools?.media?.[capability]; @@ -82,14 +88,18 @@ function resolveEntryCapabilities(params: { providerRegistry: Map; }): MediaUnderstandingCapability[] | undefined { const entryType = params.entry.type ?? (params.entry.command ? "cli" : "provider"); - if (entryType === "cli") return undefined; + if (entryType === "cli") { + return undefined; + } const providerId = normalizeMediaProviderId(params.entry.provider ?? ""); - if (!providerId) return undefined; + if (!providerId) { + return undefined; + } return params.providerRegistry.get(providerId)?.capabilities; } export function resolveModelEntries(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; capability: MediaUnderstandingCapability; config?: MediaUnderstandingConfig; providerRegistry: Map; @@ -100,7 +110,9 @@ export function resolveModelEntries(params: { ...(config?.models ?? []).map((entry) => ({ entry, source: "capability" as const })), ...sharedModels.map((entry) => ({ entry, source: "shared" as const })), ]; - if (entries.length === 0) return []; + if (entries.length === 0) { + return []; + } return entries .filter(({ entry, source }) => { @@ -126,7 +138,7 @@ export function resolveModelEntries(params: { .map(({ entry }) => entry); } -export function resolveConcurrency(cfg: MoltbotConfig): number { +export function resolveConcurrency(cfg: OpenClawConfig): number { const configured = cfg.tools?.media?.concurrency; if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) { return Math.floor(configured); @@ -135,7 +147,7 @@ export function resolveConcurrency(cfg: MoltbotConfig): number { } export function resolveEntriesWithActiveFallback(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; capability: MediaUnderstandingCapability; config?: MediaUnderstandingConfig; providerRegistry: Map; @@ -147,14 +159,24 @@ export function resolveEntriesWithActiveFallback(params: { config: params.config, providerRegistry: params.providerRegistry, }); - if (entries.length > 0) return entries; - if (params.config?.enabled !== true) return entries; + if (entries.length > 0) { + return entries; + } + if (params.config?.enabled !== true) { + return entries; + } const activeProviderRaw = params.activeModel?.provider?.trim(); - if (!activeProviderRaw) return entries; + if (!activeProviderRaw) { + return entries; + } const activeProvider = normalizeMediaProviderId(activeProviderRaw); - if (!activeProvider) return entries; + if (!activeProvider) { + return entries; + } const capabilities = params.providerRegistry.get(activeProvider)?.capabilities; - if (!capabilities || !capabilities.includes(params.capability)) return entries; + if (!capabilities || !capabilities.includes(params.capability)) { + return entries; + } return [ { type: "provider", diff --git a/src/media-understanding/runner.auto-audio.test.ts b/src/media-understanding/runner.auto-audio.test.ts index 8ddda59e6..fa437011f 100644 --- a/src/media-understanding/runner.auto-audio.test.ts +++ b/src/media-understanding/runner.auto-audio.test.ts @@ -1,11 +1,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; import type { MsgContext } from "../auto-reply/templating.js"; +import type { OpenClawConfig } from "../config/config.js"; import { buildProviderRegistry, createMediaAttachmentCache, @@ -17,7 +15,7 @@ describe("runCapability auto audio entries", () => { it("uses provider keys to auto-enable audio transcription", async () => { const originalPath = process.env.PATH; process.env.PATH = "/usr/bin:/bin"; - const tmpPath = path.join(os.tmpdir(), `moltbot-auto-audio-${Date.now()}.wav`); + const tmpPath = path.join(os.tmpdir(), `openclaw-auto-audio-${Date.now()}.wav`); await fs.writeFile(tmpPath, Buffer.from("RIFF")); const ctx: MsgContext = { MediaPath: tmpPath, MediaType: "audio/wav" }; const media = normalizeMediaAttachments(ctx); @@ -44,7 +42,7 @@ describe("runCapability auto audio entries", () => { }, }, }, - } as unknown as MoltbotConfig; + } as unknown as OpenClawConfig; try { const result = await runCapability({ @@ -68,7 +66,7 @@ describe("runCapability auto audio entries", () => { it("skips auto audio when disabled", async () => { const originalPath = process.env.PATH; process.env.PATH = "/usr/bin:/bin"; - const tmpPath = path.join(os.tmpdir(), `moltbot-auto-audio-${Date.now()}.wav`); + const tmpPath = path.join(os.tmpdir(), `openclaw-auto-audio-${Date.now()}.wav`); await fs.writeFile(tmpPath, Buffer.from("RIFF")); const ctx: MsgContext = { MediaPath: tmpPath, MediaType: "audio/wav" }; const media = normalizeMediaAttachments(ctx); @@ -98,7 +96,7 @@ describe("runCapability auto audio entries", () => { }, }, }, - } as unknown as MoltbotConfig; + } as unknown as OpenClawConfig; try { const result = await runCapability({ diff --git a/src/media-understanding/runner.deepgram.test.ts b/src/media-understanding/runner.deepgram.test.ts index f4b5983fc..ac7082adb 100644 --- a/src/media-understanding/runner.deepgram.test.ts +++ b/src/media-understanding/runner.deepgram.test.ts @@ -1,11 +1,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; import type { MsgContext } from "../auto-reply/templating.js"; +import type { OpenClawConfig } from "../config/config.js"; import { buildProviderRegistry, createMediaAttachmentCache, @@ -15,7 +13,7 @@ import { describe("runCapability deepgram provider options", () => { it("merges provider options, headers, and baseUrl overrides", async () => { - const tmpPath = path.join(os.tmpdir(), `moltbot-deepgram-${Date.now()}.wav`); + const tmpPath = path.join(os.tmpdir(), `openclaw-deepgram-${Date.now()}.wav`); await fs.writeFile(tmpPath, Buffer.from("RIFF")); const ctx: MsgContext = { MediaPath: tmpPath, MediaType: "audio/wav" }; const media = normalizeMediaAttachments(ctx); @@ -80,7 +78,7 @@ describe("runCapability deepgram provider options", () => { }, }, }, - } as unknown as MoltbotConfig; + } as unknown as OpenClawConfig; try { const result = await runCapability({ diff --git a/src/media-understanding/runner.ts b/src/media-understanding/runner.ts index ffc6e4d64..6bbcf304b 100644 --- a/src/media-understanding/runner.ts +++ b/src/media-understanding/runner.ts @@ -2,37 +2,12 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - -import type { MoltbotConfig } from "../config/config.js"; -import { - findModelInCatalog, - loadModelCatalog, - modelSupportsVision, -} from "../agents/model-catalog.js"; import type { MsgContext } from "../auto-reply/templating.js"; -import { applyTemplate } from "../auto-reply/templating.js"; -import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js"; -import { logVerbose, shouldLogVerbose } from "../globals.js"; -import { runExec } from "../process/exec.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { MediaUnderstandingConfig, MediaUnderstandingModelConfig, } from "../config/types.tools.js"; -import { MediaAttachmentCache, normalizeAttachments, selectAttachments } from "./attachments.js"; -import { - CLI_OUTPUT_MAX_BUFFER, - DEFAULT_AUDIO_MODELS, - DEFAULT_TIMEOUT_SECONDS, -} from "./defaults.js"; -import { isMediaUnderstandingSkipError, MediaUnderstandingSkipError } from "./errors.js"; -import { - resolveMaxBytes, - resolveMaxChars, - resolveModelEntries, - resolvePrompt, - resolveScopeDecision, - resolveTimeoutMs, -} from "./resolve.js"; import type { MediaAttachment, MediaUnderstandingCapability, @@ -41,12 +16,36 @@ import type { MediaUnderstandingOutput, MediaUnderstandingProvider, } from "./types.js"; +import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js"; +import { + findModelInCatalog, + loadModelCatalog, + modelSupportsVision, +} from "../agents/model-catalog.js"; +import { applyTemplate } from "../auto-reply/templating.js"; +import { logVerbose, shouldLogVerbose } from "../globals.js"; +import { runExec } from "../process/exec.js"; +import { MediaAttachmentCache, normalizeAttachments, selectAttachments } from "./attachments.js"; +import { + CLI_OUTPUT_MAX_BUFFER, + DEFAULT_AUDIO_MODELS, + DEFAULT_TIMEOUT_SECONDS, +} from "./defaults.js"; +import { isMediaUnderstandingSkipError, MediaUnderstandingSkipError } from "./errors.js"; +import { describeImageWithModel } from "./providers/image.js"; import { buildMediaUnderstandingRegistry, getMediaUnderstandingProvider, normalizeMediaProviderId, } from "./providers/index.js"; -import { describeImageWithModel } from "./providers/image.js"; +import { + resolveMaxBytes, + resolveMaxChars, + resolveModelEntries, + resolvePrompt, + resolveScopeDecision, + resolveTimeoutMs, +} from "./resolve.js"; import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.js"; const AUTO_AUDIO_KEY_PROVIDERS = ["openai", "groq", "deepgram", "google"] as const; @@ -89,10 +88,16 @@ const binaryCache = new Map>(); const geminiProbeCache = new Map>(); function expandHomeDir(value: string): string { - if (!value.startsWith("~")) return value; + if (!value.startsWith("~")) { + return value; + } const home = os.homedir(); - if (value === "~") return home; - if (value.startsWith("~/")) return path.join(home, value.slice(2)); + if (value === "~") { + return home; + } + if (value.startsWith("~/")) { + return path.join(home, value.slice(2)); + } return value; } @@ -101,9 +106,13 @@ function hasPathSeparator(value: string): boolean { } function candidateBinaryNames(name: string): string[] { - if (process.platform !== "win32") return [name]; + if (process.platform !== "win32") { + return [name]; + } const ext = path.extname(name); - if (ext) return [name]; + if (ext) { + return [name]; + } const pathext = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM") .split(";") .map((item) => item.trim()) @@ -116,8 +125,12 @@ function candidateBinaryNames(name: string): string[] { async function isExecutable(filePath: string): Promise { try { const stat = await fs.stat(filePath); - if (!stat.isFile()) return false; - if (process.platform === "win32") return true; + if (!stat.isFile()) { + return false; + } + if (process.platform === "win32") { + return true; + } await fs.access(filePath, fsConstants.X_OK); return true; } catch { @@ -127,25 +140,35 @@ async function isExecutable(filePath: string): Promise { async function findBinary(name: string): Promise { const cached = binaryCache.get(name); - if (cached) return cached; + if (cached) { + return cached; + } const resolved = (async () => { const direct = expandHomeDir(name.trim()); if (direct && hasPathSeparator(direct)) { for (const candidate of candidateBinaryNames(direct)) { - if (await isExecutable(candidate)) return candidate; + if (await isExecutable(candidate)) { + return candidate; + } } } const searchName = name.trim(); - if (!searchName) return null; + if (!searchName) { + return null; + } const pathEntries = (process.env.PATH ?? "").split(path.delimiter); const candidates = candidateBinaryNames(searchName); for (const entryRaw of pathEntries) { const entry = expandHomeDir(entryRaw.trim().replace(/^"(.*)"$/, "$1")); - if (!entry) continue; + if (!entry) { + continue; + } for (const candidate of candidates) { const fullPath = path.join(entry, candidate); - if (await isExecutable(fullPath)) return fullPath; + if (await isExecutable(fullPath)) { + return fullPath; + } } } @@ -160,7 +183,9 @@ async function hasBinary(name: string): Promise { } async function fileExists(filePath?: string | null): Promise { - if (!filePath) return false; + if (!filePath) { + return false; + } try { await fs.stat(filePath); return true; @@ -172,7 +197,9 @@ async function fileExists(filePath?: string | null): Promise { function extractLastJsonObject(raw: string): unknown { const trimmed = raw.trim(); const start = trimmed.lastIndexOf("{"); - if (start === -1) return null; + if (start === -1) { + return null; + } const slice = trimmed.slice(start); try { return JSON.parse(slice); @@ -183,9 +210,13 @@ function extractLastJsonObject(raw: string): unknown { function extractGeminiResponse(raw: string): string | null { const payload = extractLastJsonObject(raw); - if (!payload || typeof payload !== "object") return null; + if (!payload || typeof payload !== "object") { + return null; + } const response = (payload as { response?: unknown }).response; - if (typeof response !== "string") return null; + if (typeof response !== "string") { + return null; + } const trimmed = response.trim(); return trimmed || null; } @@ -193,9 +224,13 @@ function extractGeminiResponse(raw: string): string | null { function extractSherpaOnnxText(raw: string): string | null { const tryParse = (value: string): string | null => { const trimmed = value.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const head = trimmed[0]; - if (head !== "{" && head !== '"') return null; + if (head !== "{" && head !== '"') { + return null; + } try { const parsed = JSON.parse(trimmed) as unknown; if (typeof parsed === "string") { @@ -212,7 +247,9 @@ function extractSherpaOnnxText(raw: string): string | null { }; const direct = tryParse(raw); - if (direct) return direct; + if (direct) { + return direct; + } const lines = raw .split("\n") @@ -220,16 +257,22 @@ function extractSherpaOnnxText(raw: string): string | null { .filter(Boolean); for (let i = lines.length - 1; i >= 0; i -= 1) { const parsed = tryParse(lines[i] ?? ""); - if (parsed) return parsed; + if (parsed) { + return parsed; + } } return null; } async function probeGeminiCli(): Promise { const cached = geminiProbeCache.get("gemini"); - if (cached) return cached; + if (cached) { + return cached; + } const resolved = (async () => { - if (!(await hasBinary("gemini"))) return false; + if (!(await hasBinary("gemini"))) { + return false; + } try { const { stdout } = await runExec("gemini", ["--output-format", "json", "ok"], { timeoutMs: 8000, @@ -244,11 +287,15 @@ async function probeGeminiCli(): Promise { } async function resolveLocalWhisperCppEntry(): Promise { - if (!(await hasBinary("whisper-cli"))) return null; + if (!(await hasBinary("whisper-cli"))) { + return null; + } const envModel = process.env.WHISPER_CPP_MODEL?.trim(); const defaultModel = "/opt/homebrew/share/whisper-cpp/for-tests-ggml-tiny.bin"; const modelPath = envModel && (await fileExists(envModel)) ? envModel : defaultModel; - if (!(await fileExists(modelPath))) return null; + if (!(await fileExists(modelPath))) { + return null; + } return { type: "cli", command: "whisper-cli", @@ -257,7 +304,9 @@ async function resolveLocalWhisperCppEntry(): Promise { - if (!(await hasBinary("whisper"))) return null; + if (!(await hasBinary("whisper"))) { + return null; + } return { type: "cli", command: "whisper", @@ -276,17 +325,29 @@ async function resolveLocalWhisperEntry(): Promise { - if (!(await hasBinary("sherpa-onnx-offline"))) return null; + if (!(await hasBinary("sherpa-onnx-offline"))) { + return null; + } const modelDir = process.env.SHERPA_ONNX_MODEL_DIR?.trim(); - if (!modelDir) return null; + if (!modelDir) { + return null; + } const tokens = path.join(modelDir, "tokens.txt"); const encoder = path.join(modelDir, "encoder.onnx"); const decoder = path.join(modelDir, "decoder.onnx"); const joiner = path.join(modelDir, "joiner.onnx"); - if (!(await fileExists(tokens))) return null; - if (!(await fileExists(encoder))) return null; - if (!(await fileExists(decoder))) return null; - if (!(await fileExists(joiner))) return null; + if (!(await fileExists(tokens))) { + return null; + } + if (!(await fileExists(encoder))) { + return null; + } + if (!(await fileExists(decoder))) { + return null; + } + if (!(await fileExists(joiner))) { + return null; + } return { type: "cli", command: "sherpa-onnx-offline", @@ -302,16 +363,22 @@ async function resolveSherpaOnnxEntry(): Promise { const sherpa = await resolveSherpaOnnxEntry(); - if (sherpa) return sherpa; + if (sherpa) { + return sherpa; + } const whisperCpp = await resolveLocalWhisperCppEntry(); - if (whisperCpp) return whisperCpp; + if (whisperCpp) { + return whisperCpp; + } return await resolveLocalWhisperEntry(); } async function resolveGeminiCliEntry( _capability: MediaUnderstandingCapability, ): Promise { - if (!(await probeGeminiCli())) return null; + if (!(await probeGeminiCli())) { + return null; + } return { type: "cli", command: "gemini", @@ -329,7 +396,7 @@ async function resolveGeminiCliEntry( } async function resolveKeyEntry(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentDir?: string; providerRegistry: ProviderRegistry; capability: MediaUnderstandingCapability; @@ -341,10 +408,18 @@ async function resolveKeyEntry(params: { model?: string, ): Promise => { const provider = getMediaUnderstandingProvider(providerId, providerRegistry); - if (!provider) return null; - if (capability === "audio" && !provider.transcribeAudio) return null; - if (capability === "image" && !provider.describeImage) return null; - if (capability === "video" && !provider.describeVideo) return null; + if (!provider) { + return null; + } + if (capability === "audio" && !provider.transcribeAudio) { + return null; + } + if (capability === "image" && !provider.describeImage) { + return null; + } + if (capability === "video" && !provider.describeVideo) { + return null; + } try { await resolveApiKeyForProvider({ provider: providerId, cfg, agentDir }); return { type: "provider" as const, provider: providerId, model }; @@ -357,12 +432,16 @@ async function resolveKeyEntry(params: { const activeProvider = params.activeModel?.provider?.trim(); if (activeProvider) { const activeEntry = await checkProvider(activeProvider, params.activeModel?.model); - if (activeEntry) return activeEntry; + if (activeEntry) { + return activeEntry; + } } for (const providerId of AUTO_IMAGE_KEY_PROVIDERS) { const model = DEFAULT_IMAGE_MODELS[providerId]; const entry = await checkProvider(providerId, model); - if (entry) return entry; + if (entry) { + return entry; + } } return null; } @@ -371,11 +450,15 @@ async function resolveKeyEntry(params: { const activeProvider = params.activeModel?.provider?.trim(); if (activeProvider) { const activeEntry = await checkProvider(activeProvider, params.activeModel?.model); - if (activeEntry) return activeEntry; + if (activeEntry) { + return activeEntry; + } } for (const providerId of AUTO_VIDEO_KEY_PROVIDERS) { const entry = await checkProvider(providerId, undefined); - if (entry) return entry; + if (entry) { + return entry; + } } return null; } @@ -383,47 +466,65 @@ async function resolveKeyEntry(params: { const activeProvider = params.activeModel?.provider?.trim(); if (activeProvider) { const activeEntry = await checkProvider(activeProvider, params.activeModel?.model); - if (activeEntry) return activeEntry; + if (activeEntry) { + return activeEntry; + } } for (const providerId of AUTO_AUDIO_KEY_PROVIDERS) { const entry = await checkProvider(providerId, undefined); - if (entry) return entry; + if (entry) { + return entry; + } } return null; } async function resolveAutoEntries(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentDir?: string; providerRegistry: ProviderRegistry; capability: MediaUnderstandingCapability; activeModel?: ActiveMediaModel; }): Promise { const activeEntry = await resolveActiveModelEntry(params); - if (activeEntry) return [activeEntry]; + if (activeEntry) { + return [activeEntry]; + } if (params.capability === "audio") { const localAudio = await resolveLocalAudioEntry(); - if (localAudio) return [localAudio]; + if (localAudio) { + return [localAudio]; + } } const gemini = await resolveGeminiCliEntry(params.capability); - if (gemini) return [gemini]; + if (gemini) { + return [gemini]; + } const keys = await resolveKeyEntry(params); - if (keys) return [keys]; + if (keys) { + return [keys]; + } return []; } export async function resolveAutoImageModel(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentDir?: string; activeModel?: ActiveMediaModel; }): Promise { const providerRegistry = buildProviderRegistry(); const toActive = (entry: MediaUnderstandingModelConfig | null): ActiveMediaModel | null => { - if (!entry || entry.type === "cli") return null; + if (!entry || entry.type === "cli") { + return null; + } const provider = entry.provider; - if (!provider) return null; + if (!provider) { + return null; + } const model = entry.model ?? DEFAULT_IMAGE_MODELS[provider]; - if (!model) return null; + if (!model) { + return null; + } return { provider, model }; }; const activeEntry = await resolveActiveModelEntry({ @@ -434,7 +535,9 @@ export async function resolveAutoImageModel(params: { activeModel: params.activeModel, }); const resolvedActive = toActive(activeEntry); - if (resolvedActive) return resolvedActive; + if (resolvedActive) { + return resolvedActive; + } const keyEntry = await resolveKeyEntry({ cfg: params.cfg, agentDir: params.agentDir, @@ -446,21 +549,33 @@ export async function resolveAutoImageModel(params: { } async function resolveActiveModelEntry(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentDir?: string; providerRegistry: ProviderRegistry; capability: MediaUnderstandingCapability; activeModel?: ActiveMediaModel; }): Promise { const activeProviderRaw = params.activeModel?.provider?.trim(); - if (!activeProviderRaw) return null; + if (!activeProviderRaw) { + return null; + } const providerId = normalizeMediaProviderId(activeProviderRaw); - if (!providerId) return null; + if (!providerId) { + return null; + } const provider = getMediaUnderstandingProvider(providerId, params.providerRegistry); - if (!provider) return null; - if (params.capability === "audio" && !provider.transcribeAudio) return null; - if (params.capability === "image" && !provider.describeImage) return null; - if (params.capability === "video" && !provider.describeVideo) return null; + if (!provider) { + return null; + } + if (params.capability === "audio" && !provider.transcribeAudio) { + return null; + } + if (params.capability === "image" && !provider.describeImage) { + return null; + } + if (params.capability === "video" && !provider.describeVideo) { + return null; + } try { await resolveApiKeyForProvider({ provider: providerId, @@ -479,7 +594,9 @@ async function resolveActiveModelEntry(params: { function trimOutput(text: string, maxChars?: number): string { const trimmed = text.trim(); - if (!maxChars || trimmed.length <= maxChars) return trimmed; + if (!maxChars || trimmed.length <= maxChars) { + return trimmed; + } return trimmed.slice(0, maxChars).trim(); } @@ -491,7 +608,9 @@ function findArgValue(args: string[], keys: string[]): string | undefined { for (let i = 0; i < args.length; i += 1) { if (keys.includes(args[i] ?? "")) { const value = args[i + 1]; - if (value) return value; + if (value) { + return value; + } } } return undefined; @@ -504,17 +623,25 @@ function hasArg(args: string[], keys: string[]): boolean { function resolveWhisperOutputPath(args: string[], mediaPath: string): string | null { const outputDir = findArgValue(args, ["--output_dir", "-o"]); const outputFormat = findArgValue(args, ["--output_format"]); - if (!outputDir || !outputFormat) return null; + if (!outputDir || !outputFormat) { + return null; + } const formats = outputFormat.split(",").map((value) => value.trim()); - if (!formats.includes("txt")) return null; + if (!formats.includes("txt")) { + return null; + } const base = path.parse(mediaPath).name; return path.join(outputDir, `${base}.txt`); } function resolveWhisperCppOutputPath(args: string[]): string | null { - if (!hasArg(args, ["-otxt", "--output-txt"])) return null; + if (!hasArg(args, ["-otxt", "--output-txt"])) { + return null; + } const outputBase = findArgValue(args, ["-of", "--output-file"]); - if (!outputBase) return null; + if (!outputBase) { + return null; + } return `${outputBase}.txt`; } @@ -534,18 +661,24 @@ async function resolveCliOutput(params: { if (fileOutput && (await fileExists(fileOutput))) { try { const content = await fs.readFile(fileOutput, "utf8"); - if (content.trim()) return content.trim(); + if (content.trim()) { + return content.trim(); + } } catch {} } if (commandId === "gemini") { const response = extractGeminiResponse(params.stdout); - if (response) return response; + if (response) { + return response; + } } if (commandId === "sherpa-onnx-offline") { const response = extractSherpaOnnxText(params.stdout); - if (response) return response; + if (response) { + return response; + } } return params.stdout.trim(); @@ -556,10 +689,14 @@ type ProviderQuery = Record; function normalizeProviderQuery( options?: Record, ): ProviderQuery | undefined { - if (!options) return undefined; + if (!options) { + return undefined; + } const query: ProviderQuery = {}; for (const [key, value] of Object.entries(options)) { - if (value === undefined) continue; + if (value === undefined) { + continue; + } query[key] = value; } return Object.keys(query).length > 0 ? query : undefined; @@ -570,11 +707,19 @@ function buildDeepgramCompatQuery(options?: { punctuate?: boolean; smartFormat?: boolean; }): ProviderQuery | undefined { - if (!options) return undefined; + if (!options) { + return undefined; + } const query: ProviderQuery = {}; - if (typeof options.detectLanguage === "boolean") query.detect_language = options.detectLanguage; - if (typeof options.punctuate === "boolean") query.punctuate = options.punctuate; - if (typeof options.smartFormat === "boolean") query.smart_format = options.smartFormat; + if (typeof options.detectLanguage === "boolean") { + query.detect_language = options.detectLanguage; + } + if (typeof options.punctuate === "boolean") { + query.punctuate = options.punctuate; + } + if (typeof options.smartFormat === "boolean") { + query.smart_format = options.smartFormat; + } return Object.keys(query).length > 0 ? query : undefined; } @@ -663,7 +808,7 @@ function formatDecisionSummary(decision: MediaUnderstandingDecision): string { async function runProviderEntry(params: { capability: MediaUnderstandingCapability; entry: MediaUnderstandingModelConfig; - cfg: MoltbotConfig; + cfg: OpenClawConfig; ctx: MsgContext; attachmentIndex: number; cache: MediaAttachmentCache; @@ -847,7 +992,7 @@ async function runProviderEntry(params: { async function runCliEntry(params: { capability: MediaUnderstandingCapability; entry: MediaUnderstandingModelConfig; - cfg: MoltbotConfig; + cfg: OpenClawConfig; ctx: MsgContext; attachmentIndex: number; cache: MediaAttachmentCache; @@ -877,7 +1022,7 @@ async function runCliEntry(params: { maxBytes, timeoutMs, }); - const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-media-cli-")); + const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-cli-")); const mediaPath = pathResult.path; const outputBase = path.join(outputDir, path.parse(mediaPath).name); @@ -908,7 +1053,9 @@ async function runCliEntry(params: { mediaPath, }); const text = trimOutput(resolved, maxChars); - if (!text) return null; + if (!text) { + return null; + } return { kind: capability === "audio" ? "audio.transcription" : `${capability}.description`, attachmentIndex: params.attachmentIndex, @@ -923,7 +1070,7 @@ async function runCliEntry(params: { async function runAttachmentEntries(params: { capability: MediaUnderstandingCapability; - cfg: MoltbotConfig; + cfg: OpenClawConfig; ctx: MsgContext; attachmentIndex: number; agentDir?: string; @@ -964,8 +1111,12 @@ async function runAttachmentEntries(params: { }); if (result) { const decision = buildModelDecision({ entry, entryType, outcome: "success" }); - if (result.provider) decision.provider = result.provider; - if (result.model) decision.model = result.model; + if (result.provider) { + decision.provider = result.provider; + } + if (result.model) { + decision.model = result.model; + } attempts.push(decision); return { output: result, attempts }; } @@ -1006,7 +1157,7 @@ async function runAttachmentEntries(params: { export async function runCapability(params: { capability: MediaUnderstandingCapability; - cfg: MoltbotConfig; + cfg: OpenClawConfig; ctx: MsgContext; attachments: MediaAttachmentCache; media: MediaAttachment[]; @@ -1129,7 +1280,9 @@ export async function runCapability(params: { entries: resolvedEntries, config, }); - if (output) outputs.push(output); + if (output) { + outputs.push(output); + } attachmentDecisions.push({ attachmentIndex: attachment.index, attempts, diff --git a/src/media-understanding/runner.vision-skip.test.ts b/src/media-understanding/runner.vision-skip.test.ts index bbc4e1edc..8a289b845 100644 --- a/src/media-understanding/runner.vision-skip.test.ts +++ b/src/media-understanding/runner.vision-skip.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from "vitest"; - import type { MsgContext } from "../auto-reply/templating.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { buildProviderRegistry, createMediaAttachmentCache, @@ -33,7 +32,7 @@ describe("runCapability image skip", () => { const ctx: MsgContext = { MediaPath: "/tmp/image.png", MediaType: "image/png" }; const media = normalizeMediaAttachments(ctx); const cache = createMediaAttachmentCache(media); - const cfg = {} as MoltbotConfig; + const cfg = {} as OpenClawConfig; try { const result = await runCapability({ diff --git a/src/media-understanding/scope.test.ts b/src/media-understanding/scope.test.ts index 3f9bff48a..0607c4bf2 100644 --- a/src/media-understanding/scope.test.ts +++ b/src/media-understanding/scope.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { normalizeMediaUnderstandingChatType, resolveMediaUnderstandingScope } from "./scope.js"; describe("media understanding scope", () => { diff --git a/src/media-understanding/scope.ts b/src/media-understanding/scope.ts index 7aa71cc5a..f0a13db28 100644 --- a/src/media-understanding/scope.ts +++ b/src/media-understanding/scope.ts @@ -5,8 +5,12 @@ export type MediaUnderstandingScopeDecision = "allow" | "deny"; function normalizeDecision(value?: string | null): MediaUnderstandingScopeDecision | undefined { const normalized = value?.trim().toLowerCase(); - if (normalized === "allow") return "allow"; - if (normalized === "deny") return "deny"; + if (normalized === "allow") { + return "allow"; + } + if (normalized === "deny") { + return "deny"; + } return undefined; } @@ -26,23 +30,33 @@ export function resolveMediaUnderstandingScope(params: { chatType?: string; }): MediaUnderstandingScopeDecision { const scope = params.scope; - if (!scope) return "allow"; + if (!scope) { + return "allow"; + } const channel = normalizeMatch(params.channel); const chatType = normalizeMediaUnderstandingChatType(params.chatType); const sessionKey = normalizeMatch(params.sessionKey) ?? ""; for (const rule of scope.rules ?? []) { - if (!rule) continue; + if (!rule) { + continue; + } const action = normalizeDecision(rule.action) ?? "allow"; const match = rule.match ?? {}; const matchChannel = normalizeMatch(match.channel); const matchChatType = normalizeMediaUnderstandingChatType(match.chatType); const matchPrefix = normalizeMatch(match.keyPrefix); - if (matchChannel && matchChannel !== channel) continue; - if (matchChatType && matchChatType !== chatType) continue; - if (matchPrefix && !sessionKey.startsWith(matchPrefix)) continue; + if (matchChannel && matchChannel !== channel) { + continue; + } + if (matchChatType && matchChatType !== chatType) { + continue; + } + if (matchPrefix && !sessionKey.startsWith(matchPrefix)) { + continue; + } return action; } diff --git a/src/media-understanding/types.ts b/src/media-understanding/types.ts index 2c8bccd2d..252559a7a 100644 --- a/src/media-understanding/types.ts +++ b/src/media-understanding/types.ts @@ -97,7 +97,7 @@ export type ImageDescriptionRequest = { profile?: string; preferredProfile?: string; agentDir: string; - cfg: import("../config/config.js").MoltbotConfig; + cfg: import("../config/config.js").OpenClawConfig; }; export type ImageDescriptionResult = { diff --git a/src/media/audio.ts b/src/media/audio.ts index f1341740e..aeca2ce0b 100644 --- a/src/media/audio.ts +++ b/src/media/audio.ts @@ -11,8 +11,12 @@ export function isVoiceCompatibleAudio(opts: { return true; } const fileName = opts.fileName?.trim(); - if (!fileName) return false; + if (!fileName) { + return false; + } const ext = getFileExtension(fileName); - if (!ext) return false; + if (!ext) { + return false; + } return VOICE_AUDIO_EXTENSIONS.has(ext); } diff --git a/src/media/constants.ts b/src/media/constants.ts index e74ac6934..63fdc03fc 100644 --- a/src/media/constants.ts +++ b/src/media/constants.ts @@ -6,12 +6,24 @@ export const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; // 100MB export type MediaKind = "image" | "audio" | "video" | "document" | "unknown"; export function mediaKindFromMime(mime?: string | null): MediaKind { - if (!mime) return "unknown"; - if (mime.startsWith("image/")) return "image"; - if (mime.startsWith("audio/")) return "audio"; - if (mime.startsWith("video/")) return "video"; - if (mime === "application/pdf") return "document"; - if (mime.startsWith("application/")) return "document"; + if (!mime) { + return "unknown"; + } + if (mime.startsWith("image/")) { + return "image"; + } + if (mime.startsWith("audio/")) { + return "audio"; + } + if (mime.startsWith("video/")) { + return "video"; + } + if (mime === "application/pdf") { + return "document"; + } + if (mime.startsWith("application/")) { + return "document"; + } return "unknown"; } diff --git a/src/media/fetch.test.ts b/src/media/fetch.test.ts index 46445b1bb..2af4f4663 100644 --- a/src/media/fetch.test.ts +++ b/src/media/fetch.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { fetchRemoteMedia } from "./fetch.js"; function makeStream(chunks: Uint8Array[]) { diff --git a/src/media/fetch.ts b/src/media/fetch.ts index 727ab7a5d..b47213da7 100644 --- a/src/media/fetch.ts +++ b/src/media/fetch.ts @@ -1,5 +1,4 @@ import path from "node:path"; - import { detectMime, extensionForMime } from "./mime.js"; type FetchMediaResult = { @@ -34,7 +33,9 @@ function stripQuotes(value: string): string { } function parseContentDispositionFileName(header?: string | null): string | undefined { - if (!header) return undefined; + if (!header) { + return undefined; + } const starMatch = /filename\*\s*=\s*([^;]+)/i.exec(header); if (starMatch?.[1]) { const cleaned = stripQuotes(starMatch[1].trim()); @@ -46,17 +47,25 @@ function parseContentDispositionFileName(header?: string | null): string | undef } } const match = /filename\s*=\s*([^;]+)/i.exec(header); - if (match?.[1]) return path.basename(stripQuotes(match[1].trim())); + if (match?.[1]) { + return path.basename(stripQuotes(match[1].trim())); + } return undefined; } async function readErrorBodySnippet(res: Response, maxChars = 200): Promise { try { const text = await res.text(); - if (!text) return undefined; + if (!text) { + return undefined; + } const collapsed = text.replace(/\s+/g, " ").trim(); - if (!collapsed) return undefined; - if (collapsed.length <= maxChars) return collapsed; + if (!collapsed) { + return undefined; + } + if (collapsed.length <= maxChars) { + return collapsed; + } return `${collapsed.slice(0, maxChars)}…`; } catch { return undefined; @@ -85,7 +94,9 @@ export async function fetchRemoteMedia(options: FetchMediaOptions): Promise maxBytes) { diff --git a/src/media/host.test.ts b/src/media/host.test.ts index d229d5ee0..c67ccea5c 100644 --- a/src/media/host.test.ts +++ b/src/media/host.test.ts @@ -1,6 +1,5 @@ -import fs from "node:fs/promises"; import type { Server } from "node:http"; - +import fs from "node:fs/promises"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ diff --git a/src/media/host.ts b/src/media/host.ts index 6c774ba74..d2032192c 100644 --- a/src/media/host.ts +++ b/src/media/host.ts @@ -1,9 +1,9 @@ import fs from "node:fs/promises"; +import { formatCliCommand } from "../cli/command-format.js"; import { ensurePortAvailable, PortInUseError } from "../infra/ports.js"; import { getTailnetHostname } from "../infra/tailscale.js"; import { logInfo } from "../logger.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { formatCliCommand } from "../cli/command-format.js"; import { startMediaServer } from "./server.js"; import { saveMediaSource } from "./store.js"; @@ -37,7 +37,7 @@ export async function ensureMediaHosted( if (needsServerStart && !opts.startServer) { await fs.rm(saved.path).catch(() => {}); throw new Error( - `Media hosting requires the webhook/Funnel server. Start \`${formatCliCommand("moltbot webhook")}\`/\`${formatCliCommand("moltbot up")}\` or re-run with --serve-media.`, + `Media hosting requires the webhook/Funnel server. Start \`${formatCliCommand("openclaw webhook")}\`/\`${formatCliCommand("openclaw up")}\` or re-run with --serve-media.`, ); } if (needsServerStart && opts.startServer) { @@ -60,7 +60,9 @@ async function isPortFree(port: number) { await ensurePortAvailable(port); return true; } catch (err) { - if (err instanceof PortInUseError) return false; + if (err instanceof PortInUseError) { + return false; + } throw err; } } diff --git a/src/media/image-ops.ts b/src/media/image-ops.ts index 9b07b69f4..3973d4528 100644 --- a/src/media/image-ops.ts +++ b/src/media/image-ops.ts @@ -1,7 +1,6 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { runExec } from "../process/exec.js"; type Sharp = typeof import("sharp"); @@ -17,8 +16,8 @@ function isBun(): boolean { function prefersSips(): boolean { return ( - process.env.CLAWDBOT_IMAGE_BACKEND === "sips" || - (process.env.CLAWDBOT_IMAGE_BACKEND !== "sharp" && isBun() && process.platform === "darwin") + process.env.OPENCLAW_IMAGE_BACKEND === "sips" || + (process.env.OPENCLAW_IMAGE_BACKEND !== "sharp" && isBun() && process.platform === "darwin") ); } @@ -69,7 +68,9 @@ function readJpegExifOrientation(buffer: Buffer): number | null { buffer[exifStart + 5] === 0 ) { const tiffStart = exifStart + 6; - if (buffer.length < tiffStart + 8) return null; + if (buffer.length < tiffStart + 8) { + return null; + } // Check byte order (II = little-endian, MM = big-endian) const byteOrder = buffer.toString("ascii", tiffStart, tiffStart + 2); @@ -83,12 +84,16 @@ function readJpegExifOrientation(buffer: Buffer): number | null { // Read IFD0 offset const ifd0Offset = readU32(tiffStart + 4); const ifd0Start = tiffStart + ifd0Offset; - if (buffer.length < ifd0Start + 2) return null; + if (buffer.length < ifd0Start + 2) { + return null; + } const numEntries = readU16(ifd0Start); for (let i = 0; i < numEntries; i++) { const entryOffset = ifd0Start + 2 + i * 12; - if (buffer.length < entryOffset + 12) break; + if (buffer.length < entryOffset + 12) { + break; + } const tag = readU16(entryOffset); // Orientation tag = 0x0112 @@ -120,7 +125,7 @@ function readJpegExifOrientation(buffer: Buffer): number | null { } async function withTempDir(fn: (dir: string) => Promise): Promise { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-img-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-img-")); try { return await fn(dir); } finally { @@ -142,11 +147,17 @@ async function sipsMetadataFromBuffer(buffer: Buffer): Promise { // Check if the image has an alpha channel // PNG color types with alpha: 4 (grayscale+alpha), 6 (RGBA) // Sharp reports this via 'channels' (4 = RGBA) or 'hasAlpha' - return meta.hasAlpha === true || meta.channels === 4; + return meta.hasAlpha || meta.channels === 4; } catch { return false; } diff --git a/src/media/input-files.ts b/src/media/input-files.ts index 0adcfc076..677bba74f 100644 --- a/src/media/input-files.ts +++ b/src/media/input-files.ts @@ -1,10 +1,10 @@ -import { logWarn } from "../logger.js"; +import type { Dispatcher } from "undici"; import { closeDispatcher, createPinnedDispatcher, resolvePinnedHostname, } from "../infra/net/ssrf.js"; -import type { Dispatcher } from "undici"; +import { logWarn } from "../logger.js"; type CanvasModule = typeof import("@napi-rs/canvas"); type PdfJsModule = typeof import("pdfjs-dist/legacy/build/pdf.mjs"); @@ -117,7 +117,9 @@ function isRedirectStatus(status: number): boolean { } export function normalizeMimeType(value: string | undefined): string | undefined { - if (!value) return undefined; + if (!value) { + return undefined; + } const [raw] = value.split(";"); const normalized = raw?.trim().toLowerCase(); return normalized || undefined; @@ -127,7 +129,9 @@ export function parseContentType(value: string | undefined): { mimeType?: string; charset?: string; } { - if (!value) return {}; + if (!value) { + return {}; + } const parts = value.split(";").map((part) => part.trim()); const mimeType = normalizeMimeType(parts[0]); const charset = parts @@ -165,7 +169,7 @@ export async function fetchWithGuard(params: { try { const response = await fetch(parsedUrl, { signal: controller.signal, - headers: { "User-Agent": "Moltbot-Gateway/1.0" }, + headers: { "User-Agent": "OpenClaw-Gateway/1.0" }, redirect: "manual", dispatcher, } as RequestInit & { dispatcher: Dispatcher }); @@ -226,7 +230,9 @@ function decodeTextContent(buffer: Buffer, charset: string | undefined): string } function clampText(text: string, maxChars: number): string { - if (text.length <= maxChars) return text; + if (text.length <= maxChars) { + return text; + } return text.slice(0, maxChars); } @@ -250,7 +256,9 @@ async function extractPdfContent(params: { .map((item) => ("str" in item ? String(item.str) : "")) .filter(Boolean) .join(" "); - if (pageText) textParts.push(pageText); + if (pageText) { + textParts.push(pageText); + } } const text = textParts.join("\n\n"); diff --git a/src/media/mime.test.ts b/src/media/mime.test.ts index 92325a62e..df9f9c4f0 100644 --- a/src/media/mime.test.ts +++ b/src/media/mime.test.ts @@ -1,6 +1,5 @@ import JSZip from "jszip"; import { describe, expect, it } from "vitest"; - import { detectMime, extensionForMime, imageMimeFromFormat } from "./mime.js"; async function makeOoxmlZip(opts: { mainMime: string; partPath: string }): Promise { diff --git a/src/media/mime.ts b/src/media/mime.ts index c50e9152c..154bd3ba4 100644 --- a/src/media/mime.ts +++ b/src/media/mime.ts @@ -1,6 +1,5 @@ -import path from "node:path"; - import { fileTypeFromBuffer } from "file-type"; +import path from "node:path"; import { type MediaKind, mediaKindFromMime } from "./constants.js"; // Map common mimes to preferred file extensions. @@ -53,13 +52,17 @@ const AUDIO_FILE_EXTENSIONS = new Set([ ]); function normalizeHeaderMime(mime?: string | null): string | undefined { - if (!mime) return undefined; + if (!mime) { + return undefined; + } const cleaned = mime.split(";")[0]?.trim().toLowerCase(); return cleaned || undefined; } async function sniffMime(buffer?: Buffer): Promise { - if (!buffer) return undefined; + if (!buffer) { + return undefined; + } try { const type = await fileTypeFromBuffer(buffer); return type?.mime ?? undefined; @@ -69,7 +72,9 @@ async function sniffMime(buffer?: Buffer): Promise { } export function getFileExtension(filePath?: string | null): string | undefined { - if (!filePath) return undefined; + if (!filePath) { + return undefined; + } try { if (/^https?:\/\//i.test(filePath)) { const url = new URL(filePath); @@ -84,7 +89,9 @@ export function getFileExtension(filePath?: string | null): string | undefined { export function isAudioFileName(fileName?: string | null): boolean { const ext = getFileExtension(fileName); - if (!ext) return false; + if (!ext) { + return false; + } return AUDIO_FILE_EXTENSIONS.has(ext); } @@ -97,7 +104,9 @@ export function detectMime(opts: { } function isGenericMime(mime?: string): boolean { - if (!mime) return true; + if (!mime) { + return true; + } const m = mime.toLowerCase(); return m === "application/octet-stream" || m === "application/zip"; } @@ -115,17 +124,29 @@ async function detectMimeImpl(opts: { // Prefer sniffed types, but don't let generic container types override a more // specific extension mapping (e.g. XLSX vs ZIP). - if (sniffed && (!isGenericMime(sniffed) || !extMime)) return sniffed; - if (extMime) return extMime; - if (headerMime && !isGenericMime(headerMime)) return headerMime; - if (sniffed) return sniffed; - if (headerMime) return headerMime; + if (sniffed && (!isGenericMime(sniffed) || !extMime)) { + return sniffed; + } + if (extMime) { + return extMime; + } + if (headerMime && !isGenericMime(headerMime)) { + return headerMime; + } + if (sniffed) { + return sniffed; + } + if (headerMime) { + return headerMime; + } return undefined; } export function extensionForMime(mime?: string | null): string | undefined { - if (!mime) return undefined; + if (!mime) { + return undefined; + } return EXT_BY_MIME[mime.toLowerCase()]; } @@ -133,13 +154,17 @@ export function isGifMedia(opts: { contentType?: string | null; fileName?: string | null; }): boolean { - if (opts.contentType?.toLowerCase() === "image/gif") return true; + if (opts.contentType?.toLowerCase() === "image/gif") { + return true; + } const ext = getFileExtension(opts.fileName); return ext === ".gif"; } export function imageMimeFromFormat(format?: string | null): string | undefined { - if (!format) return undefined; + if (!format) { + return undefined; + } switch (format.toLowerCase()) { case "jpg": case "jpeg": diff --git a/src/media/parse.test.ts b/src/media/parse.test.ts index de60ca357..5475ae281 100644 --- a/src/media/parse.test.ts +++ b/src/media/parse.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { splitMediaFromOutput } from "./parse.js"; describe("splitMediaFromOutput", () => { @@ -9,21 +8,33 @@ describe("splitMediaFromOutput", () => { expect(result.text).toBe("Hello world"); }); - it("captures media paths with spaces", () => { + it("rejects absolute media paths to prevent LFI", () => { const result = splitMediaFromOutput("MEDIA:/Users/pete/My File.png"); - expect(result.mediaUrls).toEqual(["/Users/pete/My File.png"]); - expect(result.text).toBe(""); + expect(result.mediaUrls).toBeUndefined(); + expect(result.text).toBe("MEDIA:/Users/pete/My File.png"); }); - it("captures quoted media paths with spaces", () => { + it("rejects quoted absolute media paths to prevent LFI", () => { const result = splitMediaFromOutput('MEDIA:"/Users/pete/My File.png"'); - expect(result.mediaUrls).toEqual(["/Users/pete/My File.png"]); - expect(result.text).toBe(""); + expect(result.mediaUrls).toBeUndefined(); + expect(result.text).toBe('MEDIA:"/Users/pete/My File.png"'); }); - it("captures tilde media paths with spaces", () => { + it("rejects tilde media paths to prevent LFI", () => { const result = splitMediaFromOutput("MEDIA:~/Pictures/My File.png"); - expect(result.mediaUrls).toEqual(["~/Pictures/My File.png"]); + expect(result.mediaUrls).toBeUndefined(); + expect(result.text).toBe("MEDIA:~/Pictures/My File.png"); + }); + + it("rejects directory traversal media paths to prevent LFI", () => { + const result = splitMediaFromOutput("MEDIA:../../etc/passwd"); + expect(result.mediaUrls).toBeUndefined(); + expect(result.text).toBe("MEDIA:../../etc/passwd"); + }); + + it("captures safe relative media paths", () => { + const result = splitMediaFromOutput("MEDIA:./screenshots/image.png"); + expect(result.mediaUrls).toEqual(["./screenshots/image.png"]); expect(result.text).toBe(""); }); @@ -43,8 +54,8 @@ describe("splitMediaFromOutput", () => { }); it("parses MEDIA tags with leading whitespace", () => { - const result = splitMediaFromOutput(" MEDIA:/tmp/screenshot.png"); - expect(result.mediaUrls).toEqual(["/tmp/screenshot.png"]); + const result = splitMediaFromOutput(" MEDIA:./screenshot.png"); + expect(result.mediaUrls).toEqual(["./screenshot.png"]); expect(result.text).toBe(""); }); }); diff --git a/src/media/parse.ts b/src/media/parse.ts index de0c6a5bc..b8fe22864 100644 --- a/src/media/parse.ts +++ b/src/media/parse.ts @@ -15,24 +15,36 @@ function cleanCandidate(raw: string) { } function isValidMedia(candidate: string, opts?: { allowSpaces?: boolean }) { - if (!candidate) return false; - if (candidate.length > 4096) return false; - if (!opts?.allowSpaces && /\s/.test(candidate)) return false; - if (/^https?:\/\//i.test(candidate)) return true; - if (candidate.startsWith("/")) return true; - if (candidate.startsWith("./")) return true; - if (candidate.startsWith("../")) return true; - if (candidate.startsWith("~")) return true; - return false; + if (!candidate) { + return false; + } + if (candidate.length > 4096) { + return false; + } + if (!opts?.allowSpaces && /\s/.test(candidate)) { + return false; + } + if (/^https?:\/\//i.test(candidate)) { + return true; + } + + // Local paths: only allow safe relative paths starting with ./ that do not traverse upwards. + return candidate.startsWith("./") && !candidate.includes(".."); } function unwrapQuoted(value: string): string | undefined { const trimmed = value.trim(); - if (trimmed.length < 2) return undefined; + if (trimmed.length < 2) { + return undefined; + } const first = trimmed[0]; const last = trimmed[trimmed.length - 1]; - if (first !== last) return undefined; - if (first !== `"` && first !== "'" && first !== "`") return undefined; + if (first !== last) { + return undefined; + } + if (first !== `"` && first !== "'" && first !== "`") { + return undefined; + } return trimmed.slice(1, -1).trim(); } @@ -50,7 +62,9 @@ export function splitMediaFromOutput(raw: string): { // KNOWN: Leading whitespace is semantically meaningful in Markdown (lists, indented fences). // We only trim the end; token cleanup below handles removing `MEDIA:` lines. const trimmedRaw = raw.trimEnd(); - if (!trimmedRaw.trim()) return { text: "" }; + if (!trimmedRaw.trim()) { + return { text: "" }; + } const media: string[] = []; let foundMediaToken = false; @@ -85,10 +99,8 @@ export function splitMediaFromOutput(raw: string): { continue; } - foundMediaToken = true; const pieces: string[] = []; let cursor = 0; - let hasValidMedia = false; for (const match of matches) { const start = match.index ?? 0; @@ -101,11 +113,13 @@ export function splitMediaFromOutput(raw: string): { const mediaStartIndex = media.length; let validCount = 0; const invalidParts: string[] = []; + let hasValidMedia = false; for (const part of parts) { const candidate = normalizeMediaSource(cleanCandidate(part)); if (isValidMedia(candidate, unwrapped ? { allowSpaces: true } : undefined)) { media.push(candidate); hasValidMedia = true; + foundMediaToken = true; validCount += 1; } else { invalidParts.push(part); @@ -130,6 +144,7 @@ export function splitMediaFromOutput(raw: string): { if (isValidMedia(fallback, { allowSpaces: true })) { media.splice(mediaStartIndex, media.length - mediaStartIndex, fallback); hasValidMedia = true; + foundMediaToken = true; validCount = 1; invalidParts.length = 0; } @@ -140,12 +155,18 @@ export function splitMediaFromOutput(raw: string): { if (isValidMedia(fallback, { allowSpaces: true })) { media.push(fallback); hasValidMedia = true; + foundMediaToken = true; invalidParts.length = 0; } } - if (hasValidMedia && invalidParts.length > 0) { - pieces.push(invalidParts.join(" ")); + if (hasValidMedia) { + if (invalidParts.length > 0) { + pieces.push(invalidParts.join(" ")); + } + } else { + // If no valid media was found in this match, keep the original token text. + pieces.push(match[0]); } cursor = start + match[0].length; @@ -184,7 +205,9 @@ export function splitMediaFromOutput(raw: string): { // Return cleaned text if we found a media token OR audio tag, otherwise original text: foundMediaToken || hasAudioAsVoice ? cleanedText : trimmedRaw, }; - if (hasAudioAsVoice) result.audioAsVoice = true; + if (hasAudioAsVoice) { + result.audioAsVoice = true; + } return result; } diff --git a/src/media/server.test.ts b/src/media/server.test.ts index 34182c5f2..6273f1d8a 100644 --- a/src/media/server.test.ts +++ b/src/media/server.test.ts @@ -1,7 +1,6 @@ -import fs from "node:fs/promises"; import type { AddressInfo } from "node:net"; +import fs from "node:fs/promises"; import path from "node:path"; - import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; const MEDIA_DIR = path.join(process.cwd(), "tmp-media-test"); diff --git a/src/media/server.ts b/src/media/server.ts index 4791352d8..6f7543b1b 100644 --- a/src/media/server.ts +++ b/src/media/server.ts @@ -1,9 +1,9 @@ -import fs from "node:fs/promises"; import type { Server } from "node:http"; import express, { type Express } from "express"; +import fs from "node:fs/promises"; import { danger } from "../globals.js"; -import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { SafeOpenError, openFileWithinRoot } from "../infra/fs-safe.js"; +import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { detectMime } from "./mime.js"; import { cleanOldMedia, getMediaDir, MEDIA_MAX_BYTES } from "./store.js"; @@ -13,9 +13,15 @@ const MEDIA_ID_PATTERN = /^[\p{L}\p{N}._-]+$/u; const MAX_MEDIA_BYTES = MEDIA_MAX_BYTES; const isValidMediaId = (id: string) => { - if (!id) return false; - if (id.length > MAX_MEDIA_ID_CHARS) return false; - if (id === "." || id === "..") return false; + if (!id) { + return false; + } + if (id.length > MAX_MEDIA_ID_CHARS) { + return false; + } + if (id === "." || id === "..") { + return false; + } return MEDIA_ID_PATTERN.test(id); }; @@ -51,7 +57,9 @@ export function attachMediaRoutes( const data = await handle.readFile(); await handle.close().catch(() => {}); const mime = await detectMime({ buffer: data, filePath: realPath }); - if (mime) res.type(mime); + if (mime) { + res.type(mime); + } res.send(data); // best-effort single-use cleanup after response ends res.on("finish", () => { diff --git a/src/media/store.header-ext.test.ts b/src/media/store.header-ext.test.ts index 23f033dba..7cfa99e24 100644 --- a/src/media/store.header-ext.test.ts +++ b/src/media/store.header-ext.test.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; const realOs = await vi.importActual("node:os"); -const HOME = path.join(realOs.tmpdir(), "moltbot-home-header-ext-test"); +const HOME = path.join(realOs.tmpdir(), "openclaw-home-header-ext-test"); vi.mock("node:os", () => ({ default: { homedir: () => HOME, tmpdir: () => realOs.tmpdir() }, diff --git a/src/media/store.redirect.test.ts b/src/media/store.redirect.test.ts index d33e773d0..f940c6052 100644 --- a/src/media/store.redirect.test.ts +++ b/src/media/store.redirect.test.ts @@ -1,12 +1,11 @@ +import JSZip from "jszip"; import fs from "node:fs/promises"; import path from "node:path"; import { PassThrough } from "node:stream"; - -import JSZip from "jszip"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const realOs = await vi.importActual("node:os"); -const HOME = path.join(realOs.tmpdir(), "moltbot-home-redirect"); +const HOME = path.join(realOs.tmpdir(), "openclaw-home-redirect"); const mockRequest = vi.fn(); vi.doMock("node:os", () => ({ @@ -47,7 +46,9 @@ describe("media store redirects", () => { const res = new PassThrough(); const req = { on: (event: string, handler: (...args: unknown[]) => void) => { - if (event === "error") res.on("error", handler); + if (event === "error") { + res.on("error", handler); + } return req; }, end: () => undefined, @@ -58,14 +59,14 @@ describe("media store redirects", () => { res.statusCode = 302; res.headers = { location: "https://example.com/final" }; setImmediate(() => { - cb(res as unknown as Parameters[0]); + cb(res as unknown); res.end(); }); } else { res.statusCode = 200; res.headers = { "content-type": "text/plain" }; setImmediate(() => { - cb(res as unknown as Parameters[0]); + cb(res as unknown); res.write("redirected"); res.end(); }); @@ -88,7 +89,9 @@ describe("media store redirects", () => { const res = new PassThrough(); const req = { on: (event: string, handler: (...args: unknown[]) => void) => { - if (event === "error") res.on("error", handler); + if (event === "error") { + res.on("error", handler); + } return req; }, end: () => undefined, @@ -98,7 +101,7 @@ describe("media store redirects", () => { res.statusCode = 200; res.headers = {}; setImmediate(() => { - cb(res as unknown as Parameters[0]); + cb(res as unknown); const zip = new JSZip(); zip.file( "[Content_Types].xml", diff --git a/src/media/store.test.ts b/src/media/store.test.ts index b2ecf56f2..5e7f510a8 100644 --- a/src/media/store.test.ts +++ b/src/media/store.test.ts @@ -1,10 +1,9 @@ +import JSZip from "jszip"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import JSZip from "jszip"; import sharp from "sharp"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; - import { isPathWithinBase } from "../../test/helpers/paths.js"; describe("media store", () => { @@ -13,24 +12,27 @@ describe("media store", () => { const envSnapshot: Record = {}; const snapshotEnv = () => { - for (const key of ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", "CLAWDBOT_STATE_DIR"]) { + for (const key of ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", "OPENCLAW_STATE_DIR"]) { envSnapshot[key] = process.env[key]; } }; const restoreEnv = () => { for (const [key, value] of Object.entries(envSnapshot)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } } }; beforeAll(async () => { snapshotEnv(); - home = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-test-home-")); + home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-home-")); process.env.HOME = home; process.env.USERPROFILE = home; - process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot"); + process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); if (process.platform === "win32") { const match = home.match(/^([A-Za-z]:)(.*)$/); if (match) { @@ -38,7 +40,7 @@ describe("media store", () => { process.env.HOMEPATH = match[2] || "\\"; } } - await fs.mkdir(path.join(home, ".clawdbot"), { recursive: true }); + await fs.mkdir(path.join(home, ".openclaw"), { recursive: true }); store = await import("./store.js"); }); @@ -61,7 +63,7 @@ describe("media store", () => { await withTempStore(async (store, home) => { const dir = await store.ensureMediaDir(); expect(isPathWithinBase(home, dir)).toBe(true); - expect(path.normalize(dir)).toContain(`${path.sep}.clawdbot${path.sep}media`); + expect(path.normalize(dir)).toContain(`${path.sep}.openclaw${path.sep}media`); const stat = await fs.stat(dir); expect(stat.isDirectory()).toBe(true); }); diff --git a/src/media/store.ts b/src/media/store.ts index 268937084..43b8c5851 100644 --- a/src/media/store.ts +++ b/src/media/store.ts @@ -5,8 +5,8 @@ import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import path from "node:path"; import { pipeline } from "node:stream/promises"; -import { resolveConfigDir } from "../utils.js"; import { resolvePinnedHostname } from "../infra/net/ssrf.js"; +import { resolveConfigDir } from "../utils.js"; import { detectMime, extensionForMime } from "./mime.js"; const resolveMediaDir = () => path.join(resolveConfigDir(), "media"); @@ -21,7 +21,9 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes */ function sanitizeFilename(name: string): string { const trimmed = name.trim(); - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } const sanitized = trimmed.replace(/[^\p{L}\p{N}._-]+/gu, "_"); // Collapse multiple underscores, trim leading/trailing, limit length return sanitized.replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 60); @@ -34,7 +36,9 @@ function sanitizeFilename(name: string): string { */ export function extractOriginalFilename(filePath: string): string { const basename = path.basename(filePath); - if (!basename) return "file.bin"; // Fallback for empty input + if (!basename) { + return "file.bin"; + } // Fallback for empty input const ext = path.extname(basename); const nameWithoutExt = path.basename(basename, ext); @@ -68,7 +72,9 @@ export async function cleanOldMedia(ttlMs = DEFAULT_TTL_MS) { entries.map(async (file) => { const full = path.join(mediaDir, file); const stat = await fs.stat(full).catch(() => null); - if (!stat) return; + if (!stat) { + return; + } if (now - stat.mtimeMs > ttlMs) { await fs.rm(full).catch(() => {}); } diff --git a/src/memory/batch-gemini.ts b/src/memory/batch-gemini.ts index 516b88fe7..60c8c7e9a 100644 --- a/src/memory/batch-gemini.ts +++ b/src/memory/batch-gemini.ts @@ -1,6 +1,6 @@ -import { createSubsystemLogger } from "../logging/subsystem.js"; -import { isTruthyEnvValue } from "../infra/env.js"; import type { GeminiEmbeddingClient } from "./embeddings-gemini.js"; +import { isTruthyEnvValue } from "../infra/env.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; import { hashText } from "./internal.js"; export type GeminiBatchRequest = { @@ -34,11 +34,13 @@ export type GeminiBatchOutputLine = { }; const GEMINI_BATCH_MAX_REQUESTS = 50000; -const debugEmbeddings = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_MEMORY_EMBEDDINGS); +const debugEmbeddings = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_MEMORY_EMBEDDINGS); const log = createSubsystemLogger("memory/embeddings"); const debugLog = (message: string, meta?: Record) => { - if (!debugEmbeddings) return; + if (!debugEmbeddings) { + return; + } const suffix = meta ? ` ${JSON.stringify(meta)}` : ""; log.raw(`${message}${suffix}`); }; @@ -71,7 +73,9 @@ function getGeminiUploadUrl(baseUrl: string): string { } function splitGeminiBatchRequests(requests: GeminiBatchRequest[]): GeminiBatchRequest[][] { - if (requests.length <= GEMINI_BATCH_MAX_REQUESTS) return [requests]; + if (requests.length <= GEMINI_BATCH_MAX_REQUESTS) { + return [requests]; + } const groups: GeminiBatchRequest[][] = []; for (let i = 0; i < requests.length; i += GEMINI_BATCH_MAX_REQUESTS) { groups.push(requests.slice(i, i + GEMINI_BATCH_MAX_REQUESTS)); @@ -83,7 +87,7 @@ function buildGeminiUploadBody(params: { jsonl: string; displayName: string }): body: Blob; contentType: string; } { - const boundary = `moltbot-${hashText(params.displayName)}`; + const boundary = `openclaw-${hashText(params.displayName)}`; const jsonPart = JSON.stringify({ file: { displayName: params.displayName, @@ -218,7 +222,9 @@ async function fetchGeminiFileContent(params: { } function parseGeminiBatchOutput(text: string): GeminiBatchOutputLine[] { - if (!text.trim()) return []; + if (!text.trim()) { + return []; + } return text .split("\n") .map((line) => line.trim()) @@ -272,7 +278,9 @@ async function waitForGeminiBatch(params: { } async function runWithConcurrency(tasks: Array<() => Promise>, limit: number): Promise { - if (tasks.length === 0) return []; + if (tasks.length === 0) { + return []; + } const resolvedLimit = Math.max(1, Math.min(limit, tasks.length)); const results: T[] = Array.from({ length: tasks.length }); let next = 0; @@ -280,10 +288,14 @@ async function runWithConcurrency(tasks: Array<() => Promise>, limit: numb const workers = Array.from({ length: resolvedLimit }, async () => { while (true) { - if (firstError) return; + if (firstError) { + return; + } const index = next; next += 1; - if (index >= tasks.length) return; + if (index >= tasks.length) { + return; + } try { results[index] = await tasks[index](); } catch (err) { @@ -294,7 +306,9 @@ async function runWithConcurrency(tasks: Array<() => Promise>, limit: numb }); await Promise.allSettled(workers); - if (firstError) throw firstError; + if (firstError) { + throw firstError; + } return results; } @@ -308,7 +322,9 @@ export async function runGeminiEmbeddingBatches(params: { concurrency: number; debug?: (message: string, data?: Record) => void; }): Promise> { - if (params.requests.length === 0) return new Map(); + if (params.requests.length === 0) { + return new Map(); + } const groups = splitGeminiBatchRequests(params.requests); const byCustomId = new Map(); @@ -373,7 +389,9 @@ export async function runGeminiEmbeddingBatches(params: { for (const line of outputLines) { const customId = line.key ?? line.custom_id ?? line.request_id; - if (!customId) continue; + if (!customId) { + continue; + } remaining.delete(customId); if (line.error?.message) { errors.push(`${customId}: ${line.error.message}`); diff --git a/src/memory/batch-openai.ts b/src/memory/batch-openai.ts index 0fa90b235..292730704 100644 --- a/src/memory/batch-openai.ts +++ b/src/memory/batch-openai.ts @@ -1,5 +1,5 @@ -import { retryAsync } from "../infra/retry.js"; import type { OpenAiEmbeddingClient } from "./embeddings-openai.js"; +import { retryAsync } from "../infra/retry.js"; import { hashText } from "./internal.js"; export type OpenAiBatchRequest = { @@ -56,7 +56,9 @@ function getOpenAiHeaders( } function splitOpenAiBatchRequests(requests: OpenAiBatchRequest[]): OpenAiBatchRequest[][] { - if (requests.length <= OPENAI_BATCH_MAX_REQUESTS) return [requests]; + if (requests.length <= OPENAI_BATCH_MAX_REQUESTS) { + return [requests]; + } const groups: OpenAiBatchRequest[][] = []; for (let i = 0; i < requests.length; i += OPENAI_BATCH_MAX_REQUESTS) { groups.push(requests.slice(i, i + OPENAI_BATCH_MAX_REQUESTS)); @@ -103,7 +105,7 @@ async function submitOpenAiBatch(params: { endpoint: OPENAI_BATCH_ENDPOINT, completion_window: OPENAI_BATCH_COMPLETION_WINDOW, metadata: { - source: "moltbot-memory", + source: "openclaw-memory", agent: params.agentId, }, }), @@ -163,7 +165,9 @@ async function fetchOpenAiFileContent(params: { } function parseOpenAiBatchOutput(text: string): OpenAiBatchOutputLine[] { - if (!text.trim()) return []; + if (!text.trim()) { + return []; + } return text .split("\n") .map((line) => line.trim()) @@ -242,7 +246,9 @@ async function waitForOpenAiBatch(params: { } async function runWithConcurrency(tasks: Array<() => Promise>, limit: number): Promise { - if (tasks.length === 0) return []; + if (tasks.length === 0) { + return []; + } const resolvedLimit = Math.max(1, Math.min(limit, tasks.length)); const results: T[] = Array.from({ length: tasks.length }); let next = 0; @@ -250,10 +256,14 @@ async function runWithConcurrency(tasks: Array<() => Promise>, limit: numb const workers = Array.from({ length: resolvedLimit }, async () => { while (true) { - if (firstError) return; + if (firstError) { + return; + } const index = next; next += 1; - if (index >= tasks.length) return; + if (index >= tasks.length) { + return; + } try { results[index] = await tasks[index](); } catch (err) { @@ -264,7 +274,9 @@ async function runWithConcurrency(tasks: Array<() => Promise>, limit: numb }); await Promise.allSettled(workers); - if (firstError) throw firstError; + if (firstError) { + throw firstError; + } return results; } @@ -278,7 +290,9 @@ export async function runOpenAiEmbeddingBatches(params: { concurrency: number; debug?: (message: string, data?: Record) => void; }): Promise> { - if (params.requests.length === 0) return new Map(); + if (params.requests.length === 0) { + return new Map(); + } const groups = splitOpenAiBatchRequests(params.requests); const byCustomId = new Map(); @@ -335,7 +349,9 @@ export async function runOpenAiEmbeddingBatches(params: { for (const line of outputLines) { const customId = line.custom_id; - if (!customId) continue; + if (!customId) { + continue; + } remaining.delete(customId); if (line.error?.message) { errors.push(`${customId}: ${line.error.message}`); diff --git a/src/memory/embeddings-gemini.ts b/src/memory/embeddings-gemini.ts index 244384df6..95f8137ea 100644 --- a/src/memory/embeddings-gemini.ts +++ b/src/memory/embeddings-gemini.ts @@ -1,7 +1,7 @@ +import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js"; import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js"; export type GeminiEmbeddingClient = { baseUrl: string; @@ -12,18 +12,22 @@ export type GeminiEmbeddingClient = { const DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; export const DEFAULT_GEMINI_EMBEDDING_MODEL = "gemini-embedding-001"; -const debugEmbeddings = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_MEMORY_EMBEDDINGS); +const debugEmbeddings = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_MEMORY_EMBEDDINGS); const log = createSubsystemLogger("memory/embeddings"); const debugLog = (message: string, meta?: Record) => { - if (!debugEmbeddings) return; + if (!debugEmbeddings) { + return; + } const suffix = meta ? ` ${JSON.stringify(meta)}` : ""; log.raw(`${message}${suffix}`); }; function resolveRemoteApiKey(remoteApiKey?: string): string | undefined { const trimmed = remoteApiKey?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } if (trimmed === "GOOGLE_API_KEY" || trimmed === "GEMINI_API_KEY") { return process.env[trimmed]?.trim(); } @@ -32,17 +36,25 @@ function resolveRemoteApiKey(remoteApiKey?: string): string | undefined { function normalizeGeminiModel(model: string): string { const trimmed = model.trim(); - if (!trimmed) return DEFAULT_GEMINI_EMBEDDING_MODEL; + if (!trimmed) { + return DEFAULT_GEMINI_EMBEDDING_MODEL; + } const withoutPrefix = trimmed.replace(/^models\//, ""); - if (withoutPrefix.startsWith("gemini/")) return withoutPrefix.slice("gemini/".length); - if (withoutPrefix.startsWith("google/")) return withoutPrefix.slice("google/".length); + if (withoutPrefix.startsWith("gemini/")) { + return withoutPrefix.slice("gemini/".length); + } + if (withoutPrefix.startsWith("google/")) { + return withoutPrefix.slice("google/".length); + } return withoutPrefix; } function normalizeGeminiBaseUrl(raw: string): string { const trimmed = raw.replace(/\/+$/, ""); const openAiIndex = trimmed.indexOf("/openai"); - if (openAiIndex > -1) return trimmed.slice(0, openAiIndex); + if (openAiIndex > -1) { + return trimmed.slice(0, openAiIndex); + } return trimmed; } @@ -59,7 +71,9 @@ export async function createGeminiEmbeddingProvider( const batchUrl = `${baseUrl}/${client.modelPath}:batchEmbedContents`; const embedQuery = async (text: string): Promise => { - if (!text.trim()) return []; + if (!text.trim()) { + return []; + } const res = await fetch(embedUrl, { method: "POST", headers: client.headers, @@ -77,7 +91,9 @@ export async function createGeminiEmbeddingProvider( }; const embedBatch = async (texts: string[]): Promise => { - if (texts.length === 0) return []; + if (texts.length === 0) { + return []; + } const requests = texts.map((text) => ({ model: client.modelPath, content: { parts: [{ text }] }, diff --git a/src/memory/embeddings-openai.ts b/src/memory/embeddings-openai.ts index cfc53efae..d125fa816 100644 --- a/src/memory/embeddings-openai.ts +++ b/src/memory/embeddings-openai.ts @@ -1,5 +1,5 @@ -import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js"; import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js"; +import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js"; export type OpenAiEmbeddingClient = { baseUrl: string; @@ -12,8 +12,12 @@ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"; export function normalizeOpenAiModel(model: string): string { const trimmed = model.trim(); - if (!trimmed) return DEFAULT_OPENAI_EMBEDDING_MODEL; - if (trimmed.startsWith("openai/")) return trimmed.slice("openai/".length); + if (!trimmed) { + return DEFAULT_OPENAI_EMBEDDING_MODEL; + } + if (trimmed.startsWith("openai/")) { + return trimmed.slice("openai/".length); + } return trimmed; } @@ -24,7 +28,9 @@ export async function createOpenAiEmbeddingProvider( const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`; const embed = async (input: string[]): Promise => { - if (input.length === 0) return []; + if (input.length === 0) { + return []; + } const res = await fetch(url, { method: "POST", headers: client.headers, diff --git a/src/memory/embeddings.test.ts b/src/memory/embeddings.test.ts index 1809b24b8..de0081b3a 100644 --- a/src/memory/embeddings.test.ts +++ b/src/memory/embeddings.test.ts @@ -1,11 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; - import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js"; vi.mock("../agents/model-auth.js", () => ({ resolveApiKeyForProvider: vi.fn(), requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => { - if (auth?.apiKey) return auth.apiKey; + if (auth?.apiKey) { + return auth.apiKey; + } throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`); }, })); diff --git a/src/memory/embeddings.ts b/src/memory/embeddings.ts index 98de1ab42..a8926fe93 100644 --- a/src/memory/embeddings.ts +++ b/src/memory/embeddings.ts @@ -1,7 +1,6 @@ -import fsSync from "node:fs"; - import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp"; -import type { MoltbotConfig } from "../config/config.js"; +import fsSync from "node:fs"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveUserPath } from "../utils.js"; import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js"; import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js"; @@ -27,7 +26,7 @@ export type EmbeddingProviderResult = { }; export type EmbeddingProviderOptions = { - config: MoltbotConfig; + config: OpenClawConfig; agentDir?: string; provider: "openai" | "local" | "gemini" | "auto"; remote?: { @@ -47,8 +46,12 @@ const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma function canAutoSelectLocal(options: EmbeddingProviderOptions): boolean { const modelPath = options.local?.modelPath?.trim(); - if (!modelPath) return false; - if (/^(hf:|https?:)/i.test(modelPath)) return false; + if (!modelPath) { + return false; + } + if (/^(hf:|https?:)/i.test(modelPath)) { + return false; + } const resolved = resolveUserPath(modelPath); try { return fsSync.statSync(resolved).isFile(); @@ -95,14 +98,14 @@ async function createLocalEmbeddingProvider( embedQuery: async (text) => { const ctx = await ensureContext(); const embedding = await ctx.getEmbeddingFor(text); - return Array.from(embedding.vector) as number[]; + return Array.from(embedding.vector); }, embedBatch: async (texts) => { const ctx = await ensureContext(); const embeddings = await Promise.all( texts.map(async (text) => { const embedding = await ctx.getEmbeddingFor(text); - return Array.from(embedding.vector) as number[]; + return Array.from(embedding.vector); }), ); return embeddings; @@ -155,7 +158,7 @@ export async function createEmbeddingProvider( missingKeyErrors.push(message); continue; } - throw new Error(message); + throw new Error(message, { cause: err }); } } @@ -181,20 +184,28 @@ export async function createEmbeddingProvider( fallbackReason: reason, }; } catch (fallbackErr) { - throw new Error(`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`); + // oxlint-disable-next-line preserve-caught-error + throw new Error( + `${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`, + { cause: fallbackErr }, + ); } } - throw new Error(reason); + throw new Error(reason, { cause: primaryErr }); } } function formatError(err: unknown): string { - if (err instanceof Error) return err.message; + if (err instanceof Error) { + return err.message; + } return String(err); } function isNodeLlamaCppMissing(err: unknown): boolean { - if (!(err instanceof Error)) return false; + if (!(err instanceof Error)) { + return false; + } const code = (err as Error & { code?: unknown }).code; if (code === "ERR_MODULE_NOT_FOUND") { return err.message.includes("node-llama-cpp"); @@ -216,7 +227,7 @@ function formatLocalSetupError(err: unknown): string { "To enable local embeddings:", "1) Use Node 22 LTS (recommended for installs/updates)", missing - ? "2) Reinstall Moltbot (this should install node-llama-cpp): npm i -g moltbot@latest" + ? "2) Reinstall OpenClaw (this should install node-llama-cpp): npm i -g openclaw@latest" : null, "3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp", 'Or set agents.defaults.memorySearch.provider = "openai" (remote).', diff --git a/src/memory/headers-fingerprint.ts b/src/memory/headers-fingerprint.ts index 918500285..122ba074a 100644 --- a/src/memory/headers-fingerprint.ts +++ b/src/memory/headers-fingerprint.ts @@ -3,11 +3,15 @@ function normalizeHeaderName(name: string): string { } export function fingerprintHeaderNames(headers: Record | undefined): string[] { - if (!headers) return []; + if (!headers) { + return []; + } const out: string[] = []; for (const key of Object.keys(headers)) { const normalized = normalizeHeaderName(key); - if (!normalized) continue; + if (!normalized) { + continue; + } out.push(normalized); } out.sort((a, b) => a.localeCompare(b)); diff --git a/src/memory/hybrid.test.ts b/src/memory/hybrid.test.ts index 294dc9950..7105e9ecf 100644 --- a/src/memory/hybrid.test.ts +++ b/src/memory/hybrid.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { bm25RankToScore, buildFtsQuery, mergeHybridResults } from "./hybrid.js"; describe("memory hybrid helpers", () => { diff --git a/src/memory/hybrid.ts b/src/memory/hybrid.ts index 753748bf9..1dd7c9fda 100644 --- a/src/memory/hybrid.ts +++ b/src/memory/hybrid.ts @@ -26,7 +26,9 @@ export function buildFtsQuery(raw: string): string | null { .match(/[A-Za-z0-9_]+/g) ?.map((t) => t.trim()) .filter(Boolean) ?? []; - if (tokens.length === 0) return null; + if (tokens.length === 0) { + return null; + } const quoted = tokens.map((t) => `"${t.replaceAll('"', "")}"`); return quoted.join(" AND "); } @@ -80,7 +82,9 @@ export function mergeHybridResults(params: { const existing = byId.get(r.id); if (existing) { existing.textScore = r.textScore; - if (r.snippet && r.snippet.length > 0) existing.snippet = r.snippet; + if (r.snippet && r.snippet.length > 0) { + existing.snippet = r.snippet; + } } else { byId.set(r.id, { id: r.id, @@ -107,5 +111,5 @@ export function mergeHybridResults(params: { }; }); - return merged.sort((a, b) => b.score - a.score); + return merged.toSorted((a, b) => b.score - a.score); } diff --git a/src/memory/index.test.ts b/src/memory/index.test.ts index 58a98e580..3f01ab855 100644 --- a/src/memory/index.test.ts +++ b/src/memory/index.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; let embedBatchCalls = 0; @@ -43,7 +41,7 @@ describe("memory index", () => { beforeEach(async () => { embedBatchCalls = 0; failEmbeddings = false; - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-mem-")); + workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); await fs.writeFile( @@ -79,7 +77,9 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await result.manager.sync({ force: true }); const results = await result.manager.search("alpha"); @@ -130,7 +130,9 @@ describe("memory index", () => { agentId: "main", }); expect(first.manager).not.toBeNull(); - if (!first.manager) throw new Error("manager missing"); + if (!first.manager) { + throw new Error("manager missing"); + } await first.manager.sync({ force: true }); await first.manager.close(); @@ -151,7 +153,9 @@ describe("memory index", () => { agentId: "main", }); expect(second.manager).not.toBeNull(); - if (!second.manager) throw new Error("manager missing"); + if (!second.manager) { + throw new Error("manager missing"); + } manager = second.manager; await second.manager.sync({ reason: "test" }); const results = await second.manager.search("alpha"); @@ -177,7 +181,9 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); const afterFirst = embedBatchCalls; @@ -206,7 +212,9 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); @@ -245,11 +253,15 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const status = manager.status(); - if (!status.fts?.available) return; + if (!status.fts?.available) { + return; + } await manager.sync({ force: true }); const results = await manager.search("zebra"); @@ -294,11 +306,15 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const status = manager.status(); - if (!status.fts?.available) return; + if (!status.fts?.available) { + return; + } await manager.sync({ force: true }); const results = await manager.search("alpha beta id123"); @@ -348,11 +364,15 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const status = manager.status(); - if (!status.fts?.available) return; + if (!status.fts?.available) { + return; + } await manager.sync({ force: true }); const results = await manager.search("alpha beta id123"); @@ -382,7 +402,9 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const available = await result.manager.probeVectorAvailability(); const status = result.manager.status(); @@ -408,8 +430,60 @@ describe("memory index", () => { }; const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await expect(result.manager.readFile({ relPath: "NOTES.md" })).rejects.toThrow("path required"); }); + + it("allows reading from additional memory paths and blocks symlinks", async () => { + const extraDir = path.join(workspaceDir, "extra"); + await fs.mkdir(extraDir, { recursive: true }); + await fs.writeFile(path.join(extraDir, "extra.md"), "Extra content."); + + const cfg = { + agents: { + defaults: { + workspace: workspaceDir, + memorySearch: { + provider: "openai", + model: "mock-embed", + store: { path: indexPath }, + sync: { watch: false, onSessionStart: false, onSearch: true }, + extraPaths: [extraDir], + }, + }, + list: [{ id: "main", default: true }], + }, + }; + const result = await getMemorySearchManager({ cfg, agentId: "main" }); + expect(result.manager).not.toBeNull(); + if (!result.manager) { + throw new Error("manager missing"); + } + manager = result.manager; + await expect(result.manager.readFile({ relPath: "extra/extra.md" })).resolves.toEqual({ + path: "extra/extra.md", + text: "Extra content.", + }); + + const linkPath = path.join(extraDir, "linked.md"); + let symlinkOk = true; + try { + await fs.symlink(path.join(extraDir, "extra.md"), linkPath, "file"); + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code === "EPERM" || code === "EACCES") { + symlinkOk = false; + } else { + throw err; + } + } + if (symlinkOk) { + await expect(result.manager.readFile({ relPath: "extra/linked.md" })).rejects.toThrow( + "path required", + ); + } + }); }); diff --git a/src/memory/internal.test.ts b/src/memory/internal.test.ts index 29c698779..0f5199892 100644 --- a/src/memory/internal.test.ts +++ b/src/memory/internal.test.ts @@ -1,6 +1,115 @@ -import { describe, expect, it } from "vitest"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { chunkMarkdown, listMemoryFiles, normalizeExtraMemoryPaths } from "./internal.js"; -import { chunkMarkdown } from "./internal.js"; +describe("normalizeExtraMemoryPaths", () => { + it("trims, resolves, and dedupes paths", () => { + const workspaceDir = path.join(os.tmpdir(), "memory-test-workspace"); + const absPath = path.resolve(path.sep, "shared-notes"); + const result = normalizeExtraMemoryPaths(workspaceDir, [ + " notes ", + "./notes", + absPath, + absPath, + "", + ]); + expect(result).toEqual([path.resolve(workspaceDir, "notes"), absPath]); + }); +}); + +describe("listMemoryFiles", () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-test-")); + }); + + afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + it("includes files from additional paths (directory)", async () => { + await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory"); + const extraDir = path.join(tmpDir, "extra-notes"); + await fs.mkdir(extraDir, { recursive: true }); + await fs.writeFile(path.join(extraDir, "note1.md"), "# Note 1"); + await fs.writeFile(path.join(extraDir, "note2.md"), "# Note 2"); + await fs.writeFile(path.join(extraDir, "ignore.txt"), "Not a markdown file"); + + const files = await listMemoryFiles(tmpDir, [extraDir]); + expect(files).toHaveLength(3); + expect(files.some((file) => file.endsWith("MEMORY.md"))).toBe(true); + expect(files.some((file) => file.endsWith("note1.md"))).toBe(true); + expect(files.some((file) => file.endsWith("note2.md"))).toBe(true); + expect(files.some((file) => file.endsWith("ignore.txt"))).toBe(false); + }); + + it("includes files from additional paths (single file)", async () => { + await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory"); + const singleFile = path.join(tmpDir, "standalone.md"); + await fs.writeFile(singleFile, "# Standalone"); + + const files = await listMemoryFiles(tmpDir, [singleFile]); + expect(files).toHaveLength(2); + expect(files.some((file) => file.endsWith("standalone.md"))).toBe(true); + }); + + it("handles relative paths in additional paths", async () => { + await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory"); + const extraDir = path.join(tmpDir, "subdir"); + await fs.mkdir(extraDir, { recursive: true }); + await fs.writeFile(path.join(extraDir, "nested.md"), "# Nested"); + + const files = await listMemoryFiles(tmpDir, ["subdir"]); + expect(files).toHaveLength(2); + expect(files.some((file) => file.endsWith("nested.md"))).toBe(true); + }); + + it("ignores non-existent additional paths", async () => { + await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory"); + + const files = await listMemoryFiles(tmpDir, ["/does/not/exist"]); + expect(files).toHaveLength(1); + }); + + it("ignores symlinked files and directories", async () => { + await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory"); + const extraDir = path.join(tmpDir, "extra"); + await fs.mkdir(extraDir, { recursive: true }); + await fs.writeFile(path.join(extraDir, "note.md"), "# Note"); + + const targetFile = path.join(tmpDir, "target.md"); + await fs.writeFile(targetFile, "# Target"); + const linkFile = path.join(extraDir, "linked.md"); + + const targetDir = path.join(tmpDir, "target-dir"); + await fs.mkdir(targetDir, { recursive: true }); + await fs.writeFile(path.join(targetDir, "nested.md"), "# Nested"); + const linkDir = path.join(tmpDir, "linked-dir"); + + let symlinksOk = true; + try { + await fs.symlink(targetFile, linkFile, "file"); + await fs.symlink(targetDir, linkDir, "dir"); + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code === "EPERM" || code === "EACCES") { + symlinksOk = false; + } else { + throw err; + } + } + + const files = await listMemoryFiles(tmpDir, [extraDir, linkDir]); + expect(files.some((file) => file.endsWith("note.md"))).toBe(true); + if (symlinksOk) { + expect(files.some((file) => file.endsWith("linked.md"))).toBe(false); + expect(files.some((file) => file.endsWith("nested.md"))).toBe(false); + } + }); +}); describe("chunkMarkdown", () => { it("splits overly long lines into max-sized chunks", () => { diff --git a/src/memory/internal.ts b/src/memory/internal.ts index b68570c35..cbdb7c6c6 100644 --- a/src/memory/internal.ts +++ b/src/memory/internal.ts @@ -30,47 +30,103 @@ export function normalizeRelPath(value: string): string { return trimmed.replace(/\\/g, "/"); } -export function isMemoryPath(relPath: string): boolean { - const normalized = normalizeRelPath(relPath); - if (!normalized) return false; - if (normalized === "MEMORY.md" || normalized === "memory.md") return true; - return normalized.startsWith("memory/"); +export function normalizeExtraMemoryPaths(workspaceDir: string, extraPaths?: string[]): string[] { + if (!extraPaths?.length) { + return []; + } + const resolved = extraPaths + .map((value) => value.trim()) + .filter(Boolean) + .map((value) => + path.isAbsolute(value) ? path.resolve(value) : path.resolve(workspaceDir, value), + ); + return Array.from(new Set(resolved)); } -async function exists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { +export function isMemoryPath(relPath: string): boolean { + const normalized = normalizeRelPath(relPath); + if (!normalized) { return false; } + if (normalized === "MEMORY.md" || normalized === "memory.md") { + return true; + } + return normalized.startsWith("memory/"); } async function walkDir(dir: string, files: string[]) { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const full = path.join(dir, entry.name); + if (entry.isSymbolicLink()) { + continue; + } if (entry.isDirectory()) { await walkDir(full, files); continue; } - if (!entry.isFile()) continue; - if (!entry.name.endsWith(".md")) continue; + if (!entry.isFile()) { + continue; + } + if (!entry.name.endsWith(".md")) { + continue; + } files.push(full); } } -export async function listMemoryFiles(workspaceDir: string): Promise { +export async function listMemoryFiles( + workspaceDir: string, + extraPaths?: string[], +): Promise { const result: string[] = []; const memoryFile = path.join(workspaceDir, "MEMORY.md"); const altMemoryFile = path.join(workspaceDir, "memory.md"); - if (await exists(memoryFile)) result.push(memoryFile); - if (await exists(altMemoryFile)) result.push(altMemoryFile); const memoryDir = path.join(workspaceDir, "memory"); - if (await exists(memoryDir)) { - await walkDir(memoryDir, result); + + const addMarkdownFile = async (absPath: string) => { + try { + const stat = await fs.lstat(absPath); + if (stat.isSymbolicLink() || !stat.isFile()) { + return; + } + if (!absPath.endsWith(".md")) { + return; + } + result.push(absPath); + } catch {} + }; + + await addMarkdownFile(memoryFile); + await addMarkdownFile(altMemoryFile); + try { + const dirStat = await fs.lstat(memoryDir); + if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) { + await walkDir(memoryDir, result); + } + } catch {} + + const normalizedExtraPaths = normalizeExtraMemoryPaths(workspaceDir, extraPaths); + if (normalizedExtraPaths.length > 0) { + for (const inputPath of normalizedExtraPaths) { + try { + const stat = await fs.lstat(inputPath); + if (stat.isSymbolicLink()) { + continue; + } + if (stat.isDirectory()) { + await walkDir(inputPath, result); + continue; + } + if (stat.isFile() && inputPath.endsWith(".md")) { + result.push(inputPath); + } + } catch {} + } + } + if (result.length <= 1) { + return result; } - if (result.length <= 1) return result; const seen = new Set(); const deduped: string[] = []; for (const entry of result) { @@ -78,7 +134,9 @@ export async function listMemoryFiles(workspaceDir: string): Promise { try { key = await fs.realpath(entry); } catch {} - if (seen.has(key)) continue; + if (seen.has(key)) { + continue; + } seen.add(key); deduped.push(entry); } @@ -110,7 +168,9 @@ export function chunkMarkdown( chunking: { tokens: number; overlap: number }, ): MemoryChunk[] { const lines = content.split("\n"); - if (lines.length === 0) return []; + if (lines.length === 0) { + return []; + } const maxChars = Math.max(32, chunking.tokens * 4); const overlapChars = Math.max(0, chunking.overlap * 4); const chunks: MemoryChunk[] = []; @@ -119,10 +179,14 @@ export function chunkMarkdown( let currentChars = 0; const flush = () => { - if (current.length === 0) return; + if (current.length === 0) { + return; + } const firstEntry = current[0]; const lastEntry = current[current.length - 1]; - if (!firstEntry || !lastEntry) return; + if (!firstEntry || !lastEntry) { + return; + } const text = current.map((entry) => entry.line).join("\n"); const startLine = firstEntry.lineNo; const endLine = lastEntry.lineNo; @@ -144,10 +208,14 @@ export function chunkMarkdown( const kept: Array<{ line: string; lineNo: number }> = []; for (let i = current.length - 1; i >= 0; i -= 1) { const entry = current[i]; - if (!entry) continue; + if (!entry) { + continue; + } acc += entry.line.length + 1; kept.unshift(entry); - if (acc >= overlapChars) break; + if (acc >= overlapChars) { + break; + } } current = kept; currentChars = kept.reduce((sum, entry) => sum + entry.line.length + 1, 0); @@ -188,7 +256,9 @@ export function parseEmbedding(raw: string): number[] { } export function cosineSimilarity(a: number[], b: number[]): number { - if (a.length === 0 || b.length === 0) return 0; + if (a.length === 0 || b.length === 0) { + return 0; + } const len = Math.min(a.length, b.length); let dot = 0; let normA = 0; @@ -200,6 +270,8 @@ export function cosineSimilarity(a: number[], b: number[]): number { normA += av * av; normB += bv * bv; } - if (normA === 0 || normB === 0) return 0; + if (normA === 0 || normB === 0) { + return 0; + } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } diff --git a/src/memory/manager-cache-key.ts b/src/memory/manager-cache-key.ts index 9fbe3e436..0ab15a137 100644 --- a/src/memory/manager-cache-key.ts +++ b/src/memory/manager-cache-key.ts @@ -1,7 +1,6 @@ import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js"; - -import { hashText } from "./internal.js"; import { fingerprintHeaderNames } from "./headers-fingerprint.js"; +import { hashText } from "./internal.js"; export function computeMemoryManagerCacheKey(params: { agentId: string; @@ -12,7 +11,8 @@ export function computeMemoryManagerCacheKey(params: { const fingerprint = hashText( JSON.stringify({ enabled: settings.enabled, - sources: [...settings.sources].sort((a, b) => a.localeCompare(b)), + sources: [...settings.sources].toSorted((a, b) => a.localeCompare(b)), + extraPaths: [...settings.extraPaths].toSorted((a, b) => a.localeCompare(b)), provider: settings.provider, model: settings.model, fallback: settings.fallback, diff --git a/src/memory/manager-search.ts b/src/memory/manager-search.ts index f065a96a5..f77751a61 100644 --- a/src/memory/manager-search.ts +++ b/src/memory/manager-search.ts @@ -1,5 +1,4 @@ import type { DatabaseSync } from "node:sqlite"; - import { truncateUtf16Safe } from "../utils.js"; import { cosineSimilarity, parseEmbedding } from "./internal.js"; @@ -29,7 +28,9 @@ export async function searchVector(params: { sourceFilterVec: { sql: string; params: SearchSource[] }; sourceFilterChunks: { sql: string; params: SearchSource[] }; }): Promise { - if (params.queryVec.length === 0 || params.limit <= 0) return []; + if (params.queryVec.length === 0 || params.limit <= 0) { + return []; + } if (await params.ensureVectorReady(params.queryVec.length)) { const rows = params.db .prepare( @@ -79,7 +80,7 @@ export async function searchVector(params: { })) .filter((entry) => Number.isFinite(entry.score)); return scored - .sort((a, b) => b.score - a.score) + .toSorted((a, b) => b.score - a.score) .slice(0, params.limit) .map((entry) => ({ id: entry.chunk.id, @@ -143,9 +144,13 @@ export async function searchKeyword(params: { buildFtsQuery: (raw: string) => string | null; bm25RankToScore: (rank: number) => number; }): Promise> { - if (params.limit <= 0) return []; + if (params.limit <= 0) { + return []; + } const ftsQuery = params.buildFtsQuery(params.query); - if (!ftsQuery) return []; + if (!ftsQuery) { + return []; + } const rows = params.db .prepare( diff --git a/src/memory/manager.async-search.test.ts b/src/memory/manager.async-search.test.ts index 4d5ac61dd..7f60ef0ea 100644 --- a/src/memory/manager.async-search.test.ts +++ b/src/memory/manager.async-search.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; const embedBatch = vi.fn(async () => []); @@ -32,7 +30,7 @@ describe("memory search async sync", () => { let manager: MemoryIndexManager | null = null; beforeEach(async () => { - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-mem-async-")); + workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-async-")); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); await fs.writeFile(path.join(workspaceDir, "memory", "2026-01-07.md"), "hello\n"); @@ -67,7 +65,9 @@ describe("memory search async sync", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const pending = new Promise(() => {}); diff --git a/src/memory/manager.atomic-reindex.test.ts b/src/memory/manager.atomic-reindex.test.ts index 76147cb2b..4f4f0dc32 100644 --- a/src/memory/manager.atomic-reindex.test.ts +++ b/src/memory/manager.atomic-reindex.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; let shouldFail = false; @@ -43,7 +41,7 @@ describe("memory manager atomic reindex", () => { beforeEach(async () => { shouldFail = false; - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-mem-")); + workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Hello memory."); @@ -76,7 +74,9 @@ describe("memory manager atomic reindex", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); diff --git a/src/memory/manager.batch.test.ts b/src/memory/manager.batch.test.ts index 31327cbc8..60586d2ec 100644 --- a/src/memory/manager.batch.test.ts +++ b/src/memory/manager.batch.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; const embedBatch = vi.fn(async () => []); @@ -50,7 +48,7 @@ describe("memory indexing with OpenAI batches", () => { } return realSetTimeout(handler, delay, ...args); }) as typeof setTimeout); - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-mem-batch-")); + workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-batch-")); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); }); @@ -79,7 +77,9 @@ describe("memory indexing with OpenAI batches", () => { throw new Error("expected FormData upload"); } for (const [key, value] of body.entries()) { - if (key !== "file") continue; + if (key !== "file") { + continue; + } if (typeof value === "string") { uploadedRequests = value .split("\n") @@ -149,13 +149,17 @@ describe("memory indexing with OpenAI batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const labels: string[] = []; await manager.sync({ force: true, progress: (update) => { - if (update.label) labels.push(update.label); + if (update.label) { + labels.push(update.label); + } }, }); @@ -181,7 +185,9 @@ describe("memory indexing with OpenAI batches", () => { throw new Error("expected FormData upload"); } for (const [key, value] of body.entries()) { - if (key !== "file") continue; + if (key !== "file") { + continue; + } if (typeof value === "string") { uploadedRequests = value .split("\n") @@ -255,7 +261,9 @@ describe("memory indexing with OpenAI batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); @@ -279,7 +287,9 @@ describe("memory indexing with OpenAI batches", () => { throw new Error("expected FormData upload"); } for (const [key, value] of body.entries()) { - if (key !== "file") continue; + if (key !== "file") { + continue; + } if (typeof value === "string") { uploadedRequests = value .split("\n") @@ -352,7 +362,9 @@ describe("memory indexing with OpenAI batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); @@ -388,7 +400,9 @@ describe("memory indexing with OpenAI batches", () => { throw new Error("expected FormData upload"); } for (const [key, value] of body.entries()) { - if (key !== "file") continue; + if (key !== "file") { + continue; + } if (typeof value === "string") { uploadedRequests = value .split("\n") @@ -449,7 +463,9 @@ describe("memory indexing with OpenAI batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); diff --git a/src/memory/manager.embedding-batches.test.ts b/src/memory/manager.embedding-batches.test.ts index a7d408424..3c4019d36 100644 --- a/src/memory/manager.embedding-batches.test.ts +++ b/src/memory/manager.embedding-batches.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; const embedBatch = vi.fn(async (texts: string[]) => texts.map(() => [0, 1, 0])); @@ -29,7 +27,7 @@ describe("memory embedding batches", () => { beforeEach(async () => { embedBatch.mockClear(); embedQuery.mockClear(); - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-mem-")); + workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); }); @@ -66,7 +64,9 @@ describe("memory embedding batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); @@ -100,7 +100,9 @@ describe("memory embedding batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); @@ -131,7 +133,9 @@ describe("memory embedding batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const updates: Array<{ completed: number; total: number; label?: string }> = []; await manager.sync({ @@ -194,7 +198,9 @@ describe("memory embedding batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; try { await manager.sync({ force: true }); @@ -251,7 +257,9 @@ describe("memory embedding batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; try { await manager.sync({ force: true }); @@ -283,7 +291,9 @@ describe("memory embedding batches", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; await manager.sync({ force: true }); diff --git a/src/memory/manager.sync-errors-do-not-crash.test.ts b/src/memory/manager.sync-errors-do-not-crash.test.ts index 7f7e3c270..faa56cc11 100644 --- a/src/memory/manager.sync-errors-do-not-crash.test.ts +++ b/src/memory/manager.sync-errors-do-not-crash.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; vi.mock("chokidar", () => ({ @@ -38,7 +36,7 @@ describe("memory manager sync failures", () => { beforeEach(async () => { vi.useFakeTimers(); - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-mem-")); + workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Hello"); @@ -77,7 +75,9 @@ describe("memory manager sync failures", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const syncSpy = vi.spyOn(manager, "sync"); diff --git a/src/memory/manager.ts b/src/memory/manager.ts index 9a9991d10..684a460b8 100644 --- a/src/memory/manager.ts +++ b/src/memory/manager.ts @@ -1,18 +1,25 @@ -import { randomUUID } from "node:crypto"; -import fs from "node:fs/promises"; -import path from "node:path"; - import type { DatabaseSync } from "node:sqlite"; import chokidar, { type FSWatcher } from "chokidar"; - -import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; +import { randomUUID } from "node:crypto"; +import fsSync from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; import { resolveMemorySearchConfig } from "../agents/memory-search.js"; -import type { MoltbotConfig } from "../config/config.js"; import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js"; import { resolveUserPath } from "../utils.js"; +import { runGeminiEmbeddingBatches, type GeminiBatchRequest } from "./batch-gemini.js"; +import { + OPENAI_BATCH_ENDPOINT, + type OpenAiBatchRequest, + runOpenAiEmbeddingBatches, +} from "./batch-openai.js"; +import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js"; +import { DEFAULT_OPENAI_EMBEDDING_MODEL } from "./embeddings-openai.js"; import { createEmbeddingProvider, type EmbeddingProvider, @@ -20,14 +27,7 @@ import { type GeminiEmbeddingClient, type OpenAiEmbeddingClient, } from "./embeddings.js"; -import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js"; -import { DEFAULT_OPENAI_EMBEDDING_MODEL } from "./embeddings-openai.js"; -import { - OPENAI_BATCH_ENDPOINT, - type OpenAiBatchRequest, - runOpenAiEmbeddingBatches, -} from "./batch-openai.js"; -import { runGeminiEmbeddingBatches, type GeminiBatchRequest } from "./batch-gemini.js"; +import { bm25RankToScore, buildFtsQuery, mergeHybridResults } from "./hybrid.js"; import { buildFileEntry, chunkMarkdown, @@ -35,16 +35,15 @@ import { hashText, isMemoryPath, listMemoryFiles, + normalizeExtraMemoryPaths, type MemoryChunk, type MemoryFileEntry, - normalizeRelPath, parseEmbedding, } from "./internal.js"; -import { bm25RankToScore, buildFtsQuery, mergeHybridResults } from "./hybrid.js"; import { searchKeyword, searchVector } from "./manager-search.js"; import { ensureMemoryIndexSchema } from "./memory-schema.js"; -import { requireNodeSqlite } from "./sqlite.js"; import { loadSqliteVecExtension } from "./sqlite-vec.js"; +import { requireNodeSqlite } from "./sqlite.js"; type MemorySource = "memory" | "sessions"; @@ -117,7 +116,7 @@ const vectorToBlob = (embedding: number[]): Buffer => export class MemoryIndexManager { private readonly cacheKey: string; - private readonly cfg: MoltbotConfig; + private readonly cfg: OpenClawConfig; private readonly agentId: string; private readonly workspaceDir: string; private readonly settings: ResolvedMemorySearchConfig; @@ -173,16 +172,20 @@ export class MemoryIndexManager { private syncing: Promise | null = null; static async get(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentId: string; }): Promise { const { cfg, agentId } = params; const settings = resolveMemorySearchConfig(cfg, agentId); - if (!settings) return null; + if (!settings) { + return null; + } const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}`; const existing = INDEX_CACHE.get(key); - if (existing) return existing; + if (existing) { + return existing; + } const providerResult = await createEmbeddingProvider({ config: cfg, agentDir: resolveAgentDir(cfg, agentId), @@ -206,7 +209,7 @@ export class MemoryIndexManager { private constructor(params: { cacheKey: string; - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentId: string; workspaceDir: string; settings: ResolvedMemorySearchConfig; @@ -249,13 +252,19 @@ export class MemoryIndexManager { } async warmSession(sessionKey?: string): Promise { - if (!this.settings.sync.onSessionStart) return; + if (!this.settings.sync.onSessionStart) { + return; + } const key = sessionKey?.trim() || ""; - if (key && this.sessionWarm.has(key)) return; + if (key && this.sessionWarm.has(key)) { + return; + } void this.sync({ reason: "session-start" }).catch((err) => { log.warn(`memory sync failed (session-start): ${String(err)}`); }); - if (key) this.sessionWarm.add(key); + if (key) { + this.sessionWarm.add(key); + } } async search( @@ -273,7 +282,9 @@ export class MemoryIndexManager { }); } const cleaned = query.trim(); - if (!cleaned) return []; + if (!cleaned) { + return []; + } const minScore = opts?.minScore ?? this.settings.query.minScore; const maxResults = opts?.maxResults ?? this.settings.query.maxResults; const hybrid = this.settings.query.hybrid; @@ -332,7 +343,9 @@ export class MemoryIndexManager { query: string, limit: number, ): Promise> { - if (!this.fts.enabled || !this.fts.available) return []; + if (!this.fts.enabled || !this.fts.available) { + return []; + } const sourceFilter = this.buildSourceFilter(); const results = await searchKeyword({ db: this.db, @@ -384,7 +397,9 @@ export class MemoryIndexManager { force?: boolean; progress?: (update: MemorySyncProgressUpdate) => void; }): Promise { - if (this.syncing) return this.syncing; + if (this.syncing) { + return this.syncing; + } this.syncing = this.runSync(params).finally(() => { this.syncing = null; }); @@ -396,13 +411,54 @@ export class MemoryIndexManager { from?: number; lines?: number; }): Promise<{ text: string; path: string }> { - const relPath = normalizeRelPath(params.relPath); - if (!relPath || !isMemoryPath(relPath)) { + const rawPath = params.relPath.trim(); + if (!rawPath) { throw new Error("path required"); } - const absPath = path.resolve(this.workspaceDir, relPath); - if (!absPath.startsWith(this.workspaceDir)) { - throw new Error("path escapes workspace"); + const absPath = path.isAbsolute(rawPath) + ? path.resolve(rawPath) + : path.resolve(this.workspaceDir, rawPath); + const relPath = path.relative(this.workspaceDir, absPath).replace(/\\/g, "/"); + const inWorkspace = + relPath.length > 0 && !relPath.startsWith("..") && !path.isAbsolute(relPath); + const allowedWorkspace = inWorkspace && isMemoryPath(relPath); + let allowedAdditional = false; + if (!allowedWorkspace && this.settings.extraPaths.length > 0) { + const additionalPaths = normalizeExtraMemoryPaths( + this.workspaceDir, + this.settings.extraPaths, + ); + for (const additionalPath of additionalPaths) { + try { + const stat = await fs.lstat(additionalPath); + if (stat.isSymbolicLink()) { + continue; + } + if (stat.isDirectory()) { + if (absPath === additionalPath || absPath.startsWith(`${additionalPath}${path.sep}`)) { + allowedAdditional = true; + break; + } + continue; + } + if (stat.isFile()) { + if (absPath === additionalPath && absPath.endsWith(".md")) { + allowedAdditional = true; + break; + } + } + } catch {} + } + } + if (!allowedWorkspace && !allowedAdditional) { + throw new Error("path required"); + } + if (!absPath.endsWith(".md")) { + throw new Error("path required"); + } + const stat = await fs.lstat(absPath); + if (stat.isSymbolicLink() || !stat.isFile()) { + throw new Error("path required"); } const content = await fs.readFile(absPath, "utf-8"); if (!params.from && !params.lines) { @@ -425,6 +481,7 @@ export class MemoryIndexManager { model: string; requestedProvider: string; sources: MemorySource[]; + extraPaths: string[]; sourceCounts: Array<{ source: MemorySource; files: number; chunks: number }>; cache?: { enabled: boolean; entries?: number; maxEntries?: number }; fts?: { enabled: boolean; available: boolean; error?: string }; @@ -461,7 +518,9 @@ export class MemoryIndexManager { }; const sourceCounts = (() => { const sources = Array.from(this.sources); - if (sources.length === 0) return []; + if (sources.length === 0) { + return []; + } const bySource = new Map(); for (const source of sources) { bySource.set(source, { files: 0, chunks: 0 }); @@ -486,7 +545,7 @@ export class MemoryIndexManager { entry.chunks = row.c ?? 0; bySource.set(row.source, entry); } - return sources.map((source) => ({ source, ...bySource.get(source)! })); + return sources.map((source) => Object.assign({ source }, bySource.get(source)!)); })(); return { files: files?.c ?? 0, @@ -498,6 +557,7 @@ export class MemoryIndexManager { model: this.provider.model, requestedProvider: this.requestedProvider, sources: Array.from(this.sources), + extraPaths: this.settings.extraPaths, sourceCounts, cache: this.cache.enabled ? { @@ -541,7 +601,9 @@ export class MemoryIndexManager { } async probeVectorAvailability(): Promise { - if (!this.vector.enabled) return false; + if (!this.vector.enabled) { + return false; + } return this.ensureVectorReady(); } @@ -556,7 +618,9 @@ export class MemoryIndexManager { } async close(): Promise { - if (this.closed) return; + if (this.closed) { + return; + } this.closed = true; if (this.watchTimer) { clearTimeout(this.watchTimer); @@ -583,7 +647,9 @@ export class MemoryIndexManager { } private async ensureVectorReady(dimensions?: number): Promise { - if (!this.vector.enabled) return false; + if (!this.vector.enabled) { + return false; + } if (!this.vectorReady) { this.vectorReady = this.withTimeout( this.loadVectorExtension(), @@ -609,7 +675,9 @@ export class MemoryIndexManager { } private async loadVectorExtension(): Promise { - if (this.vector.available !== null) return this.vector.available; + if (this.vector.available !== null) { + return this.vector.available; + } if (!this.vector.enabled) { this.vector.available = false; return false; @@ -619,7 +687,9 @@ export class MemoryIndexManager { ? resolveUserPath(this.vector.extensionPath) : undefined; const loaded = await loadSqliteVecExtension({ db: this.db, extensionPath: resolvedPath }); - if (!loaded.ok) throw new Error(loaded.error ?? "unknown sqlite-vec load error"); + if (!loaded.ok) { + throw new Error(loaded.error ?? "unknown sqlite-vec load error"); + } this.vector.extensionPath = loaded.extensionPath; this.vector.available = true; return true; @@ -633,7 +703,9 @@ export class MemoryIndexManager { } private ensureVectorTable(dimensions: number): void { - if (this.vector.dims === dimensions) return; + if (this.vector.dims === dimensions) { + return; + } if (this.vector.dims && this.vector.dims !== dimensions) { this.dropVectorTable(); } @@ -657,7 +729,9 @@ export class MemoryIndexManager { private buildSourceFilter(alias?: string): { sql: string; params: MemorySource[] } { const sources = Array.from(this.sources); - if (sources.length === 0) return { sql: "", params: [] }; + if (sources.length === 0) { + return { sql: "", params: [] }; + } const column = alias ? `${alias}.source` : "source"; const placeholders = sources.map(() => "?").join(", "); return { sql: ` AND ${column} IN (${placeholders})`, params: sources }; @@ -676,7 +750,9 @@ export class MemoryIndexManager { } private seedEmbeddingCache(sourceDb: DatabaseSync): void { - if (!this.cache.enabled) return; + if (!this.cache.enabled) { + return; + } try { const rows = sourceDb .prepare( @@ -691,7 +767,9 @@ export class MemoryIndexManager { dims: number | null; updated_at: number; }>; - if (!rows.length) return; + if (!rows.length) { + return; + } const insert = this.db.prepare( `INSERT INTO ${EMBEDDING_CACHE_TABLE} (provider, model, provider_key, hash, embedding, dims, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) @@ -768,12 +846,26 @@ export class MemoryIndexManager { } private ensureWatcher() { - if (!this.sources.has("memory") || !this.settings.sync.watch || this.watcher) return; - const watchPaths = [ + if (!this.sources.has("memory") || !this.settings.sync.watch || this.watcher) { + return; + } + const additionalPaths = normalizeExtraMemoryPaths(this.workspaceDir, this.settings.extraPaths) + .map((entry) => { + try { + const stat = fsSync.lstatSync(entry); + return stat.isSymbolicLink() ? null : entry; + } catch { + return null; + } + }) + .filter((entry): entry is string => Boolean(entry)); + const watchPaths = new Set([ path.join(this.workspaceDir, "MEMORY.md"), + path.join(this.workspaceDir, "memory.md"), path.join(this.workspaceDir, "memory"), - ]; - this.watcher = chokidar.watch(watchPaths, { + ...additionalPaths, + ]); + this.watcher = chokidar.watch(Array.from(watchPaths), { ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: this.settings.sync.watchDebounceMs, @@ -790,18 +882,26 @@ export class MemoryIndexManager { } private ensureSessionListener() { - if (!this.sources.has("sessions") || this.sessionUnsubscribe) return; + if (!this.sources.has("sessions") || this.sessionUnsubscribe) { + return; + } this.sessionUnsubscribe = onSessionTranscriptUpdate((update) => { - if (this.closed) return; + if (this.closed) { + return; + } const sessionFile = update.sessionFile; - if (!this.isSessionFileForAgent(sessionFile)) return; + if (!this.isSessionFileForAgent(sessionFile)) { + return; + } this.scheduleSessionDirty(sessionFile); }); } private scheduleSessionDirty(sessionFile: string) { this.sessionPendingFiles.add(sessionFile); - if (this.sessionWatchTimer) return; + if (this.sessionWatchTimer) { + return; + } this.sessionWatchTimer = setTimeout(() => { this.sessionWatchTimer = null; void this.processSessionDeltaBatch().catch((err) => { @@ -811,13 +911,17 @@ export class MemoryIndexManager { } private async processSessionDeltaBatch(): Promise { - if (this.sessionPendingFiles.size === 0) return; + if (this.sessionPendingFiles.size === 0) { + return; + } const pending = Array.from(this.sessionPendingFiles); this.sessionPendingFiles.clear(); let shouldSync = false; for (const sessionFile of pending) { const delta = await this.updateSessionDelta(sessionFile); - if (!delta) continue; + if (!delta) { + continue; + } const bytesThreshold = delta.deltaBytes; const messagesThreshold = delta.deltaMessages; const bytesHit = @@ -826,7 +930,9 @@ export class MemoryIndexManager { messagesThreshold <= 0 ? delta.pendingMessages > 0 : delta.pendingMessages >= messagesThreshold; - if (!bytesHit && !messagesHit) continue; + if (!bytesHit && !messagesHit) { + continue; + } this.sessionsDirtyFiles.add(sessionFile); this.sessionsDirty = true; delta.pendingBytes = @@ -849,7 +955,9 @@ export class MemoryIndexManager { pendingMessages: number; } | null> { const thresholds = this.settings.sync.sessions; - if (!thresholds) return null; + if (!thresholds) { + return null; + } let stat: { size: number }; try { stat = await fs.stat(sessionFile); @@ -900,7 +1008,9 @@ export class MemoryIndexManager { } private async countNewlines(absPath: string, start: number, end: number): Promise { - if (end <= start) return 0; + if (end <= start) { + return 0; + } const handle = await fs.open(absPath, "r"); try { let offset = start; @@ -909,9 +1019,13 @@ export class MemoryIndexManager { while (offset < end) { const toRead = Math.min(buffer.length, end - offset); const { bytesRead } = await handle.read(buffer, 0, toRead, offset); - if (bytesRead <= 0) break; + if (bytesRead <= 0) { + break; + } for (let i = 0; i < bytesRead; i += 1) { - if (buffer[i] === 10) count += 1; + if (buffer[i] === 10) { + count += 1; + } } offset += bytesRead; } @@ -923,14 +1037,18 @@ export class MemoryIndexManager { private resetSessionDelta(absPath: string, size: number): void { const state = this.sessionDeltas.get(absPath); - if (!state) return; + if (!state) { + return; + } state.lastSize = size; state.pendingBytes = 0; state.pendingMessages = 0; } private isSessionFileForAgent(sessionFile: string): boolean { - if (!sessionFile) return false; + if (!sessionFile) { + return false; + } const sessionsDir = resolveSessionTranscriptsDirForAgent(this.agentId); const resolvedFile = path.resolve(sessionFile); const resolvedDir = path.resolve(sessionsDir); @@ -939,7 +1057,9 @@ export class MemoryIndexManager { private ensureIntervalSync() { const minutes = this.settings.sync.intervalMinutes; - if (!minutes || minutes <= 0 || this.intervalTimer) return; + if (!minutes || minutes <= 0 || this.intervalTimer) { + return; + } const ms = minutes * 60 * 1000; this.intervalTimer = setInterval(() => { void this.sync({ reason: "interval" }).catch((err) => { @@ -949,8 +1069,12 @@ export class MemoryIndexManager { } private scheduleWatchSync() { - if (!this.sources.has("memory") || !this.settings.sync.watch) return; - if (this.watchTimer) clearTimeout(this.watchTimer); + if (!this.sources.has("memory") || !this.settings.sync.watch) { + return; + } + if (this.watchTimer) { + clearTimeout(this.watchTimer); + } this.watchTimer = setTimeout(() => { this.watchTimer = null; void this.sync({ reason: "watch" }).catch((err) => { @@ -963,11 +1087,19 @@ export class MemoryIndexManager { params?: { reason?: string; force?: boolean }, needsFullReindex = false, ) { - if (!this.sources.has("sessions")) return false; - if (params?.force) return true; + if (!this.sources.has("sessions")) { + return false; + } + if (params?.force) { + return true; + } const reason = params?.reason; - if (reason === "session-start" || reason === "watch") return false; - if (needsFullReindex) return true; + if (reason === "session-start" || reason === "watch") { + return false; + } + if (needsFullReindex) { + return true; + } return this.sessionsDirty && this.sessionsDirtyFiles.size > 0; } @@ -975,7 +1107,7 @@ export class MemoryIndexManager { needsFullReindex: boolean; progress?: MemorySyncProgressState; }) { - const files = await listMemoryFiles(this.workspaceDir); + const files = await listMemoryFiles(this.workspaceDir, this.settings.extraPaths); const fileEntries = await Promise.all( files.map(async (file) => buildFileEntry(file, this.workspaceDir)), ); @@ -1024,7 +1156,9 @@ export class MemoryIndexManager { .prepare(`SELECT path FROM files WHERE source = ?`) .all("memory") as Array<{ path: string }>; for (const stale of staleRows) { - if (activePaths.has(stale.path)) continue; + if (activePaths.has(stale.path)) { + continue; + } this.db.prepare(`DELETE FROM files WHERE path = ? AND source = ?`).run(stale.path, "memory"); try { this.db @@ -1119,7 +1253,9 @@ export class MemoryIndexManager { .prepare(`SELECT path FROM files WHERE source = ?`) .all("sessions") as Array<{ path: string }>; for (const stale of staleRows) { - if (activePaths.has(stale.path)) continue; + if (activePaths.has(stale.path)) { + continue; + } this.db .prepare(`DELETE FROM files WHERE path = ? AND source = ?`) .run(stale.path, "sessions"); @@ -1151,7 +1287,9 @@ export class MemoryIndexManager { total: 0, label: undefined, report: (update) => { - if (update.label) state.label = update.label; + if (update.label) { + state.label = update.label; + } const label = update.total > 0 && state.label ? `${state.label} ${update.completed}/${update.total}` @@ -1262,8 +1400,12 @@ export class MemoryIndexManager { private async activateFallbackProvider(reason: string): Promise { const fallback = this.settings.fallback; - if (!fallback || fallback === "none" || fallback === this.provider.id) return false; - if (this.fallbackFrom) return false; + if (!fallback || fallback === "none" || fallback === this.provider.id) { + return false; + } + if (this.fallbackFrom) { + return false; + } const fallbackFrom = this.provider.id as "openai" | "gemini" | "local"; const fallbackModel = @@ -1415,7 +1557,9 @@ export class MemoryIndexManager { const row = this.db.prepare(`SELECT value FROM meta WHERE key = ?`).get(META_KEY) as | { value: string } | undefined; - if (!row?.value) return null; + if (!row?.value) { + return null; + } try { return JSON.parse(row.value) as MemoryIndexMeta; } catch { @@ -1462,16 +1606,26 @@ export class MemoryIndexManager { const normalized = this.normalizeSessionText(content); return normalized ? normalized : null; } - if (!Array.isArray(content)) return null; + if (!Array.isArray(content)) { + return null; + } const parts: string[] = []; for (const block of content) { - if (!block || typeof block !== "object") continue; + if (!block || typeof block !== "object") { + continue; + } const record = block as { type?: unknown; text?: unknown }; - if (record.type !== "text" || typeof record.text !== "string") continue; + if (record.type !== "text" || typeof record.text !== "string") { + continue; + } const normalized = this.normalizeSessionText(record.text); - if (normalized) parts.push(normalized); + if (normalized) { + parts.push(normalized); + } + } + if (parts.length === 0) { + return null; } - if (parts.length === 0) return null; return parts.join(" "); } @@ -1482,7 +1636,9 @@ export class MemoryIndexManager { const lines = raw.split("\n"); const collected: string[] = []; for (const line of lines) { - if (!line.trim()) continue; + if (!line.trim()) { + continue; + } let record: unknown; try { record = JSON.parse(line); @@ -1499,10 +1655,16 @@ export class MemoryIndexManager { const message = (record as { message?: unknown }).message as | { role?: unknown; content?: unknown } | undefined; - if (!message || typeof message.role !== "string") continue; - if (message.role !== "user" && message.role !== "assistant") continue; + if (!message || typeof message.role !== "string") { + continue; + } + if (message.role !== "user" && message.role !== "assistant") { + continue; + } const text = this.extractSessionText(message.content); - if (!text) continue; + if (!text) { + continue; + } const label = message.role === "user" ? "User" : "Assistant"; collected.push(`${label}: ${text}`); } @@ -1522,7 +1684,9 @@ export class MemoryIndexManager { } private estimateEmbeddingTokens(text: string): number { - if (!text) return 0; + if (!text) { + return 0; + } return Math.ceil(text.length / EMBEDDING_APPROX_CHARS_PER_TOKEN); } @@ -1555,17 +1719,27 @@ export class MemoryIndexManager { } private loadEmbeddingCache(hashes: string[]): Map { - if (!this.cache.enabled) return new Map(); - if (hashes.length === 0) return new Map(); + if (!this.cache.enabled) { + return new Map(); + } + if (hashes.length === 0) { + return new Map(); + } const unique: string[] = []; const seen = new Set(); for (const hash of hashes) { - if (!hash) continue; - if (seen.has(hash)) continue; + if (!hash) { + continue; + } + if (seen.has(hash)) { + continue; + } seen.add(hash); unique.push(hash); } - if (unique.length === 0) return new Map(); + if (unique.length === 0) { + return new Map(); + } const out = new Map(); const baseParams = [this.provider.id, this.provider.model, this.providerKey]; @@ -1587,8 +1761,12 @@ export class MemoryIndexManager { } private upsertEmbeddingCache(entries: Array<{ hash: string; embedding: number[] }>): void { - if (!this.cache.enabled) return; - if (entries.length === 0) return; + if (!this.cache.enabled) { + return; + } + if (entries.length === 0) { + return; + } const now = Date.now(); const stmt = this.db.prepare( `INSERT INTO ${EMBEDDING_CACHE_TABLE} (provider, model, provider_key, hash, embedding, dims, updated_at)\n` + @@ -1613,14 +1791,20 @@ export class MemoryIndexManager { } private pruneEmbeddingCacheIfNeeded(): void { - if (!this.cache.enabled) return; + if (!this.cache.enabled) { + return; + } const max = this.cache.maxEntries; - if (!max || max <= 0) return; + if (!max || max <= 0) { + return; + } const row = this.db.prepare(`SELECT COUNT(*) as c FROM ${EMBEDDING_CACHE_TABLE}`).get() as | { c: number } | undefined; const count = row?.c ?? 0; - if (count <= max) return; + if (count <= max) { + return; + } const excess = count - max; this.db .prepare( @@ -1635,7 +1819,9 @@ export class MemoryIndexManager { } private async embedChunksInBatches(chunks: MemoryChunk[]): Promise { - if (chunks.length === 0) return []; + if (chunks.length === 0) { + return []; + } const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash)); const embeddings: number[][] = Array.from({ length: chunks.length }, () => []); const missing: Array<{ index: number; chunk: MemoryChunk }> = []; @@ -1650,7 +1836,9 @@ export class MemoryIndexManager { } } - if (missing.length === 0) return embeddings; + if (missing.length === 0) { + return embeddings; + } const missingChunks = missing.map((m) => m.chunk); const batches = this.buildEmbeddingBatches(missingChunks); @@ -1676,7 +1864,7 @@ export class MemoryIndexManager { if (this.provider.id === "openai" && this.openAi) { const entries = Object.entries(this.openAi.headers) .filter(([key]) => key.toLowerCase() !== "authorization") - .sort(([a], [b]) => a.localeCompare(b)) + .toSorted(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => [key, value]); return hashText( JSON.stringify({ @@ -1693,7 +1881,7 @@ export class MemoryIndexManager { const lower = key.toLowerCase(); return lower !== "authorization" && lower !== "x-goog-api-key"; }) - .sort(([a], [b]) => a.localeCompare(b)) + .toSorted(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => [key, value]); return hashText( JSON.stringify({ @@ -1730,7 +1918,9 @@ export class MemoryIndexManager { if (!openAi) { return this.embedChunksInBatches(chunks); } - if (chunks.length === 0) return []; + if (chunks.length === 0) { + return []; + } const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash)); const embeddings: number[][] = Array.from({ length: chunks.length }, () => []); const missing: Array<{ index: number; chunk: MemoryChunk }> = []; @@ -1745,7 +1935,9 @@ export class MemoryIndexManager { } } - if (missing.length === 0) return embeddings; + if (missing.length === 0) { + return embeddings; + } const requests: OpenAiBatchRequest[] = []; const mapping = new Map(); @@ -1780,13 +1972,17 @@ export class MemoryIndexManager { }), fallback: async () => await this.embedChunksInBatches(chunks), }); - if (Array.isArray(batchResult)) return batchResult; + if (Array.isArray(batchResult)) { + return batchResult; + } const byCustomId = batchResult; const toCache: Array<{ hash: string; embedding: number[] }> = []; for (const [customId, embedding] of byCustomId.entries()) { const mapped = mapping.get(customId); - if (!mapped) continue; + if (!mapped) { + continue; + } embeddings[mapped.index] = embedding; toCache.push({ hash: mapped.hash, embedding }); } @@ -1803,7 +1999,9 @@ export class MemoryIndexManager { if (!gemini) { return this.embedChunksInBatches(chunks); } - if (chunks.length === 0) return []; + if (chunks.length === 0) { + return []; + } const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash)); const embeddings: number[][] = Array.from({ length: chunks.length }, () => []); const missing: Array<{ index: number; chunk: MemoryChunk }> = []; @@ -1818,7 +2016,9 @@ export class MemoryIndexManager { } } - if (missing.length === 0) return embeddings; + if (missing.length === 0) { + return embeddings; + } const requests: GeminiBatchRequest[] = []; const mapping = new Map(); @@ -1850,13 +2050,17 @@ export class MemoryIndexManager { }), fallback: async () => await this.embedChunksInBatches(chunks), }); - if (Array.isArray(batchResult)) return batchResult; + if (Array.isArray(batchResult)) { + return batchResult; + } const byCustomId = batchResult; const toCache: Array<{ hash: string; embedding: number[] }> = []; for (const [customId, embedding] of byCustomId.entries()) { const mapped = mapping.get(customId); - if (!mapped) continue; + if (!mapped) { + continue; + } embeddings[mapped.index] = embedding; toCache.push({ hash: mapped.hash, embedding }); } @@ -1865,7 +2069,9 @@ export class MemoryIndexManager { } private async embedBatchWithRetry(texts: string[]): Promise { - if (texts.length === 0) return []; + if (texts.length === 0) { + return []; + } let attempt = 0; let delayMs = EMBEDDING_RETRY_BASE_DELAY_MS; while (true) { @@ -1927,7 +2133,9 @@ export class MemoryIndexManager { timeoutMs: number, message: string, ): Promise { - if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return await promise; + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { + return await promise; + } let timer: NodeJS.Timeout | null = null; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); @@ -1935,12 +2143,16 @@ export class MemoryIndexManager { try { return (await Promise.race([promise, timeoutPromise])) as T; } finally { - if (timer) clearTimeout(timer); + if (timer) { + clearTimeout(timer); + } } } private async runWithConcurrency(tasks: Array<() => Promise>, limit: number): Promise { - if (tasks.length === 0) return []; + if (tasks.length === 0) { + return []; + } const resolvedLimit = Math.max(1, Math.min(limit, tasks.length)); const results: T[] = Array.from({ length: tasks.length }); let next = 0; @@ -1948,10 +2160,14 @@ export class MemoryIndexManager { const workers = Array.from({ length: resolvedLimit }, async () => { while (true) { - if (firstError) return; + if (firstError) { + return; + } const index = next; next += 1; - if (index >= tasks.length) return; + if (index >= tasks.length) { + return; + } try { results[index] = await tasks[index](); } catch (err) { @@ -1962,7 +2178,9 @@ export class MemoryIndexManager { }); await Promise.allSettled(workers); - if (firstError) throw firstError; + if (firstError) { + throw firstError; + } return results; } diff --git a/src/memory/manager.vector-dedupe.test.ts b/src/memory/manager.vector-dedupe.test.ts index d0872e492..eb15fb481 100644 --- a/src/memory/manager.vector-dedupe.test.ts +++ b/src/memory/manager.vector-dedupe.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; import { buildFileEntry } from "./internal.js"; @@ -27,7 +25,7 @@ describe("memory vector dedupe", () => { let manager: MemoryIndexManager | null = null; beforeEach(async () => { - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-mem-")); + workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Hello memory."); @@ -60,7 +58,9 @@ describe("memory vector dedupe", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); expect(result.manager).not.toBeNull(); - if (!result.manager) throw new Error("manager missing"); + if (!result.manager) { + throw new Error("manager missing"); + } manager = result.manager; const db = ( diff --git a/src/memory/memory-schema.ts b/src/memory/memory-schema.ts index 4667b428b..a537c35f1 100644 --- a/src/memory/memory-schema.ts +++ b/src/memory/memory-schema.ts @@ -89,6 +89,8 @@ function ensureColumn( definition: string, ): void { const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>; - if (rows.some((row) => row.name === column)) return; + if (rows.some((row) => row.name === column)) { + return; + } db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`); } diff --git a/src/memory/provider-key.ts b/src/memory/provider-key.ts index 09485c0f2..494e2445a 100644 --- a/src/memory/provider-key.ts +++ b/src/memory/provider-key.ts @@ -1,5 +1,5 @@ -import { hashText } from "./internal.js"; import { fingerprintHeaderNames } from "./headers-fingerprint.js"; +import { hashText } from "./internal.js"; export function computeEmbeddingProviderKey(params: { providerId: string; diff --git a/src/memory/search-manager.ts b/src/memory/search-manager.ts index 9bcd529f3..c4eed3229 100644 --- a/src/memory/search-manager.ts +++ b/src/memory/search-manager.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { MemoryIndexManager } from "./manager.js"; export type MemorySearchManagerResult = { @@ -7,7 +7,7 @@ export type MemorySearchManagerResult = { }; export async function getMemorySearchManager(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentId: string; }): Promise { try { diff --git a/src/memory/session-files.ts b/src/memory/session-files.ts index 82f8c1e09..1823e9669 100644 --- a/src/memory/session-files.ts +++ b/src/memory/session-files.ts @@ -1,6 +1,5 @@ import fs from "node:fs/promises"; import path from "node:path"; - import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { hashText } from "./internal.js"; @@ -46,16 +45,26 @@ export function extractSessionText(content: unknown): string | null { const normalized = normalizeSessionText(content); return normalized ? normalized : null; } - if (!Array.isArray(content)) return null; + if (!Array.isArray(content)) { + return null; + } const parts: string[] = []; for (const block of content) { - if (!block || typeof block !== "object") continue; + if (!block || typeof block !== "object") { + continue; + } const record = block as { type?: unknown; text?: unknown }; - if (record.type !== "text" || typeof record.text !== "string") continue; + if (record.type !== "text" || typeof record.text !== "string") { + continue; + } const normalized = normalizeSessionText(record.text); - if (normalized) parts.push(normalized); + if (normalized) { + parts.push(normalized); + } + } + if (parts.length === 0) { + return null; } - if (parts.length === 0) return null; return parts.join(" "); } @@ -66,7 +75,9 @@ export async function buildSessionEntry(absPath: string): Promise buildFileEntry(file, params.workspaceDir)), ); @@ -79,7 +79,9 @@ export async function syncMemoryFiles(params: { .prepare(`SELECT path FROM files WHERE source = ?`) .all("memory") as Array<{ path: string }>; for (const stale of staleRows) { - if (activePaths.has(stale.path)) continue; + if (activePaths.has(stale.path)) { + continue; + } params.db.prepare(`DELETE FROM files WHERE path = ? AND source = ?`).run(stale.path, "memory"); try { params.db diff --git a/src/memory/sync-session-files.ts b/src/memory/sync-session-files.ts index e2aba7101..efcf1b4aa 100644 --- a/src/memory/sync-session-files.ts +++ b/src/memory/sync-session-files.ts @@ -1,7 +1,6 @@ import type { DatabaseSync } from "node:sqlite"; - -import { createSubsystemLogger } from "../logging/subsystem.js"; import type { SessionFileEntry } from "./session-files.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; import { buildSessionEntry, listSessionFilesForAgent, @@ -105,7 +104,9 @@ export async function syncSessionFiles(params: { .prepare(`SELECT path FROM files WHERE source = ?`) .all("sessions") as Array<{ path: string }>; for (const stale of staleRows) { - if (activePaths.has(stale.path)) continue; + if (activePaths.has(stale.path)) { + continue; + } params.db .prepare(`DELETE FROM files WHERE path = ? AND source = ?`) .run(stale.path, "sessions"); diff --git a/src/node-host/config.ts b/src/node-host/config.ts index 42d59d4c4..ebb116145 100644 --- a/src/node-host/config.ts +++ b/src/node-host/config.ts @@ -1,7 +1,6 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; - import { resolveStateDir } from "../config/paths.js"; export type NodeHostGatewayConfig = { diff --git a/src/node-host/runner.test.ts b/src/node-host/runner.test.ts index 9d89a0097..932f811ed 100644 --- a/src/node-host/runner.test.ts +++ b/src/node-host/runner.test.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from "vitest"; - import { buildNodeInvokeResultParams } from "./runner.js"; describe("buildNodeInvokeResultParams", () => { diff --git a/src/node-host/runner.ts b/src/node-host/runner.ts index e31ad3de3..e0a425c6b 100644 --- a/src/node-host/runner.ts +++ b/src/node-host/runner.ts @@ -1,9 +1,18 @@ -import crypto from "node:crypto"; import { spawn } from "node:child_process"; +import crypto from "node:crypto"; import fs from "node:fs"; import fsPromises from "node:fs/promises"; import path from "node:path"; - +import { resolveAgentConfig } from "../agents/agent-scope.js"; +import { resolveBrowserConfig } from "../browser/config.js"; +import { + createBrowserControlContext, + startBrowserControlServiceFromConfig, +} from "../browser/control-service.js"; +import { createBrowserRouteDispatcher } from "../browser/routes/dispatcher.js"; +import { loadConfig } from "../config/config.js"; +import { GatewayClient } from "../gateway/client.js"; +import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; import { addAllowlistEntry, analyzeArgvCommand, @@ -31,22 +40,11 @@ import { type ExecHostRunResult, } from "../infra/exec-host.js"; import { getMachineDisplayName } from "../infra/machine-name.js"; -import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; -import { loadConfig } from "../config/config.js"; -import { resolveBrowserConfig } from "../browser/config.js"; -import { - createBrowserControlContext, - startBrowserControlServiceFromConfig, -} from "../browser/control-service.js"; -import { createBrowserRouteDispatcher } from "../browser/routes/dispatcher.js"; +import { ensureOpenClawCliOnPath } from "../infra/path-env.js"; import { detectMime } from "../media/mime.js"; -import { resolveAgentConfig } from "../agents/agent-scope.js"; -import { ensureMoltbotCliOnPath } from "../infra/path-env.js"; -import { VERSION } from "../version.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; - +import { VERSION } from "../version.js"; import { ensureNodeHostConfig, saveNodeHostConfig, type NodeHostGatewayConfig } from "./config.js"; -import { GatewayClient } from "../gateway/client.js"; type NodeHostRunOptions = { gatewayHost: string; @@ -151,9 +149,9 @@ const OUTPUT_EVENT_TAIL = 20_000; const DEFAULT_NODE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; const BROWSER_PROXY_MAX_FILE_BYTES = 10 * 1024 * 1024; -const execHostEnforced = process.env.CLAWDBOT_NODE_EXEC_HOST?.trim().toLowerCase() === "app"; +const execHostEnforced = process.env.OPENCLAW_NODE_EXEC_HOST?.trim().toLowerCase() === "app"; const execHostFallbackAllowed = - process.env.CLAWDBOT_NODE_EXEC_FALLBACK?.trim().toLowerCase() !== "0"; + process.env.OPENCLAW_NODE_EXEC_FALLBACK?.trim().toLowerCase() !== "0"; const blockedEnvKeys = new Set([ "NODE_OPTIONS", @@ -199,16 +197,22 @@ class SkillBinsCache { function sanitizeEnv( overrides?: Record | null, ): Record | undefined { - if (!overrides) return undefined; + if (!overrides) { + return undefined; + } const merged = { ...process.env } as Record; const basePath = process.env.PATH ?? DEFAULT_NODE_PATH; for (const [rawKey, value] of Object.entries(overrides)) { const key = rawKey.trim(); - if (!key) continue; + if (!key) { + continue; + } const upper = key.toUpperCase(); if (upper === "PATH") { const trimmed = value.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } if (!basePath || trimmed === basePath) { merged[key] = trimmed; continue; @@ -219,8 +223,12 @@ function sanitizeEnv( } continue; } - if (blockedEnvKeys.has(upper)) continue; - if (blockedEnvPrefixes.some((prefix) => upper.startsWith(prefix))) continue; + if (blockedEnvKeys.has(upper)) { + continue; + } + if (blockedEnvPrefixes.some((prefix) => upper.startsWith(prefix))) { + continue; + } merged[key] = value; } return merged; @@ -241,7 +249,9 @@ function resolveBrowserProxyConfig() { let browserControlReady: Promise | null = null; async function ensureBrowserControlService(): Promise { - if (browserControlReady) return browserControlReady; + if (browserControlReady) { + return browserControlReady; + } browserControlReady = (async () => { const cfg = loadConfig(); const resolved = resolveBrowserConfig(cfg.browser, cfg); @@ -249,7 +259,9 @@ async function ensureBrowserControlService(): Promise { throw new Error("browser control disabled"); } const started = await startBrowserControlServiceFromConfig(); - if (!started) throw new Error("browser control disabled"); + if (!started) { + throw new Error("browser control disabled"); + } })(); return browserControlReady; } @@ -259,7 +271,9 @@ async function withTimeout(promise: Promise, timeoutMs?: number, label?: s typeof timeoutMs === "number" && Number.isFinite(timeoutMs) ? Math.max(1, Math.floor(timeoutMs)) : undefined; - if (!resolved) return await promise; + if (!resolved) { + return await promise; + } let timer: ReturnType | undefined; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => { @@ -269,14 +283,20 @@ async function withTimeout(promise: Promise, timeoutMs?: number, label?: s try { return await Promise.race([promise, timeoutPromise]); } finally { - if (timer) clearTimeout(timer); + if (timer) { + clearTimeout(timer); + } } } function isProfileAllowed(params: { allowProfiles: string[]; profile?: string | null }) { const { allowProfiles, profile } = params; - if (!allowProfiles.length) return true; - if (!profile) return false; + if (!allowProfiles.length) { + return true; + } + if (!profile) { + return false; + } return allowProfiles.includes(profile.trim()); } @@ -284,20 +304,30 @@ function collectBrowserProxyPaths(payload: unknown): string[] { const paths = new Set(); const obj = typeof payload === "object" && payload !== null ? (payload as Record) : null; - if (!obj) return []; - if (typeof obj.path === "string" && obj.path.trim()) paths.add(obj.path.trim()); - if (typeof obj.imagePath === "string" && obj.imagePath.trim()) paths.add(obj.imagePath.trim()); + if (!obj) { + return []; + } + if (typeof obj.path === "string" && obj.path.trim()) { + paths.add(obj.path.trim()); + } + if (typeof obj.imagePath === "string" && obj.imagePath.trim()) { + paths.add(obj.imagePath.trim()); + } const download = obj.download; if (download && typeof download === "object") { const dlPath = (download as Record).path; - if (typeof dlPath === "string" && dlPath.trim()) paths.add(dlPath.trim()); + if (typeof dlPath === "string" && dlPath.trim()) { + paths.add(dlPath.trim()); + } } return [...paths]; } async function readBrowserProxyFile(filePath: string): Promise { const stat = await fsPromises.stat(filePath).catch(() => null); - if (!stat || !stat.isFile()) return null; + if (!stat || !stat.isFile()) { + return null; + } if (stat.size > BROWSER_PROXY_MAX_FILE_BYTES) { throw new Error( `browser proxy file exceeds ${Math.round(BROWSER_PROXY_MAX_FILE_BYTES / (1024 * 1024))}MB`, @@ -312,16 +342,22 @@ function formatCommand(argv: string[]): string { return argv .map((arg) => { const trimmed = arg.trim(); - if (!trimmed) return '""'; + if (!trimmed) { + return '""'; + } const needsQuotes = /\s|"/.test(trimmed); - if (!needsQuotes) return trimmed; + if (!needsQuotes) { + return trimmed; + } return `"${trimmed.replace(/"/g, '\\"')}"`; }) .join(" "); } function truncateOutput(raw: string, maxChars: number): { text: string; truncated: boolean } { - if (raw.length <= maxChars) return { text: raw, truncated: false }; + if (raw.length <= maxChars) { + return { text: raw, truncated: false }; + } return { text: `... (truncated) ${raw.slice(raw.length - maxChars)}`, truncated: true }; } @@ -337,7 +373,9 @@ function requireExecApprovalsBaseHash( params: SystemExecApprovalsSetParams, snapshot: ExecApprovalsSnapshot, ) { - if (!snapshot.exists) return; + if (!snapshot.exists) { + return; + } if (!snapshot.hash) { throw new Error("INVALID_REQUEST: exec approvals base hash unavailable; reload and retry"); } @@ -380,9 +418,14 @@ async function runCommand( const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk; const str = slice.toString("utf8"); outputLen += slice.length; - if (target === "stdout") stdout += str; - else stderr += str; - if (chunk.length > remaining) truncated = true; + if (target === "stdout") { + stdout += str; + } else { + stderr += str; + } + if (chunk.length > remaining) { + truncated = true; + } }; child.stdout?.on("data", (chunk) => onChunk(chunk as Buffer, "stdout")); @@ -401,9 +444,13 @@ async function runCommand( } const finalize = (exitCode?: number, error?: string | null) => { - if (settled) return; + if (settled) { + return; + } settled = true; - if (timer) clearTimeout(timer); + if (timer) { + clearTimeout(timer); + } resolve({ exitCode, timedOut, @@ -435,15 +482,19 @@ function resolveEnvPath(env?: Record): string[] { } function ensureNodePathEnv(): string { - ensureMoltbotCliOnPath({ pathEnv: process.env.PATH ?? "" }); + ensureOpenClawCliOnPath({ pathEnv: process.env.PATH ?? "" }); const current = process.env.PATH ?? ""; - if (current.trim()) return current; + if (current.trim()) { + return current; + } process.env.PATH = DEFAULT_NODE_PATH; return DEFAULT_NODE_PATH; } function resolveExecutable(bin: string, env?: Record) { - if (bin.includes("/") || bin.includes("\\")) return null; + if (bin.includes("/") || bin.includes("\\")) { + return null; + } const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? process.env.PathExt ?? ".EXE;.CMD;.BAT;.COM") @@ -453,7 +504,9 @@ function resolveExecutable(bin: string, env?: Record) { for (const dir of resolveEnvPath(env)) { for (const ext of extensions) { const candidate = path.join(dir, bin + ext); - if (fs.existsSync(candidate)) return candidate; + if (fs.existsSync(candidate)) { + return candidate; + } } } return null; @@ -464,15 +517,21 @@ async function handleSystemWhich(params: SystemWhichParams, env?: Record = {}; for (const bin of bins) { const path = resolveExecutable(bin, env); - if (path) found[bin] = path; + if (path) { + found[bin] = path; + } } return { bins: found }; } function buildExecEventPayload(payload: ExecEventPayload): ExecEventPayload { - if (!payload.output) return payload; + if (!payload.output) { + return payload; + } const trimmed = payload.output.trim(); - if (!trimmed) return payload; + if (!trimmed) { + return payload; + } const { text } = truncateOutput(trimmed, OUTPUT_EVENT_TAIL); return { ...payload, output: text }; } @@ -513,10 +572,10 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise { const browserProxyEnabled = browserProxy.enabled && resolvedBrowser.enabled; const isRemoteMode = cfg.gateway?.mode === "remote"; const token = - process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() || + process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || (isRemoteMode ? cfg.gateway?.remote?.token : cfg.gateway?.auth?.token); const password = - process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || + process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || (isRemoteMode ? cfg.gateway?.remote?.password : cfg.gateway?.auth?.password); const host = gateway.host ?? "127.0.0.1"; @@ -552,9 +611,13 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise { deviceIdentity: loadOrCreateDeviceIdentity(), tlsFingerprint: gateway.tlsFingerprint, onEvent: (evt) => { - if (evt.event !== "node.invoke.request") return; + if (evt.event !== "node.invoke.request") { + return; + } const payload = coerceNodeInvokePayload(evt.payload); - if (!payload) return; + if (!payload) { + return; + } void handleInvoke(payload, client, skillBins); }, onConnectError: (err) => { @@ -569,10 +632,7 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise { }); const skillBins = new SkillBinsCache(async () => { - const res = (await client.request("skills.bins", {})) as - | { bins?: unknown[] } - | null - | undefined; + const res = await client.request<{ bins: Array }>("skills.bins", {}); const bins = Array.isArray(res?.bins) ? res.bins.map((bin) => String(bin)) : []; return bins; }); @@ -714,7 +774,9 @@ async function handleInvoke( } const rawQuery = params.query ?? {}; for (const [key, value] of Object.entries(rawQuery)) { - if (value === undefined || value === null) continue; + if (value === undefined || value === null) { + continue; + } query[key] = typeof value === "string" ? value : String(value); } const dispatcher = createBrowserRouteDispatcher(createBrowserControlContext()); @@ -735,13 +797,15 @@ async function handleInvoke( : `HTTP ${response.status}`; throw new Error(message); } - const result = response.body as unknown; + const result = response.body; if (allowedProfiles.length > 0 && path === "/profiles") { const obj = typeof result === "object" && result !== null ? (result as Record) : {}; const profiles = Array.isArray(obj.profiles) ? obj.profiles : []; obj.profiles = profiles.filter((entry) => { - if (!entry || typeof entry !== "object") return false; + if (!entry || typeof entry !== "object") { + return false; + } const name = (entry as Record).name; return typeof name === "string" && allowedProfiles.includes(name); }); @@ -758,11 +822,15 @@ async function handleInvoke( } return file; } catch (err) { - throw new Error(`browser proxy file read failed for ${p}: ${String(err)}`); + throw new Error(`browser proxy file read failed for ${p}: ${String(err)}`, { + cause: err, + }); } }), ); - if (loaded.length > 0) files = loaded; + if (loaded.length > 0) { + files = loaded; + } } const payload: BrowserProxyResult = files ? { result, files } : { result }; await sendInvokeResult(client, frame, { @@ -997,7 +1065,9 @@ async function handleInvoke( if (analysisOk) { for (const segment of segments) { const pattern = segment.resolution?.resolvedPath ?? ""; - if (pattern) addAllowlistEntry(approvals.file, agentId, pattern); + if (pattern) { + addAllowlistEntry(approvals.file, agentId, pattern); + } } } } @@ -1024,7 +1094,9 @@ async function handleInvoke( if (allowlistMatches.length > 0) { const seen = new Set(); for (const match of allowlistMatches) { - if (!match?.pattern || seen.has(match.pattern)) continue; + if (!match?.pattern || seen.has(match.pattern)) { + continue; + } seen.add(match.pattern); recordAllowlistUse( approvals.file, @@ -1106,12 +1178,16 @@ function decodeParams(raw?: string | null): T { } function coerceNodeInvokePayload(payload: unknown): NodeInvokeRequestPayload | null { - if (!payload || typeof payload !== "object") return null; + if (!payload || typeof payload !== "object") { + return null; + } const obj = payload as Record; const id = typeof obj.id === "string" ? obj.id.trim() : ""; const nodeId = typeof obj.nodeId === "string" ? obj.nodeId.trim() : ""; const command = typeof obj.command === "string" ? obj.command.trim() : ""; - if (!id || !nodeId || !command) return null; + if (!id || !nodeId || !command) { + return null; + } const paramsJSON = typeof obj.paramsJSON === "string" ? obj.paramsJSON diff --git a/src/pairing/pairing-labels.ts b/src/pairing/pairing-labels.ts index a7a514543..b230cd2d3 100644 --- a/src/pairing/pairing-labels.ts +++ b/src/pairing/pairing-labels.ts @@ -1,5 +1,5 @@ -import { getPairingAdapter } from "../channels/plugins/pairing.js"; import type { PairingChannel } from "./pairing-store.js"; +import { getPairingAdapter } from "../channels/plugins/pairing.js"; export function resolvePairingIdLabel(channel: PairingChannel): string { return getPairingAdapter(channel)?.idLabel ?? "userId"; diff --git a/src/pairing/pairing-messages.test.ts b/src/pairing/pairing-messages.test.ts index 9884ac2f6..d8994e88c 100644 --- a/src/pairing/pairing-messages.test.ts +++ b/src/pairing/pairing-messages.test.ts @@ -1,21 +1,20 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; - import { buildPairingReply } from "./pairing-messages.js"; describe("buildPairingReply", () => { let previousProfile: string | undefined; beforeEach(() => { - previousProfile = process.env.CLAWDBOT_PROFILE; - process.env.CLAWDBOT_PROFILE = "isolated"; + previousProfile = process.env.OPENCLAW_PROFILE; + process.env.OPENCLAW_PROFILE = "isolated"; }); afterEach(() => { if (previousProfile === undefined) { - delete process.env.CLAWDBOT_PROFILE; + delete process.env.OPENCLAW_PROFILE; return; } - process.env.CLAWDBOT_PROFILE = previousProfile; + process.env.OPENCLAW_PROFILE = previousProfile; }); const cases = [ @@ -51,9 +50,9 @@ describe("buildPairingReply", () => { const text = buildPairingReply(testCase); expect(text).toContain(testCase.idLine); expect(text).toContain(`Pairing code: ${testCase.code}`); - // CLI commands should respect CLAWDBOT_PROFILE when set (most tests run with isolated profile) + // CLI commands should respect OPENCLAW_PROFILE when set (most tests run with isolated profile) const commandRe = new RegExp( - `(?:moltbot|moltbot) --profile isolated pairing approve ${testCase.channel} `, + `(?:openclaw|openclaw) --profile isolated pairing approve ${testCase.channel} `, ); expect(text).toMatch(commandRe); }); diff --git a/src/pairing/pairing-messages.ts b/src/pairing/pairing-messages.ts index 926c19612..86e3b471a 100644 --- a/src/pairing/pairing-messages.ts +++ b/src/pairing/pairing-messages.ts @@ -1,5 +1,5 @@ -import { formatCliCommand } from "../cli/command-format.js"; import type { PairingChannel } from "./pairing-store.js"; +import { formatCliCommand } from "../cli/command-format.js"; export function buildPairingReply(params: { channel: PairingChannel; @@ -8,13 +8,13 @@ export function buildPairingReply(params: { }): string { const { channel, idLine, code } = params; return [ - "Moltbot: access not configured.", + "OpenClaw: access not configured.", "", idLine, "", `Pairing code: ${code}`, "", "Ask the bot owner to approve with:", - formatCliCommand(`moltbot pairing approve ${channel} `), + formatCliCommand(`openclaw pairing approve ${channel} `), ].join("\n"); } diff --git a/src/pairing/pairing-store.test.ts b/src/pairing/pairing-store.test.ts index a72264e16..f858d0f3f 100644 --- a/src/pairing/pairing-store.test.ts +++ b/src/pairing/pairing-store.test.ts @@ -2,21 +2,22 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it, vi } from "vitest"; - import { resolveOAuthDir } from "../config/paths.js"; import { listChannelPairingRequests, upsertChannelPairingRequest } from "./pairing-store.js"; async function withTempStateDir(fn: (stateDir: string) => Promise) { - const previous = process.env.CLAWDBOT_STATE_DIR; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-pairing-")); - process.env.CLAWDBOT_STATE_DIR = dir; + const previous = process.env.OPENCLAW_STATE_DIR; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pairing-")); + process.env.OPENCLAW_STATE_DIR = dir; try { return await fn(dir); } finally { - if (previous === undefined) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = previous; + if (previous === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previous; + } await fs.rm(dir, { recursive: true, force: true }); } } diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index 5ae89dbd9..c394a1f76 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -2,10 +2,9 @@ import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - import lockfile from "proper-lockfile"; -import { getPairingAdapter } from "../channels/plugins/pairing.js"; import type { ChannelId, ChannelPairingAdapter } from "../channels/plugins/types.js"; +import { getPairingAdapter } from "../channels/plugins/pairing.js"; import { resolveOAuthDir, resolveStateDir } from "../config/paths.js"; const PAIRING_CODE_LENGTH = 8; @@ -51,9 +50,13 @@ function resolveCredentialsDir(env: NodeJS.ProcessEnv = process.env): string { /** Sanitize channel ID for use in filenames (prevent path traversal). */ function safeChannelKey(channel: PairingChannel): string { const raw = String(channel).trim().toLowerCase(); - if (!raw) throw new Error("invalid pairing channel"); + if (!raw) { + throw new Error("invalid pairing channel"); + } const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_"); - if (!safe || safe === "_") throw new Error("invalid pairing channel"); + if (!safe || safe === "_") { + throw new Error("invalid pairing channel"); + } return safe; } @@ -83,11 +86,15 @@ async function readJsonFile( try { const raw = await fs.promises.readFile(filePath, "utf-8"); const parsed = safeParseJson(raw); - if (parsed == null) return { value: fallback, exists: true }; + if (parsed == null) { + return { value: fallback, exists: true }; + } return { value: parsed, exists: true }; } catch (err) { const code = (err as { code?: string }).code; - if (code === "ENOENT") return { value: fallback, exists: false }; + if (code === "ENOENT") { + return { value: fallback, exists: false }; + } return { value: fallback, exists: false }; } } @@ -133,15 +140,21 @@ async function withFileLock( } function parseTimestamp(value: string | undefined): number | null { - if (!value) return null; + if (!value) { + return null; + } const parsed = Date.parse(value); - if (!Number.isFinite(parsed)) return null; + if (!Number.isFinite(parsed)) { + return null; + } return parsed; } function isExpired(entry: PairingRequest, nowMs: number): boolean { const createdAt = parseTimestamp(entry.createdAt); - if (!createdAt) return true; + if (!createdAt) { + return true; + } return nowMs - createdAt > PAIRING_PENDING_TTL_MS; } @@ -166,7 +179,7 @@ function pruneExcessRequests(reqs: PairingRequest[], maxPending: number) { if (maxPending <= 0 || reqs.length <= maxPending) { return { requests: reqs, removed: false }; } - const sorted = reqs.slice().sort((a, b) => resolveLastSeenAt(a) - resolveLastSeenAt(b)); + const sorted = reqs.slice().toSorted((a, b) => resolveLastSeenAt(a) - resolveLastSeenAt(b)); return { requests: sorted.slice(-maxPending), removed: true }; } @@ -183,7 +196,9 @@ function randomCode(): string { function generateUniqueCode(existing: Set): string { for (let attempt = 0; attempt < 500; attempt += 1) { const code = randomCode(); - if (!existing.has(code)) return code; + if (!existing.has(code)) { + return code; + } } throw new Error("failed to generate unique pairing code"); } @@ -194,8 +209,12 @@ function normalizeId(value: string | number): string { function normalizeAllowEntry(channel: PairingChannel, entry: string): string { const trimmed = entry.trim(); - if (!trimmed) return ""; - if (trimmed === "*") return ""; + if (!trimmed) { + return ""; + } + if (trimmed === "*") { + return ""; + } const adapter = getPairingAdapter(channel); const normalized = adapter?.normalizeAllowEntry ? adapter.normalizeAllowEntry(trimmed) : trimmed; return String(normalized).trim(); @@ -233,8 +252,12 @@ export async function addChannelAllowFromStoreEntry(params: { .map((v) => normalizeAllowEntry(params.channel, String(v))) .filter(Boolean); const normalized = normalizeAllowEntry(params.channel, normalizeId(params.entry)); - if (!normalized) return { changed: false, allowFrom: current }; - if (current.includes(normalized)) return { changed: false, allowFrom: current }; + if (!normalized) { + return { changed: false, allowFrom: current }; + } + if (current.includes(normalized)) { + return { changed: false, allowFrom: current }; + } const next = [...current, normalized]; await writeJsonFile(filePath, { version: 1, @@ -264,9 +287,13 @@ export async function removeChannelAllowFromStoreEntry(params: { .map((v) => normalizeAllowEntry(params.channel, String(v))) .filter(Boolean); const normalized = normalizeAllowEntry(params.channel, normalizeId(params.entry)); - if (!normalized) return { changed: false, allowFrom: current }; + if (!normalized) { + return { changed: false, allowFrom: current }; + } const next = current.filter((entry) => entry !== normalized); - if (next.length === current.length) return { changed: false, allowFrom: current }; + if (next.length === current.length) { + return { changed: false, allowFrom: current }; + } await writeJsonFile(filePath, { version: 1, allowFrom: next, @@ -314,7 +341,7 @@ export async function listChannelPairingRequests( typeof r.createdAt === "string", ) .slice() - .sort((a, b) => a.createdAt.localeCompare(b.createdAt)); + .toSorted((a, b) => a.createdAt.localeCompare(b.createdAt)); }, ); } @@ -423,7 +450,9 @@ export async function approveChannelPairingCode(params: { }): Promise<{ id: string; entry?: PairingRequest } | null> { const env = params.env ?? process.env; const code = params.code.trim().toUpperCase(); - if (!code) return null; + if (!code) { + return null; + } const filePath = resolvePairingPath(params.channel, env); return await withFileLock( @@ -448,7 +477,9 @@ export async function approveChannelPairingCode(params: { return null; } const entry = pruned[idx]; - if (!entry) return null; + if (!entry) { + return null; + } pruned.splice(idx, 1); await writeJsonFile(filePath, { version: 1, diff --git a/src/plugin-sdk/index.test.ts b/src/plugin-sdk/index.test.ts index 920e2af2a..ae085b00d 100644 --- a/src/plugin-sdk/index.test.ts +++ b/src/plugin-sdk/index.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import * as sdk from "./index.js"; describe("plugin-sdk exports", () => { diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 0813caefd..5eb5cbfbe 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -59,9 +59,9 @@ export type { } from "../channels/plugins/types.js"; export type { ChannelConfigSchema, ChannelPlugin } from "../channels/plugins/types.plugin.js"; export type { - MoltbotPluginApi, - MoltbotPluginService, - MoltbotPluginServiceContext, + OpenClawPluginApi, + OpenClawPluginService, + OpenClawPluginServiceContext, } from "../plugins/types.js"; export type { GatewayRequestHandler, @@ -72,7 +72,7 @@ export type { PluginRuntime } from "../plugins/runtime/types.js"; export { normalizePluginHttpPath } from "../plugins/http-path.js"; export { registerPluginHttpRoute } from "../plugins/http-registry.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; -export type { MoltbotConfig } from "../config/config.js"; +export type { OpenClawConfig } from "../config/config.js"; export type { ChannelDock } from "../channels/dock.js"; export { getChatChannelMeta } from "../channels/registry.js"; export type { diff --git a/src/plugins/bundled-dir.ts b/src/plugins/bundled-dir.ts index 33524c36f..4837ae59d 100644 --- a/src/plugins/bundled-dir.ts +++ b/src/plugins/bundled-dir.ts @@ -3,14 +3,18 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; export function resolveBundledPluginsDir(): string | undefined { - const override = process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR?.trim(); - if (override) return override; + const override = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim(); + if (override) { + return override; + } // bun --compile: ship a sibling `extensions/` next to the executable. try { const execDir = path.dirname(process.execPath); const sibling = path.join(execDir, "extensions"); - if (fs.existsSync(sibling)) return sibling; + if (fs.existsSync(sibling)) { + return sibling; + } } catch { // ignore } @@ -20,9 +24,13 @@ export function resolveBundledPluginsDir(): string | undefined { let cursor = path.dirname(fileURLToPath(import.meta.url)); for (let i = 0; i < 6; i += 1) { const candidate = path.join(cursor, "extensions"); - if (fs.existsSync(candidate)) return candidate; + if (fs.existsSync(candidate)) { + return candidate; + } const parent = path.dirname(cursor); - if (parent === cursor) break; + if (parent === cursor) { + break; + } cursor = parent; } } catch { diff --git a/src/plugins/cli.test.ts b/src/plugins/cli.test.ts index d21302c95..cbe8c1996 100644 --- a/src/plugins/cli.test.ts +++ b/src/plugins/cli.test.ts @@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({ })); vi.mock("./loader.js", () => ({ - loadMoltbotPlugins: () => ({ + loadOpenClawPlugins: () => ({ cliRegistrars: [ { pluginId: "memory-core", diff --git a/src/plugins/cli.ts b/src/plugins/cli.ts index cad44c950..fe1371855 100644 --- a/src/plugins/cli.ts +++ b/src/plugins/cli.ts @@ -1,15 +1,14 @@ import type { Command } from "commander"; - +import type { OpenClawConfig } from "../config/config.js"; +import type { PluginLogger } from "./types.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; -import type { MoltbotConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { loadMoltbotPlugins } from "./loader.js"; -import type { PluginLogger } from "./types.js"; +import { loadOpenClawPlugins } from "./loader.js"; const log = createSubsystemLogger("plugins"); -export function registerPluginCliCommands(program: Command, cfg?: MoltbotConfig) { +export function registerPluginCliCommands(program: Command, cfg?: OpenClawConfig) { const config = cfg ?? loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const logger: PluginLogger = { @@ -18,7 +17,7 @@ export function registerPluginCliCommands(program: Command, cfg?: MoltbotConfig) error: (msg: string) => log.error(msg), debug: (msg: string) => log.debug(msg), }; - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ config, workspaceDir, logger, @@ -45,8 +44,8 @@ export function registerPluginCliCommands(program: Command, cfg?: MoltbotConfig) workspaceDir, logger, }); - if (result && typeof (result as Promise).then === "function") { - void (result as Promise).catch((err) => { + if (result && typeof result.then === "function") { + void result.catch((err) => { log.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`); }); } diff --git a/src/plugins/commands.ts b/src/plugins/commands.ts index 128ad2b8c..fa7f328e2 100644 --- a/src/plugins/commands.ts +++ b/src/plugins/commands.ts @@ -5,15 +5,15 @@ * These commands are processed before built-in commands and before agent invocation. */ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { - MoltbotPluginCommandDefinition, + OpenClawPluginCommandDefinition, PluginCommandContext, PluginCommandResult, } from "./types.js"; import { logVerbose } from "../globals.js"; -type RegisteredPluginCommand = MoltbotPluginCommandDefinition & { +type RegisteredPluginCommand = OpenClawPluginCommandDefinition & { pluginId: string; }; @@ -104,7 +104,7 @@ export type CommandRegistrationResult = { */ export function registerPluginCommand( pluginId: string, - command: MoltbotPluginCommandDefinition, + command: OpenClawPluginCommandDefinition, ): CommandRegistrationResult { // Prevent registration while commands are being processed if (registryLocked) { @@ -168,7 +168,9 @@ export function matchPluginCommand( commandBody: string, ): { command: RegisteredPluginCommand; args?: string } | null { const trimmed = commandBody.trim(); - if (!trimmed.startsWith("/")) return null; + if (!trimmed.startsWith("/")) { + return null; + } // Extract command name and args const spaceIndex = trimmed.indexOf(" "); @@ -178,10 +180,14 @@ export function matchPluginCommand( const key = commandName.toLowerCase(); const command = pluginCommands.get(key); - if (!command) return null; + if (!command) { + return null; + } // If command doesn't accept args but args were provided, don't match - if (args && !command.acceptsArgs) return null; + if (args && !command.acceptsArgs) { + return null; + } return { command, args: args || undefined }; } @@ -191,7 +197,9 @@ export function matchPluginCommand( * Removes control characters and enforces length limits. */ function sanitizeArgs(args: string | undefined): string | undefined { - if (!args) return undefined; + if (!args) { + return undefined; + } // Enforce length limit if (args.length > MAX_ARGS_LENGTH) { @@ -203,7 +211,9 @@ function sanitizeArgs(args: string | undefined): string | undefined { for (const char of args) { const code = char.charCodeAt(0); const isControl = (code <= 0x1f && code !== 0x09 && code !== 0x0a) || code === 0x7f; - if (!isControl) sanitized += char; + if (!isControl) { + sanitized += char; + } } return sanitized; } @@ -221,7 +231,7 @@ export async function executePluginCommand(params: { channel: string; isAuthorizedSender: boolean; commandBody: string; - config: MoltbotConfig; + config: OpenClawConfig; }): Promise { const { command, args, senderId, channel, isAuthorizedSender, commandBody, config } = params; diff --git a/src/plugins/config-schema.ts b/src/plugins/config-schema.ts index 1fb9c502c..8a1185495 100644 --- a/src/plugins/config-schema.ts +++ b/src/plugins/config-schema.ts @@ -1,4 +1,4 @@ -import type { MoltbotPluginConfigSchema } from "./types.js"; +import type { OpenClawPluginConfigSchema } from "./types.js"; type Issue = { path: Array; message: string }; @@ -10,10 +10,12 @@ function error(message: string): SafeParseResult { return { success: false, error: { issues: [{ path: [], message }] } }; } -export function emptyPluginConfigSchema(): MoltbotPluginConfigSchema { +export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema { return { safeParse(value: unknown): SafeParseResult { - if (value === undefined) return { success: true, data: undefined }; + if (value === undefined) { + return { success: true, data: undefined }; + } if (!value || typeof value !== "object" || Array.isArray(value)) { return error("expected config object"); } diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index c5c4924eb..9a0cdf4f5 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { normalizePluginsConfig } from "./config-state.js"; describe("normalizePluginsConfig", () => { diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index bf44b5fe4..72344daa3 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -1,6 +1,6 @@ -import type { MoltbotConfig } from "../config/config.js"; -import { defaultSlotIdForKey } from "./slots.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { PluginRecord } from "./registry.js"; +import { defaultSlotIdForKey } from "./slots.js"; export type NormalizedPluginsConfig = { enabled: boolean; @@ -16,15 +16,23 @@ export type NormalizedPluginsConfig = { export const BUNDLED_ENABLED_BY_DEFAULT = new Set(); const normalizeList = (value: unknown): string[] => { - if (!Array.isArray(value)) return []; + if (!Array.isArray(value)) { + return []; + } return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); }; const normalizeSlotValue = (value: unknown): string | null | undefined => { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const trimmed = value.trim(); - if (!trimmed) return undefined; - if (trimmed.toLowerCase() === "none") return null; + if (!trimmed) { + return undefined; + } + if (trimmed.toLowerCase() === "none") { + return null; + } return trimmed; }; @@ -34,7 +42,9 @@ const normalizePluginEntries = (entries: unknown): NormalizedPluginsConfig["entr } const normalized: NormalizedPluginsConfig["entries"] = {}; for (const [key, value] of Object.entries(entries)) { - if (!key.trim()) continue; + if (!key.trim()) { + continue; + } if (!value || typeof value !== "object" || Array.isArray(value)) { normalized[key] = {}; continue; @@ -49,7 +59,7 @@ const normalizePluginEntries = (entries: unknown): NormalizedPluginsConfig["entr }; export const normalizePluginsConfig = ( - config?: MoltbotConfig["plugins"], + config?: OpenClawConfig["plugins"], ): NormalizedPluginsConfig => { const memorySlot = normalizeSlotValue(config?.slots?.memory); return { @@ -64,6 +74,89 @@ export const normalizePluginsConfig = ( }; }; +const hasExplicitMemorySlot = (plugins?: OpenClawConfig["plugins"]) => + Boolean(plugins?.slots && Object.prototype.hasOwnProperty.call(plugins.slots, "memory")); + +const hasExplicitMemoryEntry = (plugins?: OpenClawConfig["plugins"]) => + Boolean(plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, "memory-core")); + +const hasExplicitPluginConfig = (plugins?: OpenClawConfig["plugins"]) => { + if (!plugins) { + return false; + } + if (typeof plugins.enabled === "boolean") { + return true; + } + if (Array.isArray(plugins.allow) && plugins.allow.length > 0) { + return true; + } + if (Array.isArray(plugins.deny) && plugins.deny.length > 0) { + return true; + } + if (plugins.load?.paths && Array.isArray(plugins.load.paths) && plugins.load.paths.length > 0) { + return true; + } + if (plugins.slots && Object.keys(plugins.slots).length > 0) { + return true; + } + if (plugins.entries && Object.keys(plugins.entries).length > 0) { + return true; + } + return false; +}; + +export function applyTestPluginDefaults( + cfg: OpenClawConfig, + env: NodeJS.ProcessEnv = process.env, +): OpenClawConfig { + if (!env.VITEST) { + return cfg; + } + const plugins = cfg.plugins; + const explicitConfig = hasExplicitPluginConfig(plugins); + if (explicitConfig) { + if (hasExplicitMemorySlot(plugins) || hasExplicitMemoryEntry(plugins)) { + return cfg; + } + return { + ...cfg, + plugins: { + ...plugins, + slots: { + ...plugins?.slots, + memory: "none", + }, + }, + }; + } + + return { + ...cfg, + plugins: { + ...plugins, + enabled: false, + slots: { + ...plugins?.slots, + memory: "none", + }, + }, + }; +} + +export function isTestDefaultMemorySlotDisabled( + cfg: OpenClawConfig, + env: NodeJS.ProcessEnv = process.env, +): boolean { + if (!env.VITEST) { + return false; + } + const plugins = cfg.plugins; + if (hasExplicitMemorySlot(plugins) || hasExplicitMemoryEntry(plugins)) { + return false; + } + return true; +} + export function resolveEnableState( id: string, origin: PluginRecord["origin"], @@ -103,7 +196,9 @@ export function resolveMemorySlotDecision(params: { slot: string | null | undefined; selectedId: string | null; }): { enabled: boolean; reason?: string; selected?: boolean } { - if (params.kind !== "memory") return { enabled: true }; + if (params.kind !== "memory") { + return { enabled: true }; + } if (params.slot === null) { return { enabled: false, reason: "memory slot disabled" }; } diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index a5c40ceb1..54d046699 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -7,30 +7,30 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const tempDirs: string[] = []; function makeTempDir() { - const dir = path.join(os.tmpdir(), `moltbot-plugins-${randomUUID()}`); + const dir = path.join(os.tmpdir(), `openclaw-plugins-${randomUUID()}`); fs.mkdirSync(dir, { recursive: true }); tempDirs.push(dir); return dir; } async function withStateDir(stateDir: string, fn: () => Promise) { - const prev = process.env.CLAWDBOT_STATE_DIR; - const prevBundled = process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR; - process.env.CLAWDBOT_STATE_DIR = stateDir; - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + const prev = process.env.OPENCLAW_STATE_DIR; + const prevBundled = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + process.env.OPENCLAW_STATE_DIR = stateDir; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; vi.resetModules(); try { return await fn(); } finally { if (prev === undefined) { - delete process.env.CLAWDBOT_STATE_DIR; + delete process.env.OPENCLAW_STATE_DIR; } else { - process.env.CLAWDBOT_STATE_DIR = prev; + process.env.OPENCLAW_STATE_DIR = prev; } if (prevBundled === undefined) { - delete process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR; + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; } else { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = prevBundled; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundled; } vi.resetModules(); } @@ -46,7 +46,7 @@ afterEach(() => { } }); -describe("discoverMoltbotPlugins", () => { +describe("discoverOpenClawPlugins", () => { it("discovers global and workspace extensions", async () => { const stateDir = makeTempDir(); const workspaceDir = path.join(stateDir, "workspace"); @@ -55,13 +55,13 @@ describe("discoverMoltbotPlugins", () => { fs.mkdirSync(globalExt, { recursive: true }); fs.writeFileSync(path.join(globalExt, "alpha.ts"), "export default function () {}", "utf-8"); - const workspaceExt = path.join(workspaceDir, ".clawdbot", "extensions"); + const workspaceExt = path.join(workspaceDir, ".openclaw", "extensions"); fs.mkdirSync(workspaceExt, { recursive: true }); fs.writeFileSync(path.join(workspaceExt, "beta.ts"), "export default function () {}", "utf-8"); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverMoltbotPlugins } = await import("./discovery.js"); - return discoverMoltbotPlugins({ workspaceDir }); + const { discoverOpenClawPlugins } = await import("./discovery.js"); + return discoverOpenClawPlugins({ workspaceDir }); }); const ids = candidates.map((c) => c.idHint); @@ -78,7 +78,7 @@ describe("discoverMoltbotPlugins", () => { path.join(globalExt, "package.json"), JSON.stringify({ name: "pack", - moltbot: { extensions: ["./src/one.ts", "./src/two.ts"] }, + openclaw: { extensions: ["./src/one.ts", "./src/two.ts"] }, }), "utf-8", ); @@ -94,8 +94,8 @@ describe("discoverMoltbotPlugins", () => { ); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverMoltbotPlugins } = await import("./discovery.js"); - return discoverMoltbotPlugins({}); + const { discoverOpenClawPlugins } = await import("./discovery.js"); + return discoverOpenClawPlugins({}); }); const ids = candidates.map((c) => c.idHint); @@ -111,8 +111,8 @@ describe("discoverMoltbotPlugins", () => { fs.writeFileSync( path.join(globalExt, "package.json"), JSON.stringify({ - name: "@moltbot/voice-call", - moltbot: { extensions: ["./src/index.ts"] }, + name: "@openclaw/voice-call", + openclaw: { extensions: ["./src/index.ts"] }, }), "utf-8", ); @@ -123,8 +123,8 @@ describe("discoverMoltbotPlugins", () => { ); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverMoltbotPlugins } = await import("./discovery.js"); - return discoverMoltbotPlugins({}); + const { discoverOpenClawPlugins } = await import("./discovery.js"); + return discoverOpenClawPlugins({}); }); const ids = candidates.map((c) => c.idHint); @@ -139,16 +139,16 @@ describe("discoverMoltbotPlugins", () => { fs.writeFileSync( path.join(packDir, "package.json"), JSON.stringify({ - name: "@moltbot/demo-plugin-dir", - moltbot: { extensions: ["./index.js"] }, + name: "@openclaw/demo-plugin-dir", + openclaw: { extensions: ["./index.js"] }, }), "utf-8", ); fs.writeFileSync(path.join(packDir, "index.js"), "module.exports = {}", "utf-8"); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverMoltbotPlugins } = await import("./discovery.js"); - return discoverMoltbotPlugins({ extraPaths: [packDir] }); + const { discoverOpenClawPlugins } = await import("./discovery.js"); + return discoverOpenClawPlugins({ extraPaths: [packDir] }); }); const ids = candidates.map((c) => c.idHint); diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index a03e4f38b..fd9ca62c2 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -1,14 +1,13 @@ import fs from "node:fs"; import path from "node:path"; - +import type { PluginDiagnostic, PluginOrigin } from "./types.js"; import { resolveConfigDir, resolveUserPath } from "../utils.js"; import { resolveBundledPluginsDir } from "./bundled-dir.js"; import { getPackageManifestMetadata, - type MoltbotPackageManifest, + type OpenClawPackageManifest, type PackageManifest, } from "./manifest.js"; -import type { PluginDiagnostic, PluginOrigin } from "./types.js"; const EXTENSION_EXTS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]); @@ -22,7 +21,7 @@ export type PluginCandidate = { packageVersion?: string; packageDescription?: string; packageDir?: string; - packageMoltbot?: MoltbotPackageManifest; + packageManifest?: OpenClawPackageManifest; }; export type PluginDiscoveryResult = { @@ -32,13 +31,17 @@ export type PluginDiscoveryResult = { function isExtensionFile(filePath: string): boolean { const ext = path.extname(filePath); - if (!EXTENSION_EXTS.has(ext)) return false; + if (!EXTENSION_EXTS.has(ext)) { + return false; + } return !filePath.endsWith(".d.ts"); } function readPackageManifest(dir: string): PackageManifest | null { const manifestPath = path.join(dir, "package.json"); - if (!fs.existsSync(manifestPath)) return null; + if (!fs.existsSync(manifestPath)) { + return null; + } try { const raw = fs.readFileSync(manifestPath, "utf-8"); return JSON.parse(raw) as PackageManifest; @@ -49,7 +52,9 @@ function readPackageManifest(dir: string): PackageManifest | null { function resolvePackageExtensions(manifest: PackageManifest): string[] { const raw = getPackageManifestMetadata(manifest)?.extensions; - if (!Array.isArray(raw)) return []; + if (!Array.isArray(raw)) { + return []; + } return raw.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); } @@ -60,15 +65,19 @@ function deriveIdHint(params: { }): string { const base = path.basename(params.filePath, path.extname(params.filePath)); const rawPackageName = params.packageName?.trim(); - if (!rawPackageName) return base; + if (!rawPackageName) { + return base; + } // Prefer the unscoped name so config keys stay stable even when the npm - // package is scoped (example: @moltbot/voice-call -> voice-call). + // package is scoped (example: @openclaw/voice-call -> voice-call). const unscoped = rawPackageName.includes("/") ? (rawPackageName.split("/").pop() ?? rawPackageName) : rawPackageName; - if (!params.hasMultipleExtensions) return unscoped; + if (!params.hasMultipleExtensions) { + return unscoped; + } return `${unscoped}/${base}`; } @@ -84,7 +93,9 @@ function addCandidate(params: { packageDir?: string; }) { const resolved = path.resolve(params.source); - if (params.seen.has(resolved)) return; + if (params.seen.has(resolved)) { + return; + } params.seen.add(resolved); const manifest = params.manifest ?? null; params.candidates.push({ @@ -97,7 +108,7 @@ function addCandidate(params: { packageVersion: manifest?.version?.trim() || undefined, packageDescription: manifest?.description?.trim() || undefined, packageDir: params.packageDir, - packageMoltbot: getPackageManifestMetadata(manifest ?? undefined), + packageManifest: getPackageManifestMetadata(manifest ?? undefined), }); } @@ -109,7 +120,9 @@ function discoverInDirectory(params: { diagnostics: PluginDiagnostic[]; seen: Set; }) { - if (!fs.existsSync(params.dir)) return; + if (!fs.existsSync(params.dir)) { + return; + } let entries: fs.Dirent[] = []; try { entries = fs.readdirSync(params.dir, { withFileTypes: true }); @@ -125,7 +138,9 @@ function discoverInDirectory(params: { for (const entry of entries) { const fullPath = path.join(params.dir, entry.name); if (entry.isFile()) { - if (!isExtensionFile(fullPath)) continue; + if (!isExtensionFile(fullPath)) { + continue; + } addCandidate({ candidates: params.candidates, seen: params.seen, @@ -136,7 +151,9 @@ function discoverInDirectory(params: { workspaceDir: params.workspaceDir, }); } - if (!entry.isDirectory()) continue; + if (!entry.isDirectory()) { + continue; + } const manifest = readPackageManifest(fullPath); const extensions = manifest ? resolvePackageExtensions(manifest) : []; @@ -281,7 +298,7 @@ function discoverFromPath(params: { } } -export function discoverMoltbotPlugins(params: { +export function discoverOpenClawPlugins(params: { workspaceDir?: string; extraPaths?: string[]; }): PluginDiscoveryResult { @@ -292,9 +309,13 @@ export function discoverMoltbotPlugins(params: { const extra = params.extraPaths ?? []; for (const extraPath of extra) { - if (typeof extraPath !== "string") continue; + if (typeof extraPath !== "string") { + continue; + } const trimmed = extraPath.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } discoverFromPath({ rawPath: trimmed, origin: "config", @@ -306,15 +327,17 @@ export function discoverMoltbotPlugins(params: { } if (workspaceDir) { const workspaceRoot = resolveUserPath(workspaceDir); - const workspaceExt = path.join(workspaceRoot, ".clawdbot", "extensions"); - discoverInDirectory({ - dir: workspaceExt, - origin: "workspace", - workspaceDir: workspaceRoot, - candidates, - diagnostics, - seen, - }); + const workspaceExtDirs = [path.join(workspaceRoot, ".openclaw", "extensions")]; + for (const dir of workspaceExtDirs) { + discoverInDirectory({ + dir, + origin: "workspace", + workspaceDir: workspaceRoot, + candidates, + diagnostics, + seen, + }); + } } const globalDir = path.join(resolveConfigDir(), "extensions"); diff --git a/src/plugins/enable.ts b/src/plugins/enable.ts index 38bfd314d..9f5cc4792 100644 --- a/src/plugins/enable.ts +++ b/src/plugins/enable.ts @@ -1,14 +1,16 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; export type PluginEnableResult = { - config: MoltbotConfig; + config: OpenClawConfig; enabled: boolean; reason?: string; }; -function ensureAllowlisted(cfg: MoltbotConfig, pluginId: string): MoltbotConfig { +function ensureAllowlisted(cfg: OpenClawConfig, pluginId: string): OpenClawConfig { const allow = cfg.plugins?.allow; - if (!Array.isArray(allow) || allow.includes(pluginId)) return cfg; + if (!Array.isArray(allow) || allow.includes(pluginId)) { + return cfg; + } return { ...cfg, plugins: { @@ -18,7 +20,7 @@ function ensureAllowlisted(cfg: MoltbotConfig, pluginId: string): MoltbotConfig }; } -export function enablePluginInConfig(cfg: MoltbotConfig, pluginId: string): PluginEnableResult { +export function enablePluginInConfig(cfg: OpenClawConfig, pluginId: string): PluginEnableResult { if (cfg.plugins?.enabled === false) { return { config: cfg, enabled: false, reason: "plugins disabled" }; } @@ -33,7 +35,7 @@ export function enablePluginInConfig(cfg: MoltbotConfig, pluginId: string): Plug enabled: true, }, }; - let next: MoltbotConfig = { + let next: OpenClawConfig = { ...cfg, plugins: { ...cfg.plugins, diff --git a/src/plugins/hook-runner-global.ts b/src/plugins/hook-runner-global.ts index 368d76684..28d741c79 100644 --- a/src/plugins/hook-runner-global.ts +++ b/src/plugins/hook-runner-global.ts @@ -5,9 +5,9 @@ * and can be called from anywhere in the codebase. */ +import type { PluginRegistry } from "./registry.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { createHookRunner, type HookRunner } from "./hooks.js"; -import type { PluginRegistry } from "./registry.js"; const log = createSubsystemLogger("plugins"); diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index 41fd501f7..987e78942 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -84,7 +84,7 @@ function getHooksForName( ): PluginHookRegistration[] { return (registry.typedHooks as PluginHookRegistration[]) .filter((h) => h.hookName === hookName) - .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); + .toSorted((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); } /** @@ -104,7 +104,9 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp ctx: Parameters["handler"]>>[1], ): Promise { const hooks = getHooksForName(registry, hookName); - if (hooks.length === 0) return; + if (hooks.length === 0) { + return; + } logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers)`); @@ -116,7 +118,7 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp if (catchErrors) { logger?.error(msg); } else { - throw new Error(msg); + throw new Error(msg, { cause: err }); } } }); @@ -135,7 +137,9 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp mergeResults?: (accumulated: TResult | undefined, next: TResult) => TResult, ): Promise { const hooks = getHooksForName(registry, hookName); - if (hooks.length === 0) return undefined; + if (hooks.length === 0) { + return undefined; + } logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers, sequential)`); @@ -159,7 +163,7 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp if (catchErrors) { logger?.error(msg); } else { - throw new Error(msg); + throw new Error(msg, { cause: err }); } } } @@ -323,7 +327,9 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp ctx: PluginHookToolResultPersistContext, ): PluginHookToolResultPersistResult | undefined { const hooks = getHooksForName(registry, "tool_result_persist"); - if (hooks.length === 0) return undefined; + if (hooks.length === 0) { + return undefined; + } let current = event.message; @@ -347,13 +353,15 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp } const next = (out as PluginHookToolResultPersistResult | undefined)?.message; - if (next) current = next; + if (next) { + current = next; + } } catch (err) { const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} failed: ${String(err)}`; if (catchErrors) { logger?.error(msg); } else { - throw new Error(msg); + throw new Error(msg, { cause: err }); } } } diff --git a/src/plugins/http-path.ts b/src/plugins/http-path.ts index 341b91dcd..069b5ff8d 100644 --- a/src/plugins/http-path.ts +++ b/src/plugins/http-path.ts @@ -5,7 +5,9 @@ export function normalizePluginHttpPath( const trimmed = path?.trim(); if (!trimmed) { const fallbackTrimmed = fallback?.trim(); - if (!fallbackTrimmed) return null; + if (!fallbackTrimmed) { + return null; + } return fallbackTrimmed.startsWith("/") ? fallbackTrimmed : `/${fallbackTrimmed}`; } return trimmed.startsWith("/") ? trimmed : `/${trimmed}`; diff --git a/src/plugins/http-registry.ts b/src/plugins/http-registry.ts index ae84fc91c..4234d3c2b 100644 --- a/src/plugins/http-registry.ts +++ b/src/plugins/http-registry.ts @@ -1,8 +1,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; - import type { PluginHttpRouteRegistration, PluginRegistry } from "./registry.js"; -import { requireActivePluginRegistry } from "./runtime.js"; import { normalizePluginHttpPath } from "./http-path.js"; +import { requireActivePluginRegistry } from "./runtime.js"; export type PluginHttpRouteHandler = ( req: IncomingMessage, diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index 8299100d6..22bd0990b 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -1,15 +1,15 @@ +import JSZip from "jszip"; import { spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import JSZip from "jszip"; import { afterEach, describe, expect, it } from "vitest"; const tempDirs: string[] = []; function makeTempDir() { - const dir = path.join(os.tmpdir(), `moltbot-plugin-install-${randomUUID()}`); + const dir = path.join(os.tmpdir(), `openclaw-plugin-install-${randomUUID()}`); fs.mkdirSync(dir, { recursive: true }); tempDirs.push(dir); return dir; @@ -28,7 +28,9 @@ function resolveNpmCliJs() { "bin", "npm-cli.js", ); - if (fs.existsSync(fromNodeDir)) return fromNodeDir; + if (fs.existsSync(fromNodeDir)) { + return fromNodeDir; + } const fromLibNodeModules = path.resolve( path.dirname(process.execPath), @@ -39,7 +41,9 @@ function resolveNpmCliJs() { "bin", "npm-cli.js", ); - if (fs.existsSync(fromLibNodeModules)) return fromLibNodeModules; + if (fs.existsSync(fromLibNodeModules)) { + return fromLibNodeModules; + } return null; } @@ -88,7 +92,7 @@ afterEach(() => { }); describe("installPluginFromArchive", () => { - it("installs into ~/.clawdbot/extensions and uses unscoped id", async () => { + it("installs into ~/.openclaw/extensions and uses unscoped id", async () => { const stateDir = makeTempDir(); const workDir = makeTempDir(); const pkgDir = path.join(workDir, "package"); @@ -96,9 +100,9 @@ describe("installPluginFromArchive", () => { fs.writeFileSync( path.join(pkgDir, "package.json"), JSON.stringify({ - name: "@moltbot/voice-call", + name: "@openclaw/voice-call", version: "0.0.1", - moltbot: { extensions: ["./dist/index.js"] }, + openclaw: { extensions: ["./dist/index.js"] }, }), "utf-8", ); @@ -112,9 +116,14 @@ describe("installPluginFromArchive", () => { const extensionsDir = path.join(stateDir, "extensions"); const { installPluginFromArchive } = await import("./install.js"); - const result = await installPluginFromArchive({ archivePath, extensionsDir }); + const result = await installPluginFromArchive({ + archivePath, + extensionsDir, + }); expect(result.ok).toBe(true); - if (!result.ok) return; + if (!result.ok) { + return; + } expect(result.pluginId).toBe("voice-call"); expect(result.targetDir).toBe(path.join(stateDir, "extensions", "voice-call")); expect(fs.existsSync(path.join(result.targetDir, "package.json"))).toBe(true); @@ -129,9 +138,9 @@ describe("installPluginFromArchive", () => { fs.writeFileSync( path.join(pkgDir, "package.json"), JSON.stringify({ - name: "@moltbot/voice-call", + name: "@openclaw/voice-call", version: "0.0.1", - moltbot: { extensions: ["./dist/index.js"] }, + openclaw: { extensions: ["./dist/index.js"] }, }), "utf-8", ); @@ -145,12 +154,20 @@ describe("installPluginFromArchive", () => { const extensionsDir = path.join(stateDir, "extensions"); const { installPluginFromArchive } = await import("./install.js"); - const first = await installPluginFromArchive({ archivePath, extensionsDir }); - const second = await installPluginFromArchive({ archivePath, extensionsDir }); + const first = await installPluginFromArchive({ + archivePath, + extensionsDir, + }); + const second = await installPluginFromArchive({ + archivePath, + extensionsDir, + }); expect(first.ok).toBe(true); expect(second.ok).toBe(false); - if (second.ok) return; + if (second.ok) { + return; + } expect(second.error).toContain("already exists"); }); @@ -163,9 +180,9 @@ describe("installPluginFromArchive", () => { zip.file( "package/package.json", JSON.stringify({ - name: "@moltbot/zipper", + name: "@openclaw/zipper", version: "0.0.1", - moltbot: { extensions: ["./dist/index.js"] }, + openclaw: { extensions: ["./dist/index.js"] }, }), ); zip.file("package/dist/index.js", "export {};"); @@ -174,10 +191,15 @@ describe("installPluginFromArchive", () => { const extensionsDir = path.join(stateDir, "extensions"); const { installPluginFromArchive } = await import("./install.js"); - const result = await installPluginFromArchive({ archivePath, extensionsDir }); + const result = await installPluginFromArchive({ + archivePath, + extensionsDir, + }); expect(result.ok).toBe(true); - if (!result.ok) return; + if (!result.ok) { + return; + } expect(result.pluginId).toBe("zipper"); expect(result.targetDir).toBe(path.join(stateDir, "extensions", "zipper")); expect(fs.existsSync(path.join(result.targetDir, "package.json"))).toBe(true); @@ -192,9 +214,9 @@ describe("installPluginFromArchive", () => { fs.writeFileSync( path.join(pkgDir, "package.json"), JSON.stringify({ - name: "@moltbot/voice-call", + name: "@openclaw/voice-call", version: "0.0.1", - moltbot: { extensions: ["./dist/index.js"] }, + openclaw: { extensions: ["./dist/index.js"] }, }), "utf-8", ); @@ -210,9 +232,9 @@ describe("installPluginFromArchive", () => { fs.writeFileSync( path.join(pkgDir, "package.json"), JSON.stringify({ - name: "@moltbot/voice-call", + name: "@openclaw/voice-call", version: "0.0.2", - moltbot: { extensions: ["./dist/index.js"] }, + openclaw: { extensions: ["./dist/index.js"] }, }), "utf-8", ); @@ -237,21 +259,23 @@ describe("installPluginFromArchive", () => { expect(first.ok).toBe(true); expect(second.ok).toBe(true); - if (!second.ok) return; + if (!second.ok) { + return; + } const manifest = JSON.parse( fs.readFileSync(path.join(second.targetDir, "package.json"), "utf-8"), ) as { version?: string }; expect(manifest.version).toBe("0.0.2"); }); - it("rejects packages without moltbot.extensions", async () => { + it("rejects packages without openclaw.extensions", async () => { const stateDir = makeTempDir(); const workDir = makeTempDir(); const pkgDir = path.join(workDir, "package"); fs.mkdirSync(pkgDir, { recursive: true }); fs.writeFileSync( path.join(pkgDir, "package.json"), - JSON.stringify({ name: "@moltbot/nope", version: "0.0.1" }), + JSON.stringify({ name: "@openclaw/nope", version: "0.0.1" }), "utf-8", ); @@ -263,9 +287,14 @@ describe("installPluginFromArchive", () => { const extensionsDir = path.join(stateDir, "extensions"); const { installPluginFromArchive } = await import("./install.js"); - const result = await installPluginFromArchive({ archivePath, extensionsDir }); + const result = await installPluginFromArchive({ + archivePath, + extensionsDir, + }); expect(result.ok).toBe(false); - if (result.ok) return; - expect(result.error).toContain("moltbot.extensions"); + if (result.ok) { + return; + } + expect(result.error).toContain("openclaw.extensions"); }); }); diff --git a/src/plugins/install.ts b/src/plugins/install.ts index c66db9667..fa27dad2a 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { LEGACY_MANIFEST_KEY } from "../compat/legacy-names.js"; -import { runCommandWithTimeout } from "../process/exec.js"; -import { CONFIG_DIR, resolveUserPath } from "../utils.js"; +import { MANIFEST_KEY } from "../compat/legacy-names.js"; import { extractArchive, fileExists, @@ -11,6 +9,8 @@ import { resolveArchiveKind, resolvePackedRootDir, } from "../infra/archive.js"; +import { runCommandWithTimeout } from "../process/exec.js"; +import { CONFIG_DIR, resolveUserPath } from "../utils.js"; type PluginInstallLogger = { info?: (message: string) => void; @@ -21,9 +21,7 @@ type PackageManifest = { name?: string; version?: string; dependencies?: Record; - moltbot?: { extensions?: string[] }; - [LEGACY_MANIFEST_KEY]?: { extensions?: string[] }; -}; +} & Partial>; export type InstallPluginResult = | { @@ -40,13 +38,17 @@ const defaultLogger: PluginInstallLogger = {}; function unscopedPackageName(name: string): string { const trimmed = name.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed; } function safeDirName(input: string): string { const trimmed = input.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } return trimmed.replaceAll("/", "__"); } @@ -54,14 +56,14 @@ function safeFileName(input: string): string { return safeDirName(input); } -async function ensureMoltbotExtensions(manifest: PackageManifest) { - const extensions = manifest.moltbot?.extensions ?? manifest[LEGACY_MANIFEST_KEY]?.extensions; +async function ensureOpenClawExtensions(manifest: PackageManifest) { + const extensions = manifest[MANIFEST_KEY]?.extensions; if (!Array.isArray(extensions)) { - throw new Error("package.json missing moltbot.extensions"); + throw new Error("package.json missing openclaw.extensions"); } const list = extensions.map((e) => (typeof e === "string" ? e.trim() : "")).filter(Boolean); if (list.length === 0) { - throw new Error("package.json moltbot.extensions is empty"); + throw new Error("package.json openclaw.extensions is empty"); } return list; } @@ -101,7 +103,7 @@ async function installPluginFromPackageDir(params: { let extensions: string[]; try { - extensions = await ensureMoltbotExtensions(manifest); + extensions = await ensureOpenClawExtensions(manifest); } catch (err) { return { ok: false, error: String(err) }; } @@ -219,7 +221,7 @@ export async function installPluginFromArchive(params: { return { ok: false, error: `unsupported archive: ${archivePath}` }; } - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-plugin-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-")); const extractDir = path.join(tmpDir, "extract"); await fs.mkdir(extractDir, { recursive: true }); @@ -350,9 +352,11 @@ export async function installPluginFromNpmSpec(params: { const dryRun = params.dryRun ?? false; const expectedPluginId = params.expectedPluginId; const spec = params.spec.trim(); - if (!spec) return { ok: false, error: "missing npm spec" }; + if (!spec) { + return { ok: false, error: "missing npm spec" }; + } - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-npm-pack-")); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-npm-pack-")); logger.info?.(`Downloading ${spec}…`); const res = await runCommandWithTimeout(["npm", "pack", spec], { timeoutMs: Math.max(timeoutMs, 300_000), diff --git a/src/plugins/installs.ts b/src/plugins/installs.ts index 7cc026076..45a9fa855 100644 --- a/src/plugins/installs.ts +++ b/src/plugins/installs.ts @@ -1,12 +1,12 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; export type PluginInstallUpdate = PluginInstallRecord & { pluginId: string }; export function recordPluginInstall( - cfg: MoltbotConfig, + cfg: OpenClawConfig, update: PluginInstallUpdate, -): MoltbotConfig { +): OpenClawConfig { const { pluginId, ...record } = update; const installs = { ...cfg.plugins?.installs, diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 7e9316171..cd27cc69e 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -3,17 +3,16 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; - -import { loadMoltbotPlugins } from "./loader.js"; +import { loadOpenClawPlugins } from "./loader.js"; type TempPlugin = { dir: string; file: string; id: string }; const tempDirs: string[] = []; -const prevBundledDir = process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR; +const prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} }; function makeTempDir() { - const dir = path.join(os.tmpdir(), `moltbot-plugin-${randomUUID()}`); + const dir = path.join(os.tmpdir(), `openclaw-plugin-${randomUUID()}`); fs.mkdirSync(dir, { recursive: true }); tempDirs.push(dir); return dir; @@ -30,7 +29,7 @@ function writePlugin(params: { const file = path.join(dir, filename); fs.writeFileSync(file, params.body, "utf-8"); fs.writeFileSync( - path.join(dir, "moltbot.plugin.json"), + path.join(dir, "openclaw.plugin.json"), JSON.stringify( { id: params.id, @@ -53,13 +52,13 @@ afterEach(() => { } } if (prevBundledDir === undefined) { - delete process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR; + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; } else { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = prevBundledDir; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundledDir; } }); -describe("loadMoltbotPlugins", () => { +describe("loadOpenClawPlugins", () => { it("disables bundled plugins by default", () => { const bundledDir = makeTempDir(); writePlugin({ @@ -68,9 +67,9 @@ describe("loadMoltbotPlugins", () => { dir: bundledDir, filename: "bundled.ts", }); - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -82,7 +81,7 @@ describe("loadMoltbotPlugins", () => { const bundled = registry.plugins.find((entry) => entry.id === "bundled"); expect(bundled?.status).toBe("disabled"); - const enabledRegistry = loadMoltbotPlugins({ + const enabledRegistry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -125,9 +124,9 @@ describe("loadMoltbotPlugins", () => { dir: bundledDir, filename: "telegram.ts", }); - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -152,9 +151,9 @@ describe("loadMoltbotPlugins", () => { dir: bundledDir, filename: "memory-core.ts", }); - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -177,10 +176,10 @@ describe("loadMoltbotPlugins", () => { fs.writeFileSync( path.join(pluginDir, "package.json"), JSON.stringify({ - name: "@moltbot/memory-core", + name: "@openclaw/memory-core", version: "1.2.3", description: "Memory plugin package", - moltbot: { extensions: ["./index.ts"] }, + openclaw: { extensions: ["./index.ts"] }, }), "utf-8", ); @@ -191,9 +190,9 @@ describe("loadMoltbotPlugins", () => { filename: "index.ts", }); - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -211,13 +210,13 @@ describe("loadMoltbotPlugins", () => { expect(memory?.version).toBe("1.2.3"); }); it("loads plugins from config paths", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const plugin = writePlugin({ id: "allowed", body: `export default { id: "allowed", register(api) { api.registerGatewayMethod("allowed.ping", ({ respond }) => respond(true, { ok: true })); } };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: plugin.dir, config: { @@ -234,13 +233,13 @@ describe("loadMoltbotPlugins", () => { }); it("denylist disables plugins even if allowed", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const plugin = writePlugin({ id: "blocked", body: `export default { id: "blocked", register() {} };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: plugin.dir, config: { @@ -257,13 +256,13 @@ describe("loadMoltbotPlugins", () => { }); it("fails fast on invalid plugin config", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const plugin = writePlugin({ id: "configurable", body: `export default { id: "configurable", register() {} };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: plugin.dir, config: { @@ -284,7 +283,7 @@ describe("loadMoltbotPlugins", () => { }); it("registers channel plugins", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const plugin = writePlugin({ id: "channel-demo", body: `export default { id: "channel-demo", register(api) { @@ -309,7 +308,7 @@ describe("loadMoltbotPlugins", () => { } };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: plugin.dir, config: { @@ -325,7 +324,7 @@ describe("loadMoltbotPlugins", () => { }); it("registers http handlers", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const plugin = writePlugin({ id: "http-demo", body: `export default { id: "http-demo", register(api) { @@ -333,7 +332,7 @@ describe("loadMoltbotPlugins", () => { } };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: plugin.dir, config: { @@ -351,7 +350,7 @@ describe("loadMoltbotPlugins", () => { }); it("registers http routes", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const plugin = writePlugin({ id: "http-route-demo", body: `export default { id: "http-route-demo", register(api) { @@ -359,7 +358,7 @@ describe("loadMoltbotPlugins", () => { } };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: plugin.dir, config: { @@ -378,13 +377,13 @@ describe("loadMoltbotPlugins", () => { }); it("respects explicit disable in config", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const plugin = writePlugin({ id: "config-disable", body: `export default { id: "config-disable", register() {} };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -401,7 +400,7 @@ describe("loadMoltbotPlugins", () => { }); it("enforces memory slot selection", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const memoryA = writePlugin({ id: "memory-a", body: `export default { id: "memory-a", kind: "memory", register() {} };`, @@ -411,7 +410,7 @@ describe("loadMoltbotPlugins", () => { body: `export default { id: "memory-b", kind: "memory", register() {} };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -428,13 +427,13 @@ describe("loadMoltbotPlugins", () => { }); it("disables memory plugins when slot is none", () => { - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const memory = writePlugin({ id: "memory-off", body: `export default { id: "memory-off", kind: "memory", register() {} };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { @@ -456,14 +455,14 @@ describe("loadMoltbotPlugins", () => { dir: bundledDir, filename: "shadow.js", }); - process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir; + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; const override = writePlugin({ id: "shadow", body: `export default { id: "shadow", register() {} };`, }); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ cache: false, config: { plugins: { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 174441bfc..6ea8a2215 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1,37 +1,36 @@ +import { createJiti } from "jiti"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { createJiti } from "jiti"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; +import type { + OpenClawPluginDefinition, + OpenClawPluginModule, + PluginDiagnostic, + PluginLogger, +} from "./types.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveUserPath } from "../utils.js"; -import { discoverMoltbotPlugins } from "./discovery.js"; -import { loadPluginManifestRegistry } from "./manifest-registry.js"; +import { clearPluginCommands } from "./commands.js"; import { normalizePluginsConfig, resolveEnableState, resolveMemorySlotDecision, type NormalizedPluginsConfig, } from "./config-state.js"; +import { discoverOpenClawPlugins } from "./discovery.js"; import { initializeGlobalHookRunner } from "./hook-runner-global.js"; -import { clearPluginCommands } from "./commands.js"; +import { loadPluginManifestRegistry } from "./manifest-registry.js"; import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js"; -import { createPluginRuntime } from "./runtime/index.js"; import { setActivePluginRegistry } from "./runtime.js"; +import { createPluginRuntime } from "./runtime/index.js"; import { validateJsonSchemaValue } from "./schema-validator.js"; -import type { - MoltbotPluginDefinition, - MoltbotPluginModule, - PluginDiagnostic, - PluginLogger, -} from "./types.js"; export type PluginLoadResult = PluginRegistry; export type PluginLoadOptions = { - config?: MoltbotConfig; + config?: OpenClawConfig; workspaceDir?: string; logger?: PluginLogger; coreGatewayHandlers?: Record; @@ -56,10 +55,14 @@ const resolvePluginSdkAlias = (): string | null => { ? [distCandidate, srcCandidate] : [srcCandidate, distCandidate]; for (const candidate of orderedCandidates) { - if (fs.existsSync(candidate)) return candidate; + if (fs.existsSync(candidate)) { + return candidate; + } } const parent = path.dirname(cursor); - if (parent === cursor) break; + if (parent === cursor) { + break; + } cursor = parent; } } catch { @@ -98,8 +101,8 @@ function validatePluginConfig(params: { } function resolvePluginModuleExport(moduleExport: unknown): { - definition?: MoltbotPluginDefinition; - register?: MoltbotPluginDefinition["register"]; + definition?: OpenClawPluginDefinition; + register?: OpenClawPluginDefinition["register"]; } { const resolved = moduleExport && @@ -109,11 +112,11 @@ function resolvePluginModuleExport(moduleExport: unknown): { : moduleExport; if (typeof resolved === "function") { return { - register: resolved as MoltbotPluginDefinition["register"], + register: resolved as OpenClawPluginDefinition["register"], }; } if (resolved && typeof resolved === "object") { - const def = resolved as MoltbotPluginDefinition; + const def = resolved as OpenClawPluginDefinition; const register = def.register ?? def.activate; return { definition: def, register }; } @@ -161,7 +164,7 @@ function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnost diagnostics.push(...append); } -export function loadMoltbotPlugins(options: PluginLoadOptions = {}): PluginRegistry { +export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry { const cfg = options.config ?? {}; const logger = options.logger ?? defaultLogger(); const validateOnly = options.mode === "validate"; @@ -189,7 +192,7 @@ export function loadMoltbotPlugins(options: PluginLoadOptions = {}): PluginRegis coreGatewayHandlers: options.coreGatewayHandlers as Record, }); - const discovery = discoverMoltbotPlugins({ + const discovery = discoverOpenClawPlugins({ workspaceDir: options.workspaceDir, extraPaths: normalized.loadPaths, }); @@ -208,10 +211,7 @@ export function loadMoltbotPlugins(options: PluginLoadOptions = {}): PluginRegis extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"], ...(pluginSdkAlias ? { - alias: { - "clawdbot/plugin-sdk": pluginSdkAlias, - "moltbot/plugin-sdk": pluginSdkAlias, - }, + alias: { "openclaw/plugin-sdk": pluginSdkAlias }, } : {}), }); @@ -289,9 +289,9 @@ export function loadMoltbotPlugins(options: PluginLoadOptions = {}): PluginRegis continue; } - let mod: MoltbotPluginModule | null = null; + let mod: OpenClawPluginModule | null = null; try { - mod = jiti(candidate.source) as MoltbotPluginModule; + mod = jiti(candidate.source) as OpenClawPluginModule; } catch (err) { logger.error(`[plugins] ${record.id} failed to load from ${record.source}: ${String(err)}`); record.status = "error"; @@ -408,7 +408,7 @@ export function loadMoltbotPlugins(options: PluginLoadOptions = {}): PluginRegis try { const result = register(api); - if (result && typeof (result as Promise).then === "function") { + if (result && typeof result.then === "function") { registry.diagnostics.push({ level: "warn", pluginId: record.id, diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 9a53c9d70..4980ddad6 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -1,11 +1,10 @@ import fs from "node:fs"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { PluginConfigUiHint, PluginDiagnostic, PluginKind, PluginOrigin } from "./types.js"; import { resolveUserPath } from "../utils.js"; import { normalizePluginsConfig, type NormalizedPluginsConfig } from "./config-state.js"; -import { discoverMoltbotPlugins, type PluginCandidate } from "./discovery.js"; +import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js"; import { loadPluginManifest, type PluginManifest } from "./manifest.js"; -import type { PluginConfigUiHint, PluginDiagnostic, PluginKind, PluginOrigin } from "./types.js"; export type PluginManifestRecord = { id: string; @@ -36,17 +35,25 @@ const registryCache = new Map 0; } @@ -100,7 +107,7 @@ function buildRecord(params: { } export function loadPluginManifestRegistry(params: { - config?: MoltbotConfig; + config?: OpenClawConfig; workspaceDir?: string; cache?: boolean; env?: NodeJS.ProcessEnv; @@ -114,7 +121,9 @@ export function loadPluginManifestRegistry(params: { const cacheEnabled = params.cache !== false && shouldUseManifestCache(env); if (cacheEnabled) { const cached = registryCache.get(cacheKey); - if (cached && cached.expiresAt > Date.now()) return cached.registry; + if (cached && cached.expiresAt > Date.now()) { + return cached.registry; + } } const discovery = params.candidates @@ -122,7 +131,7 @@ export function loadPluginManifestRegistry(params: { candidates: params.candidates, diagnostics: params.diagnostics ?? [], } - : discoverMoltbotPlugins({ + : discoverOpenClawPlugins({ workspaceDir: params.workspaceDir, extraPaths: normalized.loadPaths, }); diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 1d8633847..023dc28d4 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -1,14 +1,10 @@ import fs from "node:fs"; import path from "node:path"; - -import { LEGACY_MANIFEST_KEY, LEGACY_PLUGIN_MANIFEST_FILENAME } from "../compat/legacy-names.js"; import type { PluginConfigUiHint, PluginKind } from "./types.js"; +import { MANIFEST_KEY } from "../compat/legacy-names.js"; -export const PLUGIN_MANIFEST_FILENAME = "moltbot.plugin.json"; -export const PLUGIN_MANIFEST_FILENAMES = [ - PLUGIN_MANIFEST_FILENAME, - LEGACY_PLUGIN_MANIFEST_FILENAME, -] as const; +export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json"; +export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const; export type PluginManifest = { id: string; @@ -28,7 +24,9 @@ export type PluginManifestLoadResult = | { ok: false; error: string; manifestPath: string }; function normalizeStringList(value: unknown): string[] { - if (!Array.isArray(value)) return []; + if (!Array.isArray(value)) { + return []; + } return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); } @@ -39,7 +37,9 @@ function isRecord(value: unknown): value is Record { export function resolvePluginManifestPath(rootDir: string): string { for (const filename of PLUGIN_MANIFEST_FILENAMES) { const candidate = path.join(rootDir, filename); - if (fs.existsSync(candidate)) return candidate; + if (fs.existsSync(candidate)) { + return candidate; + } } return path.join(rootDir, PLUGIN_MANIFEST_FILENAME); } @@ -102,7 +102,7 @@ export function loadPluginManifest(rootDir: string): PluginManifestLoadResult { }; } -// package.json "moltbot" metadata (used for onboarding/catalog) +// package.json "openclaw" metadata (used for onboarding/catalog) export type PluginPackageChannel = { id?: string; label?: string; @@ -130,23 +130,25 @@ export type PluginPackageInstall = { defaultChoice?: "npm" | "local"; }; -export type MoltbotPackageManifest = { +export type OpenClawPackageManifest = { extensions?: string[]; channel?: PluginPackageChannel; install?: PluginPackageInstall; }; +export type ManifestKey = typeof MANIFEST_KEY; + export type PackageManifest = { name?: string; version?: string; description?: string; - moltbot?: MoltbotPackageManifest; - [LEGACY_MANIFEST_KEY]?: MoltbotPackageManifest; -}; +} & Partial>; export function getPackageManifestMetadata( manifest: PackageManifest | undefined, -): MoltbotPackageManifest | undefined { - if (!manifest) return undefined; - return manifest.moltbot ?? manifest[LEGACY_MANIFEST_KEY]; +): OpenClawPackageManifest | undefined { + if (!manifest) { + return undefined; + } + return manifest[MANIFEST_KEY]; } diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 88d168836..0236a5d4d 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -1,6 +1,6 @@ -import { createSubsystemLogger } from "../logging/subsystem.js"; -import { loadMoltbotPlugins, type PluginLoadOptions } from "./loader.js"; import type { ProviderPlugin } from "./types.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; +import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js"; const log = createSubsystemLogger("plugins"); @@ -8,7 +8,7 @@ export function resolvePluginProviders(params: { config?: PluginLoadOptions["config"]; workspaceDir?: string; }): ProviderPlugin[] { - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ config: params.config, workspaceDir: params.workspaceDir, logger: { diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 226f6e496..ef7639100 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import type { AnyAgentTool } from "../agents/tools/common.js"; import type { ChannelDock } from "../channels/dock.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; @@ -5,20 +6,20 @@ import type { GatewayRequestHandler, GatewayRequestHandlers, } from "../gateway/server-methods/types.js"; -import { registerInternalHook } from "../hooks/internal-hooks.js"; -import { resolveUserPath } from "../utils.js"; +import type { HookEntry } from "../hooks/types.js"; +import type { PluginRuntime } from "./runtime/types.js"; import type { - MoltbotPluginApi, - MoltbotPluginChannelRegistration, - MoltbotPluginCliRegistrar, - MoltbotPluginCommandDefinition, - MoltbotPluginHttpHandler, - MoltbotPluginHttpRouteHandler, - MoltbotPluginHookOptions, + OpenClawPluginApi, + OpenClawPluginChannelRegistration, + OpenClawPluginCliRegistrar, + OpenClawPluginCommandDefinition, + OpenClawPluginHttpHandler, + OpenClawPluginHttpRouteHandler, + OpenClawPluginHookOptions, ProviderPlugin, - MoltbotPluginService, - MoltbotPluginToolContext, - MoltbotPluginToolFactory, + OpenClawPluginService, + OpenClawPluginToolContext, + OpenClawPluginToolFactory, PluginConfigUiHint, PluginDiagnostic, PluginLogger, @@ -28,15 +29,14 @@ import type { PluginHookHandlerMap, PluginHookRegistration as TypedPluginHookRegistration, } from "./types.js"; +import { registerInternalHook } from "../hooks/internal-hooks.js"; +import { resolveUserPath } from "../utils.js"; import { registerPluginCommand } from "./commands.js"; -import type { PluginRuntime } from "./runtime/types.js"; -import type { HookEntry } from "../hooks/types.js"; -import path from "node:path"; import { normalizePluginHttpPath } from "./http-path.js"; export type PluginToolRegistration = { pluginId: string; - factory: MoltbotPluginToolFactory; + factory: OpenClawPluginToolFactory; names: string[]; optional: boolean; source: string; @@ -44,21 +44,21 @@ export type PluginToolRegistration = { export type PluginCliRegistration = { pluginId: string; - register: MoltbotPluginCliRegistrar; + register: OpenClawPluginCliRegistrar; commands: string[]; source: string; }; export type PluginHttpRegistration = { pluginId: string; - handler: MoltbotPluginHttpHandler; + handler: OpenClawPluginHttpHandler; source: string; }; export type PluginHttpRouteRegistration = { pluginId?: string; path: string; - handler: MoltbotPluginHttpRouteHandler; + handler: OpenClawPluginHttpRouteHandler; source?: string; }; @@ -84,13 +84,13 @@ export type PluginHookRegistration = { export type PluginServiceRegistration = { pluginId: string; - service: MoltbotPluginService; + service: OpenClawPluginService; source: string; }; export type PluginCommandRegistration = { pluginId: string; - command: MoltbotPluginCommandDefinition; + command: OpenClawPluginCommandDefinition; source: string; }; @@ -167,13 +167,13 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { const registerTool = ( record: PluginRecord, - tool: AnyAgentTool | MoltbotPluginToolFactory, + tool: AnyAgentTool | OpenClawPluginToolFactory, opts?: { name?: string; names?: string[]; optional?: boolean }, ) => { const names = opts?.names ?? (opts?.name ? [opts.name] : []); const optional = opts?.optional === true; - const factory: MoltbotPluginToolFactory = - typeof tool === "function" ? tool : (_ctx: MoltbotPluginToolContext) => tool; + const factory: OpenClawPluginToolFactory = + typeof tool === "function" ? tool : (_ctx: OpenClawPluginToolContext) => tool; if (typeof tool !== "function") { names.push(tool.name); @@ -196,8 +196,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { record: PluginRecord, events: string | string[], handler: Parameters[1], - opts: MoltbotPluginHookOptions | undefined, - config: MoltbotPluginApi["config"], + opts: OpenClawPluginHookOptions | undefined, + config: OpenClawPluginApi["config"], ) => { const eventList = Array.isArray(events) ? events : [events]; const normalizedEvents = eventList.map((event) => event.trim()).filter(Boolean); @@ -221,7 +221,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { ...entry.hook, name, description, - source: "moltbot-plugin", + source: "openclaw-plugin", pluginId: record.id, }, metadata: { @@ -233,7 +233,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { hook: { name, description, - source: "moltbot-plugin", + source: "openclaw-plugin", pluginId: record.id, filePath: record.source, baseDir: path.dirname(record.source), @@ -268,7 +268,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { handler: GatewayRequestHandler, ) => { const trimmed = method.trim(); - if (!trimmed) return; + if (!trimmed) { + return; + } if (coreGatewayMethods.has(trimmed) || registry.gatewayHandlers[trimmed]) { pushDiagnostic({ level: "error", @@ -282,7 +284,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { record.gatewayMethods.push(trimmed); }; - const registerHttpHandler = (record: PluginRecord, handler: MoltbotPluginHttpHandler) => { + const registerHttpHandler = (record: PluginRecord, handler: OpenClawPluginHttpHandler) => { record.httpHandlers += 1; registry.httpHandlers.push({ pluginId: record.id, @@ -293,7 +295,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { const registerHttpRoute = ( record: PluginRecord, - params: { path: string; handler: MoltbotPluginHttpRouteHandler }, + params: { path: string; handler: OpenClawPluginHttpRouteHandler }, ) => { const normalizedPath = normalizePluginHttpPath(params.path); if (!normalizedPath) { @@ -325,11 +327,11 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { const registerChannel = ( record: PluginRecord, - registration: MoltbotPluginChannelRegistration | ChannelPlugin, + registration: OpenClawPluginChannelRegistration | ChannelPlugin, ) => { const normalized = - typeof (registration as MoltbotPluginChannelRegistration).plugin === "object" - ? (registration as MoltbotPluginChannelRegistration) + typeof (registration as OpenClawPluginChannelRegistration).plugin === "object" + ? (registration as OpenClawPluginChannelRegistration) : { plugin: registration as ChannelPlugin }; const plugin = normalized.plugin; const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim(); @@ -382,7 +384,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { const registerCli = ( record: PluginRecord, - registrar: MoltbotPluginCliRegistrar, + registrar: OpenClawPluginCliRegistrar, opts?: { commands?: string[] }, ) => { const commands = (opts?.commands ?? []).map((cmd) => cmd.trim()).filter(Boolean); @@ -395,9 +397,11 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { }); }; - const registerService = (record: PluginRecord, service: MoltbotPluginService) => { + const registerService = (record: PluginRecord, service: OpenClawPluginService) => { const id = service.id.trim(); - if (!id) return; + if (!id) { + return; + } record.services.push(id); registry.services.push({ pluginId: record.id, @@ -406,7 +410,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { }); }; - const registerCommand = (record: PluginRecord, command: MoltbotPluginCommandDefinition) => { + const registerCommand = (record: PluginRecord, command: OpenClawPluginCommandDefinition) => { const name = command.name.trim(); if (!name) { pushDiagnostic({ @@ -464,10 +468,10 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { const createApi = ( record: PluginRecord, params: { - config: MoltbotPluginApi["config"]; + config: OpenClawPluginApi["config"]; pluginConfig?: Record; }, - ): MoltbotPluginApi => { + ): OpenClawPluginApi => { return { id: record.id, name: record.name, diff --git a/src/plugins/runtime.ts b/src/plugins/runtime.ts index 70e958b5a..cebd88d41 100644 --- a/src/plugins/runtime.ts +++ b/src/plugins/runtime.ts @@ -16,7 +16,7 @@ const createEmptyRegistry = (): PluginRegistry => ({ diagnostics: [], }); -const REGISTRY_STATE = Symbol.for("moltbot.pluginRegistryState"); +const REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); type RegistryState = { registry: PluginRegistry | null; @@ -33,7 +33,7 @@ const state: RegistryState = (() => { key: null, }; } - return globalState[REGISTRY_STATE] as RegistryState; + return globalState[REGISTRY_STATE]; })(); export function setActivePluginRegistry(registry: PluginRegistry, cacheKey?: string) { diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index 685dcb38b..4f3618a76 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -1,5 +1,9 @@ import { createRequire } from "node:module"; - +import type { PluginRuntime } from "./types.js"; +import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js"; +import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js"; +import { handleSlackAction } from "../../agents/tools/slack-actions.js"; +import { handleWhatsAppAction } from "../../agents/tools/whatsapp-actions.js"; import { chunkByNewline, chunkMarkdownText, @@ -15,16 +19,17 @@ import { shouldComputeCommandAuthorized, } from "../../auto-reply/command-detection.js"; import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js"; -import { - createInboundDebouncer, - resolveInboundDebounceMs, -} from "../../auto-reply/inbound-debounce.js"; import { formatAgentEnvelope, formatInboundEnvelope, resolveEnvelopeFormatOptions, } from "../../auto-reply/envelope.js"; +import { + createInboundDebouncer, + resolveInboundDebounceMs, +} from "../../auto-reply/inbound-debounce.js"; import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js"; +import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { buildMentionRegexes, matchesMentionPatterns, @@ -32,26 +37,22 @@ import { } from "../../auto-reply/reply/mentions.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply/provider-dispatcher.js"; import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js"; -import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; -import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js"; -import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js"; -import { handleSlackAction } from "../../agents/tools/slack-actions.js"; -import { handleWhatsAppAction } from "../../agents/tools/whatsapp-actions.js"; import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; -import { recordInboundSession } from "../../channels/session.js"; import { discordMessageActions } from "../../channels/plugins/actions/discord.js"; import { signalMessageActions } from "../../channels/plugins/actions/signal.js"; import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js"; import { createWhatsAppLoginTool } from "../../channels/plugins/agent-tools/whatsapp-login.js"; +import { recordInboundSession } from "../../channels/session.js"; import { monitorWebChannel } from "../../channels/web/index.js"; +import { registerMemoryCli } from "../../cli/memory-cli.js"; +import { loadConfig, writeConfigFile } from "../../config/config.js"; import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention, } from "../../config/group-policy.js"; import { resolveMarkdownTableMode } from "../../config/markdown-tables.js"; import { resolveStateDir } from "../../config/paths.js"; -import { loadConfig, writeConfigFile } from "../../config/config.js"; import { readSessionUpdatedAt, recordSessionMetaFromInbound, @@ -68,15 +69,34 @@ import { probeDiscord } from "../../discord/probe.js"; import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js"; import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js"; import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js"; -import { getChannelActivity, recordChannelActivity } from "../../infra/channel-activity.js"; -import { enqueueSystemEvent } from "../../infra/system-events.js"; +import { shouldLogVerbose } from "../../globals.js"; import { monitorIMessageProvider } from "../../imessage/monitor.js"; import { probeIMessage } from "../../imessage/probe.js"; import { sendMessageIMessage } from "../../imessage/send.js"; -import { shouldLogVerbose } from "../../globals.js"; -import { convertMarkdownTables } from "../../markdown/tables.js"; +import { getChannelActivity, recordChannelActivity } from "../../infra/channel-activity.js"; +import { enqueueSystemEvent } from "../../infra/system-events.js"; +import { + listLineAccountIds, + normalizeAccountId as normalizeLineAccountId, + resolveDefaultLineAccountId, + resolveLineAccount, +} from "../../line/accounts.js"; +import { monitorLineProvider } from "../../line/monitor.js"; +import { probeLineBot } from "../../line/probe.js"; +import { + createQuickReplyItems, + pushMessageLine, + pushMessagesLine, + pushFlexMessage, + pushTemplateMessage, + pushLocationMessage, + pushTextMessageWithQuickReplies, + sendMessageLine, +} from "../../line/send.js"; +import { buildTemplateMessageFromPayload } from "../../line/template-messages.js"; import { getChildLogger } from "../../logging.js"; import { normalizeLogLevel } from "../../logging/levels.js"; +import { convertMarkdownTables } from "../../markdown/tables.js"; import { isVoiceCompatibleAudio } from "../../media/audio.js"; import { mediaKindFromMime } from "../../media/constants.js"; import { fetchRemoteMedia } from "../../media/fetch.js"; @@ -93,11 +113,11 @@ import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { monitorSignalProvider } from "../../signal/index.js"; import { probeSignal } from "../../signal/probe.js"; import { sendMessageSignal } from "../../signal/send.js"; -import { monitorSlackProvider } from "../../slack/index.js"; import { listSlackDirectoryGroupsLive, listSlackDirectoryPeersLive, } from "../../slack/directory-live.js"; +import { monitorSlackProvider } from "../../slack/index.js"; import { probeSlack } from "../../slack/probe.js"; import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js"; import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js"; @@ -110,7 +130,7 @@ import { monitorTelegramProvider } from "../../telegram/monitor.js"; import { probeTelegram } from "../../telegram/probe.js"; import { sendMessageTelegram } from "../../telegram/send.js"; import { resolveTelegramToken } from "../../telegram/token.js"; -import { loadWebMedia } from "../../web/media.js"; +import { textToSpeechTelephony } from "../../tts/tts.js"; import { getActiveWebListener } from "../../web/active-listener.js"; import { getWebAuthAgeMs, @@ -119,38 +139,18 @@ import { readWebSelfId, webAuthExists, } from "../../web/auth-store.js"; -import { loginWeb } from "../../web/login.js"; import { startWebLoginWithQr, waitForWebLogin } from "../../web/login-qr.js"; +import { loginWeb } from "../../web/login.js"; +import { loadWebMedia } from "../../web/media.js"; import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js"; -import { registerMemoryCli } from "../../cli/memory-cli.js"; import { formatNativeDependencyHint } from "./native-deps.js"; -import { textToSpeechTelephony } from "../../tts/tts.js"; -import { - listLineAccountIds, - normalizeAccountId as normalizeLineAccountId, - resolveDefaultLineAccountId, - resolveLineAccount, -} from "../../line/accounts.js"; -import { probeLineBot } from "../../line/probe.js"; -import { - createQuickReplyItems, - pushMessageLine, - pushMessagesLine, - pushFlexMessage, - pushTemplateMessage, - pushLocationMessage, - pushTextMessageWithQuickReplies, - sendMessageLine, -} from "../../line/send.js"; -import { monitorLineProvider } from "../../line/monitor.js"; -import { buildTemplateMessageFromPayload } from "../../line/template-messages.js"; - -import type { PluginRuntime } from "./types.js"; let cachedVersion: string | null = null; function resolveVersion(): string { - if (cachedVersion) return cachedVersion; + if (cachedVersion) { + return cachedVersion; + } try { const require = createRequire(import.meta.url); const pkg = require("../../../package.json") as { version?: string }; diff --git a/src/plugins/schema-validator.ts b/src/plugins/schema-validator.ts index 97adb9b3b..1244dfc76 100644 --- a/src/plugins/schema-validator.ts +++ b/src/plugins/schema-validator.ts @@ -14,7 +14,9 @@ type CachedValidator = { const schemaCache = new Map(); function formatAjvErrors(errors: ErrorObject[] | null | undefined): string[] { - if (!errors || errors.length === 0) return ["invalid config"]; + if (!errors || errors.length === 0) { + return ["invalid config"]; + } return errors.map((error) => { const path = error.instancePath?.replace(/^\//, "").replace(/\//g, ".") || ""; const message = error.message ?? "invalid"; @@ -29,12 +31,14 @@ export function validateJsonSchemaValue(params: { }): { ok: true } | { ok: false; errors: string[] } { let cached = schemaCache.get(params.cacheKey); if (!cached || cached.schema !== params.schema) { - const validate = ajv.compile(params.schema) as ValidateFunction; + const validate = ajv.compile(params.schema); cached = { validate, schema: params.schema }; schemaCache.set(params.cacheKey, cached); } const ok = cached.validate(params.value); - if (ok) return { ok: true }; + if (ok) { + return { ok: true }; + } return { ok: false, errors: formatAjvErrors(cached.validate.errors) }; } diff --git a/src/plugins/services.ts b/src/plugins/services.ts index 8f1f5f8ba..09e96634c 100644 --- a/src/plugins/services.ts +++ b/src/plugins/services.ts @@ -1,7 +1,7 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { PluginRegistry } from "./registry.js"; import { STATE_DIR } from "../config/paths.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import type { PluginRegistry } from "./registry.js"; const log = createSubsystemLogger("plugins"); @@ -11,7 +11,7 @@ export type PluginServicesHandle = { export async function startPluginServices(params: { registry: PluginRegistry; - config: MoltbotConfig; + config: OpenClawConfig; workspaceDir?: string; }): Promise { const running: Array<{ @@ -57,8 +57,10 @@ export async function startPluginServices(params: { return { stop: async () => { - for (const entry of running.reverse()) { - if (!entry.stop) continue; + for (const entry of running.toReversed()) { + if (!entry.stop) { + continue; + } try { await entry.stop(); } catch (err) { diff --git a/src/plugins/slots.test.ts b/src/plugins/slots.test.ts index ae0cf4d6a..bc1cca8d9 100644 --- a/src/plugins/slots.test.ts +++ b/src/plugins/slots.test.ts @@ -1,11 +1,10 @@ import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { applyExclusiveSlotSelection } from "./slots.js"; describe("applyExclusiveSlotSelection", () => { it("selects the slot and disables other entries for the same kind", () => { - const config: MoltbotConfig = { + const config: OpenClawConfig = { plugins: { slots: { memory: "memory-core" }, entries: { @@ -37,7 +36,7 @@ describe("applyExclusiveSlotSelection", () => { }); it("does nothing when the slot already matches", () => { - const config: MoltbotConfig = { + const config: OpenClawConfig = { plugins: { slots: { memory: "memory" }, entries: { @@ -59,7 +58,7 @@ describe("applyExclusiveSlotSelection", () => { }); it("warns when the slot falls back to a default", () => { - const config: MoltbotConfig = { + const config: OpenClawConfig = { plugins: { entries: { memory: { enabled: true }, @@ -81,7 +80,7 @@ describe("applyExclusiveSlotSelection", () => { }); it("skips changes when no exclusive slot applies", () => { - const config: MoltbotConfig = {}; + const config: OpenClawConfig = {}; const result = applyExclusiveSlotSelection({ config, selectedId: "custom", diff --git a/src/plugins/slots.ts b/src/plugins/slots.ts index 07c99c3e3..8fee7172a 100644 --- a/src/plugins/slots.ts +++ b/src/plugins/slots.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { PluginSlotsConfig } from "../config/types.plugins.js"; import type { PluginKind } from "./types.js"; @@ -18,7 +18,9 @@ const DEFAULT_SLOT_BY_KEY: Record = { }; export function slotKeyForPluginKind(kind?: PluginKind): PluginSlotKey | null { - if (!kind) return null; + if (!kind) { + return null; + } return SLOT_BY_KIND[kind] ?? null; } @@ -27,13 +29,13 @@ export function defaultSlotIdForKey(slotKey: PluginSlotKey): string { } export type SlotSelectionResult = { - config: MoltbotConfig; + config: OpenClawConfig; warnings: string[]; changed: boolean; }; export function applyExclusiveSlotSelection(params: { - config: MoltbotConfig; + config: OpenClawConfig; selectedId: string; selectedKind?: PluginKind; registry?: { plugins: SlotPluginRecord[] }; @@ -62,8 +64,12 @@ export function applyExclusiveSlotSelection(params: { const disabledIds: string[] = []; if (params.registry) { for (const plugin of params.registry.plugins) { - if (plugin.id === params.selectedId) continue; - if (plugin.kind !== params.selectedKind) continue; + if (plugin.id === params.selectedId) { + continue; + } + if (plugin.kind !== params.selectedKind) { + continue; + } const entry = entries[plugin.id]; if (!entry || entry.enabled !== false) { entries[plugin.id] = { @@ -76,7 +82,9 @@ export function applyExclusiveSlotSelection(params: { } if (disabledIds.length > 0) { - warnings.push(`Disabled other "${slotKey}" slot plugins: ${disabledIds.sort().join(", ")}.`); + warnings.push( + `Disabled other "${slotKey}" slot plugins: ${disabledIds.toSorted().join(", ")}.`, + ); } const changed = prevSlot !== params.selectedId || disabledIds.length > 0; diff --git a/src/plugins/status.ts b/src/plugins/status.ts index 3de6435f8..9077602a4 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -1,9 +1,9 @@ +import type { PluginRegistry } from "./registry.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js"; import { loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { loadMoltbotPlugins } from "./loader.js"; -import type { PluginRegistry } from "./registry.js"; +import { loadOpenClawPlugins } from "./loader.js"; export type PluginStatusReport = PluginRegistry & { workspaceDir?: string; @@ -21,7 +21,7 @@ export function buildPluginStatusReport(params?: { : (resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? resolveDefaultAgentWorkspaceDir()); - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ config, workspaceDir, logger: { diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index e596288b6..1f15eec90 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -2,9 +2,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - import { afterEach, describe, expect, it } from "vitest"; - import { resolvePluginTools } from "./tools.js"; type TempPlugin = { dir: string; file: string; id: string }; @@ -13,7 +11,7 @@ const tempDirs: string[] = []; const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} }; function makeTempDir() { - const dir = path.join(os.tmpdir(), `moltbot-plugin-tools-${randomUUID()}`); + const dir = path.join(os.tmpdir(), `openclaw-plugin-tools-${randomUUID()}`); fs.mkdirSync(dir, { recursive: true }); tempDirs.push(dir); return dir; @@ -24,7 +22,7 @@ function writePlugin(params: { id: string; body: string }): TempPlugin { const file = path.join(dir, `${params.id}.js`); fs.writeFileSync(file, params.body, "utf-8"); fs.writeFileSync( - path.join(dir, "moltbot.plugin.json"), + path.join(dir, "openclaw.plugin.json"), JSON.stringify( { id: params.id, diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 09e4af8bc..4284c87d6 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -1,8 +1,8 @@ import type { AnyAgentTool } from "../agents/tools/common.js"; +import type { OpenClawPluginToolContext } from "./types.js"; import { normalizeToolName } from "../agents/tool-policy.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { loadMoltbotPlugins } from "./loader.js"; -import type { MoltbotPluginToolContext } from "./types.js"; +import { loadOpenClawPlugins } from "./loader.js"; const log = createSubsystemLogger("plugins"); @@ -26,20 +26,26 @@ function isOptionalToolAllowed(params: { pluginId: string; allowlist: Set; }): boolean { - if (params.allowlist.size === 0) return false; + if (params.allowlist.size === 0) { + return false; + } const toolName = normalizeToolName(params.toolName); - if (params.allowlist.has(toolName)) return true; + if (params.allowlist.has(toolName)) { + return true; + } const pluginKey = normalizeToolName(params.pluginId); - if (params.allowlist.has(pluginKey)) return true; + if (params.allowlist.has(pluginKey)) { + return true; + } return params.allowlist.has("group:plugins"); } export function resolvePluginTools(params: { - context: MoltbotPluginToolContext; + context: OpenClawPluginToolContext; existingToolNames?: Set; toolAllowlist?: string[]; }): AnyAgentTool[] { - const registry = loadMoltbotPlugins({ + const registry = loadOpenClawPlugins({ config: params.context.config, workspaceDir: params.context.workspaceDir, logger: { @@ -57,7 +63,9 @@ export function resolvePluginTools(params: { const blockedPlugins = new Set(); for (const entry of registry.tools) { - if (blockedPlugins.has(entry.pluginId)) continue; + if (blockedPlugins.has(entry.pluginId)) { + continue; + } const pluginIdKey = normalizeToolName(entry.pluginId); if (existingNormalized.has(pluginIdKey)) { const message = `plugin id conflicts with core tool name (${entry.pluginId})`; @@ -78,7 +86,9 @@ export function resolvePluginTools(params: { log.error(`plugin tool failed (${entry.pluginId}): ${String(err)}`); continue; } - if (!resolved) continue; + if (!resolved) { + continue; + } const listRaw = Array.isArray(resolved) ? resolved : [resolved]; const list = entry.optional ? listRaw.filter((tool) => @@ -89,7 +99,9 @@ export function resolvePluginTools(params: { }), ) : listRaw; - if (list.length === 0) continue; + if (list.length === 0) { + continue; + } const nameSet = new Set(); for (const tool of list) { if (nameSet.has(tool.name) || existing.has(tool.name)) { diff --git a/src/plugins/types.ts b/src/plugins/types.ts index f9aaa5709..4dbab48ff 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1,21 +1,19 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; -import type { Command } from "commander"; - import type { AgentMessage } from "@mariozechner/pi-agent-core"; - +import type { Command } from "commander"; +import type { IncomingMessage, ServerResponse } from "node:http"; import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; +import type { ReplyPayload } from "../auto-reply/types.js"; import type { ChannelDock } from "../channels/dock.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { ModelProviderConfig } from "../config/types.js"; +import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; import type { InternalHookHandler } from "../hooks/internal-hooks.js"; import type { HookEntry } from "../hooks/types.js"; -import type { ModelProviderConfig } from "../config/types.js"; import type { RuntimeEnv } from "../runtime.js"; -import type { ReplyPayload } from "../auto-reply/types.js"; import type { WizardPrompter } from "../wizard/prompts.js"; -import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js"; -import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; import type { PluginRuntime } from "./runtime/types.js"; export type { PluginRuntime } from "./runtime/types.js"; @@ -41,7 +39,7 @@ export type PluginConfigValidation = | { ok: true; value?: unknown } | { ok: false; errors: string[] }; -export type MoltbotPluginConfigSchema = { +export type OpenClawPluginConfigSchema = { safeParse?: (value: unknown) => { success: boolean; data?: unknown; @@ -55,8 +53,8 @@ export type MoltbotPluginConfigSchema = { jsonSchema?: Record; }; -export type MoltbotPluginToolContext = { - config?: MoltbotConfig; +export type OpenClawPluginToolContext = { + config?: OpenClawConfig; workspaceDir?: string; agentDir?: string; agentId?: string; @@ -66,17 +64,17 @@ export type MoltbotPluginToolContext = { sandboxed?: boolean; }; -export type MoltbotPluginToolFactory = ( - ctx: MoltbotPluginToolContext, +export type OpenClawPluginToolFactory = ( + ctx: OpenClawPluginToolContext, ) => AnyAgentTool | AnyAgentTool[] | null | undefined; -export type MoltbotPluginToolOptions = { +export type OpenClawPluginToolOptions = { name?: string; names?: string[]; optional?: boolean; }; -export type MoltbotPluginHookOptions = { +export type OpenClawPluginHookOptions = { entry?: HookEntry; name?: string; description?: string; @@ -87,13 +85,13 @@ export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | " export type ProviderAuthResult = { profiles: Array<{ profileId: string; credential: AuthProfileCredential }>; - configPatch?: Partial; + configPatch?: Partial; defaultModel?: string; notes?: string[]; }; export type ProviderAuthContext = { - config: MoltbotConfig; + config: OpenClawConfig; agentDir?: string; workspaceDir?: string; prompter: WizardPrompter; @@ -125,7 +123,7 @@ export type ProviderPlugin = { refreshOAuth?: (cred: OAuthCredential) => Promise; }; -export type MoltbotPluginGatewayMethod = { +export type OpenClawPluginGatewayMethod = { method: string; handler: GatewayRequestHandler; }; @@ -148,8 +146,8 @@ export type PluginCommandContext = { args?: string; /** The full normalized command body */ commandBody: string; - /** Current moltbot configuration */ - config: MoltbotConfig; + /** Current OpenClaw configuration */ + config: OpenClawConfig; }; /** @@ -167,7 +165,7 @@ export type PluginCommandHandler = ( /** * Definition for a plugin-registered command. */ -export type MoltbotPluginCommandDefinition = { +export type OpenClawPluginCommandDefinition = { /** Command name without leading slash (e.g., "tts") */ name: string; /** Description shown in /help and command menus */ @@ -180,90 +178,90 @@ export type MoltbotPluginCommandDefinition = { handler: PluginCommandHandler; }; -export type MoltbotPluginHttpHandler = ( +export type OpenClawPluginHttpHandler = ( req: IncomingMessage, res: ServerResponse, ) => Promise | boolean; -export type MoltbotPluginHttpRouteHandler = ( +export type OpenClawPluginHttpRouteHandler = ( req: IncomingMessage, res: ServerResponse, ) => Promise | void; -export type MoltbotPluginCliContext = { +export type OpenClawPluginCliContext = { program: Command; - config: MoltbotConfig; + config: OpenClawConfig; workspaceDir?: string; logger: PluginLogger; }; -export type MoltbotPluginCliRegistrar = (ctx: MoltbotPluginCliContext) => void | Promise; +export type OpenClawPluginCliRegistrar = (ctx: OpenClawPluginCliContext) => void | Promise; -export type MoltbotPluginServiceContext = { - config: MoltbotConfig; +export type OpenClawPluginServiceContext = { + config: OpenClawConfig; workspaceDir?: string; stateDir: string; logger: PluginLogger; }; -export type MoltbotPluginService = { +export type OpenClawPluginService = { id: string; - start: (ctx: MoltbotPluginServiceContext) => void | Promise; - stop?: (ctx: MoltbotPluginServiceContext) => void | Promise; + start: (ctx: OpenClawPluginServiceContext) => void | Promise; + stop?: (ctx: OpenClawPluginServiceContext) => void | Promise; }; -export type MoltbotPluginChannelRegistration = { +export type OpenClawPluginChannelRegistration = { plugin: ChannelPlugin; dock?: ChannelDock; }; -export type MoltbotPluginDefinition = { +export type OpenClawPluginDefinition = { id?: string; name?: string; description?: string; version?: string; kind?: PluginKind; - configSchema?: MoltbotPluginConfigSchema; - register?: (api: MoltbotPluginApi) => void | Promise; - activate?: (api: MoltbotPluginApi) => void | Promise; + configSchema?: OpenClawPluginConfigSchema; + register?: (api: OpenClawPluginApi) => void | Promise; + activate?: (api: OpenClawPluginApi) => void | Promise; }; -export type MoltbotPluginModule = - | MoltbotPluginDefinition - | ((api: MoltbotPluginApi) => void | Promise); +export type OpenClawPluginModule = + | OpenClawPluginDefinition + | ((api: OpenClawPluginApi) => void | Promise); -export type MoltbotPluginApi = { +export type OpenClawPluginApi = { id: string; name: string; version?: string; description?: string; source: string; - config: MoltbotConfig; + config: OpenClawConfig; pluginConfig?: Record; runtime: PluginRuntime; logger: PluginLogger; registerTool: ( - tool: AnyAgentTool | MoltbotPluginToolFactory, - opts?: MoltbotPluginToolOptions, + tool: AnyAgentTool | OpenClawPluginToolFactory, + opts?: OpenClawPluginToolOptions, ) => void; registerHook: ( events: string | string[], handler: InternalHookHandler, - opts?: MoltbotPluginHookOptions, + opts?: OpenClawPluginHookOptions, ) => void; - registerHttpHandler: (handler: MoltbotPluginHttpHandler) => void; - registerHttpRoute: (params: { path: string; handler: MoltbotPluginHttpRouteHandler }) => void; - registerChannel: (registration: MoltbotPluginChannelRegistration | ChannelPlugin) => void; + registerHttpHandler: (handler: OpenClawPluginHttpHandler) => void; + registerHttpRoute: (params: { path: string; handler: OpenClawPluginHttpRouteHandler }) => void; + registerChannel: (registration: OpenClawPluginChannelRegistration | ChannelPlugin) => void; registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void; - registerCli: (registrar: MoltbotPluginCliRegistrar, opts?: { commands?: string[] }) => void; - registerService: (service: MoltbotPluginService) => void; + registerCli: (registrar: OpenClawPluginCliRegistrar, opts?: { commands?: string[] }) => void; + registerService: (service: OpenClawPluginService) => void; registerProvider: (provider: ProviderPlugin) => void; /** * Register a custom command that bypasses the LLM agent. * Plugin commands are processed before built-in commands and before agent invocation. * Use this for simple state-toggling or status commands that don't need AI reasoning. */ - registerCommand: (command: MoltbotPluginCommandDefinition) => void; + registerCommand: (command: OpenClawPluginCommandDefinition) => void; resolvePath: (input: string) => string; /** Register a lifecycle hook handler */ on: ( diff --git a/src/plugins/update.ts b/src/plugins/update.ts index 67ca694c5..df312dc1e 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -1,9 +1,8 @@ import fs from "node:fs/promises"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { UpdateChannel } from "../infra/update-channels.js"; import { resolveUserPath } from "../utils.js"; -import { discoverMoltbotPlugins } from "./discovery.js"; +import { discoverOpenClawPlugins } from "./discovery.js"; import { installPluginFromNpmSpec, resolvePluginInstallDir } from "./install.js"; import { recordPluginInstall } from "./installs.js"; import { loadPluginManifest } from "./manifest.js"; @@ -25,7 +24,7 @@ export type PluginUpdateOutcome = { }; export type PluginUpdateSummary = { - config: MoltbotConfig; + config: OpenClawConfig; changed: boolean; outcomes: PluginUpdateOutcome[]; }; @@ -38,7 +37,7 @@ export type PluginChannelSyncSummary = { }; export type PluginChannelSyncResult = { - config: MoltbotConfig; + config: OpenClawConfig; changed: boolean; summary: PluginChannelSyncSummary; }; @@ -62,18 +61,24 @@ async function readInstalledPackageVersion(dir: string): Promise { - const discovery = discoverMoltbotPlugins({ workspaceDir: params.workspaceDir }); + const discovery = discoverOpenClawPlugins({ workspaceDir: params.workspaceDir }); const bundled = new Map(); for (const candidate of discovery.candidates) { - if (candidate.origin !== "bundled") continue; + if (candidate.origin !== "bundled") { + continue; + } const manifest = loadPluginManifest(candidate.rootDir); - if (!manifest.ok) continue; + if (!manifest.ok) { + continue; + } const pluginId = manifest.manifest.id; - if (bundled.has(pluginId)) continue; + if (bundled.has(pluginId)) { + continue; + } const npmSpec = - candidate.packageMoltbot?.install?.npmSpec?.trim() || + candidate.packageManifest?.install?.npmSpec?.trim() || candidate.packageName?.trim() || undefined; @@ -88,7 +93,9 @@ function resolveBundledPluginSources(params: { } function pathsEqual(left?: string, right?: string): boolean { - if (!left || !right) return false; + if (!left || !right) { + return false; + } return resolveUserPath(left) === resolveUserPath(right); } @@ -100,7 +107,9 @@ function buildLoadPathHelpers(existing: string[]) { const addPath = (value: string) => { const normalized = resolveUserPath(value); - if (resolved.has(normalized)) return; + if (resolved.has(normalized)) { + return; + } paths.push(value); resolved.add(normalized); changed = true; @@ -108,7 +117,9 @@ function buildLoadPathHelpers(existing: string[]) { const removePath = (value: string) => { const normalized = resolveUserPath(value); - if (!resolved.has(normalized)) return; + if (!resolved.has(normalized)) { + return; + } paths = paths.filter((entry) => resolveUserPath(entry) !== normalized); resolved = resolveSet(); changed = true; @@ -127,7 +138,7 @@ function buildLoadPathHelpers(existing: string[]) { } export async function updateNpmInstalledPlugins(params: { - config: MoltbotConfig; + config: OpenClawConfig; logger?: PluginUpdateLogger; pluginIds?: string[]; skipIds?: Set; @@ -290,7 +301,7 @@ export async function updateNpmInstalledPlugins(params: { } export async function syncPluginsForUpdateChannel(params: { - config: MoltbotConfig; + config: OpenClawConfig; channel: UpdateChannel; workspaceDir?: string; logger?: PluginUpdateLogger; @@ -314,13 +325,17 @@ export async function syncPluginsForUpdateChannel(params: { if (params.channel === "dev") { for (const [pluginId, record] of Object.entries(installs)) { const bundledInfo = bundled.get(pluginId); - if (!bundledInfo) continue; + if (!bundledInfo) { + continue; + } loadHelpers.addPath(bundledInfo.localPath); const alreadyBundled = record.source === "path" && pathsEqual(record.sourcePath, bundledInfo.localPath); - if (alreadyBundled) continue; + if (alreadyBundled) { + continue; + } next = recordPluginInstall(next, { pluginId, @@ -336,15 +351,21 @@ export async function syncPluginsForUpdateChannel(params: { } else { for (const [pluginId, record] of Object.entries(installs)) { const bundledInfo = bundled.get(pluginId); - if (!bundledInfo) continue; + if (!bundledInfo) { + continue; + } if (record.source === "npm") { loadHelpers.removePath(bundledInfo.localPath); continue; } - if (record.source !== "path") continue; - if (!pathsEqual(record.sourcePath, bundledInfo.localPath)) continue; + if (record.source !== "path") { + continue; + } + if (!pathsEqual(record.sourcePath, bundledInfo.localPath)) { + continue; + } const spec = record.spec ?? bundledInfo.npmSpec; if (!spec) { diff --git a/src/polls.test.ts b/src/polls.test.ts index 6cf2fed81..f5cf5d200 100644 --- a/src/polls.test.ts +++ b/src/polls.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { normalizePollDurationHours, normalizePollInput } from "./polls.js"; describe("polls", () => { diff --git a/src/postinstall-patcher.test.ts b/src/postinstall-patcher.test.ts index 3e46e2365..2d3f6c168 100644 --- a/src/postinstall-patcher.test.ts +++ b/src/postinstall-patcher.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; - import { applyPatchSet, detectPackageManager, @@ -10,7 +9,7 @@ import { } from "../scripts/postinstall.js"; function makeTempDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-patch-")); + return fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-patch-")); } describe("postinstall patcher", () => { diff --git a/src/process/child-process-bridge.test.ts b/src/process/child-process-bridge.test.ts index d6c886d4a..0a37ac750 100644 --- a/src/process/child-process-bridge.test.ts +++ b/src/process/child-process-bridge.test.ts @@ -2,9 +2,7 @@ import { spawn } from "node:child_process"; import net from "node:net"; import path from "node:path"; import process from "node:process"; - import { afterEach, describe, expect, it } from "vitest"; - import { attachChildProcessBridge } from "./child-process-bridge.js"; function waitForLine(stream: NodeJS.ReadableStream, timeoutMs = 10_000): Promise { @@ -90,7 +88,9 @@ describe("attachChildProcessBridge", () => { const afterSigterm = process.listeners("SIGTERM"); const addedSigterm = afterSigterm.find((listener) => !beforeSigterm.has(listener)); - if (!child.stdout) throw new Error("expected stdout"); + if (!child.stdout) { + throw new Error("expected stdout"); + } const portLine = await waitForLine(child.stdout); const port = Number(portLine); expect(Number.isFinite(port)).toBe(true); @@ -98,7 +98,9 @@ describe("attachChildProcessBridge", () => { expect(await canConnect(port)).toBe(true); // Simulate systemd sending SIGTERM to the parent process. - if (!addedSigterm) throw new Error("expected SIGTERM listener"); + if (!addedSigterm) { + throw new Error("expected SIGTERM listener"); + } addedSigterm(); await new Promise((resolve, reject) => { diff --git a/src/process/command-queue.ts b/src/process/command-queue.ts index 2f2857130..f9f2f0093 100644 --- a/src/process/command-queue.ts +++ b/src/process/command-queue.ts @@ -1,5 +1,5 @@ -import { CommandLane } from "./lanes.js"; import { diagnosticLogger as diag, logLaneDequeue, logLaneEnqueue } from "../logging/diagnostic.js"; +import { CommandLane } from "./lanes.js"; // Minimal in-process queue to serialize command executions. // Default lane ("main") preserves the existing behavior. Additional lanes allow @@ -27,7 +27,9 @@ const lanes = new Map(); function getLaneState(lane: string): LaneState { const existing = lanes.get(lane); - if (existing) return existing; + if (existing) { + return existing; + } const created: LaneState = { lane, queue: [], @@ -41,7 +43,9 @@ function getLaneState(lane: string): LaneState { function drainLane(lane: string) { const state = getLaneState(lane); - if (state.draining) return; + if (state.draining) { + return; + } state.draining = true; const pump = () => { @@ -130,7 +134,9 @@ export function enqueueCommand( export function getQueueSize(lane: string = CommandLane.Main) { const resolved = lane.trim() || CommandLane.Main; const state = lanes.get(resolved); - if (!state) return 0; + if (!state) { + return 0; + } return state.queue.length + state.active; } @@ -145,7 +151,9 @@ export function getTotalQueueSize() { export function clearCommandLane(lane: string = CommandLane.Main) { const cleaned = lane.trim() || CommandLane.Main; const state = lanes.get(cleaned); - if (!state) return 0; + if (!state) { + return 0; + } const removed = state.queue.length; state.queue.length = 0; return removed; diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index eeb7cc7a7..ae8a865ad 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -1,14 +1,13 @@ import { describe, expect, it } from "vitest"; - import { runCommandWithTimeout } from "./exec.js"; describe("runCommandWithTimeout", () => { it("passes env overrides to child", async () => { const result = await runCommandWithTimeout( - [process.execPath, "-e", 'process.stdout.write(process.env.CLAWDBOT_TEST_ENV ?? "")'], + [process.execPath, "-e", 'process.stdout.write(process.env.OPENCLAW_TEST_ENV ?? "")'], { timeoutMs: 5_000, - env: { CLAWDBOT_TEST_ENV: "ok" }, + env: { OPENCLAW_TEST_ENV: "ok" }, }, ); @@ -17,18 +16,18 @@ describe("runCommandWithTimeout", () => { }); it("merges custom env with process.env", async () => { - const previous = process.env.CLAWDBOT_BASE_ENV; - process.env.CLAWDBOT_BASE_ENV = "base"; + const previous = process.env.OPENCLAW_BASE_ENV; + process.env.OPENCLAW_BASE_ENV = "base"; try { const result = await runCommandWithTimeout( [ process.execPath, "-e", - 'process.stdout.write((process.env.CLAWDBOT_BASE_ENV ?? "") + "|" + (process.env.CLAWDBOT_TEST_ENV ?? ""))', + 'process.stdout.write((process.env.OPENCLAW_BASE_ENV ?? "") + "|" + (process.env.OPENCLAW_TEST_ENV ?? ""))', ], { timeoutMs: 5_000, - env: { CLAWDBOT_TEST_ENV: "ok" }, + env: { OPENCLAW_TEST_ENV: "ok" }, }, ); @@ -36,9 +35,9 @@ describe("runCommandWithTimeout", () => { expect(result.stdout).toBe("base|ok"); } finally { if (previous === undefined) { - delete process.env.CLAWDBOT_BASE_ENV; + delete process.env.OPENCLAW_BASE_ENV; } else { - process.env.CLAWDBOT_BASE_ENV = previous; + process.env.OPENCLAW_BASE_ENV = previous; } } }); diff --git a/src/process/exec.ts b/src/process/exec.ts index 44f8b2ce0..43b92ed53 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -1,7 +1,6 @@ import { execFile, spawn } from "node:child_process"; import path from "node:path"; import { promisify } from "node:util"; - import { danger, shouldLogVerbose } from "../globals.js"; import { logDebug, logError } from "../logger.js"; import { resolveCommandStdio } from "./spawn-utils.js"; @@ -25,8 +24,12 @@ export async function runExec( try { const { stdout, stderr } = await execFileAsync(command, args, options); if (shouldLogVerbose()) { - if (stdout.trim()) logDebug(stdout.trim()); - if (stderr.trim()) logError(stderr.trim()); + if (stdout.trim()) { + logDebug(stdout.trim()); + } + if (stderr.trim()) { + logError(stderr.trim()); + } } return { stdout, stderr }; } catch (err) { @@ -65,7 +68,9 @@ export async function runCommandWithTimeout( const shouldSuppressNpmFund = (() => { const cmd = path.basename(argv[0] ?? ""); - if (cmd === "npm" || cmd === "npm.cmd" || cmd === "npm.exe") return true; + if (cmd === "npm" || cmd === "npm.cmd" || cmd === "npm.exe") { + return true; + } if (cmd === "node" || cmd === "node.exe") { const script = argv[1] ?? ""; return script.includes("npm-cli.js"); @@ -75,8 +80,12 @@ export async function runCommandWithTimeout( const resolvedEnv = env ? { ...process.env, ...env } : { ...process.env }; if (shouldSuppressNpmFund) { - if (resolvedEnv.NPM_CONFIG_FUND == null) resolvedEnv.NPM_CONFIG_FUND = "false"; - if (resolvedEnv.npm_config_fund == null) resolvedEnv.npm_config_fund = "false"; + if (resolvedEnv.NPM_CONFIG_FUND == null) { + resolvedEnv.NPM_CONFIG_FUND = "false"; + } + if (resolvedEnv.npm_config_fund == null) { + resolvedEnv.npm_config_fund = "false"; + } } const stdio = resolveCommandStdio({ hasInput, preferInherit: true }); @@ -109,13 +118,17 @@ export async function runCommandWithTimeout( stderr += d.toString(); }); child.on("error", (err) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); reject(err); }); child.on("close", (code, signal) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); resolve({ stdout, stderr, code, signal, killed: child.killed }); diff --git a/src/process/spawn-utils.test.ts b/src/process/spawn-utils.test.ts index d290d5938..cb3e0dc1d 100644 --- a/src/process/spawn-utils.test.ts +++ b/src/process/spawn-utils.test.ts @@ -1,8 +1,7 @@ +import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; -import type { ChildProcess } from "node:child_process"; import { describe, expect, it, vi } from "vitest"; - import { spawnWithFallback } from "./spawn-utils.js"; function createStubChild() { diff --git a/src/process/spawn-utils.ts b/src/process/spawn-utils.ts index 2d4604432..58d49598a 100644 --- a/src/process/spawn-utils.ts +++ b/src/process/spawn-utils.ts @@ -32,14 +32,24 @@ export function resolveCommandStdio(params: { } export function formatSpawnError(err: unknown): string { - if (!(err instanceof Error)) return String(err); + if (!(err instanceof Error)) { + return String(err); + } const details = err as NodeJS.ErrnoException; const parts: string[] = []; const message = err.message?.trim(); - if (message) parts.push(message); - if (details.code && !message?.includes(details.code)) parts.push(details.code); - if (details.syscall) parts.push(`syscall=${details.syscall}`); - if (typeof details.errno === "number") parts.push(`errno=${details.errno}`); + if (message) { + parts.push(message); + } + if (details.code && !message?.includes(details.code)) { + parts.push(details.code); + } + if (details.syscall) { + parts.push(`syscall=${details.syscall}`); + } + if (typeof details.errno === "number") { + parts.push(`errno=${details.errno}`); + } return parts.join(" "); } @@ -63,13 +73,17 @@ async function spawnAndWaitForSpawn( child.removeListener("spawn", onSpawn); }; const finishResolve = () => { - if (settled) return; + if (settled) { + return; + } settled = true; cleanup(); resolve(child); }; const onError = (err: unknown) => { - if (settled) return; + if (settled) { + return; + } settled = true; cleanup(); reject(err); diff --git a/src/providers/github-copilot-auth.ts b/src/providers/github-copilot-auth.ts index be81164a0..e0f1cf55c 100644 --- a/src/providers/github-copilot-auth.ts +++ b/src/providers/github-copilot-auth.ts @@ -1,10 +1,9 @@ import { intro, note, outro, spinner } from "@clack/prompts"; - +import type { RuntimeEnv } from "../runtime.js"; import { ensureAuthProfileStore, upsertAuthProfile } from "../agents/auth-profiles.js"; import { updateConfig } from "../commands/models/shared.js"; import { applyAuthProfileConfig } from "../commands/onboard-auth.js"; import { logConfigUpdated } from "../config/logging.js"; -import type { RuntimeEnv } from "../runtime.js"; import { stylePromptTitle } from "../terminal/prompt-style.js"; const CLIENT_ID = "Iv1.b507a08c87ecfe98"; diff --git a/src/providers/github-copilot-models.ts b/src/providers/github-copilot-models.ts index 31b126b64..700df6aaa 100644 --- a/src/providers/github-copilot-models.ts +++ b/src/providers/github-copilot-models.ts @@ -22,7 +22,9 @@ export function getDefaultCopilotModelIds(): string[] { export function buildCopilotModelDefinition(modelId: string): ModelDefinitionConfig { const id = modelId.trim(); - if (!id) throw new Error("Model id required"); + if (!id) { + throw new Error("Model id required"); + } return { id, name: id, diff --git a/src/providers/github-copilot-token.test.ts b/src/providers/github-copilot-token.test.ts index 5ef8acc5e..04c32c1b6 100644 --- a/src/providers/github-copilot-token.test.ts +++ b/src/providers/github-copilot-token.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const loadJsonFile = vi.fn(); const saveJsonFile = vi.fn(); -const resolveStateDir = vi.fn().mockReturnValue("/tmp/moltbot-state"); +const resolveStateDir = vi.fn().mockReturnValue("/tmp/openclaw-state"); vi.mock("../infra/json-file.js", () => ({ loadJsonFile, @@ -19,7 +19,7 @@ describe("github-copilot token", () => { loadJsonFile.mockReset(); saveJsonFile.mockReset(); resolveStateDir.mockReset(); - resolveStateDir.mockReturnValue("/tmp/moltbot-state"); + resolveStateDir.mockReturnValue("/tmp/openclaw-state"); }); it("derives baseUrl from token", async () => { diff --git a/src/providers/github-copilot-token.ts b/src/providers/github-copilot-token.ts index 19efd4a9d..37ec22a3f 100644 --- a/src/providers/github-copilot-token.ts +++ b/src/providers/github-copilot-token.ts @@ -1,5 +1,4 @@ import path from "node:path"; - import { resolveStateDir } from "../config/paths.js"; import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; @@ -57,18 +56,24 @@ export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilo export function deriveCopilotApiBaseUrlFromToken(token: string): string | null { const trimmed = token.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } // The token returned from the Copilot token endpoint is a semicolon-delimited // set of key/value pairs. One of them is `proxy-ep=...`. const match = trimmed.match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i); const proxyEp = match?.[1]?.trim(); - if (!proxyEp) return null; + if (!proxyEp) { + return null; + } // pi-ai expects converting proxy.* -> api.* // (see upstream getGitHubCopilotBaseUrl). const host = proxyEp.replace(/^https?:\/\//, "").replace(/^proxy\./i, "api."); - if (!host) return null; + if (!host) { + return null; + } return `https://${host}`; } diff --git a/src/providers/google-shared.ensures-function-call-comes-after-user-turn.test.ts b/src/providers/google-shared.ensures-function-call-comes-after-user-turn.test.ts index c8d6a604f..f3ecc5d34 100644 --- a/src/providers/google-shared.ensures-function-call-comes-after-user-turn.test.ts +++ b/src/providers/google-shared.ensures-function-call-comes-after-user-turn.test.ts @@ -1,5 +1,5 @@ -import { convertMessages } from "@mariozechner/pi-ai/dist/providers/google-shared.js"; import type { Context, Model } from "@mariozechner/pi-ai/dist/types.js"; +import { convertMessages } from "@mariozechner/pi-ai/dist/providers/google-shared.js"; import { describe, expect, it } from "vitest"; const asRecord = (value: unknown): Record => { diff --git a/src/providers/google-shared.preserves-parameters-type-is-missing.test.ts b/src/providers/google-shared.preserves-parameters-type-is-missing.test.ts index 576f5f5b5..f7f3781ed 100644 --- a/src/providers/google-shared.preserves-parameters-type-is-missing.test.ts +++ b/src/providers/google-shared.preserves-parameters-type-is-missing.test.ts @@ -1,5 +1,5 @@ -import { convertMessages, convertTools } from "@mariozechner/pi-ai/dist/providers/google-shared.js"; import type { Context, Model, Tool } from "@mariozechner/pi-ai/dist/types.js"; +import { convertMessages, convertTools } from "@mariozechner/pi-ai/dist/providers/google-shared.js"; import { describe, expect, it } from "vitest"; const asRecord = (value: unknown): Record => { diff --git a/src/providers/qwen-portal-oauth.test.ts b/src/providers/qwen-portal-oauth.test.ts index eac761633..0abe4eddb 100644 --- a/src/providers/qwen-portal-oauth.test.ts +++ b/src/providers/qwen-portal-oauth.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi, afterEach } from "vitest"; - import { refreshQwenPortalCredentials } from "./qwen-portal-oauth.js"; const originalFetch = globalThis.fetch; diff --git a/src/providers/qwen-portal-oauth.ts b/src/providers/qwen-portal-oauth.ts index 9926be289..bbed888c9 100644 --- a/src/providers/qwen-portal-oauth.ts +++ b/src/providers/qwen-portal-oauth.ts @@ -29,7 +29,7 @@ export async function refreshQwenPortalCredentials( const text = await response.text(); if (response.status === 400) { throw new Error( - `Qwen OAuth refresh token expired or invalid. Re-authenticate with \`${formatCliCommand("moltbot models auth login --provider qwen-portal")}\`.`, + `Qwen OAuth refresh token expired or invalid. Re-authenticate with \`${formatCliCommand("openclaw models auth login --provider qwen-portal")}\`.`, ); } throw new Error(`Qwen OAuth refresh failed: ${text || response.statusText}`); diff --git a/src/routing/bindings.ts b/src/routing/bindings.ts index 48517528c..fadd19362 100644 --- a/src/routing/bindings.ts +++ b/src/routing/bindings.ts @@ -1,73 +1,107 @@ +import type { OpenClawConfig } from "../config/config.js"; +import type { AgentBinding } from "../config/types.agents.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { normalizeChatChannelId } from "../channels/registry.js"; -import type { MoltbotConfig } from "../config/config.js"; -import type { AgentBinding } from "../config/types.agents.js"; import { normalizeAccountId, normalizeAgentId } from "./session-key.js"; function normalizeBindingChannelId(raw?: string | null): string | null { const normalized = normalizeChatChannelId(raw); - if (normalized) return normalized; + if (normalized) { + return normalized; + } const fallback = (raw ?? "").trim().toLowerCase(); return fallback || null; } -export function listBindings(cfg: MoltbotConfig): AgentBinding[] { +export function listBindings(cfg: OpenClawConfig): AgentBinding[] { return Array.isArray(cfg.bindings) ? cfg.bindings : []; } -export function listBoundAccountIds(cfg: MoltbotConfig, channelId: string): string[] { +export function listBoundAccountIds(cfg: OpenClawConfig, channelId: string): string[] { const normalizedChannel = normalizeBindingChannelId(channelId); - if (!normalizedChannel) return []; + if (!normalizedChannel) { + return []; + } const ids = new Set(); for (const binding of listBindings(cfg)) { - if (!binding || typeof binding !== "object") continue; + if (!binding || typeof binding !== "object") { + continue; + } const match = binding.match; - if (!match || typeof match !== "object") continue; + if (!match || typeof match !== "object") { + continue; + } const channel = normalizeBindingChannelId(match.channel); - if (!channel || channel !== normalizedChannel) continue; + if (!channel || channel !== normalizedChannel) { + continue; + } const accountId = typeof match.accountId === "string" ? match.accountId.trim() : ""; - if (!accountId || accountId === "*") continue; + if (!accountId || accountId === "*") { + continue; + } ids.add(normalizeAccountId(accountId)); } - return Array.from(ids).sort((a, b) => a.localeCompare(b)); + return Array.from(ids).toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultAgentBoundAccountId( - cfg: MoltbotConfig, + cfg: OpenClawConfig, channelId: string, ): string | null { const normalizedChannel = normalizeBindingChannelId(channelId); - if (!normalizedChannel) return null; + if (!normalizedChannel) { + return null; + } const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg)); for (const binding of listBindings(cfg)) { - if (!binding || typeof binding !== "object") continue; - if (normalizeAgentId(binding.agentId) !== defaultAgentId) continue; + if (!binding || typeof binding !== "object") { + continue; + } + if (normalizeAgentId(binding.agentId) !== defaultAgentId) { + continue; + } const match = binding.match; - if (!match || typeof match !== "object") continue; + if (!match || typeof match !== "object") { + continue; + } const channel = normalizeBindingChannelId(match.channel); - if (!channel || channel !== normalizedChannel) continue; + if (!channel || channel !== normalizedChannel) { + continue; + } const accountId = typeof match.accountId === "string" ? match.accountId.trim() : ""; - if (!accountId || accountId === "*") continue; + if (!accountId || accountId === "*") { + continue; + } return normalizeAccountId(accountId); } return null; } -export function buildChannelAccountBindings(cfg: MoltbotConfig) { +export function buildChannelAccountBindings(cfg: OpenClawConfig) { const map = new Map>(); for (const binding of listBindings(cfg)) { - if (!binding || typeof binding !== "object") continue; + if (!binding || typeof binding !== "object") { + continue; + } const match = binding.match; - if (!match || typeof match !== "object") continue; + if (!match || typeof match !== "object") { + continue; + } const channelId = normalizeBindingChannelId(match.channel); - if (!channelId) continue; + if (!channelId) { + continue; + } const accountId = typeof match.accountId === "string" ? match.accountId.trim() : ""; - if (!accountId || accountId === "*") continue; + if (!accountId || accountId === "*") { + continue; + } const agentId = normalizeAgentId(binding.agentId); const byAgent = map.get(channelId) ?? new Map(); const list = byAgent.get(agentId) ?? []; const normalizedAccountId = normalizeAccountId(accountId); - if (!list.includes(normalizedAccountId)) list.push(normalizedAccountId); + if (!list.includes(normalizedAccountId)) { + list.push(normalizedAccountId); + } byAgent.set(agentId, list); map.set(channelId, byAgent); } @@ -79,6 +113,8 @@ export function resolvePreferredAccountId(params: { defaultAccountId: string; boundAccounts: string[]; }): string { - if (params.boundAccounts.length > 0) return params.boundAccounts[0]; + if (params.boundAccounts.length > 0) { + return params.boundAccounts[0]; + } return params.defaultAccountId; } diff --git a/src/routing/resolve-route.test.ts b/src/routing/resolve-route.test.ts index aed0fa755..cd38a496b 100644 --- a/src/routing/resolve-route.test.ts +++ b/src/routing/resolve-route.test.ts @@ -1,11 +1,10 @@ import { describe, expect, test } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentRoute } from "./resolve-route.js"; describe("resolveAgentRoute", () => { test("defaults to main/default when no bindings exist", () => { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const route = resolveAgentRoute({ cfg, channel: "whatsapp", @@ -19,7 +18,7 @@ describe("resolveAgentRoute", () => { }); test("dmScope=per-peer isolates DM sessions by sender id", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { session: { dmScope: "per-peer" }, }; const route = resolveAgentRoute({ @@ -32,7 +31,7 @@ describe("resolveAgentRoute", () => { }); test("dmScope=per-channel-peer isolates DM sessions per channel and sender", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { session: { dmScope: "per-channel-peer" }, }; const route = resolveAgentRoute({ @@ -45,7 +44,7 @@ describe("resolveAgentRoute", () => { }); test("identityLinks collapses per-peer DM sessions across providers", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { session: { dmScope: "per-peer", identityLinks: { @@ -63,7 +62,7 @@ describe("resolveAgentRoute", () => { }); test("identityLinks applies to per-channel-peer DM sessions", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { session: { dmScope: "per-channel-peer", identityLinks: { @@ -81,7 +80,7 @@ describe("resolveAgentRoute", () => { }); test("peer binding wins over account binding", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { bindings: [ { agentId: "a", @@ -109,7 +108,7 @@ describe("resolveAgentRoute", () => { }); test("discord channel peer binding wins over guild binding", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { bindings: [ { agentId: "chan", @@ -142,7 +141,7 @@ describe("resolveAgentRoute", () => { }); test("guild binding wins over account binding when peer not bound", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { bindings: [ { agentId: "guild", @@ -170,7 +169,7 @@ describe("resolveAgentRoute", () => { }); test("missing accountId in binding matches default account only", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { bindings: [{ agentId: "defaultAcct", match: { channel: "whatsapp" } }], }; @@ -193,7 +192,7 @@ describe("resolveAgentRoute", () => { }); test("accountId=* matches any account as a channel fallback", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { bindings: [ { agentId: "any", @@ -212,9 +211,9 @@ describe("resolveAgentRoute", () => { }); test("defaultAgentId is used when no binding matches", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { - list: [{ id: "home", default: true, workspace: "~/clawd-home" }], + list: [{ id: "home", default: true, workspace: "~/openclaw-home" }], }, }; const route = resolveAgentRoute({ @@ -229,7 +228,7 @@ describe("resolveAgentRoute", () => { }); test("dmScope=per-account-channel-peer isolates DM sessions per account, channel and sender", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { session: { dmScope: "per-account-channel-peer" }, }; const route = resolveAgentRoute({ @@ -242,7 +241,7 @@ test("dmScope=per-account-channel-peer isolates DM sessions per account, channel }); test("dmScope=per-account-channel-peer uses default accountId when not provided", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { session: { dmScope: "per-account-channel-peer" }, }; const route = resolveAgentRoute({ diff --git a/src/routing/resolve-route.ts b/src/routing/resolve-route.ts index 0c63f77c8..0dca0e188 100644 --- a/src/routing/resolve-route.ts +++ b/src/routing/resolve-route.ts @@ -1,5 +1,5 @@ +import type { OpenClawConfig } from "../config/config.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import type { MoltbotConfig } from "../config/config.js"; import { listBindings } from "./bindings.js"; import { buildAgentMainSessionKey, @@ -18,7 +18,7 @@ export type RoutePeer = { }; export type ResolveAgentRouteInput = { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: string; accountId?: string | null; peer?: RoutePeer | null; @@ -61,8 +61,12 @@ function normalizeAccountId(value: string | undefined | null): string { function matchesAccountId(match: string | undefined, actual: string): boolean { const trimmed = (match ?? "").trim(); - if (!trimmed) return actual === DEFAULT_ACCOUNT_ID; - if (trimmed === "*") return true; + if (!trimmed) { + return actual === DEFAULT_ACCOUNT_ID; + } + if (trimmed === "*") { + return true; + } return trimmed === actual; } @@ -89,19 +93,25 @@ export function buildAgentSessionKey(params: { }); } -function listAgents(cfg: MoltbotConfig) { +function listAgents(cfg: OpenClawConfig) { const agents = cfg.agents?.list; return Array.isArray(agents) ? agents : []; } -function pickFirstExistingAgentId(cfg: MoltbotConfig, agentId: string): string { +function pickFirstExistingAgentId(cfg: OpenClawConfig, agentId: string): string { const trimmed = (agentId ?? "").trim(); - if (!trimmed) return sanitizeAgentId(resolveDefaultAgentId(cfg)); + if (!trimmed) { + return sanitizeAgentId(resolveDefaultAgentId(cfg)); + } const normalized = normalizeAgentId(trimmed); const agents = listAgents(cfg); - if (agents.length === 0) return sanitizeAgentId(trimmed); + if (agents.length === 0) { + return sanitizeAgentId(trimmed); + } const match = agents.find((agent) => normalizeAgentId(agent.id) === normalized); - if (match?.id?.trim()) return sanitizeAgentId(match.id.trim()); + if (match?.id?.trim()) { + return sanitizeAgentId(match.id.trim()); + } return sanitizeAgentId(resolveDefaultAgentId(cfg)); } @@ -110,7 +120,9 @@ function matchesChannel( channel: string, ): boolean { const key = normalizeToken(match?.channel); - if (!key) return false; + if (!key) { + return false; + } return key === channel; } @@ -119,10 +131,14 @@ function matchesPeer( peer: RoutePeer, ): boolean { const m = match?.peer; - if (!m) return false; + if (!m) { + return false; + } const kind = normalizeToken(m.kind); const id = normalizeId(m.id); - if (!kind || !id) return false; + if (!kind || !id) { + return false; + } return kind === peer.kind && id === peer.id; } @@ -131,13 +147,17 @@ function matchesGuild( guildId: string, ): boolean { const id = normalizeId(match?.guildId); - if (!id) return false; + if (!id) { + return false; + } return id === guildId; } function matchesTeam(match: { teamId?: string | undefined } | undefined, teamId: string): boolean { const id = normalizeId(match?.teamId); - if (!id) return false; + if (!id) { + return false; + } return id === teamId; } @@ -149,8 +169,12 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR const teamId = normalizeId(input.teamId); const bindings = listBindings(input.cfg).filter((binding) => { - if (!binding || typeof binding !== "object") return false; - if (!matchesChannel(binding.match, channel)) return false; + if (!binding || typeof binding !== "object") { + return false; + } + if (!matchesChannel(binding.match, channel)) { + return false; + } return matchesAccountId(binding.match?.accountId, accountId); }); @@ -183,30 +207,40 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR if (peer) { const peerMatch = bindings.find((b) => matchesPeer(b.match, peer)); - if (peerMatch) return choose(peerMatch.agentId, "binding.peer"); + if (peerMatch) { + return choose(peerMatch.agentId, "binding.peer"); + } } if (guildId) { const guildMatch = bindings.find((b) => matchesGuild(b.match, guildId)); - if (guildMatch) return choose(guildMatch.agentId, "binding.guild"); + if (guildMatch) { + return choose(guildMatch.agentId, "binding.guild"); + } } if (teamId) { const teamMatch = bindings.find((b) => matchesTeam(b.match, teamId)); - if (teamMatch) return choose(teamMatch.agentId, "binding.team"); + if (teamMatch) { + return choose(teamMatch.agentId, "binding.team"); + } } const accountMatch = bindings.find( (b) => b.match?.accountId?.trim() !== "*" && !b.match?.peer && !b.match?.guildId && !b.match?.teamId, ); - if (accountMatch) return choose(accountMatch.agentId, "binding.account"); + if (accountMatch) { + return choose(accountMatch.agentId, "binding.account"); + } const anyAccountMatch = bindings.find( (b) => b.match?.accountId?.trim() === "*" && !b.match?.peer && !b.match?.guildId && !b.match?.teamId, ); - if (anyAccountMatch) return choose(anyAccountMatch.agentId, "binding.channel"); + if (anyAccountMatch) { + return choose(anyAccountMatch.agentId, "binding.channel"); + } return choose(resolveDefaultAgentId(input.cfg), "default"); } diff --git a/src/routing/session-key.ts b/src/routing/session-key.ts index 320ffeb83..8f2b4ab0d 100644 --- a/src/routing/session-key.ts +++ b/src/routing/session-key.ts @@ -28,7 +28,9 @@ export function normalizeMainKey(value: string | undefined | null): string { export function toAgentRequestSessionKey(storeKey: string | undefined | null): string | undefined { const raw = (storeKey ?? "").trim(); - if (!raw) return undefined; + if (!raw) { + return undefined; + } return parseAgentSessionKey(raw)?.rest ?? raw; } @@ -42,7 +44,9 @@ export function toAgentStoreSessionKey(params: { return buildAgentMainSessionKey({ agentId: params.agentId, mainKey: params.mainKey }); } const lowered = raw.toLowerCase(); - if (lowered.startsWith("agent:")) return lowered; + if (lowered.startsWith("agent:")) { + return lowered; + } if (lowered.startsWith("subagent:")) { return `agent:${normalizeAgentId(params.agentId)}:${lowered}`; } @@ -56,9 +60,13 @@ export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | nu export function normalizeAgentId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); - if (!trimmed) return DEFAULT_AGENT_ID; + if (!trimmed) { + return DEFAULT_AGENT_ID; + } // Keep it path-safe + shell-friendly. - if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase(); + if (VALID_ID_RE.test(trimmed)) { + return trimmed.toLowerCase(); + } // Best-effort fallback: collapse invalid characters to "-" return ( trimmed @@ -72,8 +80,12 @@ export function normalizeAgentId(value: string | undefined | null): string { export function sanitizeAgentId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); - if (!trimmed) return DEFAULT_AGENT_ID; - if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase(); + if (!trimmed) { + return DEFAULT_AGENT_ID; + } + if (VALID_ID_RE.test(trimmed)) { + return trimmed.toLowerCase(); + } return ( trimmed .toLowerCase() @@ -86,8 +98,12 @@ export function sanitizeAgentId(value: string | undefined | null): string { export function normalizeAccountId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); - if (!trimmed) return DEFAULT_ACCOUNT_ID; - if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase(); + if (!trimmed) { + return DEFAULT_ACCOUNT_ID; + } + if (VALID_ID_RE.test(trimmed)) { + return trimmed.toLowerCase(); + } return ( trimmed .toLowerCase() @@ -130,7 +146,9 @@ export function buildAgentPeerSessionKey(params: { channel: params.channel, peerId, }); - if (linkedPeerId) peerId = linkedPeerId; + if (linkedPeerId) { + peerId = linkedPeerId; + } peerId = peerId.toLowerCase(); if (dmScope === "per-account-channel-peer" && peerId) { const channel = (params.channel ?? "").trim().toLowerCase() || "unknown"; @@ -160,22 +178,36 @@ function resolveLinkedPeerId(params: { peerId: string; }): string | null { const identityLinks = params.identityLinks; - if (!identityLinks) return null; + if (!identityLinks) { + return null; + } const peerId = params.peerId.trim(); - if (!peerId) return null; + if (!peerId) { + return null; + } const candidates = new Set(); const rawCandidate = normalizeToken(peerId); - if (rawCandidate) candidates.add(rawCandidate); + if (rawCandidate) { + candidates.add(rawCandidate); + } const channel = normalizeToken(params.channel); if (channel) { const scopedCandidate = normalizeToken(`${channel}:${peerId}`); - if (scopedCandidate) candidates.add(scopedCandidate); + if (scopedCandidate) { + candidates.add(scopedCandidate); + } + } + if (candidates.size === 0) { + return null; } - if (candidates.size === 0) return null; for (const [canonical, ids] of Object.entries(identityLinks)) { const canonicalName = canonical.trim(); - if (!canonicalName) continue; - if (!Array.isArray(ids)) continue; + if (!canonicalName) { + continue; + } + if (!Array.isArray(ids)) { + continue; + } for (const id of ids) { const normalized = normalizeToken(id); if (normalized && candidates.has(normalized)) { diff --git a/src/scripts/canvas-a2ui-copy.test.ts b/src/scripts/canvas-a2ui-copy.test.ts index a981f81aa..207c36e23 100644 --- a/src/scripts/canvas-a2ui-copy.test.ts +++ b/src/scripts/canvas-a2ui-copy.test.ts @@ -1,14 +1,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { copyA2uiAssets } from "../../scripts/canvas-a2ui-copy.js"; describe("canvas a2ui copy", () => { it("throws a helpful error when assets are missing", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-a2ui-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-a2ui-")); try { await expect(copyA2uiAssets({ srcDir: dir, outDir: path.join(dir, "out") })).rejects.toThrow( @@ -20,7 +18,7 @@ describe("canvas a2ui copy", () => { }); it("copies bundled assets to dist", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-a2ui-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-a2ui-")); const srcDir = path.join(dir, "src"); const outDir = path.join(dir, "dist"); diff --git a/src/security/audit-extra.ts b/src/security/audit-extra.ts index 3a92a30a8..c784dc853 100644 --- a/src/security/audit-extra.ts +++ b/src/security/audit-extra.ts @@ -1,25 +1,24 @@ +import JSON5 from "json5"; import fs from "node:fs/promises"; import path from "node:path"; - -import JSON5 from "json5"; - -import type { MoltbotConfig, ConfigFileSnapshot } from "../config/config.js"; -import { createConfigIO } from "../config/config.js"; -import { resolveNativeSkillsEnabled } from "../config/commands.js"; -import { resolveOAuthDir } from "../config/paths.js"; -import { formatCliCommand } from "../cli/command-format.js"; -import { resolveDefaultAgentId } from "../agents/agent-scope.js"; +import type { SandboxToolPolicy } from "../agents/sandbox/types.js"; +import type { OpenClawConfig, ConfigFileSnapshot } from "../config/config.js"; import type { AgentToolsConfig } from "../config/types.tools.js"; -import { resolveBrowserConfig } from "../browser/config.js"; +import type { ExecFn } from "./windows-acl.js"; +import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js"; -import { resolveToolProfilePolicy } from "../agents/tool-policy.js"; import { resolveSandboxConfigForAgent, resolveSandboxToolPolicyForAgent, } from "../agents/sandbox.js"; -import { resolveGatewayAuth } from "../gateway/auth.js"; -import type { SandboxToolPolicy } from "../agents/sandbox/types.js"; +import { resolveToolProfilePolicy } from "../agents/tool-policy.js"; +import { resolveBrowserConfig } from "../browser/config.js"; +import { formatCliCommand } from "../cli/command-format.js"; +import { resolveNativeSkillsEnabled } from "../config/commands.js"; +import { createConfigIO } from "../config/config.js"; import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js"; +import { resolveOAuthDir } from "../config/paths.js"; +import { resolveGatewayAuth } from "../gateway/auth.js"; import { normalizeAgentId } from "../routing/session-key.js"; import { formatPermissionDetail, @@ -27,7 +26,6 @@ import { inspectPathPermissions, safeStat, } from "./audit-fs.js"; -import type { ExecFn } from "./windows-acl.js"; export type SecurityAuditFinding = { checkId: string; @@ -40,36 +38,52 @@ export type SecurityAuditFinding = { const SMALL_MODEL_PARAM_B_MAX = 300; function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null { - if (!p.startsWith("~")) return p; + if (!p.startsWith("~")) { + return p; + } const home = typeof env.HOME === "string" && env.HOME.trim() ? env.HOME.trim() : null; - if (!home) return null; - if (p === "~") return home; - if (p.startsWith("~/") || p.startsWith("~\\")) return path.join(home, p.slice(2)); + if (!home) { + return null; + } + if (p === "~") { + return home; + } + if (p.startsWith("~/") || p.startsWith("~\\")) { + return path.join(home, p.slice(2)); + } return null; } -function summarizeGroupPolicy(cfg: MoltbotConfig): { +function summarizeGroupPolicy(cfg: OpenClawConfig): { open: number; allowlist: number; other: number; } { const channels = cfg.channels as Record | undefined; - if (!channels || typeof channels !== "object") return { open: 0, allowlist: 0, other: 0 }; + if (!channels || typeof channels !== "object") { + return { open: 0, allowlist: 0, other: 0 }; + } let open = 0; let allowlist = 0; let other = 0; for (const value of Object.values(channels)) { - if (!value || typeof value !== "object") continue; + if (!value || typeof value !== "object") { + continue; + } const section = value as Record; const policy = section.groupPolicy; - if (policy === "open") open += 1; - else if (policy === "allowlist") allowlist += 1; - else other += 1; + if (policy === "open") { + open += 1; + } else if (policy === "allowlist") { + allowlist += 1; + } else { + other += 1; + } } return { open, allowlist, other }; } -export function collectAttackSurfaceSummaryFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +export function collectAttackSurfaceSummaryFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const group = summarizeGroupPolicy(cfg); const elevated = cfg.tools?.elevated?.enabled !== false; const hooksEnabled = cfg.hooks?.enabled === true; @@ -116,7 +130,7 @@ export function collectSyncedFolderFindings(params: { severity: "warn", title: "State/config path looks like a synced folder", detail: `stateDir=${params.stateDir}, configPath=${params.configPath}. Synced folders (iCloud/Dropbox/OneDrive/Google Drive) can leak tokens and transcripts onto other devices.`, - remediation: `Keep CLAWDBOT_STATE_DIR on a local-only volume and re-run "${formatCliCommand("moltbot security audit --fix")}".`, + remediation: `Keep OPENCLAW_STATE_DIR on a local-only volume and re-run "${formatCliCommand("openclaw security audit --fix")}".`, }); } return findings; @@ -127,7 +141,7 @@ function looksLikeEnvRef(value: string): boolean { return v.startsWith("${") && v.endsWith("}"); } -export function collectSecretsInConfigFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +export function collectSecretsInConfigFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; const password = typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway.auth.password.trim() : ""; @@ -139,7 +153,7 @@ export function collectSecretsInConfigFindings(cfg: MoltbotConfig): SecurityAudi detail: "gateway.auth.password is set in the config file; prefer environment variables for secrets when possible.", remediation: - "Prefer CLAWDBOT_GATEWAY_PASSWORD (env) and remove gateway.auth.password from disk.", + "Prefer OPENCLAW_GATEWAY_PASSWORD (env) and remove gateway.auth.password from disk.", }); } @@ -157,9 +171,11 @@ export function collectSecretsInConfigFindings(cfg: MoltbotConfig): SecurityAudi return findings; } -export function collectHooksHardeningFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +export function collectHooksHardeningFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; - if (cfg.hooks?.enabled !== true) return findings; + if (cfg.hooks?.enabled !== true) { + return findings; + } const token = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : ""; if (token && token.length < 24) { @@ -209,24 +225,32 @@ export function collectHooksHardeningFindings(cfg: MoltbotConfig): SecurityAudit type ModelRef = { id: string; source: string }; function addModel(models: ModelRef[], raw: unknown, source: string) { - if (typeof raw !== "string") return; + if (typeof raw !== "string") { + return; + } const id = raw.trim(); - if (!id) return; + if (!id) { + return; + } models.push({ id, source }); } -function collectModels(cfg: MoltbotConfig): ModelRef[] { +function collectModels(cfg: OpenClawConfig): ModelRef[] { const out: ModelRef[] = []; addModel(out, cfg.agents?.defaults?.model?.primary, "agents.defaults.model.primary"); - for (const f of cfg.agents?.defaults?.model?.fallbacks ?? []) + for (const f of cfg.agents?.defaults?.model?.fallbacks ?? []) { addModel(out, f, "agents.defaults.model.fallbacks"); + } addModel(out, cfg.agents?.defaults?.imageModel?.primary, "agents.defaults.imageModel.primary"); - for (const f of cfg.agents?.defaults?.imageModel?.fallbacks ?? []) + for (const f of cfg.agents?.defaults?.imageModel?.fallbacks ?? []) { addModel(out, f, "agents.defaults.imageModel.fallbacks"); + } const list = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : []; for (const agent of list ?? []) { - if (!agent || typeof agent !== "object") continue; + if (!agent || typeof agent !== "object") { + continue; + } const id = typeof (agent as { id?: unknown }).id === "string" ? (agent as { id: string }).id : ""; const model = (agent as { model?: unknown }).model; @@ -236,7 +260,9 @@ function collectModels(cfg: MoltbotConfig): ModelRef[] { addModel(out, (model as { primary?: unknown }).primary, `agents.list.${id}.model.primary`); const fallbacks = (model as { fallbacks?: unknown }).fallbacks; if (Array.isArray(fallbacks)) { - for (const f of fallbacks) addModel(out, f, `agents.list.${id}.model.fallbacks`); + for (const f of fallbacks) { + addModel(out, f, `agents.list.${id}.model.fallbacks`); + } } } } @@ -259,10 +285,16 @@ function inferParamBFromIdOrName(text: string): number | null { let best: number | null = null; for (const match of matches) { const numRaw = match[1]; - if (!numRaw) continue; + if (!numRaw) { + continue; + } const value = Number(numRaw); - if (!Number.isFinite(value) || value <= 0) continue; - if (best === null || value > best) best = value; + if (!Number.isFinite(value) || value <= 0) { + continue; + } + if (best === null || value > best) { + best = value; + } } return best; } @@ -286,10 +318,12 @@ function isClaude45OrHigher(id: string): boolean { return /\bclaude-[^\s/]*?(?:-4-?5\b|4\.5\b)/i.test(id); } -export function collectModelHygieneFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +export function collectModelHygieneFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; const models = collectModels(cfg); - if (models.length === 0) return findings; + if (models.length === 0) { + return findings; + } const weakMatches = new Map(); const addWeakMatch = (model: string, source: string, reason: string) => { @@ -299,7 +333,9 @@ export function collectModelHygieneFindings(cfg: MoltbotConfig): SecurityAuditFi weakMatches.set(key, { model, source, reasons: [reason] }); return; } - if (!existing.reasons.includes(reason)) existing.reasons.push(reason); + if (!existing.reasons.includes(reason)) { + existing.reasons.push(reason); + } }; for (const entry of models) { @@ -373,15 +409,19 @@ function extractAgentIdFromSource(source: string): string | null { } function pickToolPolicy(config?: { allow?: string[]; deny?: string[] }): SandboxToolPolicy | null { - if (!config) return null; + if (!config) { + return null; + } const allow = Array.isArray(config.allow) ? config.allow : undefined; const deny = Array.isArray(config.deny) ? config.deny : undefined; - if (!allow && !deny) return null; + if (!allow && !deny) { + return null; + } return { allow, deny }; } function resolveToolPolicies(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentTools?: AgentToolsConfig; sandboxMode?: "off" | "non-main" | "all"; agentId?: string | null; @@ -389,13 +429,19 @@ function resolveToolPolicies(params: { const policies: SandboxToolPolicy[] = []; const profile = params.agentTools?.profile ?? params.cfg.tools?.profile; const profilePolicy = resolveToolProfilePolicy(profile); - if (profilePolicy) policies.push(profilePolicy); + if (profilePolicy) { + policies.push(profilePolicy); + } const globalPolicy = pickToolPolicy(params.cfg.tools ?? undefined); - if (globalPolicy) policies.push(globalPolicy); + if (globalPolicy) { + policies.push(globalPolicy); + } const agentPolicy = pickToolPolicy(params.agentTools); - if (agentPolicy) policies.push(agentPolicy); + if (agentPolicy) { + policies.push(agentPolicy); + } if (params.sandboxMode === "all") { const sandboxPolicy = resolveSandboxToolPolicyForAgent(params.cfg, params.agentId ?? undefined); @@ -405,7 +451,7 @@ function resolveToolPolicies(params: { return policies; } -function hasWebSearchKey(cfg: MoltbotConfig, env: NodeJS.ProcessEnv): boolean { +function hasWebSearchKey(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean { const search = cfg.tools?.web?.search; return Boolean( search?.apiKey || @@ -416,20 +462,26 @@ function hasWebSearchKey(cfg: MoltbotConfig, env: NodeJS.ProcessEnv): boolean { ); } -function isWebSearchEnabled(cfg: MoltbotConfig, env: NodeJS.ProcessEnv): boolean { +function isWebSearchEnabled(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean { const enabled = cfg.tools?.web?.search?.enabled; - if (enabled === false) return false; - if (enabled === true) return true; + if (enabled === false) { + return false; + } + if (enabled === true) { + return true; + } return hasWebSearchKey(cfg, env); } -function isWebFetchEnabled(cfg: MoltbotConfig): boolean { +function isWebFetchEnabled(cfg: OpenClawConfig): boolean { const enabled = cfg.tools?.web?.fetch?.enabled; - if (enabled === false) return false; + if (enabled === false) { + return false; + } return true; } -function isBrowserEnabled(cfg: MoltbotConfig): boolean { +function isBrowserEnabled(cfg: OpenClawConfig): boolean { try { return resolveBrowserConfig(cfg.browser, cfg).enabled; } catch { @@ -438,22 +490,28 @@ function isBrowserEnabled(cfg: MoltbotConfig): boolean { } export function collectSmallModelRiskFindings(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; env: NodeJS.ProcessEnv; }): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; const models = collectModels(params.cfg).filter((entry) => !entry.source.includes("imageModel")); - if (models.length === 0) return findings; + if (models.length === 0) { + return findings; + } const smallModels = models .map((entry) => { const paramB = inferParamBFromIdOrName(entry.id); - if (!paramB || paramB > SMALL_MODEL_PARAM_B_MAX) return null; + if (!paramB || paramB > SMALL_MODEL_PARAM_B_MAX) { + return null; + } return { ...entry, paramB }; }) .filter((entry): entry is { id: string; source: string; paramB: number } => Boolean(entry)); - if (smallModels.length === 0) return findings; + if (smallModels.length === 0) { + return findings; + } let hasUnsafe = false; const modelLines: string[] = []; @@ -473,19 +531,29 @@ export function collectSmallModelRiskFindings(params: { }); const exposed: string[] = []; if (isWebSearchEnabled(params.cfg, params.env)) { - if (isToolAllowedByPolicies("web_search", policies)) exposed.push("web_search"); + if (isToolAllowedByPolicies("web_search", policies)) { + exposed.push("web_search"); + } } if (isWebFetchEnabled(params.cfg)) { - if (isToolAllowedByPolicies("web_fetch", policies)) exposed.push("web_fetch"); + if (isToolAllowedByPolicies("web_fetch", policies)) { + exposed.push("web_fetch"); + } } if (isBrowserEnabled(params.cfg)) { - if (isToolAllowedByPolicies("browser", policies)) exposed.push("browser"); + if (isToolAllowedByPolicies("browser", policies)) { + exposed.push("browser"); + } + } + for (const tool of exposed) { + exposureSet.add(tool); } - for (const tool of exposed) exposureSet.add(tool); const sandboxLabel = sandboxMode === "all" ? "sandbox=all" : `sandbox=${sandboxMode}`; const exposureLabel = exposed.length > 0 ? ` web=[${exposed.join(", ")}]` : " web=[off]"; const safe = sandboxMode === "all" && exposed.length === 0; - if (!safe) hasUnsafe = true; + if (!safe) { + hasUnsafe = true; + } const statusLabel = safe ? "ok" : "unsafe"; modelLines.push( `- ${entry.id} (${entry.paramB}B) @ ${entry.source} (${statusLabel}; ${sandboxLabel};${exposureLabel})`, @@ -517,20 +585,24 @@ export function collectSmallModelRiskFindings(params: { } export async function collectPluginsTrustFindings(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; stateDir: string; }): Promise { const findings: SecurityAuditFinding[] = []; const extensionsDir = path.join(params.stateDir, "extensions"); const st = await safeStat(extensionsDir); - if (!st.ok || !st.isDir) return findings; + if (!st.ok || !st.isDir) { + return findings; + } const entries = await fs.readdir(extensionsDir, { withFileTypes: true }).catch(() => []); const pluginDirs = entries .filter((e) => e.isDirectory()) .map((e) => e.name) .filter(Boolean); - if (pluginDirs.length === 0) return findings; + if (pluginDirs.length === 0) { + return findings; + } const allow = params.cfg.plugins?.allow; const allowConfigured = Array.isArray(allow) && allow.length > 0; @@ -623,21 +695,32 @@ function resolveIncludePath(baseConfigPath: string, includePath: string): string function listDirectIncludes(parsed: unknown): string[] { const out: string[] = []; const visit = (value: unknown) => { - if (!value) return; - if (Array.isArray(value)) { - for (const item of value) visit(item); + if (!value) { + return; + } + if (Array.isArray(value)) { + for (const item of value) { + visit(item); + } + return; + } + if (typeof value !== "object") { return; } - if (typeof value !== "object") return; const rec = value as Record; const includeVal = rec[INCLUDE_KEY]; - if (typeof includeVal === "string") out.push(includeVal); - else if (Array.isArray(includeVal)) { + if (typeof includeVal === "string") { + out.push(includeVal); + } else if (Array.isArray(includeVal)) { for (const item of includeVal) { - if (typeof item === "string") out.push(item); + if (typeof item === "string") { + out.push(item); + } } } - for (const v of Object.values(rec)) visit(v); + for (const v of Object.values(rec)) { + visit(v); + } }; visit(parsed); return out; @@ -651,17 +734,23 @@ async function collectIncludePathsRecursive(params: { const result: string[] = []; const walk = async (basePath: string, parsed: unknown, depth: number): Promise => { - if (depth > MAX_INCLUDE_DEPTH) return; + if (depth > MAX_INCLUDE_DEPTH) { + return; + } for (const raw of listDirectIncludes(parsed)) { const resolved = resolveIncludePath(basePath, raw); - if (visited.has(resolved)) continue; + if (visited.has(resolved)) { + continue; + } visited.add(resolved); result.push(resolved); const rawText = await fs.readFile(resolved, "utf-8").catch(() => null); - if (!rawText) continue; + if (!rawText) { + continue; + } const nestedParsed = (() => { try { - return JSON5.parse(rawText) as unknown; + return JSON5.parse(rawText); } catch { return null; } @@ -684,14 +773,18 @@ export async function collectIncludeFilePermFindings(params: { execIcacls?: ExecFn; }): Promise { const findings: SecurityAuditFinding[] = []; - if (!params.configSnapshot.exists) return findings; + if (!params.configSnapshot.exists) { + return findings; + } const configPath = params.configSnapshot.path; const includePaths = await collectIncludePathsRecursive({ configPath, parsed: params.configSnapshot.parsed, }); - if (includePaths.length === 0) return findings; + if (includePaths.length === 0) { + return findings; + } for (const p of includePaths) { // eslint-disable-next-line no-await-in-loop @@ -700,7 +793,9 @@ export async function collectIncludeFilePermFindings(params: { platform: params.platform, exec: params.execIcacls, }); - if (!perms.ok) continue; + if (!perms.ok) { + continue; + } if (perms.worldWritable || perms.groupWritable) { findings.push({ checkId: "fs.config_include.perms_writable", @@ -750,7 +845,7 @@ export async function collectIncludeFilePermFindings(params: { } export async function collectStateDeepFilesystemFindings(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; env: NodeJS.ProcessEnv; stateDir: string; platform?: NodeJS.Platform; @@ -905,31 +1000,42 @@ export async function collectStateDeepFilesystemFindings(params: { return findings; } -function listGroupPolicyOpen(cfg: MoltbotConfig): string[] { +function listGroupPolicyOpen(cfg: OpenClawConfig): string[] { const out: string[] = []; const channels = cfg.channels as Record | undefined; - if (!channels || typeof channels !== "object") return out; + if (!channels || typeof channels !== "object") { + return out; + } for (const [channelId, value] of Object.entries(channels)) { - if (!value || typeof value !== "object") continue; + if (!value || typeof value !== "object") { + continue; + } const section = value as Record; - if (section.groupPolicy === "open") out.push(`channels.${channelId}.groupPolicy`); + if (section.groupPolicy === "open") { + out.push(`channels.${channelId}.groupPolicy`); + } const accounts = section.accounts; if (accounts && typeof accounts === "object") { for (const [accountId, accountVal] of Object.entries(accounts)) { - if (!accountVal || typeof accountVal !== "object") continue; + if (!accountVal || typeof accountVal !== "object") { + continue; + } const acc = accountVal as Record; - if (acc.groupPolicy === "open") + if (acc.groupPolicy === "open") { out.push(`channels.${channelId}.accounts.${accountId}.groupPolicy`); + } } } } return out; } -export function collectExposureMatrixFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +export function collectExposureMatrixFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; const openGroups = listGroupPolicyOpen(cfg); - if (openGroups.length === 0) return findings; + if (openGroups.length === 0) { + return findings; + } const elevatedEnabled = cfg.tools?.elevated?.enabled !== false; if (elevatedEnabled) { diff --git a/src/security/audit-fs.ts b/src/security/audit-fs.ts index 6bf0aec26..7d6d6130f 100644 --- a/src/security/audit-fs.ts +++ b/src/security/audit-fs.ts @@ -1,5 +1,4 @@ import fs from "node:fs/promises"; - import { formatIcaclsResetCommand, formatWindowsAclSummary, @@ -153,31 +152,43 @@ export function formatPermissionRemediation(params: { } export function modeBits(mode: number | null): number | null { - if (mode == null) return null; + if (mode == null) { + return null; + } return mode & 0o777; } export function formatOctal(bits: number | null): string { - if (bits == null) return "unknown"; + if (bits == null) { + return "unknown"; + } return bits.toString(8).padStart(3, "0"); } export function isWorldWritable(bits: number | null): boolean { - if (bits == null) return false; + if (bits == null) { + return false; + } return (bits & 0o002) !== 0; } export function isGroupWritable(bits: number | null): boolean { - if (bits == null) return false; + if (bits == null) { + return false; + } return (bits & 0o020) !== 0; } export function isWorldReadable(bits: number | null): boolean { - if (bits == null) return false; + if (bits == null) { + return false; + } return (bits & 0o004) !== 0; } export function isGroupReadable(bits: number | null): boolean { - if (bits == null) return false; + if (bits == null) { + return false; + } return (bits & 0o040) !== 0; } diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 40124d39a..d12b54744 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -1,20 +1,19 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; -import type { ChannelPlugin } from "../channels/plugins/types.js"; -import { runSecurityAudit } from "./audit.js"; -import { discordPlugin } from "../../extensions/discord/src/channel.js"; -import { slackPlugin } from "../../extensions/slack/src/channel.js"; -import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { ChannelPlugin } from "../channels/plugins/types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { discordPlugin } from "../../extensions/discord/src/channel.js"; +import { slackPlugin } from "../../extensions/slack/src/channel.js"; +import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; +import { runSecurityAudit } from "./audit.js"; const isWindows = process.platform === "win32"; describe("security audit", () => { it("includes an attack surface summary (info)", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { whatsapp: { groupPolicy: "open" }, telegram: { groupPolicy: "allowlist" } }, tools: { elevated: { enabled: true, allowFrom: { whatsapp: ["+1"] } } }, hooks: { enabled: true }, @@ -35,7 +34,7 @@ describe("security audit", () => { }); it("flags non-loopback bind without auth as critical", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { bind: "lan", auth: {}, @@ -55,7 +54,7 @@ describe("security audit", () => { }); it("warns when loopback control UI lacks trusted proxies", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { bind: "loopback", controlUi: { enabled: true }, @@ -79,7 +78,7 @@ describe("security audit", () => { }); it("flags loopback control UI without auth as critical", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { bind: "loopback", controlUi: { enabled: true }, @@ -105,7 +104,7 @@ describe("security audit", () => { }); it("flags logging.redactSensitive=off", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { logging: { redactSensitive: "off" }, }; @@ -123,10 +122,10 @@ describe("security audit", () => { }); it("treats Windows ACL-only perms as secure", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-win-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-win-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true }); - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile(configPath, "{}\n", "utf-8"); const user = "DESKTOP-TEST\\Tester"; @@ -160,10 +159,10 @@ describe("security audit", () => { }); it("flags Windows ACLs when Users can read the state dir", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-win-open-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-win-open-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true }); - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile(configPath, "{}\n", "utf-8"); const user = "DESKTOP-TEST\\Tester"; @@ -200,7 +199,7 @@ describe("security audit", () => { }); it("warns when small models are paired with web/browser tools", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { model: { primary: "ollama/mistral-8b" } } }, tools: { web: { @@ -226,7 +225,7 @@ describe("security audit", () => { }); it("treats small models as safe when sandbox is on and web tools are disabled", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { model: { primary: "ollama/mistral-8b" }, sandbox: { mode: "all" } } }, tools: { web: { @@ -250,7 +249,7 @@ describe("security audit", () => { }); it("flags tools.elevated allowFrom wildcard as critical", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { elevated: { allowFrom: { whatsapp: ["*"] }, @@ -275,7 +274,7 @@ describe("security audit", () => { }); it("warns when remote CDP uses HTTP", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { browser: { profiles: { remote: { cdpUrl: "http://example.com:9222", color: "#0066CC" }, @@ -297,7 +296,7 @@ describe("security audit", () => { }); it("warns when control UI allows insecure auth", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { controlUi: { allowInsecureAuth: true }, }, @@ -320,7 +319,7 @@ describe("security audit", () => { }); it("warns when control UI device auth is disabled", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { controlUi: { dangerouslyDisableDeviceAuth: true }, }, @@ -343,7 +342,7 @@ describe("security audit", () => { }); it("warns when multiple DM senders share the main session", async () => { - const cfg: MoltbotConfig = { session: { dmScope: "main" } }; + const cfg: OpenClawConfig = { session: { dmScope: "main" } }; const plugins: ChannelPlugin[] = [ { id: "whatsapp", @@ -391,12 +390,12 @@ describe("security audit", () => { }); it("flags Discord native commands without a guild user allowlist", async () => { - const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-discord-")); - process.env.CLAWDBOT_STATE_DIR = tmp; + const prevStateDir = process.env.OPENCLAW_STATE_DIR; + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-discord-")); + process.env.OPENCLAW_STATE_DIR = tmp; await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { discord: { enabled: true, @@ -429,20 +428,23 @@ describe("security audit", () => { ]), ); } finally { - if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = prevStateDir; + if (prevStateDir == null) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prevStateDir; + } } }); it("does not flag Discord slash commands when dm.allowFrom includes a Discord snowflake id", async () => { - const prevStateDir = process.env.CLAWDBOT_STATE_DIR; + const prevStateDir = process.env.OPENCLAW_STATE_DIR; const tmp = await fs.mkdtemp( - path.join(os.tmpdir(), "moltbot-security-audit-discord-allowfrom-snowflake-"), + path.join(os.tmpdir(), "openclaw-security-audit-discord-allowfrom-snowflake-"), ); - process.env.CLAWDBOT_STATE_DIR = tmp; + process.env.OPENCLAW_STATE_DIR = tmp; await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { discord: { enabled: true, @@ -475,18 +477,21 @@ describe("security audit", () => { ]), ); } finally { - if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = prevStateDir; + if (prevStateDir == null) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prevStateDir; + } } }); it("flags Discord slash commands when access-group enforcement is disabled and no users allowlist exists", async () => { - const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-discord-open-")); - process.env.CLAWDBOT_STATE_DIR = tmp; + const prevStateDir = process.env.OPENCLAW_STATE_DIR; + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-discord-open-")); + process.env.OPENCLAW_STATE_DIR = tmp; await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { commands: { useAccessGroups: false }, channels: { discord: { @@ -520,18 +525,21 @@ describe("security audit", () => { ]), ); } finally { - if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = prevStateDir; + if (prevStateDir == null) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prevStateDir; + } } }); it("flags Slack slash commands without a channel users allowlist", async () => { - const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-slack-")); - process.env.CLAWDBOT_STATE_DIR = tmp; + const prevStateDir = process.env.OPENCLAW_STATE_DIR; + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-slack-")); + process.env.OPENCLAW_STATE_DIR = tmp; await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { slack: { enabled: true, @@ -559,18 +567,21 @@ describe("security audit", () => { ]), ); } finally { - if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = prevStateDir; + if (prevStateDir == null) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prevStateDir; + } } }); it("flags Slack slash commands when access-group enforcement is disabled", async () => { - const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-slack-open-")); - process.env.CLAWDBOT_STATE_DIR = tmp; + const prevStateDir = process.env.OPENCLAW_STATE_DIR; + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-slack-open-")); + process.env.OPENCLAW_STATE_DIR = tmp; await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { commands: { useAccessGroups: false }, channels: { slack: { @@ -599,18 +610,21 @@ describe("security audit", () => { ]), ); } finally { - if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = prevStateDir; + if (prevStateDir == null) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prevStateDir; + } } }); it("flags Telegram group commands without a sender allowlist", async () => { - const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-telegram-")); - process.env.CLAWDBOT_STATE_DIR = tmp; + const prevStateDir = process.env.OPENCLAW_STATE_DIR; + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-telegram-")); + process.env.OPENCLAW_STATE_DIR = tmp; await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { enabled: true, @@ -637,13 +651,16 @@ describe("security audit", () => { ]), ); } finally { - if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = prevStateDir; + if (prevStateDir == null) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prevStateDir; + } } }); it("adds a warning when deep probe fails", async () => { - const cfg: MoltbotConfig = { gateway: { mode: "local" } }; + const cfg: OpenClawConfig = { gateway: { mode: "local" } }; const res = await runSecurityAudit({ config: cfg, @@ -672,7 +689,7 @@ describe("security audit", () => { }); it("adds a warning when deep probe throws", async () => { - const cfg: MoltbotConfig = { gateway: { mode: "local" } }; + const cfg: OpenClawConfig = { gateway: { mode: "local" } }; const res = await runSecurityAudit({ config: cfg, @@ -695,7 +712,7 @@ describe("security audit", () => { }); it("warns on legacy model configuration", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { model: { primary: "openai/gpt-3.5-turbo" } } }, }; @@ -713,7 +730,7 @@ describe("security audit", () => { }); it("warns on weak model tiers", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { model: { primary: "anthropic/claude-haiku-4-5" } } }, }; @@ -732,7 +749,7 @@ describe("security audit", () => { it("does not warn on Venice-style opus-45 model names", async () => { // Venice uses "claude-opus-45" format (no dash between 4 and 5) - const cfg: ClawdbotConfig = { + const cfg: OpenClawConfig = { agents: { defaults: { model: { primary: "venice/claude-opus-45" } } }, }; @@ -748,7 +765,7 @@ describe("security audit", () => { }); it("warns when hooks token looks short", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { hooks: { enabled: true, token: "short" }, }; @@ -766,9 +783,9 @@ describe("security audit", () => { }); it("warns when hooks token reuses the gateway env token", async () => { - const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - process.env.CLAWDBOT_GATEWAY_TOKEN = "shared-gateway-token-1234567890"; - const cfg: MoltbotConfig = { + const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_GATEWAY_TOKEN = "shared-gateway-token-1234567890"; + const cfg: OpenClawConfig = { hooks: { enabled: true, token: "shared-gateway-token-1234567890" }, }; @@ -785,20 +802,23 @@ describe("security audit", () => { ]), ); } finally { - if (prevToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; - else process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + if (prevToken === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; + } } }); it("warns when state/config look like a synced folder", async () => { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const res = await runSecurityAudit({ config: cfg, includeFilesystem: false, includeChannelSecurity: false, - stateDir: "/Users/test/Dropbox/.clawdbot", - configPath: "/Users/test/Dropbox/.clawdbot/moltbot.json", + stateDir: "/Users/test/Dropbox/.openclaw", + configPath: "/Users/test/Dropbox/.openclaw/openclaw.json", }); expect(res.findings).toEqual( @@ -809,7 +829,7 @@ describe("security audit", () => { }); it("flags group/world-readable config include files", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true, mode: 0o700 }); @@ -823,12 +843,12 @@ describe("security audit", () => { await fs.chmod(includePath, 0o644); } - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile(configPath, `{ "$include": "./extra.json5" }\n`, "utf-8"); await fs.chmod(configPath, 0o600); try { - const cfg: MoltbotConfig = { logging: { redactSensitive: "off" } }; + const cfg: OpenClawConfig = { logging: { redactSensitive: "off" } }; const user = "DESKTOP-TEST\\Tester"; const execIcacls = isWindows ? async (_cmd: string, args: string[]) => { @@ -882,7 +902,7 @@ describe("security audit", () => { delete process.env.TELEGRAM_BOT_TOKEN; delete process.env.SLACK_BOT_TOKEN; delete process.env.SLACK_APP_TOKEN; - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(path.join(stateDir, "extensions", "some-plugin"), { recursive: true, @@ -890,13 +910,13 @@ describe("security audit", () => { }); try { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; const res = await runSecurityAudit({ config: cfg, includeFilesystem: true, includeChannelSecurity: false, stateDir, - configPath: path.join(stateDir, "moltbot.json"), + configPath: path.join(stateDir, "openclaw.json"), }); expect(res.findings).toEqual( @@ -905,21 +925,33 @@ describe("security audit", () => { ]), ); } finally { - if (prevDiscordToken == null) delete process.env.DISCORD_BOT_TOKEN; - else process.env.DISCORD_BOT_TOKEN = prevDiscordToken; - if (prevTelegramToken == null) delete process.env.TELEGRAM_BOT_TOKEN; - else process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken; - if (prevSlackBotToken == null) delete process.env.SLACK_BOT_TOKEN; - else process.env.SLACK_BOT_TOKEN = prevSlackBotToken; - if (prevSlackAppToken == null) delete process.env.SLACK_APP_TOKEN; - else process.env.SLACK_APP_TOKEN = prevSlackAppToken; + if (prevDiscordToken == null) { + delete process.env.DISCORD_BOT_TOKEN; + } else { + process.env.DISCORD_BOT_TOKEN = prevDiscordToken; + } + if (prevTelegramToken == null) { + delete process.env.TELEGRAM_BOT_TOKEN; + } else { + process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken; + } + if (prevSlackBotToken == null) { + delete process.env.SLACK_BOT_TOKEN; + } else { + process.env.SLACK_BOT_TOKEN = prevSlackBotToken; + } + if (prevSlackAppToken == null) { + delete process.env.SLACK_APP_TOKEN; + } else { + process.env.SLACK_APP_TOKEN = prevSlackAppToken; + } } }); it("flags unallowlisted extensions as critical when native skill commands are exposed", async () => { const prevDiscordToken = process.env.DISCORD_BOT_TOKEN; delete process.env.DISCORD_BOT_TOKEN; - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-audit-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(path.join(stateDir, "extensions", "some-plugin"), { recursive: true, @@ -927,7 +959,7 @@ describe("security audit", () => { }); try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { discord: { enabled: true, token: "t" }, }, @@ -937,7 +969,7 @@ describe("security audit", () => { includeFilesystem: true, includeChannelSecurity: false, stateDir, - configPath: path.join(stateDir, "moltbot.json"), + configPath: path.join(stateDir, "openclaw.json"), }); expect(res.findings).toEqual( @@ -949,13 +981,16 @@ describe("security audit", () => { ]), ); } finally { - if (prevDiscordToken == null) delete process.env.DISCORD_BOT_TOKEN; - else process.env.DISCORD_BOT_TOKEN = prevDiscordToken; + if (prevDiscordToken == null) { + delete process.env.DISCORD_BOT_TOKEN; + } else { + process.env.DISCORD_BOT_TOKEN = prevDiscordToken; + } } }); it("flags open groupPolicy when tools.elevated is enabled", async () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { tools: { elevated: { enabled: true, allowFrom: { whatsapp: ["+1"] } } }, channels: { whatsapp: { groupPolicy: "open" } }, }; @@ -977,30 +1012,30 @@ describe("security audit", () => { }); describe("maybeProbeGateway auth selection", () => { - const originalEnvToken = process.env.CLAWDBOT_GATEWAY_TOKEN; - const originalEnvPassword = process.env.CLAWDBOT_GATEWAY_PASSWORD; + const originalEnvToken = process.env.OPENCLAW_GATEWAY_TOKEN; + const originalEnvPassword = process.env.OPENCLAW_GATEWAY_PASSWORD; beforeEach(() => { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; - delete process.env.CLAWDBOT_GATEWAY_PASSWORD; + delete process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_PASSWORD; }); afterEach(() => { if (originalEnvToken == null) { - delete process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_TOKEN; } else { - process.env.CLAWDBOT_GATEWAY_TOKEN = originalEnvToken; + process.env.OPENCLAW_GATEWAY_TOKEN = originalEnvToken; } if (originalEnvPassword == null) { - delete process.env.CLAWDBOT_GATEWAY_PASSWORD; + delete process.env.OPENCLAW_GATEWAY_PASSWORD; } else { - process.env.CLAWDBOT_GATEWAY_PASSWORD = originalEnvPassword; + process.env.OPENCLAW_GATEWAY_PASSWORD = originalEnvPassword; } }); it("uses local auth when gateway.mode is local", async () => { let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { mode: "local", auth: { token: "local-token-abc123" }, @@ -1033,9 +1068,9 @@ describe("security audit", () => { }); it("prefers env token over local config token", async () => { - process.env.CLAWDBOT_GATEWAY_TOKEN = "env-token"; + process.env.OPENCLAW_GATEWAY_TOKEN = "env-token"; let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { mode: "local", auth: { token: "local-token" }, @@ -1069,7 +1104,7 @@ describe("security audit", () => { it("uses local auth when gateway.mode is undefined (default)", async () => { let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { auth: { token: "default-local-token" }, }, @@ -1102,7 +1137,7 @@ describe("security audit", () => { it("uses remote auth when gateway.mode is remote with URL", async () => { let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { mode: "remote", auth: { token: "local-token-should-not-use" }, @@ -1139,9 +1174,9 @@ describe("security audit", () => { }); it("ignores env token when gateway.mode is remote", async () => { - process.env.CLAWDBOT_GATEWAY_TOKEN = "env-token"; + process.env.OPENCLAW_GATEWAY_TOKEN = "env-token"; let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { mode: "remote", auth: { token: "local-token-should-not-use" }, @@ -1179,7 +1214,7 @@ describe("security audit", () => { it("uses remote password when env is unset", async () => { let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { mode: "remote", remote: { @@ -1215,9 +1250,9 @@ describe("security audit", () => { }); it("prefers env password over remote password", async () => { - process.env.CLAWDBOT_GATEWAY_PASSWORD = "env-pass"; + process.env.OPENCLAW_GATEWAY_PASSWORD = "env-pass"; let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { mode: "remote", remote: { @@ -1254,7 +1289,7 @@ describe("security audit", () => { it("falls back to local auth when gateway.mode is remote but URL is missing", async () => { let capturedAuth: { token?: string; password?: string } | undefined; - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { gateway: { mode: "remote", auth: { token: "fallback-local-token" }, diff --git a/src/security/audit.ts b/src/security/audit.ts index 681d14c1d..583d9130f 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -1,13 +1,16 @@ -import { listChannelPlugins } from "../channels/plugins/index.js"; -import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; import type { ChannelId } from "../channels/plugins/types.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { ExecFn } from "./windows-acl.js"; import { resolveBrowserConfig, resolveProfile } from "../browser/config.js"; +import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; +import { listChannelPlugins } from "../channels/plugins/index.js"; +import { formatCliCommand } from "../cli/command-format.js"; +import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../config/commands.js"; import { resolveConfigPath, resolveStateDir } from "../config/paths.js"; import { resolveGatewayAuth } from "../gateway/auth.js"; -import { formatCliCommand } from "../cli/command-format.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { probeGateway } from "../gateway/probe.js"; +import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, @@ -21,14 +24,11 @@ import { collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js"; -import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; -import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../config/commands.js"; import { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions, } from "./audit-fs.js"; -import type { ExecFn } from "./windows-acl.js"; export type SecurityAuditSeverity = "info" | "warn" | "critical"; @@ -62,7 +62,7 @@ export type SecurityAuditReport = { }; export type SecurityAuditOptions = { - config: MoltbotConfig; + config: OpenClawConfig; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; deep?: boolean; @@ -87,15 +87,21 @@ function countBySeverity(findings: SecurityAuditFinding[]): SecurityAuditSummary let warn = 0; let info = 0; for (const f of findings) { - if (f.severity === "critical") critical += 1; - else if (f.severity === "warn") warn += 1; - else info += 1; + if (f.severity === "critical") { + critical += 1; + } else if (f.severity === "warn") { + warn += 1; + } else { + info += 1; + } } return { critical, warn, info }; } function normalizeAllowFromList(list: Array | undefined | null): string[] { - if (!Array.isArray(list)) return []; + if (!Array.isArray(list)) { + return []; + } return list.map((v) => String(v).trim()).filter(Boolean); } @@ -145,7 +151,7 @@ async function collectFilesystemFindings(params: { checkId: "fs.state_dir.perms_world_writable", severity: "critical", title: "State dir is world-writable", - detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your Moltbot state.`, + detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your OpenClaw state.`, remediation: formatPermissionRemediation({ targetPath: params.stateDir, perms: stateDirPerms, @@ -159,7 +165,7 @@ async function collectFilesystemFindings(params: { checkId: "fs.state_dir.perms_group_writable", severity: "warn", title: "State dir is group-writable", - detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your Moltbot state.`, + detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your OpenClaw state.`, remediation: formatPermissionRemediation({ targetPath: params.stateDir, perms: stateDirPerms, @@ -248,7 +254,7 @@ async function collectFilesystemFindings(params: { } function collectGatewayConfigFindings( - cfg: MoltbotConfig, + cfg: OpenClawConfig, env: NodeJS.ProcessEnv, ): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; @@ -264,7 +270,7 @@ function collectGatewayConfigFindings( const hasPassword = typeof auth.password === "string" && auth.password.trim().length > 0; const hasSharedSecret = (auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword); - const hasTailscaleAuth = auth.allowTailscale === true && tailscaleMode === "serve"; + const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve"; const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth; if (bind !== "loopback" && !hasSharedSecret) { @@ -356,7 +362,7 @@ function collectGatewayConfigFindings( return findings; } -function collectBrowserControlFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +function collectBrowserControlFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; let resolved: ReturnType; @@ -368,16 +374,20 @@ function collectBrowserControlFindings(cfg: MoltbotConfig): SecurityAuditFinding severity: "warn", title: "Browser control config looks invalid", detail: String(err), - remediation: `Fix browser.cdpUrl in ${resolveConfigPath()} and re-run "${formatCliCommand("moltbot security audit --deep")}".`, + remediation: `Fix browser.cdpUrl in ${resolveConfigPath()} and re-run "${formatCliCommand("openclaw security audit --deep")}".`, }); return findings; } - if (!resolved.enabled) return findings; + if (!resolved.enabled) { + return findings; + } for (const name of Object.keys(resolved.profiles)) { const profile = resolveProfile(resolved, name); - if (!profile || profile.cdpIsLoopback) continue; + if (!profile || profile.cdpIsLoopback) { + continue; + } let url: URL; try { url = new URL(profile.cdpUrl); @@ -398,9 +408,11 @@ function collectBrowserControlFindings(cfg: MoltbotConfig): SecurityAuditFinding return findings; } -function collectLoggingFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +function collectLoggingFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const redact = cfg.logging?.redactSensitive; - if (redact !== "off") return []; + if (redact !== "off") { + return []; + } return [ { checkId: "logging.redact_off", @@ -412,14 +424,18 @@ function collectLoggingFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { ]; } -function collectElevatedFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { +function collectElevatedFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; const enabled = cfg.tools?.elevated?.enabled; const allowFrom = cfg.tools?.elevated?.allowFrom ?? {}; const anyAllowFromKeys = Object.keys(allowFrom).length > 0; - if (enabled === false) return findings; - if (!anyAllowFromKeys) return findings; + if (enabled === false) { + return findings; + } + if (!anyAllowFromKeys) { + return findings; + } for (const [provider, list] of Object.entries(allowFrom)) { const normalized = normalizeAllowFromList(list); @@ -444,15 +460,21 @@ function collectElevatedFindings(cfg: MoltbotConfig): SecurityAuditFinding[] { } async function collectChannelSecurityFindings(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; plugins: ReturnType; }): Promise { const findings: SecurityAuditFinding[] = []; const coerceNativeSetting = (value: unknown): boolean | "auto" | undefined => { - if (value === true) return true; - if (value === false) return false; - if (value === "auto") return "auto"; + if (value === true) { + return true; + } + if (value === false) { + return false; + } + if (value === "auto") { + return "auto"; + } return undefined; }; @@ -526,7 +548,9 @@ async function collectChannelSecurityFindings(params: { }; for (const plugin of params.plugins) { - if (!plugin.security) continue; + if (!plugin.security) { + continue; + } const accountIds = plugin.config.listAccountIds(params.cfg); const defaultAccountId = resolveChannelDefaultAccountId({ plugin, @@ -535,11 +559,15 @@ async function collectChannelSecurityFindings(params: { }); const account = plugin.config.resolveAccount(params.cfg, defaultAccountId); const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, params.cfg) : true; - if (!enabled) continue; + if (!enabled) { + continue; + } const configured = plugin.config.isConfigured ? await plugin.config.isConfigured(account, params.cfg) : true; - if (!configured) continue; + if (!configured) { + continue; + } if (plugin.id === "discord") { const discordCfg = @@ -567,13 +595,21 @@ async function collectChannelSecurityFindings(params: { const guildEntries = (discordCfg.guilds as Record | undefined) ?? {}; const guildsConfigured = Object.keys(guildEntries).length > 0; const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => { - if (!guild || typeof guild !== "object") return false; + if (!guild || typeof guild !== "object") { + return false; + } const g = guild as Record; - if (Array.isArray(g.users) && g.users.length > 0) return true; + if (Array.isArray(g.users) && g.users.length > 0) { + return true; + } const channels = g.channels; - if (!channels || typeof channels !== "object") return false; + if (!channels || typeof channels !== "object") { + return false; + } return Object.values(channels as Record).some((channel) => { - if (!channel || typeof channel !== "object") return false; + if (!channel || typeof channel !== "object") { + return false; + } const c = channel as Record; return Array.isArray(c.users) && c.users.length > 0; }); @@ -662,7 +698,9 @@ async function collectChannelSecurityFindings(params: { normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0; const channels = (slackCfg.channels as Record | undefined) ?? {}; const hasAnyChannelUsersAllowlist = Object.values(channels).some((value) => { - if (!value || typeof value !== "object") return false; + if (!value || typeof value !== "object") { + return false; + } const channel = value as Record; return Array.isArray(channel.users) && channel.users.length > 0; }); @@ -706,7 +744,9 @@ async function collectChannelSecurityFindings(params: { }); for (const message of warnings ?? []) { const trimmed = String(message).trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } findings.push({ checkId: `channels.${plugin.id}.warning.${findings.length + 1}`, severity: classifyChannelWarningSeverity(trimmed), @@ -718,7 +758,9 @@ async function collectChannelSecurityFindings(params: { if (plugin.id === "telegram") { const allowTextCommands = params.cfg.commands?.text !== false; - if (!allowTextCommands) continue; + if (!allowTextCommands) { + continue; + } const telegramCfg = (account as { config?: Record } | null)?.config ?? @@ -730,7 +772,9 @@ async function collectChannelSecurityFindings(params: { const groupsConfigured = Boolean(groups) && Object.keys(groups ?? {}).length > 0; const groupAccessPossible = groupPolicy === "open" || (groupPolicy === "allowlist" && groupsConfigured); - if (!groupAccessPossible) continue; + if (!groupAccessPossible) { + continue; + } const storeAllowFrom = await readChannelAllowFromStore("telegram").catch(() => []); const storeHasWildcard = storeAllowFrom.some((v) => String(v).trim() === "*"); @@ -741,14 +785,22 @@ async function collectChannelSecurityFindings(params: { const anyGroupOverride = Boolean( groups && Object.values(groups).some((value) => { - if (!value || typeof value !== "object") return false; + if (!value || typeof value !== "object") { + return false; + } const group = value as Record; const allowFrom = Array.isArray(group.allowFrom) ? group.allowFrom : []; - if (allowFrom.length > 0) return true; + if (allowFrom.length > 0) { + return true; + } const topics = group.topics; - if (!topics || typeof topics !== "object") return false; + if (!topics || typeof topics !== "object") { + return false; + } return Object.values(topics as Record).some((topicValue) => { - if (!topicValue || typeof topicValue !== "object") return false; + if (!topicValue || typeof topicValue !== "object") { + return false; + } const topic = topicValue as Record; const topicAllow = Array.isArray(topic.allowFrom) ? topic.allowFrom : []; return topicAllow.length > 0; @@ -798,7 +850,7 @@ async function collectChannelSecurityFindings(params: { } async function maybeProbeGateway(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; timeoutMs: number; probe: typeof probeGateway; }): Promise { @@ -818,10 +870,10 @@ async function maybeProbeGateway(params: { ? typeof remote?.token === "string" && remote.token.trim() ? remote.token.trim() : undefined - : process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() || + : process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || (typeof authToken === "string" && authToken.trim() ? authToken.trim() : undefined); const password = - process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || + process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || (mode === "remote" ? typeof remote?.password === "string" && remote.password.trim() ? remote.password.trim() @@ -918,13 +970,13 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise { describe("security fix", () => { it("tightens groupPolicy + filesystem perms", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-fix-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-fix-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true }); await fs.chmod(stateDir, 0o755); - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile( configPath, `${JSON.stringify( @@ -54,8 +52,8 @@ describe("security fix", () => { const env = { ...process.env, - CLAWDBOT_STATE_DIR: stateDir, - CLAWDBOT_CONFIG_PATH: "", + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_CONFIG_PATH: "", }; const res = await fixSecurityFootguns({ env }); @@ -90,11 +88,11 @@ describe("security fix", () => { }); it("applies allowlist per-account and seeds WhatsApp groupAllowFrom from store", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-fix-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-fix-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true }); - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile( configPath, `${JSON.stringify( @@ -123,8 +121,8 @@ describe("security fix", () => { const env = { ...process.env, - CLAWDBOT_STATE_DIR: stateDir, - CLAWDBOT_CONFIG_PATH: "", + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_CONFIG_PATH: "", }; const res = await fixSecurityFootguns({ env }); @@ -132,7 +130,7 @@ describe("security fix", () => { const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as Record; const channels = parsed.channels as Record>; - const whatsapp = channels.whatsapp as Record; + const whatsapp = channels.whatsapp; const accounts = whatsapp.accounts as Record>; expect(accounts.a1.groupPolicy).toBe("allowlist"); @@ -140,11 +138,11 @@ describe("security fix", () => { }); it("does not seed WhatsApp groupAllowFrom if allowFrom is set", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-fix-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-fix-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true }); - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile( configPath, `${JSON.stringify( @@ -169,8 +167,8 @@ describe("security fix", () => { const env = { ...process.env, - CLAWDBOT_STATE_DIR: stateDir, - CLAWDBOT_CONFIG_PATH: "", + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_CONFIG_PATH: "", }; const res = await fixSecurityFootguns({ env }); @@ -183,19 +181,19 @@ describe("security fix", () => { }); it("returns ok=false for invalid config but still tightens perms", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-fix-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-fix-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true }); await fs.chmod(stateDir, 0o755); - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile(configPath, "{ this is not json }\n", "utf-8"); await fs.chmod(configPath, 0o644); const env = { ...process.env, - CLAWDBOT_STATE_DIR: stateDir, - CLAWDBOT_CONFIG_PATH: "", + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_CONFIG_PATH: "", }; const res = await fixSecurityFootguns({ env }); @@ -209,7 +207,7 @@ describe("security fix", () => { }); it("tightens perms for credentials + agent auth/sessions + include files", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-security-fix-")); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-fix-")); const stateDir = path.join(tmp, "state"); await fs.mkdir(stateDir, { recursive: true }); @@ -219,7 +217,7 @@ describe("security fix", () => { await fs.writeFile(includePath, "{ logging: { redactSensitive: 'off' } }\n", "utf-8"); await fs.chmod(includePath, 0o644); - const configPath = path.join(stateDir, "moltbot.json"); + const configPath = path.join(stateDir, "openclaw.json"); await fs.writeFile( configPath, `{ "$include": "./includes/extra.json5", channels: { whatsapp: { groupPolicy: "open" } } }\n`, @@ -251,8 +249,8 @@ describe("security fix", () => { const env = { ...process.env, - CLAWDBOT_STATE_DIR: stateDir, - CLAWDBOT_CONFIG_PATH: "", + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_CONFIG_PATH: "", }; const res = await fixSecurityFootguns({ env }); diff --git a/src/security/fix.ts b/src/security/fix.ts index 6f7ab0022..0ecfc1e7d 100644 --- a/src/security/fix.ts +++ b/src/security/fix.ts @@ -1,16 +1,14 @@ +import JSON5 from "json5"; import fs from "node:fs/promises"; import path from "node:path"; - -import JSON5 from "json5"; - -import type { MoltbotConfig } from "../config/config.js"; -import { createConfigIO } from "../config/config.js"; -import { resolveConfigPath, resolveOAuthDir, resolveStateDir } from "../config/paths.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { createConfigIO } from "../config/config.js"; import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js"; -import { normalizeAgentId } from "../routing/session-key.js"; +import { resolveConfigPath, resolveOAuthDir, resolveStateDir } from "../config/paths.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { runExec } from "../process/exec.js"; +import { normalizeAgentId } from "../routing/session-key.js"; import { createIcaclsResetCommand, formatIcaclsResetCommand, type ExecFn } from "./windows-acl.js"; export type SecurityFixChmodAction = { @@ -187,16 +185,20 @@ async function safeAclReset(params: { } function setGroupPolicyAllowlist(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel: string; changes: string[]; policyFlips: Set; }): void { - if (!params.cfg.channels) return; - const section = params.cfg.channels[params.channel as keyof MoltbotConfig["channels"]] as + if (!params.cfg.channels) { + return; + } + const section = params.cfg.channels[params.channel as keyof OpenClawConfig["channels"]] as | Record | undefined; - if (!section || typeof section !== "object") return; + if (!section || typeof section !== "object") { + return; + } const topPolicy = section.groupPolicy; if (topPolicy === "open") { @@ -206,10 +208,16 @@ function setGroupPolicyAllowlist(params: { } const accounts = section.accounts; - if (!accounts || typeof accounts !== "object") return; + if (!accounts || typeof accounts !== "object") { + return; + } for (const [accountId, accountValue] of Object.entries(accounts)) { - if (!accountId) continue; - if (!accountValue || typeof accountValue !== "object") continue; + if (!accountId) { + continue; + } + if (!accountValue || typeof accountValue !== "object") { + continue; + } const account = accountValue as Record; if (account.groupPolicy === "open") { account.groupPolicy = "allowlist"; @@ -222,21 +230,31 @@ function setGroupPolicyAllowlist(params: { } function setWhatsAppGroupAllowFromFromStore(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; storeAllowFrom: string[]; changes: string[]; policyFlips: Set; }): void { const section = params.cfg.channels?.whatsapp as Record | undefined; - if (!section || typeof section !== "object") return; - if (params.storeAllowFrom.length === 0) return; + if (!section || typeof section !== "object") { + return; + } + if (params.storeAllowFrom.length === 0) { + return; + } const maybeApply = (prefix: string, obj: Record) => { - if (!params.policyFlips.has(prefix)) return; + if (!params.policyFlips.has(prefix)) { + return; + } const allowFrom = Array.isArray(obj.allowFrom) ? obj.allowFrom : []; const groupAllowFrom = Array.isArray(obj.groupAllowFrom) ? obj.groupAllowFrom : []; - if (allowFrom.length > 0) return; - if (groupAllowFrom.length > 0) return; + if (allowFrom.length > 0) { + return; + } + if (groupAllowFrom.length > 0) { + return; + } obj.groupAllowFrom = params.storeAllowFrom; params.changes.push(`${prefix}groupAllowFrom=pairing-store`); }; @@ -244,16 +262,20 @@ function setWhatsAppGroupAllowFromFromStore(params: { maybeApply("channels.whatsapp.", section); const accounts = section.accounts; - if (!accounts || typeof accounts !== "object") return; + if (!accounts || typeof accounts !== "object") { + return; + } for (const [accountId, accountValue] of Object.entries(accounts)) { - if (!accountValue || typeof accountValue !== "object") continue; + if (!accountValue || typeof accountValue !== "object") { + continue; + } const account = accountValue as Record; maybeApply(`channels.whatsapp.accounts.${accountId}.`, account); } } -function applyConfigFixes(params: { cfg: MoltbotConfig; env: NodeJS.ProcessEnv }): { - cfg: MoltbotConfig; +function applyConfigFixes(params: { cfg: OpenClawConfig; env: NodeJS.ProcessEnv }): { + cfg: OpenClawConfig; changes: string[]; policyFlips: Set; } { @@ -284,21 +306,32 @@ function applyConfigFixes(params: { cfg: MoltbotConfig; env: NodeJS.ProcessEnv } function listDirectIncludes(parsed: unknown): string[] { const out: string[] = []; const visit = (value: unknown) => { - if (!value) return; - if (Array.isArray(value)) { - for (const item of value) visit(item); + if (!value) { + return; + } + if (Array.isArray(value)) { + for (const item of value) { + visit(item); + } + return; + } + if (typeof value !== "object") { return; } - if (typeof value !== "object") return; const rec = value as Record; const includeVal = rec[INCLUDE_KEY]; - if (typeof includeVal === "string") out.push(includeVal); - else if (Array.isArray(includeVal)) { + if (typeof includeVal === "string") { + out.push(includeVal); + } else if (Array.isArray(includeVal)) { for (const item of includeVal) { - if (typeof item === "string") out.push(item); + if (typeof item === "string") { + out.push(item); + } } } - for (const v of Object.values(rec)) visit(v); + for (const v of Object.values(rec)) { + visit(v); + } }; visit(parsed); return out; @@ -320,17 +353,23 @@ async function collectIncludePathsRecursive(params: { const result: string[] = []; const walk = async (basePath: string, parsed: unknown, depth: number): Promise => { - if (depth > MAX_INCLUDE_DEPTH) return; + if (depth > MAX_INCLUDE_DEPTH) { + return; + } for (const raw of listDirectIncludes(parsed)) { const resolved = resolveIncludePath(basePath, raw); - if (visited.has(resolved)) continue; + if (visited.has(resolved)) { + continue; + } visited.add(resolved); result.push(resolved); const rawText = await fs.readFile(resolved, "utf-8").catch(() => null); - if (!rawText) continue; + if (!rawText) { + continue; + } const nestedParsed = (() => { try { - return JSON5.parse(rawText) as unknown; + return JSON5.parse(rawText); } catch { return null; } @@ -349,7 +388,7 @@ async function collectIncludePathsRecursive(params: { async function chmodCredentialsAndAgentState(params: { env: NodeJS.ProcessEnv; stateDir: string; - cfg: MoltbotConfig; + cfg: OpenClawConfig; actions: SecurityFixAction[]; applyPerms: (params: { path: string; @@ -362,8 +401,12 @@ async function chmodCredentialsAndAgentState(params: { const credsEntries = await fs.readdir(credsDir, { withFileTypes: true }).catch(() => []); for (const entry of credsEntries) { - if (!entry.isFile()) continue; - if (!entry.name.endsWith(".json")) continue; + if (!entry.isFile()) { + continue; + } + if (!entry.name.endsWith(".json")) { + continue; + } const p = path.join(credsDir, entry.name); // eslint-disable-next-line no-await-in-loop params.actions.push(await safeChmod({ path: p, mode: 0o600, require: "file" })); @@ -373,10 +416,14 @@ async function chmodCredentialsAndAgentState(params: { ids.add(resolveDefaultAgentId(params.cfg)); const list = Array.isArray(params.cfg.agents?.list) ? params.cfg.agents?.list : []; for (const agent of list ?? []) { - if (!agent || typeof agent !== "object") continue; + if (!agent || typeof agent !== "object") { + continue; + } const id = typeof (agent as { id?: unknown }).id === "string" ? (agent as { id: string }).id.trim() : ""; - if (id) ids.add(id); + if (id) { + ids.add(id); + } } for (const agentId of ids) { diff --git a/src/security/windows-acl.ts b/src/security/windows-acl.ts index 0a6779214..01d2c6ef9 100644 --- a/src/security/windows-acl.ts +++ b/src/security/windows-acl.ts @@ -1,5 +1,4 @@ import os from "node:os"; - import { runExec } from "../process/exec.js"; export type ExecFn = typeof runExec; @@ -42,7 +41,9 @@ const normalize = (value: string) => value.trim().toLowerCase(); export function resolveWindowsUserPrincipal(env?: NodeJS.ProcessEnv): string | null { const username = env?.USERNAME?.trim() || os.userInfo().username?.trim(); - if (!username) return null; + if (!username) { + return null; + } const domain = env?.USERDOMAIN?.trim(); return domain ? `${domain}\\${username}` : username; } @@ -54,7 +55,9 @@ function buildTrustedPrincipals(env?: NodeJS.ProcessEnv): Set { trusted.add(normalize(principal)); const parts = principal.split("\\"); const userOnly = parts.at(-1); - if (userOnly) trusted.add(normalize(userOnly)); + if (userOnly) { + trusted.add(normalize(userOnly)); + } } return trusted; } @@ -65,10 +68,12 @@ function classifyPrincipal( ): "trusted" | "world" | "group" { const normalized = normalize(principal); const trusted = buildTrustedPrincipals(env); - if (trusted.has(normalized) || TRUSTED_SUFFIXES.some((s) => normalized.endsWith(s))) + if (trusted.has(normalized) || TRUSTED_SUFFIXES.some((s) => normalized.endsWith(s))) { return "trusted"; - if (WORLD_PRINCIPALS.has(normalized) || WORLD_SUFFIXES.some((s) => normalized.endsWith(s))) + } + if (WORLD_PRINCIPALS.has(normalized) || WORLD_SUFFIXES.some((s) => normalized.endsWith(s))) { return "world"; + } return "group"; } @@ -89,7 +94,9 @@ export function parseIcaclsOutput(output: string, targetPath: string): WindowsAc for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.trimEnd(); - if (!line.trim()) continue; + if (!line.trim()) { + continue; + } const trimmed = line.trim(); const lower = trimmed.toLowerCase(); if ( @@ -107,10 +114,14 @@ export function parseIcaclsOutput(output: string, targetPath: string): WindowsAc } else if (lower.startsWith(quotedLower)) { entry = trimmed.slice(quotedTarget.length).trim(); } - if (!entry) continue; + if (!entry) { + continue; + } const idx = entry.indexOf(":"); - if (idx === -1) continue; + if (idx === -1) { + continue; + } const principal = entry.slice(0, idx).trim(); const rawRights = entry.slice(idx + 1).trim(); @@ -119,9 +130,13 @@ export function parseIcaclsOutput(output: string, targetPath: string): WindowsAc .match(/\(([^)]+)\)/g) ?.map((token) => token.slice(1, -1).trim()) .filter(Boolean) ?? []; - if (tokens.some((token) => token.toUpperCase() === "DENY")) continue; + if (tokens.some((token) => token.toUpperCase() === "DENY")) { + continue; + } const rights = tokens.filter((token) => !INHERIT_FLAGS.has(token.toUpperCase())); - if (rights.length === 0) continue; + if (rights.length === 0) { + continue; + } const { canRead, canWrite } = rightsFromTokens(rights); entries.push({ principal, rights, rawRights, canRead, canWrite }); } @@ -138,9 +153,13 @@ export function summarizeWindowsAcl( const untrustedGroup: WindowsAclEntry[] = []; for (const entry of entries) { const classification = classifyPrincipal(entry.principal, env); - if (classification === "trusted") trusted.push(entry); - else if (classification === "world") untrustedWorld.push(entry); - else untrustedGroup.push(entry); + if (classification === "trusted") { + trusted.push(entry); + } else if (classification === "world") { + untrustedWorld.push(entry); + } else { + untrustedGroup.push(entry); + } } return { trusted, untrustedWorld, untrustedGroup }; } @@ -169,9 +188,13 @@ export async function inspectWindowsAcl( } export function formatWindowsAclSummary(summary: WindowsAclSummary): string { - if (!summary.ok) return "unknown"; + if (!summary.ok) { + return "unknown"; + } const untrusted = [...summary.untrustedWorld, ...summary.untrustedGroup]; - if (untrusted.length === 0) return "trusted-only"; + if (untrusted.length === 0) { + return "trusted-only"; + } return untrusted.map((entry) => `${entry.principal}:${entry.rawRights}`).join(", "); } @@ -189,7 +212,9 @@ export function createIcaclsResetCommand( opts: { isDir: boolean; env?: NodeJS.ProcessEnv }, ): { command: string; args: string[]; display: string } | null { const user = resolveWindowsUserPrincipal(opts.env); - if (!user) return null; + if (!user) { + return null; + } const grant = opts.isDir ? "(OI)(CI)F" : "F"; const args = [ targetPath, diff --git a/src/sessions/level-overrides.ts b/src/sessions/level-overrides.ts index 5b0d079d9..f0016fa43 100644 --- a/src/sessions/level-overrides.ts +++ b/src/sessions/level-overrides.ts @@ -1,11 +1,15 @@ -import { normalizeVerboseLevel, type VerboseLevel } from "../auto-reply/thinking.js"; import type { SessionEntry } from "../config/sessions.js"; +import { normalizeVerboseLevel, type VerboseLevel } from "../auto-reply/thinking.js"; export function parseVerboseOverride( raw: unknown, ): { ok: true; value: VerboseLevel | null | undefined } | { ok: false; error: string } { - if (raw === null) return { ok: true, value: null }; - if (raw === undefined) return { ok: true, value: undefined }; + if (raw === null) { + return { ok: true, value: null }; + } + if (raw === undefined) { + return { ok: true, value: undefined }; + } if (typeof raw !== "string") { return { ok: false, error: 'invalid verboseLevel (use "on"|"off")' }; } @@ -17,7 +21,9 @@ export function parseVerboseOverride( } export function applyVerboseOverride(entry: SessionEntry, level: VerboseLevel | null | undefined) { - if (level === undefined) return; + if (level === undefined) { + return; + } if (level === null) { delete entry.verboseLevel; return; diff --git a/src/sessions/send-policy.test.ts b/src/sessions/send-policy.test.ts index 2778e450f..ed01e2d53 100644 --- a/src/sessions/send-policy.test.ts +++ b/src/sessions/send-policy.test.ts @@ -1,18 +1,18 @@ import { describe, expect, it } from "vitest"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; import { resolveSendPolicy } from "./send-policy.js"; describe("resolveSendPolicy", () => { it("defaults to allow", () => { - const cfg = {} as MoltbotConfig; + const cfg = {} as OpenClawConfig; expect(resolveSendPolicy({ cfg })).toBe("allow"); }); it("entry override wins", () => { const cfg = { session: { sendPolicy: { default: "allow" } }, - } as MoltbotConfig; + } as OpenClawConfig; const entry: SessionEntry = { sessionId: "s", updatedAt: 0, @@ -34,7 +34,7 @@ describe("resolveSendPolicy", () => { ], }, }, - } as MoltbotConfig; + } as OpenClawConfig; const entry: SessionEntry = { sessionId: "s", updatedAt: 0, @@ -52,7 +52,7 @@ describe("resolveSendPolicy", () => { rules: [{ action: "deny", match: { keyPrefix: "cron:" } }], }, }, - } as MoltbotConfig; + } as OpenClawConfig; expect(resolveSendPolicy({ cfg, sessionKey: "cron:job-1" })).toBe("deny"); }); }); diff --git a/src/sessions/send-policy.ts b/src/sessions/send-policy.ts index fcaf2250f..6f635c1ae 100644 --- a/src/sessions/send-policy.ts +++ b/src/sessions/send-policy.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SessionChatType, SessionEntry } from "../config/sessions.js"; import { normalizeChatType } from "../channels/chat-type.js"; @@ -6,8 +6,12 @@ export type SessionSendPolicyDecision = "allow" | "deny"; export function normalizeSendPolicy(raw?: string | null): SessionSendPolicyDecision | undefined { const value = raw?.trim().toLowerCase(); - if (value === "allow") return "allow"; - if (value === "deny") return "deny"; + if (value === "allow") { + return "allow"; + } + if (value === "deny") { + return "deny"; + } return undefined; } @@ -17,7 +21,9 @@ function normalizeMatchValue(raw?: string | null) { } function deriveChannelFromKey(key?: string) { - if (!key) return undefined; + if (!key) { + return undefined; + } const parts = key.split(":").filter(Boolean); if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) { return normalizeMatchValue(parts[0]); @@ -26,24 +32,34 @@ function deriveChannelFromKey(key?: string) { } function deriveChatTypeFromKey(key?: string): SessionChatType | undefined { - if (!key) return undefined; - if (key.includes(":group:")) return "group"; - if (key.includes(":channel:")) return "channel"; + if (!key) { + return undefined; + } + if (key.includes(":group:")) { + return "group"; + } + if (key.includes(":channel:")) { + return "channel"; + } return undefined; } export function resolveSendPolicy(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; entry?: SessionEntry; sessionKey?: string; channel?: string; chatType?: SessionChatType; }): SessionSendPolicyDecision { const override = normalizeSendPolicy(params.entry?.sendPolicy); - if (override) return override; + if (override) { + return override; + } const policy = params.cfg.session?.sendPolicy; - if (!policy) return "allow"; + if (!policy) { + return "allow"; + } const channel = normalizeMatchValue(params.channel) ?? @@ -57,21 +73,33 @@ export function resolveSendPolicy(params: { let allowedMatch = false; for (const rule of policy.rules ?? []) { - if (!rule) continue; + if (!rule) { + continue; + } const action = normalizeSendPolicy(rule.action) ?? "allow"; const match = rule.match ?? {}; const matchChannel = normalizeMatchValue(match.channel); const matchChatType = normalizeChatType(match.chatType); const matchPrefix = normalizeMatchValue(match.keyPrefix); - if (matchChannel && matchChannel !== channel) continue; - if (matchChatType && matchChatType !== chatType) continue; - if (matchPrefix && !sessionKey.startsWith(matchPrefix)) continue; - if (action === "deny") return "deny"; + if (matchChannel && matchChannel !== channel) { + continue; + } + if (matchChatType && matchChatType !== chatType) { + continue; + } + if (matchPrefix && !sessionKey.startsWith(matchPrefix)) { + continue; + } + if (action === "deny") { + return "deny"; + } allowedMatch = true; } - if (allowedMatch) return "allow"; + if (allowedMatch) { + return "allow"; + } const fallback = normalizeSendPolicy(policy.default); return fallback ?? "allow"; diff --git a/src/sessions/session-key-utils.ts b/src/sessions/session-key-utils.ts index 10baeb607..ba867f552 100644 --- a/src/sessions/session-key-utils.ts +++ b/src/sessions/session-key-utils.ts @@ -7,29 +7,45 @@ export function parseAgentSessionKey( sessionKey: string | undefined | null, ): ParsedAgentSessionKey | null { const raw = (sessionKey ?? "").trim(); - if (!raw) return null; + if (!raw) { + return null; + } const parts = raw.split(":").filter(Boolean); - if (parts.length < 3) return null; - if (parts[0] !== "agent") return null; + if (parts.length < 3) { + return null; + } + if (parts[0] !== "agent") { + return null; + } const agentId = parts[1]?.trim(); const rest = parts.slice(2).join(":"); - if (!agentId || !rest) return null; + if (!agentId || !rest) { + return null; + } return { agentId, rest }; } export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean { const raw = (sessionKey ?? "").trim(); - if (!raw) return false; - if (raw.toLowerCase().startsWith("subagent:")) return true; + if (!raw) { + return false; + } + if (raw.toLowerCase().startsWith("subagent:")) { + return true; + } const parsed = parseAgentSessionKey(raw); return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("subagent:")); } export function isAcpSessionKey(sessionKey: string | undefined | null): boolean { const raw = (sessionKey ?? "").trim(); - if (!raw) return false; + if (!raw) { + return false; + } const normalized = raw.toLowerCase(); - if (normalized.startsWith("acp:")) return true; + if (normalized.startsWith("acp:")) { + return true; + } const parsed = parseAgentSessionKey(raw); return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("acp:")); } @@ -40,14 +56,20 @@ export function resolveThreadParentSessionKey( sessionKey: string | undefined | null, ): string | null { const raw = (sessionKey ?? "").trim(); - if (!raw) return null; + if (!raw) { + return null; + } const normalized = raw.toLowerCase(); let idx = -1; for (const marker of THREAD_SESSION_MARKERS) { const candidate = normalized.lastIndexOf(marker); - if (candidate > idx) idx = candidate; + if (candidate > idx) { + idx = candidate; + } + } + if (idx <= 0) { + return null; } - if (idx <= 0) return null; const parent = raw.slice(0, idx).trim(); return parent ? parent : null; } diff --git a/src/sessions/session-label.ts b/src/sessions/session-label.ts index f916737bd..882e4c98a 100644 --- a/src/sessions/session-label.ts +++ b/src/sessions/session-label.ts @@ -7,7 +7,9 @@ export function parseSessionLabel(raw: unknown): ParsedSessionLabel { return { ok: false, error: "invalid label: must be a string" }; } const trimmed = raw.trim(); - if (!trimmed) return { ok: false, error: "invalid label: empty" }; + if (!trimmed) { + return { ok: false, error: "invalid label: empty" }; + } if (trimmed.length > SESSION_LABEL_MAX_LENGTH) { return { ok: false, diff --git a/src/sessions/transcript-events.ts b/src/sessions/transcript-events.ts index 88a9cd7b7..d00be113a 100644 --- a/src/sessions/transcript-events.ts +++ b/src/sessions/transcript-events.ts @@ -15,7 +15,9 @@ export function onSessionTranscriptUpdate(listener: SessionTranscriptListener): export function emitSessionTranscriptUpdate(sessionFile: string): void { const trimmed = sessionFile.trim(); - if (!trimmed) return; + if (!trimmed) { + return; + } const update = { sessionFile: trimmed }; for (const listener of SESSION_TRANSCRIPT_LISTENERS) { listener(update); diff --git a/src/shared/text/reasoning-tags.test.ts b/src/shared/text/reasoning-tags.test.ts new file mode 100644 index 000000000..d72d0cde2 --- /dev/null +++ b/src/shared/text/reasoning-tags.test.ts @@ -0,0 +1,218 @@ +import { describe, expect, it } from "vitest"; +import { stripReasoningTagsFromText } from "./reasoning-tags.js"; + +describe("stripReasoningTagsFromText", () => { + describe("basic functionality", () => { + it("returns text unchanged when no reasoning tags present", () => { + const input = "Hello, this is a normal message."; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("strips proper think tags", () => { + const input = "Hello internal reasoning world!"; + expect(stripReasoningTagsFromText(input)).toBe("Hello world!"); + }); + + it("strips thinking tags", () => { + const input = "Before some thought after"; + expect(stripReasoningTagsFromText(input)).toBe("Before after"); + }); + + it("strips thought tags", () => { + const input = "A hmm B"; + expect(stripReasoningTagsFromText(input)).toBe("A B"); + }); + + it("strips antthinking tags", () => { + const input = "X internal Y"; + expect(stripReasoningTagsFromText(input)).toBe("X Y"); + }); + + it("strips multiple reasoning blocks", () => { + const input = "firstAsecondB"; + expect(stripReasoningTagsFromText(input)).toBe("AB"); + }); + }); + + describe("code block preservation (issue #3952)", () => { + it("preserves think tags inside fenced code blocks", () => { + const input = "Use the tag like this:\n```\nreasoning\n```\nThat's it!"; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("preserves think tags inside inline code", () => { + const input = + "The `` tag is used for reasoning. Don't forget the closing `` tag."; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("preserves tags in fenced code blocks with language specifier", () => { + const input = "Example:\n```xml\n\n nested\n\n```\nDone!"; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("handles mixed real tags and code tags", () => { + const input = "hiddenVisible text with `` example."; + expect(stripReasoningTagsFromText(input)).toBe("Visible text with `` example."); + }); + + it("preserves both opening and closing tags in backticks", () => { + const input = "Use `` to open and `` to close."; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("preserves think tags in code block at EOF without trailing newline", () => { + const input = "Example:\n```\nreasoning\n```"; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("preserves final tags inside code blocks", () => { + const input = "Use `` for final answers in code: ```\n42\n```"; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("handles code block followed by real tags", () => { + const input = "```\ncode\n```\nreal hiddenvisible"; + expect(stripReasoningTagsFromText(input)).toBe("```\ncode\n```\nvisible"); + }); + + it("handles multiple code blocks with tags", () => { + const input = "First `` then ```\nblock\n``` then ``"; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + }); + + describe("edge cases", () => { + it("preserves unclosed { + const input = "Here is how to use { + const input = "You can start with "; + expect(stripReasoningTagsFromText(input)).toBe( + "You can start with { + const input = "A < think >content< /think > B"; + expect(stripReasoningTagsFromText(input)).toBe("A B"); + }); + + it("handles empty input", () => { + expect(stripReasoningTagsFromText("")).toBe(""); + }); + + it("handles null-ish input", () => { + expect(stripReasoningTagsFromText(null as unknown as string)).toBe(null); + }); + + it("preserves think tags inside tilde fenced code blocks", () => { + const input = "Example:\n~~~\nreasoning\n~~~\nDone!"; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("preserves tags in tilde block at EOF without trailing newline", () => { + const input = "Example:\n~~~js\ncode\n~~~"; + expect(stripReasoningTagsFromText(input)).toBe(input); + }); + + it("handles nested think patterns (first close ends block)", () => { + const input = "outer inner still outervisible"; + expect(stripReasoningTagsFromText(input)).toBe("still outervisible"); + }); + + it("strips final tag markup but preserves content (by design)", () => { + const input = "A1B2C"; + expect(stripReasoningTagsFromText(input)).toBe("A1B2C"); + }); + + it("preserves final tags in inline code (markup only stripped outside)", () => { + const input = "`` in code, visible outside"; + expect(stripReasoningTagsFromText(input)).toBe("`` in code, visible outside"); + }); + + it("handles double backtick inline code with tags", () => { + const input = "Use ``code`` with hidden text"; + expect(stripReasoningTagsFromText(input)).toBe("Use ``code`` with text"); + }); + + it("handles fenced code blocks with content", () => { + const input = "Before\n```\ncode\n```\nAfter with hidden"; + expect(stripReasoningTagsFromText(input)).toBe("Before\n```\ncode\n```\nAfter with"); + }); + + it("does not match mismatched fence types (``` vs ~~~)", () => { + const input = "```\nnot protected\n~~~\ntext"; + const result = stripReasoningTagsFromText(input); + expect(result).toBe(input); + }); + + it("handles unicode content inside and around tags", () => { + const input = "你好 思考 🤔 世界"; + expect(stripReasoningTagsFromText(input)).toBe("你好 世界"); + }); + + it("handles very long content between tags efficiently", () => { + const longContent = "x".repeat(10000); + const input = `${longContent}visible`; + expect(stripReasoningTagsFromText(input)).toBe("visible"); + }); + + it("handles tags with attributes", () => { + const input = "A hidden B"; + expect(stripReasoningTagsFromText(input)).toBe("A B"); + }); + + it("is case-insensitive for tag names", () => { + const input = "A hidden also hidden B"; + expect(stripReasoningTagsFromText(input)).toBe("A B"); + }); + + it("handles pathological nested backtick patterns without hanging", () => { + const input = "`".repeat(100) + "test" + "`".repeat(100); + const start = Date.now(); + stripReasoningTagsFromText(input); + const elapsed = Date.now() - start; + expect(elapsed).toBeLessThan(1000); + }); + + it("handles unclosed inline code gracefully", () => { + const input = "Start `unclosed hidden end"; + const result = stripReasoningTagsFromText(input); + expect(result).toBe("Start `unclosed end"); + }); + }); + + describe("strict vs preserve mode", () => { + it("strict mode truncates on unclosed tag", () => { + const input = "Before unclosed content after"; + expect(stripReasoningTagsFromText(input, { mode: "strict" })).toBe("Before"); + }); + + it("preserve mode keeps content after unclosed tag", () => { + const input = "Before unclosed content after"; + expect(stripReasoningTagsFromText(input, { mode: "preserve" })).toBe( + "Before unclosed content after", + ); + }); + }); + + describe("trim options", () => { + it("trims both sides by default", () => { + const input = " x result y "; + expect(stripReasoningTagsFromText(input)).toBe("result"); + }); + + it("trim=none preserves whitespace", () => { + const input = " x result "; + expect(stripReasoningTagsFromText(input, { trim: "none" })).toBe(" result "); + }); + + it("trim=start only trims start", () => { + const input = " x result "; + expect(stripReasoningTagsFromText(input, { trim: "start" })).toBe("result "); + }); + }); +}); diff --git a/src/shared/text/reasoning-tags.ts b/src/shared/text/reasoning-tags.ts index 822138e55..426d08322 100644 --- a/src/shared/text/reasoning-tags.ts +++ b/src/shared/text/reasoning-tags.ts @@ -2,12 +2,48 @@ export type ReasoningTagMode = "strict" | "preserve"; export type ReasoningTagTrim = "none" | "start" | "both"; const QUICK_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking|final)\b/i; -const FINAL_TAG_RE = /<\s*\/?\s*final\b[^>]*>/gi; -const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi; +const FINAL_TAG_RE = /<\s*\/?\s*final\b[^<>]*>/gi; +const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi; + +interface CodeRegion { + start: number; + end: number; +} + +function findCodeRegions(text: string): CodeRegion[] { + const regions: CodeRegion[] = []; + + const fencedRe = /(^|\n)(```|~~~)[^\n]*\n[\s\S]*?(?:\n\2(?:\n|$)|$)/g; + for (const match of text.matchAll(fencedRe)) { + const start = (match.index ?? 0) + match[1].length; + regions.push({ start, end: start + match[0].length - match[1].length }); + } + + const inlineRe = /`+[^`]+`+/g; + for (const match of text.matchAll(inlineRe)) { + const start = match.index ?? 0; + const end = start + match[0].length; + const insideFenced = regions.some((r) => start >= r.start && end <= r.end); + if (!insideFenced) { + regions.push({ start, end }); + } + } + + regions.sort((a, b) => a.start - b.start); + return regions; +} + +function isInsideCode(pos: number, regions: CodeRegion[]): boolean { + return regions.some((r) => pos >= r.start && pos < r.end); +} function applyTrim(value: string, mode: ReasoningTagTrim): string { - if (mode === "none") return value; - if (mode === "start") return value.trimStart(); + if (mode === "none") { + return value; + } + if (mode === "start") { + return value.trimStart(); + } return value.trim(); } @@ -18,8 +54,12 @@ export function stripReasoningTagsFromText( trim?: ReasoningTagTrim; }, ): string { - if (!text) return text; - if (!QUICK_TAG_RE.test(text)) return text; + if (!text) { + return text; + } + if (!QUICK_TAG_RE.test(text)) { + return text; + } const mode = options?.mode ?? "strict"; const trimMode = options?.trim ?? "both"; @@ -27,11 +67,29 @@ export function stripReasoningTagsFromText( let cleaned = text; if (FINAL_TAG_RE.test(cleaned)) { FINAL_TAG_RE.lastIndex = 0; - cleaned = cleaned.replace(FINAL_TAG_RE, ""); + const finalMatches: Array<{ start: number; length: number; inCode: boolean }> = []; + const preCodeRegions = findCodeRegions(cleaned); + for (const match of cleaned.matchAll(FINAL_TAG_RE)) { + const start = match.index ?? 0; + finalMatches.push({ + start, + length: match[0].length, + inCode: isInsideCode(start, preCodeRegions), + }); + } + + for (let i = finalMatches.length - 1; i >= 0; i--) { + const m = finalMatches[i]; + if (!m.inCode) { + cleaned = cleaned.slice(0, m.start) + cleaned.slice(m.start + m.length); + } + } } else { FINAL_TAG_RE.lastIndex = 0; } + const codeRegions = findCodeRegions(cleaned); + THINKING_TAG_RE.lastIndex = 0; let result = ""; let lastIndex = 0; @@ -41,6 +99,10 @@ export function stripReasoningTagsFromText( const idx = match.index ?? 0; const isClose = match[1] === "/"; + if (isInsideCode(idx, codeRegions)) { + continue; + } + if (!inThinking) { result += cleaned.slice(lastIndex, idx); if (!isClose) { diff --git a/src/signal/accounts.ts b/src/signal/accounts.ts index f17d51735..3d96a3d83 100644 --- a/src/signal/accounts.ts +++ b/src/signal/accounts.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SignalAccountConfig } from "../config/types.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; @@ -11,34 +11,42 @@ export type ResolvedSignalAccount = { config: SignalAccountConfig; }; -function listConfiguredAccountIds(cfg: MoltbotConfig): string[] { +function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = cfg.channels?.signal?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } -export function listSignalAccountIds(cfg: MoltbotConfig): string[] { +export function listSignalAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } -export function resolveDefaultSignalAccountId(cfg: MoltbotConfig): string { +export function resolveDefaultSignalAccountId(cfg: OpenClawConfig): string { const ids = listSignalAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } function resolveAccountConfig( - cfg: MoltbotConfig, + cfg: OpenClawConfig, accountId: string, ): SignalAccountConfig | undefined { const accounts = cfg.channels?.signal?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as SignalAccountConfig | undefined; } -function mergeSignalAccountConfig(cfg: MoltbotConfig, accountId: string): SignalAccountConfig { +function mergeSignalAccountConfig(cfg: OpenClawConfig, accountId: string): SignalAccountConfig { const { accounts: _ignored, ...base } = (cfg.channels?.signal ?? {}) as SignalAccountConfig & { accounts?: unknown; }; @@ -47,7 +55,7 @@ function mergeSignalAccountConfig(cfg: MoltbotConfig, accountId: string): Signal } export function resolveSignalAccount(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; }): ResolvedSignalAccount { const accountId = normalizeAccountId(params.accountId); @@ -76,7 +84,7 @@ export function resolveSignalAccount(params: { }; } -export function listEnabledSignalAccounts(cfg: MoltbotConfig): ResolvedSignalAccount[] { +export function listEnabledSignalAccounts(cfg: OpenClawConfig): ResolvedSignalAccount[] { return listSignalAccountIds(cfg) .map((accountId) => resolveSignalAccount({ cfg, accountId })) .filter((account) => account.enabled); diff --git a/src/signal/client.ts b/src/signal/client.ts index 5595edb5b..1551183f1 100644 --- a/src/signal/client.ts +++ b/src/signal/client.ts @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto"; - import { resolveFetch } from "../infra/fetch.js"; export type SignalRpcOptions = { @@ -33,7 +32,9 @@ function normalizeBaseUrl(url: string): string { if (!trimmed) { throw new Error("Signal base URL is required"); } - if (/^https?:\/\//i.test(trimmed)) return trimmed.replace(/\/+$/, ""); + if (/^https?:\/\//i.test(trimmed)) { + return trimmed.replace(/\/+$/, ""); + } return `http://${trimmed}`.replace(/\/+$/, ""); } @@ -117,7 +118,9 @@ export async function streamSignalEvents(params: { }): Promise { const baseUrl = normalizeBaseUrl(params.baseUrl); const url = new URL(`${baseUrl}/api/v1/events`); - if (params.account) url.searchParams.set("account", params.account); + if (params.account) { + url.searchParams.set("account", params.account); + } const fetchImpl = resolveFetch(); if (!fetchImpl) { @@ -138,7 +141,9 @@ export async function streamSignalEvents(params: { let currentEvent: SignalSseEvent = {}; const flushEvent = () => { - if (!currentEvent.data && !currentEvent.event && !currentEvent.id) return; + if (!currentEvent.data && !currentEvent.event && !currentEvent.id) { + return; + } params.onEvent({ event: currentEvent.event, data: currentEvent.data, @@ -149,13 +154,17 @@ export async function streamSignalEvents(params: { while (true) { const { value, done } = await reader.read(); - if (done) break; + if (done) { + break; + } buffer += decoder.decode(value, { stream: true }); let lineEnd = buffer.indexOf("\n"); while (lineEnd !== -1) { let line = buffer.slice(0, lineEnd); buffer = buffer.slice(lineEnd + 1); - if (line.endsWith("\r")) line = line.slice(0, -1); + if (line.endsWith("\r")) { + line = line.slice(0, -1); + } if (line === "") { flushEvent(); diff --git a/src/signal/daemon.test.ts b/src/signal/daemon.test.ts index 396713ef4..b83208654 100644 --- a/src/signal/daemon.test.ts +++ b/src/signal/daemon.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { classifySignalCliLogLine } from "./daemon.js"; describe("classifySignalCliLogLine", () => { diff --git a/src/signal/daemon.ts b/src/signal/daemon.ts index ca1b01b60..7f1311a9c 100644 --- a/src/signal/daemon.ts +++ b/src/signal/daemon.ts @@ -20,11 +20,17 @@ export type SignalDaemonHandle = { export function classifySignalCliLogLine(line: string): "log" | "error" | null { const trimmed = line.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } // signal-cli commonly writes all logs to stderr; treat severity explicitly. - if (/\b(ERROR|WARN|WARNING)\b/.test(trimmed)) return "error"; + if (/\b(ERROR|WARN|WARNING)\b/.test(trimmed)) { + return "error"; + } // Some signal-cli failures are not tagged with WARN/ERROR but should still be surfaced loudly. - if (/\b(FAILED|SEVERE|EXCEPTION)\b/i.test(trimmed)) return "error"; + if (/\b(FAILED|SEVERE|EXCEPTION)\b/i.test(trimmed)) { + return "error"; + } return "log"; } @@ -40,9 +46,15 @@ function buildDaemonArgs(opts: SignalDaemonOpts): string[] { if (opts.receiveMode) { args.push("--receive-mode", opts.receiveMode); } - if (opts.ignoreAttachments) args.push("--ignore-attachments"); - if (opts.ignoreStories) args.push("--ignore-stories"); - if (opts.sendReadReceipts) args.push("--send-read-receipts"); + if (opts.ignoreAttachments) { + args.push("--ignore-attachments"); + } + if (opts.ignoreStories) { + args.push("--ignore-stories"); + } + if (opts.sendReadReceipts) { + args.push("--send-read-receipts"); + } return args; } @@ -58,15 +70,21 @@ export function spawnSignalDaemon(opts: SignalDaemonOpts): SignalDaemonHandle { child.stdout?.on("data", (data) => { for (const line of data.toString().split(/\r?\n/)) { const kind = classifySignalCliLogLine(line); - if (kind === "log") log(`signal-cli: ${line.trim()}`); - else if (kind === "error") error(`signal-cli: ${line.trim()}`); + if (kind === "log") { + log(`signal-cli: ${line.trim()}`); + } else if (kind === "error") { + error(`signal-cli: ${line.trim()}`); + } } }); child.stderr?.on("data", (data) => { for (const line of data.toString().split(/\r?\n/)) { const kind = classifySignalCliLogLine(line); - if (kind === "log") log(`signal-cli: ${line.trim()}`); - else if (kind === "error") error(`signal-cli: ${line.trim()}`); + if (kind === "log") { + log(`signal-cli: ${line.trim()}`); + } else if (kind === "error") { + error(`signal-cli: ${line.trim()}`); + } } }); child.on("error", (err) => { diff --git a/src/signal/format.test.ts b/src/signal/format.test.ts index 7c66e3013..40e509fa8 100644 --- a/src/signal/format.test.ts +++ b/src/signal/format.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { markdownToSignalText } from "./format.js"; describe("markdownToSignalText", () => { diff --git a/src/signal/format.ts b/src/signal/format.ts index 127884e89..f310b75a6 100644 --- a/src/signal/format.ts +++ b/src/signal/format.ts @@ -1,10 +1,10 @@ +import type { MarkdownTableMode } from "../config/types.base.js"; import { chunkMarkdownIR, markdownToIR, type MarkdownIR, type MarkdownStyle, } from "../markdown/ir.js"; -import type { MarkdownTableMode } from "../config/types.base.js"; type SignalTextStyle = "BOLD" | "ITALIC" | "STRIKETHROUGH" | "MONOSPACE" | "SPOILER"; @@ -53,9 +53,13 @@ function mapStyle(style: MarkdownStyle): SignalTextStyle | null { } function mergeStyles(styles: SignalTextStyleRange[]): SignalTextStyleRange[] { - const sorted = [...styles].sort((a, b) => { - if (a.start !== b.start) return a.start - b.start; - if (a.length !== b.length) return a.length - b.length; + const sorted = [...styles].toSorted((a, b) => { + if (a.start !== b.start) { + return a.start - b.start; + } + if (a.length !== b.length) { + return a.length - b.length; + } return a.style.localeCompare(b.style); }); @@ -80,7 +84,9 @@ function clampStyles(styles: SignalTextStyleRange[], maxLength: number): SignalT const start = Math.max(0, Math.min(style.start, maxLength)); const end = Math.min(style.start + style.length, maxLength); const length = end - start; - if (length > 0) clamped.push({ start, length, style: style.style }); + if (length > 0) { + clamped.push({ start, length, style: style.style }); + } } return clamped; } @@ -89,8 +95,10 @@ function applyInsertionsToStyles( spans: SignalStyleSpan[], insertions: Insertion[], ): SignalStyleSpan[] { - if (insertions.length === 0) return spans; - const sortedInsertions = [...insertions].sort((a, b) => a.pos - b.pos); + if (insertions.length === 0) { + return spans; + } + const sortedInsertions = [...insertions].toSorted((a, b) => a.pos - b.pos); let updated = spans; for (const insertion of sortedInsertions) { @@ -135,15 +143,19 @@ function applyInsertionsToStyles( function renderSignalText(ir: MarkdownIR): SignalFormattedText { const text = ir.text ?? ""; - if (!text) return { text: "", styles: [] }; + if (!text) { + return { text: "", styles: [] }; + } - const sortedLinks = [...ir.links].sort((a, b) => a.start - b.start); + const sortedLinks = [...ir.links].toSorted((a, b) => a.start - b.start); let out = ""; let cursor = 0; const insertions: Insertion[] = []; for (const link of sortedLinks) { - if (link.start < cursor) continue; + if (link.start < cursor) { + continue; + } out += text.slice(cursor, link.end); const href = link.href.trim(); @@ -170,7 +182,9 @@ function renderSignalText(ir: MarkdownIR): SignalFormattedText { const mappedStyles: SignalStyleSpan[] = ir.styles .map((span) => { const mapped = mapStyle(span.style); - if (!mapped) return null; + if (!mapped) { + return null; + } return { start: span.start, end: span.end, style: mapped }; }) .filter((span): span is SignalStyleSpan => span !== null); diff --git a/src/signal/identity.ts b/src/signal/identity.ts index ca833943d..95d27e042 100644 --- a/src/signal/identity.ts +++ b/src/signal/identity.ts @@ -17,7 +17,9 @@ function looksLikeUuid(value: string): boolean { return true; } const compact = value.replace(/-/g, ""); - if (!/^[0-9a-f]+$/i.test(compact)) return false; + if (!/^[0-9a-f]+$/i.test(compact)) { + return false; + } return /[a-f]/i.test(compact); } @@ -69,14 +71,20 @@ export function resolveSignalPeerId(sender: SignalSender): string { function parseSignalAllowEntry(entry: string): SignalAllowEntry | null { const trimmed = entry.trim(); - if (!trimmed) return null; - if (trimmed === "*") return { kind: "any" }; + if (!trimmed) { + return null; + } + if (trimmed === "*") { + return { kind: "any" }; + } const stripped = stripSignalPrefix(trimmed); const lower = stripped.toLowerCase(); if (lower.startsWith("uuid:")) { const raw = stripped.slice("uuid:".length).trim(); - if (!raw) return null; + if (!raw) { + return null; + } return { kind: "uuid", raw }; } @@ -88,11 +96,15 @@ function parseSignalAllowEntry(entry: string): SignalAllowEntry | null { } export function isSignalSenderAllowed(sender: SignalSender, allowFrom: string[]): boolean { - if (allowFrom.length === 0) return false; + if (allowFrom.length === 0) { + return false; + } const parsed = allowFrom .map(parseSignalAllowEntry) .filter((entry): entry is SignalAllowEntry => entry !== null); - if (parsed.some((entry) => entry.kind === "any")) return true; + if (parsed.some((entry) => entry.kind === "any")) { + return true; + } return parsed.some((entry) => { if (entry.kind === "phone" && sender.kind === "phone") { return entry.e164 === sender.e164; @@ -110,8 +122,14 @@ export function isSignalGroupAllowed(params: { sender: SignalSender; }): boolean { const { groupPolicy, allowFrom, sender } = params; - if (groupPolicy === "disabled") return false; - if (groupPolicy === "open") return true; - if (allowFrom.length === 0) return false; + if (groupPolicy === "disabled") { + return false; + } + if (groupPolicy === "open") { + return true; + } + if (allowFrom.length === 0) { + return false; + } return isSignalSenderAllowed(sender, allowFrom); } diff --git a/src/signal/monitor.event-handler.sender-prefix.test.ts b/src/signal/monitor.event-handler.sender-prefix.test.ts index bf8ee7136..c53340918 100644 --- a/src/signal/monitor.event-handler.sender-prefix.test.ts +++ b/src/signal/monitor.event-handler.sender-prefix.test.ts @@ -39,7 +39,7 @@ describe("signal event handler sender prefix", () => { }, }, cfg: { - agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } }, + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, channels: { signal: {} }, } as never, baseUrl: "http://localhost", diff --git a/src/signal/monitor.test.ts b/src/signal/monitor.test.ts index 0012166b4..a15956ce1 100644 --- a/src/signal/monitor.test.ts +++ b/src/signal/monitor.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { isSignalGroupAllowed } from "./identity.js"; describe("signal groupPolicy gating", () => { diff --git a/src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts b/src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts index 68b3eebe7..c64f2cd10 100644 --- a/src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts +++ b/src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { resetSystemEventsForTest } from "../infra/system-events.js"; import { monitorSignalProvider } from "./monitor.js"; @@ -35,7 +34,7 @@ vi.mock("../pairing/pairing-store.js", () => ({ })); vi.mock("../config/sessions.js", () => ({ - resolveStorePath: vi.fn(() => "/tmp/moltbot-sessions.json"), + resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), readSessionUpdatedAt: vi.fn(() => undefined), recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined), diff --git a/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index 5d2760513..5d29bee6d 100644 --- a/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - +import type { OpenClawConfig } from "../config/config.js"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import type { MoltbotConfig } from "../config/config.js"; import { peekSystemEvents, resetSystemEventsForTest } from "../infra/system-events.js"; import { resolveAgentRoute } from "../routing/resolve-route.js"; import { normalizeE164 } from "../utils.js"; @@ -39,7 +38,7 @@ vi.mock("../pairing/pairing-store.js", () => ({ })); vi.mock("../config/sessions.js", () => ({ - resolveStorePath: vi.fn(() => "/tmp/moltbot-sessions.json"), + resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), readSessionUpdatedAt: vi.fn(() => undefined), recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined), @@ -412,7 +411,7 @@ describe("monitorSignalProvider tool results", () => { await flush(); const route = resolveAgentRoute({ - cfg: config as MoltbotConfig, + cfg: config as OpenClawConfig, channel: "signal", accountId: "default", peer: { kind: "dm", id: normalizeE164("+15550001111") }, @@ -468,7 +467,7 @@ describe("monitorSignalProvider tool results", () => { await flush(); const route = resolveAgentRoute({ - cfg: config as MoltbotConfig, + cfg: config as OpenClawConfig, channel: "signal", accountId: "default", peer: { kind: "dm", id: normalizeE164("+15550001111") }, diff --git a/src/signal/monitor.ts b/src/signal/monitor.ts index e143fdac6..aabe0021b 100644 --- a/src/signal/monitor.ts +++ b/src/signal/monitor.ts @@ -1,13 +1,13 @@ +import type { ReplyPayload } from "../auto-reply/types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { SignalReactionNotificationMode } from "../config/types.js"; +import type { RuntimeEnv } from "../runtime.js"; import { chunkTextWithMode, resolveChunkMode, resolveTextChunkLimit } from "../auto-reply/chunk.js"; import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "../auto-reply/reply/history.js"; -import type { ReplyPayload } from "../auto-reply/types.js"; -import type { MoltbotConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; -import type { SignalReactionNotificationMode } from "../config/types.js"; -import { saveMediaBuffer } from "../media/store.js"; -import type { RuntimeEnv } from "../runtime.js"; -import { normalizeE164 } from "../utils.js"; import { waitForTransportReady } from "../infra/transport-ready.js"; +import { saveMediaBuffer } from "../media/store.js"; +import { normalizeE164 } from "../utils.js"; import { resolveSignalAccount } from "./accounts.js"; import { signalCheck, signalRpcRequest } from "./client.js"; import { spawnSignalDaemon } from "./daemon.js"; @@ -40,7 +40,7 @@ export type MonitorSignalOpts = { abortSignal?: AbortSignal; account?: string; accountId?: string; - config?: MoltbotConfig; + config?: OpenClawConfig; baseUrl?: string; autoStart?: boolean; startupTimeoutMs?: number; @@ -95,7 +95,9 @@ function resolveSignalReactionTargets(reaction: SignalReactionMessage): SignalRe function isSignalReactionMessage( reaction: SignalReactionMessage | null | undefined, ): reaction is SignalReactionMessage { - if (!reaction) return false; + if (!reaction) { + return false; + } const emoji = reaction.emoji?.trim(); const timestamp = reaction.targetSentTimestamp; const hasTarget = Boolean(reaction.targetAuthor?.trim() || reaction.targetAuthorUuid?.trim()); @@ -111,10 +113,14 @@ function shouldEmitSignalReactionNotification(params: { }) { const { mode, account, targets, sender, allowlist } = params; const effectiveMode = mode ?? "own"; - if (effectiveMode === "off") return false; + if (effectiveMode === "off") { + return false; + } if (effectiveMode === "own") { const accountId = account?.trim(); - if (!accountId || !targets || targets.length === 0) return false; + if (!accountId || !targets || targets.length === 0) { + return false; + } const normalizedAccount = normalizeE164(accountId); return targets.some((target) => { if (target.kind === "uuid") { @@ -124,7 +130,9 @@ function shouldEmitSignalReactionNotification(params: { }); } if (effectiveMode === "allowlist") { - if (!sender || !allowlist || allowlist.length === 0) return false; + if (!sender || !allowlist || allowlist.length === 0) { + return false; + } return isSignalSenderAllowed(sender, allowlist); } return true; @@ -160,7 +168,9 @@ async function waitForSignalDaemonReady(params: { runtime: params.runtime, check: async () => { const res = await signalCheck(params.baseUrl, 1000); - if (res.ok) return { ok: true }; + if (res.ok) { + return { ok: true }; + } return { ok: false, error: res.error ?? (res.status ? `HTTP ${res.status}` : "unreachable"), @@ -178,7 +188,9 @@ async function fetchAttachment(params: { maxBytes: number; }): Promise<{ path: string; contentType?: string } | null> { const { attachment } = params; - if (!attachment?.id) return null; + if (!attachment?.id) { + return null; + } if (attachment.size && attachment.size > params.maxBytes) { throw new Error( `Signal attachment ${attachment.id} exceeds ${(params.maxBytes / (1024 * 1024)).toFixed(0)}MB limit`, @@ -187,15 +199,23 @@ async function fetchAttachment(params: { const rpcParams: Record = { id: attachment.id, }; - if (params.account) rpcParams.account = params.account; - if (params.groupId) rpcParams.groupId = params.groupId; - else if (params.sender) rpcParams.recipient = params.sender; - else return null; + if (params.account) { + rpcParams.account = params.account; + } + if (params.groupId) { + rpcParams.groupId = params.groupId; + } else if (params.sender) { + rpcParams.recipient = params.sender; + } else { + return null; + } const result = await signalRpcRequest<{ data?: string }>("getAttachment", rpcParams, { baseUrl: params.baseUrl, }); - if (!result?.data) return null; + if (!result?.data) { + return null; + } const buffer = Buffer.from(result.data, "base64"); const saved = await saveMediaBuffer( buffer, @@ -222,7 +242,9 @@ async function deliverReplies(params: { for (const payload of replies) { const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const text = payload.text ?? ""; - if (!text && mediaList.length === 0) continue; + if (!text && mediaList.length === 0) { + continue; + } if (mediaList.length === 0) { for (const chunk of chunkTextWithMode(text, textLimit, chunkMode)) { await sendMessageSignal(target, chunk, { @@ -367,7 +389,9 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi }, }); } catch (err) { - if (opts.abortSignal?.aborted) return; + if (opts.abortSignal?.aborted) { + return; + } throw err; } finally { opts.abortSignal?.removeEventListener("abort", onAbort); diff --git a/src/signal/monitor/event-handler.inbound-contract.test.ts b/src/signal/monitor/event-handler.inbound-contract.test.ts index d073357ff..f2a833bc7 100644 --- a/src/signal/monitor/event-handler.inbound-contract.test.ts +++ b/src/signal/monitor/event-handler.inbound-contract.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import type { MsgContext } from "../../auto-reply/templating.js"; import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js"; diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index 72195ff78..34e21dc41 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -1,5 +1,7 @@ +import type { SignalEventHandlerDeps, SignalReceivePayload } from "./event-handler.types.js"; import { resolveHumanDelayConfig } from "../../agents/identity.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; +import { dispatchInboundMessage } from "../../auto-reply/dispatch.js"; import { formatInboundEnvelope, formatInboundFromLabel, @@ -9,13 +11,13 @@ import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js"; -import { dispatchInboundMessage } from "../../auto-reply/dispatch.js"; import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, } from "../../auto-reply/reply/history.js"; import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js"; +import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { logInboundDrop, logTypingFailure } from "../../channels/logging.js"; import { createReplyPrefixContext } from "../../channels/reply-prefix.js"; import { recordInboundSession } from "../../channels/session.js"; @@ -31,7 +33,6 @@ import { } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { normalizeE164 } from "../../utils.js"; -import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { formatSignalPairingIdLine, formatSignalSenderDisplay, @@ -43,8 +44,6 @@ import { } from "../identity.js"; import { sendMessageSignal, sendReadReceiptSignal, sendTypingSignal } from "../send.js"; -import type { SignalEventHandlerDeps, SignalReceivePayload } from "./event-handler.types.js"; - export function createSignalEventHandler(deps: SignalEventHandlerDeps) { const inboundDebounceMs = resolveInboundDebounceMs({ cfg: deps.cfg, channel: "signal" }); @@ -176,7 +175,9 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { const typingCallbacks = createTypingCallbacks({ start: async () => { - if (!ctxPayload.To) return; + if (!ctxPayload.To) { + return; + } await sendTypingSignal(ctxPayload.To, { baseUrl: deps.baseUrl, account: deps.account, @@ -252,17 +253,25 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { debounceMs: inboundDebounceMs, buildKey: (entry) => { const conversationId = entry.isGroup ? (entry.groupId ?? "unknown") : entry.senderPeerId; - if (!conversationId || !entry.senderPeerId) return null; + if (!conversationId || !entry.senderPeerId) { + return null; + } return `signal:${deps.accountId}:${conversationId}:${entry.senderPeerId}`; }, shouldDebounce: (entry) => { - if (!entry.bodyText.trim()) return false; - if (entry.mediaPath || entry.mediaType) return false; + if (!entry.bodyText.trim()) { + return false; + } + if (entry.mediaPath || entry.mediaType) { + return false; + } return !hasControlCommand(entry.bodyText, deps.cfg); }, onFlush: async (entries) => { const last = entries.at(-1); - if (!last) return; + if (!last) { + return; + } if (entries.length === 1) { await handleSignalInboundMessage(last); return; @@ -271,7 +280,9 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { .map((entry) => entry.bodyText) .filter(Boolean) .join("\\n"); - if (!combinedText.trim()) return; + if (!combinedText.trim()) { + return; + } await handleSignalInboundMessage({ ...last, bodyText: combinedText, @@ -285,7 +296,9 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { }); return async (event: { event?: string; data?: string }) => { - if (event.event !== "receive" || !event.data) return; + if (event.event !== "receive" || !event.data) { + return; + } let payload: SignalReceivePayload | null = null; try { @@ -298,13 +311,21 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { deps.runtime.error?.(`receive exception: ${payload.exception.message}`); } const envelope = payload?.envelope; - if (!envelope) return; - if (envelope.syncMessage) return; + if (!envelope) { + return; + } + if (envelope.syncMessage) { + return; + } const sender = resolveSignalSender(envelope); - if (!sender) return; + if (!sender) { + return; + } if (deps.account && sender.kind === "phone") { - if (sender.e164 === normalizeE164(deps.account)) return; + if (sender.e164 === normalizeE164(deps.account)) { + return; + } } const dataMessage = envelope.dataMessage ?? envelope.editMessage?.dataMessage; @@ -319,7 +340,9 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { Boolean(messageText || quoteText) || Boolean(!reaction && dataMessage?.attachments?.length); if (reaction && !hasBodyContent) { - if (reaction.isRemove) return; // Ignore reaction removals + if (reaction.isRemove) { + return; + } // Ignore reaction removals const emojiLabel = reaction.emoji?.trim() || "emoji"; const senderDisplay = formatSignalSenderDisplay(sender); const senderName = envelope.sourceName ?? senderDisplay; @@ -332,7 +355,9 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { sender, allowlist: deps.reactionAllowlist, }); - if (!shouldNotify) return; + if (!shouldNotify) { + return; + } const groupId = reaction.groupInfo?.groupId ?? undefined; const groupName = reaction.groupInfo?.groupName ?? undefined; @@ -373,13 +398,17 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { enqueueSystemEvent(text, { sessionKey: route.sessionKey, contextKey }); return; } - if (!dataMessage) return; + if (!dataMessage) { + return; + } const senderDisplay = formatSignalSenderDisplay(sender); const senderRecipient = resolveSignalRecipient(sender); const senderPeerId = resolveSignalPeerId(sender); const senderAllowId = formatSignalSenderId(sender); - if (!senderRecipient) return; + if (!senderRecipient) { + return; + } const senderIdLine = formatSignalPairingIdLine(sender); const groupId = dataMessage.groupInfo?.groupId ?? undefined; const groupName = dataMessage.groupInfo?.groupName ?? undefined; @@ -391,7 +420,9 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { deps.dmPolicy === "open" ? true : isSignalSenderAllowed(sender, effectiveDmAllow); if (!isGroup) { - if (deps.dmPolicy === "disabled") return; + if (deps.dmPolicy === "disabled") { + return; + } if (!dmAllowed) { if (deps.dmPolicy === "pairing") { const senderId = senderAllowId; @@ -490,11 +521,16 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { } const kind = mediaKindFromMime(mediaType ?? undefined); - if (kind) placeholder = ``; - else if (dataMessage.attachments?.length) placeholder = ""; + if (kind) { + placeholder = ``; + } else if (dataMessage.attachments?.length) { + placeholder = ""; + } const bodyText = messageText || placeholder || dataMessage.quote?.text?.trim() || ""; - if (!bodyText) return; + if (!bodyText) { + return; + } const receiptTimestamp = typeof envelope.timestamp === "number" diff --git a/src/signal/monitor/event-handler.types.ts b/src/signal/monitor/event-handler.types.ts index 1ee0a039e..34b26d876 100644 --- a/src/signal/monitor/event-handler.types.ts +++ b/src/signal/monitor/event-handler.types.ts @@ -1,6 +1,6 @@ import type { HistoryEntry } from "../../auto-reply/reply/history.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import type { DmPolicy, GroupPolicy, SignalReactionNotificationMode } from "../../config/types.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { SignalSender } from "../identity.js"; @@ -60,7 +60,7 @@ export type SignalReceivePayload = { export type SignalEventHandlerDeps = { runtime: RuntimeEnv; - cfg: MoltbotConfig; + cfg: OpenClawConfig; baseUrl: string; account?: string; accountId: string; diff --git a/src/signal/probe.test.ts b/src/signal/probe.test.ts index 5a0c699d0..5b813b859 100644 --- a/src/signal/probe.test.ts +++ b/src/signal/probe.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { probeSignal } from "./probe.js"; const signalCheckMock = vi.fn(); diff --git a/src/signal/probe.ts b/src/signal/probe.ts index b89f07561..9a6238048 100644 --- a/src/signal/probe.ts +++ b/src/signal/probe.ts @@ -9,10 +9,14 @@ export type SignalProbe = { }; function parseSignalVersion(value: unknown): string | null { - if (typeof value === "string" && value.trim()) return value.trim(); + if (typeof value === "string" && value.trim()) { + return value.trim(); + } if (typeof value === "object" && value !== null) { const version = (value as { version?: unknown }).version; - if (typeof version === "string" && version.trim()) return version.trim(); + if (typeof version === "string" && version.trim()) { + return version.trim(); + } } return null; } @@ -36,7 +40,7 @@ export async function probeSignal(baseUrl: string, timeoutMs: number): Promise("version", undefined, { + const version = await signalRpcRequest("version", undefined, { baseUrl, timeoutMs, }); diff --git a/src/signal/reaction-level.ts b/src/signal/reaction-level.ts index 5cc6aac83..5aa14b374 100644 --- a/src/signal/reaction-level.ts +++ b/src/signal/reaction-level.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveSignalAccount } from "./accounts.js"; export type SignalReactionLevel = "off" | "ack" | "minimal" | "extensive"; @@ -23,7 +23,7 @@ export type ResolvedSignalReactionLevel = { * - "extensive": Agent can react liberally */ export function resolveSignalReactionLevel(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string; }): ResolvedSignalReactionLevel { const account = resolveSignalAccount({ diff --git a/src/signal/send-reactions.ts b/src/signal/send-reactions.ts index 0caf606ea..329832932 100644 --- a/src/signal/send-reactions.ts +++ b/src/signal/send-reactions.ts @@ -23,13 +23,17 @@ export type SignalReactionResult = { function normalizeSignalId(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } return trimmed.replace(/^signal:/i, "").trim(); } function normalizeSignalUuid(raw: string): string { const trimmed = normalizeSignalId(raw); - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } if (trimmed.toLowerCase().startsWith("uuid:")) { return trimmed.slice("uuid:".length).trim(); } @@ -44,9 +48,13 @@ function resolveTargetAuthorParams(params: { const candidates = [params.targetAuthor, params.targetAuthorUuid, params.fallback]; for (const candidate of candidates) { const raw = candidate?.trim(); - if (!raw) continue; + if (!raw) { + continue; + } const normalized = normalizeSignalUuid(raw); - if (normalized) return { targetAuthor: normalized }; + if (normalized) { + return { targetAuthor: normalized }; + } } return {}; } @@ -118,9 +126,15 @@ export async function sendReactionSignal( targetTimestamp, ...targetAuthorParams, }; - if (normalizedRecipient) params.recipients = [normalizedRecipient]; - if (groupId) params.groupIds = [groupId]; - if (account) params.account = account; + if (normalizedRecipient) { + params.recipients = [normalizedRecipient]; + } + if (groupId) { + params.groupIds = [groupId]; + } + if (account) { + params.account = account; + } const result = await signalRpcRequest<{ timestamp?: number }>("sendReaction", params, { baseUrl, @@ -179,9 +193,15 @@ export async function removeReactionSignal( remove: true, ...targetAuthorParams, }; - if (normalizedRecipient) params.recipients = [normalizedRecipient]; - if (groupId) params.groupIds = [groupId]; - if (account) params.account = account; + if (normalizedRecipient) { + params.recipients = [normalizedRecipient]; + } + if (groupId) { + params.groupIds = [groupId]; + } + if (account) { + params.account = account; + } const result = await signalRpcRequest<{ timestamp?: number }>("sendReaction", params, { baseUrl, diff --git a/src/signal/send.ts b/src/signal/send.ts index 32ca09094..045c572e9 100644 --- a/src/signal/send.ts +++ b/src/signal/send.ts @@ -34,7 +34,9 @@ type SignalTarget = function parseTarget(raw: string): SignalTarget { let value = raw.trim(); - if (!value) throw new Error("Signal recipient is required"); + if (!value) { + throw new Error("Signal recipient is required"); + } const lower = value.toLowerCase(); if (lower.startsWith("signal:")) { value = value.slice("signal:".length).trim(); @@ -72,15 +74,21 @@ function buildTargetParams( allow: SignalTargetAllowlist, ): SignalTargetParams | null { if (target.type === "recipient") { - if (!allow.recipient) return null; + if (!allow.recipient) { + return null; + } return { recipient: [target.recipient] }; } if (target.type === "group") { - if (!allow.group) return null; + if (!allow.group) { + return null; + } return { groupId: target.groupId }; } if (target.type === "username") { - if (!allow.username) return null; + if (!allow.username) { + return null; + } return { username: [target.username] }; } return null; @@ -139,7 +147,9 @@ export async function sendMessageSignal( let textStyles: SignalTextStyleRange[] = []; const textMode = opts.textMode ?? "markdown"; const maxBytes = (() => { - if (typeof opts.maxBytes === "number") return opts.maxBytes; + if (typeof opts.maxBytes === "number") { + return opts.maxBytes; + } if (typeof accountInfo.config.mediaMaxMb === "number") { return accountInfo.config.mediaMaxMb * 1024 * 1024; } @@ -186,7 +196,9 @@ export async function sendMessageSignal( (style) => `${style.start}:${style.length}:${style.style}`, ); } - if (account) params.account = account; + if (account) { + params.account = account; + } if (attachments && attachments.length > 0) { params.attachments = attachments; } @@ -221,10 +233,16 @@ export async function sendTypingSignal( recipient: true, group: true, }); - if (!targetParams) return false; + if (!targetParams) { + return false; + } const params: Record = { ...targetParams }; - if (account) params.account = account; - if (opts.stop) params.stop = true; + if (account) { + params.account = account; + } + if (opts.stop) { + params.stop = true; + } await signalRpcRequest("sendTyping", params, { baseUrl, timeoutMs: opts.timeoutMs, @@ -237,18 +255,24 @@ export async function sendReadReceiptSignal( targetTimestamp: number, opts: SignalRpcOpts & { type?: SignalReceiptType } = {}, ): Promise { - if (!Number.isFinite(targetTimestamp) || targetTimestamp <= 0) return false; + if (!Number.isFinite(targetTimestamp) || targetTimestamp <= 0) { + return false; + } const { baseUrl, account } = resolveSignalRpcContext(opts); const targetParams = buildTargetParams(parseTarget(to), { recipient: true, }); - if (!targetParams) return false; + if (!targetParams) { + return false; + } const params: Record = { ...targetParams, targetTimestamp, type: opts.type ?? "read", }; - if (account) params.account = account; + if (account) { + params.account = account; + } await signalRpcRequest("sendReceipt", params, { baseUrl, timeoutMs: opts.timeoutMs, diff --git a/src/signal/sse-reconnect.ts b/src/signal/sse-reconnect.ts index 9b333f77b..c6dfd5d8a 100644 --- a/src/signal/sse-reconnect.ts +++ b/src/signal/sse-reconnect.ts @@ -1,7 +1,7 @@ -import { logVerbose, shouldLogVerbose } from "../globals.js"; import type { BackoffPolicy } from "../infra/backoff.js"; -import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; import type { RuntimeEnv } from "../runtime.js"; +import { logVerbose, shouldLogVerbose } from "../globals.js"; +import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; import { type SignalSseEvent, streamSignalEvents } from "./client.js"; const DEFAULT_RECONNECT_POLICY: BackoffPolicy = { @@ -35,7 +35,9 @@ export async function runSignalSseLoop({ let reconnectAttempts = 0; const logReconnectVerbose = (message: string) => { - if (!shouldLogVerbose()) return; + if (!shouldLogVerbose()) { + return; + } logVerbose(message); }; @@ -50,13 +52,17 @@ export async function runSignalSseLoop({ onEvent(event); }, }); - if (abortSignal?.aborted) return; + if (abortSignal?.aborted) { + return; + } reconnectAttempts += 1; const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts); logReconnectVerbose(`Signal SSE stream ended, reconnecting in ${delayMs / 1000}s...`); await sleepWithAbort(delayMs, abortSignal); } catch (err) { - if (abortSignal?.aborted) return; + if (abortSignal?.aborted) { + return; + } runtime.error?.(`Signal SSE stream error: ${String(err)}`); reconnectAttempts += 1; const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts); @@ -64,7 +70,9 @@ export async function runSignalSseLoop({ try { await sleepWithAbort(delayMs, abortSignal); } catch (sleepErr) { - if (abortSignal?.aborted) return; + if (abortSignal?.aborted) { + return; + } throw sleepErr; } } diff --git a/src/slack/accounts.ts b/src/slack/accounts.ts index 1ae42b5aa..f492b2526 100644 --- a/src/slack/accounts.ts +++ b/src/slack/accounts.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SlackAccountConfig } from "../config/types.js"; import { normalizeChatType } from "../channels/chat-type.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; @@ -28,34 +28,42 @@ export type ResolvedSlackAccount = { channels?: SlackAccountConfig["channels"]; }; -function listConfiguredAccountIds(cfg: MoltbotConfig): string[] { +function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = cfg.channels?.slack?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } -export function listSlackAccountIds(cfg: MoltbotConfig): string[] { +export function listSlackAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } -export function resolveDefaultSlackAccountId(cfg: MoltbotConfig): string { +export function resolveDefaultSlackAccountId(cfg: OpenClawConfig): string { const ids = listSlackAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } function resolveAccountConfig( - cfg: MoltbotConfig, + cfg: OpenClawConfig, accountId: string, ): SlackAccountConfig | undefined { const accounts = cfg.channels?.slack?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as SlackAccountConfig | undefined; } -function mergeSlackAccountConfig(cfg: MoltbotConfig, accountId: string): SlackAccountConfig { +function mergeSlackAccountConfig(cfg: OpenClawConfig, accountId: string): SlackAccountConfig { const { accounts: _ignored, ...base } = (cfg.channels?.slack ?? {}) as SlackAccountConfig & { accounts?: unknown; }; @@ -64,7 +72,7 @@ function mergeSlackAccountConfig(cfg: MoltbotConfig, accountId: string): SlackAc } export function resolveSlackAccount(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; }): ResolvedSlackAccount { const accountId = normalizeAccountId(params.accountId); @@ -105,7 +113,7 @@ export function resolveSlackAccount(params: { }; } -export function listEnabledSlackAccounts(cfg: MoltbotConfig): ResolvedSlackAccount[] { +export function listEnabledSlackAccounts(cfg: OpenClawConfig): ResolvedSlackAccount[] { return listSlackAccountIds(cfg) .map((accountId) => resolveSlackAccount({ cfg, accountId })) .filter((account) => account.enabled); diff --git a/src/slack/actions.read.test.ts b/src/slack/actions.read.test.ts index cfd72d61c..af9f61a3f 100644 --- a/src/slack/actions.read.test.ts +++ b/src/slack/actions.read.test.ts @@ -1,7 +1,5 @@ -import { describe, expect, it, vi } from "vitest"; - import type { WebClient } from "@slack/web-api"; - +import { describe, expect, it, vi } from "vitest"; import { readSlackMessages } from "./actions.js"; function createClient() { diff --git a/src/slack/actions.ts b/src/slack/actions.ts index 4ce6b37ca..f6ef345bd 100644 --- a/src/slack/actions.ts +++ b/src/slack/actions.ts @@ -1,5 +1,4 @@ import type { WebClient } from "@slack/web-api"; - import { loadConfig } from "../config/config.js"; import { logVerbose } from "../globals.js"; import { resolveSlackAccount } from "./accounts.js"; @@ -107,13 +106,17 @@ export async function removeOwnSlackReactions( const toRemove = new Set(); for (const reaction of reactions ?? []) { const name = reaction?.name; - if (!name) continue; + if (!name) { + continue; + } const users = reaction?.users ?? []; if (users.includes(userId)) { toRemove.add(name); } } - if (toRemove.size === 0) return []; + if (toRemove.size === 0) { + return []; + } await Promise.all( Array.from(toRemove, (name) => client.reactions.remove({ diff --git a/src/slack/channel-migration.test.ts b/src/slack/channel-migration.test.ts index b2837b554..86cc11542 100644 --- a/src/slack/channel-migration.test.ts +++ b/src/slack/channel-migration.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { migrateSlackChannelConfig } from "./channel-migration.js"; describe("migrateSlackChannelConfig", () => { diff --git a/src/slack/channel-migration.ts b/src/slack/channel-migration.ts index c27769611..09017e061 100644 --- a/src/slack/channel-migration.ts +++ b/src/slack/channel-migration.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { SlackChannelConfig } from "../config/types.slack.js"; import { normalizeAccountId } from "../routing/session-key.js"; @@ -13,15 +13,21 @@ export type SlackChannelMigrationResult = { }; function resolveAccountChannels( - cfg: MoltbotConfig, + cfg: OpenClawConfig, accountId?: string | null, ): { channels?: SlackChannels } { - if (!accountId) return {}; + if (!accountId) { + return {}; + } const normalized = normalizeAccountId(accountId); const accounts = cfg.channels?.slack?.accounts; - if (!accounts || typeof accounts !== "object") return {}; + if (!accounts || typeof accounts !== "object") { + return {}; + } const exact = accounts[normalized]; - if (exact?.channels) return { channels: exact.channels }; + if (exact?.channels) { + return { channels: exact.channels }; + } const matchKey = Object.keys(accounts).find( (key) => key.toLowerCase() === normalized.toLowerCase(), ); @@ -33,17 +39,25 @@ export function migrateSlackChannelsInPlace( oldChannelId: string, newChannelId: string, ): { migrated: boolean; skippedExisting: boolean } { - if (!channels) return { migrated: false, skippedExisting: false }; - if (oldChannelId === newChannelId) return { migrated: false, skippedExisting: false }; - if (!Object.hasOwn(channels, oldChannelId)) return { migrated: false, skippedExisting: false }; - if (Object.hasOwn(channels, newChannelId)) return { migrated: false, skippedExisting: true }; + if (!channels) { + return { migrated: false, skippedExisting: false }; + } + if (oldChannelId === newChannelId) { + return { migrated: false, skippedExisting: false }; + } + if (!Object.hasOwn(channels, oldChannelId)) { + return { migrated: false, skippedExisting: false }; + } + if (Object.hasOwn(channels, newChannelId)) { + return { migrated: false, skippedExisting: true }; + } channels[newChannelId] = channels[oldChannelId]; delete channels[oldChannelId]; return { migrated: true, skippedExisting: false }; } export function migrateSlackChannelConfig(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; oldChannelId: string; newChannelId: string; @@ -63,7 +77,9 @@ export function migrateSlackChannelConfig(params: { migrated = true; scopes.push("account"); } - if (result.skippedExisting) skippedExisting = true; + if (result.skippedExisting) { + skippedExisting = true; + } } const globalChannels = params.cfg.channels?.slack?.channels; @@ -77,7 +93,9 @@ export function migrateSlackChannelConfig(params: { migrated = true; scopes.push("global"); } - if (result.skippedExisting) skippedExisting = true; + if (result.skippedExisting) { + skippedExisting = true; + } } return { migrated, skippedExisting, scopes }; diff --git a/src/slack/directory-live.ts b/src/slack/directory-live.ts index 57da909e0..05387ee2e 100644 --- a/src/slack/directory-live.ts +++ b/src/slack/directory-live.ts @@ -1,8 +1,7 @@ -import { createSlackWebClient } from "./client.js"; - -import type { ChannelDirectoryEntry } from "../channels/plugins/types.js"; import type { DirectoryConfigParams } from "../channels/plugins/directory-config.js"; +import type { ChannelDirectoryEntry } from "../channels/plugins/types.js"; import { resolveSlackAccount } from "./accounts.js"; +import { createSlackWebClient } from "./client.js"; type SlackUser = { id?: string; @@ -47,8 +46,12 @@ function normalizeQuery(value?: string | null): string { function buildUserRank(user: SlackUser): number { let rank = 0; - if (!user.deleted) rank += 2; - if (!user.is_bot && !user.is_app_user) rank += 1; + if (!user.deleted) { + rank += 2; + } + if (!user.is_bot && !user.is_app_user) { + rank += 1; + } return rank; } @@ -60,7 +63,9 @@ export async function listSlackDirectoryPeersLive( params: DirectoryConfigParams, ): Promise { const token = resolveReadToken(params); - if (!token) return []; + if (!token) { + return []; + } const client = createSlackWebClient(token); const query = normalizeQuery(params.query); const members: SlackUser[] = []; @@ -71,7 +76,9 @@ export async function listSlackDirectoryPeersLive( limit: 200, cursor, })) as SlackListUsersResponse; - if (Array.isArray(res.members)) members.push(...res.members); + if (Array.isArray(res.members)) { + members.push(...res.members); + } const next = res.response_metadata?.next_cursor?.trim(); cursor = next ? next : undefined; } while (cursor); @@ -83,14 +90,18 @@ export async function listSlackDirectoryPeersLive( const candidates = [name, handle, email] .map((item) => item?.trim().toLowerCase()) .filter(Boolean); - if (!query) return true; + if (!query) { + return true; + } return candidates.some((candidate) => candidate?.includes(query)); }); const rows = filtered .map((member) => { const id = member.id?.trim(); - if (!id) return null; + if (!id) { + return null; + } const handle = member.name?.trim(); const display = member.profile?.display_name?.trim() || @@ -118,7 +129,9 @@ export async function listSlackDirectoryGroupsLive( params: DirectoryConfigParams, ): Promise { const token = resolveReadToken(params); - if (!token) return []; + if (!token) { + return []; + } const client = createSlackWebClient(token); const query = normalizeQuery(params.query); const channels: SlackChannel[] = []; @@ -131,14 +144,18 @@ export async function listSlackDirectoryGroupsLive( limit: 1000, cursor, })) as SlackListChannelsResponse; - if (Array.isArray(res.channels)) channels.push(...res.channels); + if (Array.isArray(res.channels)) { + channels.push(...res.channels); + } const next = res.response_metadata?.next_cursor?.trim(); cursor = next ? next : undefined; } while (cursor); const filtered = channels.filter((channel) => { const name = channel.name?.trim().toLowerCase(); - if (!query) return true; + if (!query) { + return true; + } return Boolean(name && name.includes(query)); }); @@ -146,7 +163,9 @@ export async function listSlackDirectoryGroupsLive( .map((channel) => { const id = channel.id?.trim(); const name = channel.name?.trim(); - if (!id || !name) return null; + if (!id || !name) { + return null; + } return { kind: "group", id: `channel:${id}`, diff --git a/src/slack/format.test.ts b/src/slack/format.test.ts index 6e090a7d7..7ccda8e87 100644 --- a/src/slack/format.test.ts +++ b/src/slack/format.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { markdownToSlackMrkdwn } from "./format.js"; describe("markdownToSlackMrkdwn", () => { diff --git a/src/slack/format.ts b/src/slack/format.ts index 7f44b5df2..97fbed7d1 100644 --- a/src/slack/format.ts +++ b/src/slack/format.ts @@ -1,5 +1,5 @@ -import { chunkMarkdownIR, markdownToIR, type MarkdownLinkSpan } from "../markdown/ir.js"; import type { MarkdownTableMode } from "../config/types.base.js"; +import { chunkMarkdownIR, markdownToIR, type MarkdownLinkSpan } from "../markdown/ir.js"; import { renderMarkdownWithMarkers } from "../markdown/render.js"; // Escape special characters for Slack mrkdwn format. @@ -11,7 +11,9 @@ function escapeSlackMrkdwnSegment(text: string): string { const SLACK_ANGLE_TOKEN_RE = /<[^>\n]+>/g; function isAllowedSlackAngleToken(token: string): boolean { - if (!token.startsWith("<") || !token.endsWith(">")) return false; + if (!token.startsWith("<") || !token.endsWith(">")) { + return false; + } const inner = token.slice(1, -1); return ( inner.startsWith("@") || @@ -68,13 +70,17 @@ function escapeSlackMrkdwnText(text: string): string { function buildSlackLink(link: MarkdownLinkSpan, text: string) { const href = link.href.trim(); - if (!href) return null; + if (!href) { + return null; + } const label = text.slice(link.start, link.end); const trimmedLabel = label.trim(); const comparableHref = href.startsWith("mailto:") ? href.slice("mailto:".length) : href; const useMarkup = trimmedLabel.length > 0 && trimmedLabel !== href && trimmedLabel !== comparableHref; - if (!useMarkup) return null; + if (!useMarkup) { + return null; + } const safeHref = escapeSlackMrkdwnSegment(href); return { start: link.start, diff --git a/src/slack/http/registry.test.ts b/src/slack/http/registry.test.ts index 9deee5b74..a17c678b7 100644 --- a/src/slack/http/registry.test.ts +++ b/src/slack/http/registry.test.ts @@ -1,6 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { afterEach, describe, expect, it, vi } from "vitest"; - import { handleSlackHttpRequest, normalizeSlackWebhookPath, @@ -23,7 +22,9 @@ describe("registerSlackHttpHandler", () => { const unregisters: Array<() => void> = []; afterEach(() => { - for (const unregister of unregisters.splice(0)) unregister(); + for (const unregister of unregisters.splice(0)) { + unregister(); + } }); it("routes requests to a registered handler", async () => { diff --git a/src/slack/http/registry.ts b/src/slack/http/registry.ts index 630c52fec..dadf8e56c 100644 --- a/src/slack/http/registry.ts +++ b/src/slack/http/registry.ts @@ -16,7 +16,9 @@ const slackHttpRoutes = new Map(); export function normalizeSlackWebhookPath(path?: string | null): string { const trimmed = path?.trim(); - if (!trimmed) return "/slack/events"; + if (!trimmed) { + return "/slack/events"; + } return trimmed.startsWith("/") ? trimmed : `/${trimmed}`; } @@ -39,7 +41,9 @@ export async function handleSlackHttpRequest( ): Promise { const url = new URL(req.url ?? "/", "http://localhost"); const handler = slackHttpRoutes.get(url.pathname); - if (!handler) return false; + if (!handler) { + return false; + } await handler(req, res); return true; } diff --git a/src/slack/monitor.test-helpers.ts b/src/slack/monitor.test-helpers.ts index b81e80b12..b50f871a2 100644 --- a/src/slack/monitor.test-helpers.ts +++ b/src/slack/monitor.test-helpers.ts @@ -1,8 +1,16 @@ -import { vi } from "vitest"; +import { Mock, vi } from "vitest"; type SlackHandler = (args: unknown) => Promise; -const slackTestState = vi.hoisted(() => ({ +const slackTestState: { + config: Record; + sendMock: Mock<(...args: unknown[]) => Promise>; + replyMock: Mock<(...args: unknown[]) => unknown>; + updateLastRouteMock: Mock<(...args: unknown[]) => unknown>; + reactMock: Mock<(...args: unknown[]) => unknown>; + readAllowFromStoreMock: Mock<(...args: unknown[]) => Promise>; + upsertPairingRequestMock: Mock<(...args: unknown[]) => Promise>; +} = vi.hoisted(() => ({ config: {} as Record, sendMock: vi.fn(), replyMock: vi.fn(), @@ -12,7 +20,7 @@ const slackTestState = vi.hoisted(() => ({ upsertPairingRequestMock: vi.fn(), })); -export const getSlackTestState = () => slackTestState; +export const getSlackTestState: () => void = () => slackTestState; export const getSlackHandlers = () => ( @@ -28,7 +36,9 @@ export const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); export async function waitForSlackEvent(name: string) { for (let i = 0; i < 10; i += 1) { - if (getSlackHandlers()?.has(name)) return; + if (getSlackHandlers()?.has(name)) { + return; + } await flush(); } } @@ -94,7 +104,7 @@ vi.mock("../pairing/pairing-store.js", () => ({ })); vi.mock("../config/sessions.js", () => ({ - resolveStorePath: vi.fn(() => "/tmp/moltbot-sessions.json"), + resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => slackTestState.updateLastRouteMock(...args), resolveSessionKey: vi.fn(), readSessionUpdatedAt: vi.fn(() => undefined), diff --git a/src/slack/monitor.test.ts b/src/slack/monitor.test.ts index e4aa58192..9a1e5e991 100644 --- a/src/slack/monitor.test.ts +++ b/src/slack/monitor.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { buildSlackSlashCommandMatcher, isSlackChannelAllowedByPolicy, @@ -159,16 +158,16 @@ describe("resolveSlackThreadTs", () => { describe("buildSlackSlashCommandMatcher", () => { it("matches with or without a leading slash", () => { - const matcher = buildSlackSlashCommandMatcher("clawd"); + const matcher = buildSlackSlashCommandMatcher("openclaw"); - expect(matcher.test("clawd")).toBe(true); - expect(matcher.test("/clawd")).toBe(true); + expect(matcher.test("openclaw")).toBe(true); + expect(matcher.test("/openclaw")).toBe(true); }); it("does not match similar names", () => { - const matcher = buildSlackSlashCommandMatcher("clawd"); + const matcher = buildSlackSlashCommandMatcher("openclaw"); - expect(matcher.test("/clawd-bot")).toBe(false); - expect(matcher.test("clawd-bot")).toBe(false); + expect(matcher.test("/openclaw-bot")).toBe(false); + expect(matcher.test("openclaw-bot")).toBe(false); }); }); diff --git a/src/slack/monitor.threading.missing-thread-ts.test.ts b/src/slack/monitor.threading.missing-thread-ts.test.ts index de75b993e..31b95b0de 100644 --- a/src/slack/monitor.threading.missing-thread-ts.test.ts +++ b/src/slack/monitor.threading.missing-thread-ts.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { monitorSlackProvider } from "./monitor.js"; @@ -51,7 +50,7 @@ vi.mock("../pairing/pairing-store.js", () => ({ })); vi.mock("../config/sessions.js", () => ({ - resolveStorePath: vi.fn(() => "/tmp/moltbot-sessions.json"), + resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), resolveSessionKey: vi.fn(), readSessionUpdatedAt: vi.fn(() => undefined), @@ -106,7 +105,9 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); async function waitForEvent(name: string) { for (let i = 0; i < 10; i += 1) { - if (getSlackHandlers()?.has(name)) return; + if (getSlackHandlers()?.has(name)) { + return; + } await flush(); } } @@ -137,7 +138,9 @@ describe("monitorSlackProvider threading", () => { replyMock.mockResolvedValue({ text: "thread reply" }); const client = getSlackClient(); - if (!client) throw new Error("Slack client not registered"); + if (!client) { + throw new Error("Slack client not registered"); + } const conversations = client.conversations as { history: ReturnType; }; @@ -154,7 +157,9 @@ describe("monitorSlackProvider threading", () => { await waitForEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { diff --git a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts index c4b4b96ed..1906c7478 100644 --- a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts +++ b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { monitorSlackProvider } from "./monitor.js"; import { defaultSlackTestConfig, flush, @@ -10,7 +10,6 @@ import { resetSlackTestState, waitForSlackEvent, } from "./monitor.test-helpers.js"; -import { monitorSlackProvider } from "./monitor.js"; const slackTestState = getSlackTestState(); const { sendMock, replyMock, reactMock, upsertPairingRequestMock } = slackTestState; @@ -46,7 +45,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -70,7 +71,9 @@ describe("monitorSlackProvider tool results", () => { it("reacts to mention-gated room messages when ackReaction is enabled", async () => { replyMock.mockResolvedValue(undefined); const client = getSlackClient(); - if (!client) throw new Error("Slack client not registered"); + if (!client) { + throw new Error("Slack client not registered"); + } const conversations = client.conversations as { info: ReturnType; }; @@ -87,7 +90,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -132,7 +137,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -180,7 +187,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } const baseEvent = { type: "message", diff --git a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index ce7015399..eae8fad0e 100644 --- a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; +import { monitorSlackProvider } from "./monitor.js"; import { defaultSlackTestConfig, flush, @@ -12,7 +12,6 @@ import { resetSlackTestState, waitForSlackEvent, } from "./monitor.test-helpers.js"; -import { monitorSlackProvider } from "./monitor.js"; const slackTestState = getSlackTestState(); const { sendMock, replyMock } = slackTestState; @@ -35,7 +34,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -58,7 +59,9 @@ describe("monitorSlackProvider tool results", () => { it("drops events with mismatched api_app_id", async () => { const client = getSlackClient(); - if (!client) throw new Error("Slack client not registered"); + if (!client) { + throw new Error("Slack client not registered"); + } (client.auth as { test: ReturnType }).test.mockResolvedValue({ user_id: "bot-user", team_id: "T1", @@ -74,7 +77,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ body: { api_app_id: "A2", team_id: "T1" }, @@ -137,7 +142,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -185,7 +192,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -248,7 +257,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -311,7 +322,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -351,7 +364,7 @@ describe("monitorSlackProvider tool results", () => { slackTestState.config = { messages: { responsePrefix: "PFX", - groupChat: { mentionPatterns: ["\\bclawd\\b"] }, + groupChat: { mentionPatterns: ["\\bopenclaw\\b"] }, }, channels: { slack: { @@ -371,13 +384,15 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { type: "message", user: "U1", - text: "clawd: hello", + text: "openclaw: hello", ts: "123", channel: "C1", channel_type: "channel", @@ -392,11 +407,11 @@ describe("monitorSlackProvider tool results", () => { expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); }); - it("skips channel messages when another user is explicitly mentioned", async () => { + it("accepts channel messages when mentionPatterns match even if another user is mentioned", async () => { slackTestState.config = { messages: { responsePrefix: "PFX", - groupChat: { mentionPatterns: ["\\bclawd\\b"] }, + groupChat: { mentionPatterns: ["\\bopenclaw\\b"] }, }, channels: { slack: { @@ -416,13 +431,15 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { type: "message", user: "U1", - text: "clawd: hello <@U2>", + text: "openclaw: hello <@U2>", ts: "123", channel: "C1", channel_type: "channel", @@ -433,8 +450,8 @@ describe("monitorSlackProvider tool results", () => { controller.abort(); await run; - expect(replyMock).not.toHaveBeenCalled(); - expect(sendMock).not.toHaveBeenCalled(); + expect(replyMock).toHaveBeenCalledTimes(1); + expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); }); it("treats replies to bot threads as implicit mentions", async () => { @@ -457,7 +474,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -501,7 +520,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -535,7 +556,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -581,7 +604,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { diff --git a/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts b/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts index 6825eb4a1..c0143355c 100644 --- a/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts +++ b/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from "vitest"; - import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { monitorSlackProvider } from "./monitor.js"; import { defaultSlackTestConfig, flush, @@ -10,7 +10,6 @@ import { resetSlackTestState, waitForSlackEvent, } from "./monitor.test-helpers.js"; -import { monitorSlackProvider } from "./monitor.js"; const slackTestState = getSlackTestState(); const { sendMock, replyMock } = slackTestState; @@ -46,7 +45,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -79,7 +80,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -130,7 +133,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -191,7 +196,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -257,7 +264,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -309,7 +318,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { @@ -355,7 +366,9 @@ describe("monitorSlackProvider tool results", () => { await waitForSlackEvent("message"); const handler = getSlackHandlers()?.get("message"); - if (!handler) throw new Error("Slack message handler not registered"); + if (!handler) { + throw new Error("Slack message handler not registered"); + } await handler({ event: { diff --git a/src/slack/monitor/allow-list.ts b/src/slack/monitor/allow-list.ts index 42b31b31d..7cae54205 100644 --- a/src/slack/monitor/allow-list.ts +++ b/src/slack/monitor/allow-list.ts @@ -2,7 +2,9 @@ import type { AllowlistMatch } from "../../channels/allowlist-match.js"; export function normalizeSlackSlug(raw?: string) { const trimmed = raw?.trim().toLowerCase() ?? ""; - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } const dashed = trimmed.replace(/\s+/g, "-"); const cleaned = dashed.replace(/[^a-z0-9#@._+-]+/g, "-"); return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, ""); @@ -26,7 +28,9 @@ export function resolveSlackAllowListMatch(params: { name?: string; }): SlackAllowListMatch { const allowList = params.allowList; - if (allowList.length === 0) return { allowed: false }; + if (allowList.length === 0) { + return { allowed: false }; + } if (allowList.includes("*")) { return { allowed: true, matchKey: "*", matchSource: "wildcard" }; } @@ -42,7 +46,9 @@ export function resolveSlackAllowListMatch(params: { { value: slug, source: "slug" }, ]; for (const candidate of candidates) { - if (!candidate.value) continue; + if (!candidate.value) { + continue; + } if (allowList.includes(candidate.value)) { return { allowed: true, @@ -64,7 +70,9 @@ export function resolveSlackUserAllowed(params: { userName?: string; }) { const allowList = normalizeAllowListLower(params.allowList); - if (allowList.length === 0) return true; + if (allowList.length === 0) { + return true; + } return allowListMatches({ allowList, id: params.userId, diff --git a/src/slack/monitor/auth.ts b/src/slack/monitor/auth.ts index c4a3700e0..2bfbbed59 100644 --- a/src/slack/monitor/auth.ts +++ b/src/slack/monitor/auth.ts @@ -1,7 +1,6 @@ -import { readChannelAllowFromStore } from "../../pairing/pairing-store.js"; - -import { allowListMatches, normalizeAllowList, normalizeAllowListLower } from "./allow-list.js"; import type { SlackMonitorContext } from "./context.js"; +import { readChannelAllowFromStore } from "../../pairing/pairing-store.js"; +import { allowListMatches, normalizeAllowList, normalizeAllowListLower } from "./allow-list.js"; export async function resolveSlackEffectiveAllowFrom(ctx: SlackMonitorContext) { const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []); diff --git a/src/slack/monitor/channel-config.test.ts b/src/slack/monitor/channel-config.test.ts index d090d8ac5..9303605a9 100644 --- a/src/slack/monitor/channel-config.test.ts +++ b/src/slack/monitor/channel-config.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { resolveSlackChannelConfig } from "./channel-config.js"; describe("resolveSlackChannelConfig", () => { diff --git a/src/slack/monitor/channel-config.ts b/src/slack/monitor/channel-config.ts index 3e3c541c9..6d35cb1ae 100644 --- a/src/slack/monitor/channel-config.ts +++ b/src/slack/monitor/channel-config.ts @@ -21,7 +21,9 @@ export type SlackChannelConfigResolved = { function firstDefined(...values: Array) { for (const value of values) { - if (typeof value !== "undefined") return value; + if (typeof value !== "undefined") { + return value; + } } return undefined; } @@ -36,13 +38,19 @@ export function shouldEmitSlackReactionNotification(params: { }) { const { mode, botId, messageAuthorId, userId, userName, allowlist } = params; const effectiveMode = mode ?? "own"; - if (effectiveMode === "off") return false; + if (effectiveMode === "off") { + return false; + } if (effectiveMode === "own") { - if (!botId || !messageAuthorId) return false; + if (!botId || !messageAuthorId) { + return false; + } return messageAuthorId === botId; } if (effectiveMode === "allowlist") { - if (!Array.isArray(allowlist) || allowlist.length === 0) return false; + if (!Array.isArray(allowlist) || allowlist.length === 0) { + return false; + } const users = normalizeAllowListLower(allowlist); return allowListMatches({ allowList: users, diff --git a/src/slack/monitor/commands.ts b/src/slack/monitor/commands.ts index 8c738410e..f26be177d 100644 --- a/src/slack/monitor/commands.ts +++ b/src/slack/monitor/commands.ts @@ -7,8 +7,8 @@ export function normalizeSlackSlashCommandName(raw: string) { export function resolveSlackSlashCommandConfig( raw?: SlackSlashCommandConfig, ): Required { - const normalizedName = normalizeSlackSlashCommandName(raw?.name?.trim() || "clawd"); - const name = normalizedName || "clawd"; + const normalizedName = normalizeSlackSlashCommandName(raw?.name?.trim() || "openclaw"); + const name = normalizedName || "openclaw"; return { enabled: raw?.enabled === true, name, @@ -18,6 +18,7 @@ export function resolveSlackSlashCommandConfig( } export function buildSlackSlashCommandMatcher(name: string) { - const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const normalized = normalizeSlackSlashCommandName(name); + const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return new RegExp(`^/?${escaped}$`); } diff --git a/src/slack/monitor/context.test.ts b/src/slack/monitor/context.test.ts index 1d6f5ec62..0afde2346 100644 --- a/src/slack/monitor/context.test.ts +++ b/src/slack/monitor/context.test.ts @@ -1,12 +1,11 @@ import type { App } from "@slack/bolt"; import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; import { createSlackMonitorContext, normalizeSlackChannelType } from "./context.js"; const baseParams = () => ({ - cfg: {} as MoltbotConfig, + cfg: {} as OpenClawConfig, accountId: "default", botToken: "token", app: { client: {} } as App, @@ -30,7 +29,7 @@ const baseParams = () => ({ replyToMode: "off" as const, slashCommand: { enabled: false, - name: "clawd", + name: "openclaw", sessionPrefix: "slack:slash", ephemeral: true, }, diff --git a/src/slack/monitor/context.ts b/src/slack/monitor/context.ts index 876c22ae6..57f5fbc25 100644 --- a/src/slack/monitor/context.ts +++ b/src/slack/monitor/context.ts @@ -1,15 +1,14 @@ import type { App } from "@slack/bolt"; import type { HistoryEntry } from "../../auto-reply/reply/history.js"; -import type { MoltbotConfig, SlackReactionNotificationMode } from "../../config/config.js"; -import { resolveSessionKey, type SessionScope } from "../../config/sessions.js"; +import type { OpenClawConfig, SlackReactionNotificationMode } from "../../config/config.js"; import type { DmPolicy, GroupPolicy } from "../../config/types.js"; -import { logVerbose } from "../../globals.js"; -import { createDedupeCache } from "../../infra/dedupe.js"; -import { getChildLogger } from "../../logging.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { SlackMessageEvent } from "../types.js"; import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; - +import { resolveSessionKey, type SessionScope } from "../../config/sessions.js"; +import { logVerbose } from "../../globals.js"; +import { createDedupeCache } from "../../infra/dedupe.js"; +import { getChildLogger } from "../../logging.js"; import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js"; import { resolveSlackChannelConfig } from "./channel-config.js"; import { isSlackChannelAllowedByPolicy } from "./policy.js"; @@ -18,10 +17,18 @@ export function inferSlackChannelType( channelId?: string | null, ): SlackMessageEvent["channel_type"] | undefined { const trimmed = channelId?.trim(); - if (!trimmed) return undefined; - if (trimmed.startsWith("D")) return "im"; - if (trimmed.startsWith("C")) return "channel"; - if (trimmed.startsWith("G")) return "group"; + if (!trimmed) { + return undefined; + } + if (trimmed.startsWith("D")) { + return "im"; + } + if (trimmed.startsWith("C")) { + return "channel"; + } + if (trimmed.startsWith("G")) { + return "group"; + } return undefined; } @@ -42,7 +49,7 @@ export function normalizeSlackChannelType( } export type SlackMonitorContext = { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId: string; botToken: string; app: App; @@ -115,7 +122,7 @@ export type SlackMonitorContext = { }; export function createSlackMonitorContext(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId: string; botToken: string; app: App; @@ -169,7 +176,9 @@ export function createSlackMonitorContext(params: { const defaultRequireMention = params.defaultRequireMention ?? true; const markMessageSeen = (channelId: string | undefined, ts?: string) => { - if (!channelId || !ts) return false; + if (!channelId || !ts) { + return false; + } return seenMessages.check(`${channelId}:${ts}`); }; @@ -178,7 +187,9 @@ export function createSlackMonitorContext(params: { channelType?: string | null; }) => { const channelId = p.channelId?.trim() ?? ""; - if (!channelId) return params.mainKey; + if (!channelId) { + return params.mainKey; + } const channelType = normalizeSlackChannelType(p.channelType, channelId); const isDirectMessage = channelType === "im"; const isGroup = channelType === "mpim"; @@ -197,7 +208,9 @@ export function createSlackMonitorContext(params: { const resolveChannelName = async (channelId: string) => { const cached = channelCache.get(channelId); - if (cached) return cached; + if (cached) { + return cached; + } try { const info = await params.app.client.conversations.info({ token: params.botToken, @@ -227,7 +240,9 @@ export function createSlackMonitorContext(params: { const resolveUserName = async (userId: string) => { const cached = userCache.get(userId); - if (cached) return cached; + if (cached) { + return cached; + } try { const info = await params.app.client.users.info({ token: params.botToken, @@ -248,7 +263,9 @@ export function createSlackMonitorContext(params: { threadTs?: string; status: string; }) => { - if (!p.threadTs) return; + if (!p.threadTs) { + return; + } const payload = { token: params.botToken, channel_id: p.channelId, @@ -286,8 +303,12 @@ export function createSlackMonitorContext(params: { const isGroupDm = channelType === "mpim"; const isRoom = channelType === "channel" || channelType === "group"; - if (isDirectMessage && !params.dmEnabled) return false; - if (isGroupDm && !params.groupDmEnabled) return false; + if (isDirectMessage && !params.dmEnabled) { + return false; + } + if (isGroupDm && !params.groupDmEnabled) { + return false; + } if (isGroupDm && groupDmChannels.length > 0) { const allowList = normalizeAllowListLower(groupDmChannels); @@ -301,7 +322,9 @@ export function createSlackMonitorContext(params: { .map((value) => value.toLowerCase()); const permitted = allowList.includes("*") || candidates.some((candidate) => allowList.includes(candidate)); - if (!permitted) return false; + if (!permitted) { + return false; + } } if (isRoom && p.channelId) { @@ -342,7 +365,9 @@ export function createSlackMonitorContext(params: { }; const shouldDropMismatchedSlackEvent = (body: unknown) => { - if (!body || typeof body !== "object") return false; + if (!body || typeof body !== "object") { + return false; + } const raw = body as { api_app_id?: unknown; team_id?: unknown }; const incomingApiAppId = typeof raw.api_app_id === "string" ? raw.api_app_id : ""; const incomingTeamId = typeof raw.team_id === "string" ? raw.team_id : ""; diff --git a/src/slack/monitor/events.ts b/src/slack/monitor/events.ts index a4130d67b..90ad3e16f 100644 --- a/src/slack/monitor/events.ts +++ b/src/slack/monitor/events.ts @@ -1,12 +1,11 @@ import type { ResolvedSlackAccount } from "../accounts.js"; - import type { SlackMonitorContext } from "./context.js"; +import type { SlackMessageHandler } from "./message-handler.js"; import { registerSlackChannelEvents } from "./events/channels.js"; import { registerSlackMemberEvents } from "./events/members.js"; import { registerSlackMessageEvents } from "./events/messages.js"; import { registerSlackPinEvents } from "./events/pins.js"; import { registerSlackReactionEvents } from "./events/reactions.js"; -import type { SlackMessageHandler } from "./message-handler.js"; export function registerSlackMonitorEvents(params: { ctx: SlackMonitorContext; diff --git a/src/slack/monitor/events/channels.ts b/src/slack/monitor/events/channels.ts index 8d9fb9e59..94492da24 100644 --- a/src/slack/monitor/events/channels.ts +++ b/src/slack/monitor/events/channels.ts @@ -1,18 +1,16 @@ import type { SlackEventMiddlewareArgs } from "@slack/bolt"; - -import { loadConfig, writeConfigFile } from "../../../config/config.js"; -import { resolveChannelConfigWrites } from "../../../channels/plugins/config-writes.js"; -import { danger, warn } from "../../../globals.js"; -import { enqueueSystemEvent } from "../../../infra/system-events.js"; - -import { resolveSlackChannelLabel } from "../channel-config.js"; import type { SlackMonitorContext } from "../context.js"; -import { migrateSlackChannelConfig } from "../../channel-migration.js"; import type { SlackChannelCreatedEvent, SlackChannelIdChangedEvent, SlackChannelRenamedEvent, } from "../types.js"; +import { resolveChannelConfigWrites } from "../../../channels/plugins/config-writes.js"; +import { loadConfig, writeConfigFile } from "../../../config/config.js"; +import { danger, warn } from "../../../globals.js"; +import { enqueueSystemEvent } from "../../../infra/system-events.js"; +import { migrateSlackChannelConfig } from "../../channel-migration.js"; +import { resolveSlackChannelLabel } from "../channel-config.js"; export function registerSlackChannelEvents(params: { ctx: SlackMonitorContext }) { const { ctx } = params; @@ -21,7 +19,9 @@ export function registerSlackChannelEvents(params: { ctx: SlackMonitorContext }) "channel_created", async ({ event, body }: SlackEventMiddlewareArgs<"channel_created">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const payload = event as SlackChannelCreatedEvent; const channelId = payload.channel?.id; @@ -54,7 +54,9 @@ export function registerSlackChannelEvents(params: { ctx: SlackMonitorContext }) "channel_rename", async ({ event, body }: SlackEventMiddlewareArgs<"channel_rename">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const payload = event as SlackChannelRenamedEvent; const channelId = payload.channel?.id; @@ -87,12 +89,16 @@ export function registerSlackChannelEvents(params: { ctx: SlackMonitorContext }) "channel_id_changed", async ({ event, body }: SlackEventMiddlewareArgs<"channel_id_changed">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const payload = event as SlackChannelIdChangedEvent; const oldChannelId = payload.old_channel_id; const newChannelId = payload.new_channel_id; - if (!oldChannelId || !newChannelId) return; + if (!oldChannelId || !newChannelId) { + return; + } const channelInfo = await ctx.resolveChannelName(newChannelId); const label = resolveSlackChannelLabel({ diff --git a/src/slack/monitor/events/members.ts b/src/slack/monitor/events/members.ts index c26e815ee..cf7b5b03e 100644 --- a/src/slack/monitor/events/members.ts +++ b/src/slack/monitor/events/members.ts @@ -1,11 +1,9 @@ import type { SlackEventMiddlewareArgs } from "@slack/bolt"; - -import { danger } from "../../../globals.js"; -import { enqueueSystemEvent } from "../../../infra/system-events.js"; - -import { resolveSlackChannelLabel } from "../channel-config.js"; import type { SlackMonitorContext } from "../context.js"; import type { SlackMemberChannelEvent } from "../types.js"; +import { danger } from "../../../globals.js"; +import { enqueueSystemEvent } from "../../../infra/system-events.js"; +import { resolveSlackChannelLabel } from "../channel-config.js"; export function registerSlackMemberEvents(params: { ctx: SlackMonitorContext }) { const { ctx } = params; @@ -14,7 +12,9 @@ export function registerSlackMemberEvents(params: { ctx: SlackMonitorContext }) "member_joined_channel", async ({ event, body }: SlackEventMiddlewareArgs<"member_joined_channel">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const payload = event as SlackMemberChannelEvent; const channelId = payload.channel; const channelInfo = channelId ? await ctx.resolveChannelName(channelId) : {}; @@ -52,7 +52,9 @@ export function registerSlackMemberEvents(params: { ctx: SlackMonitorContext }) "member_left_channel", async ({ event, body }: SlackEventMiddlewareArgs<"member_left_channel">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const payload = event as SlackMemberChannelEvent; const channelId = payload.channel; const channelInfo = channelId ? await ctx.resolveChannelName(channelId) : {}; diff --git a/src/slack/monitor/events/messages.ts b/src/slack/monitor/events/messages.ts index 514f0e0d2..3aacb80c0 100644 --- a/src/slack/monitor/events/messages.ts +++ b/src/slack/monitor/events/messages.ts @@ -1,10 +1,5 @@ import type { SlackEventMiddlewareArgs } from "@slack/bolt"; - -import { danger } from "../../../globals.js"; -import { enqueueSystemEvent } from "../../../infra/system-events.js"; - import type { SlackAppMentionEvent, SlackMessageEvent } from "../../types.js"; -import { resolveSlackChannelLabel } from "../channel-config.js"; import type { SlackMonitorContext } from "../context.js"; import type { SlackMessageHandler } from "../message-handler.js"; import type { @@ -12,6 +7,9 @@ import type { SlackMessageDeletedEvent, SlackThreadBroadcastEvent, } from "../types.js"; +import { danger } from "../../../globals.js"; +import { enqueueSystemEvent } from "../../../infra/system-events.js"; +import { resolveSlackChannelLabel } from "../channel-config.js"; export function registerSlackMessageEvents(params: { ctx: SlackMonitorContext; @@ -21,7 +19,9 @@ export function registerSlackMessageEvents(params: { ctx.app.event("message", async ({ event, body }: SlackEventMiddlewareArgs<"message">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const message = event as SlackMessageEvent; if (message.subtype === "message_changed") { @@ -119,7 +119,9 @@ export function registerSlackMessageEvents(params: { ctx.app.event("app_mention", async ({ event, body }: SlackEventMiddlewareArgs<"app_mention">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const mention = event as SlackAppMentionEvent; await handleSlackMessage(mention as unknown as SlackMessageEvent, { diff --git a/src/slack/monitor/events/pins.ts b/src/slack/monitor/events/pins.ts index 886db4383..c1259179e 100644 --- a/src/slack/monitor/events/pins.ts +++ b/src/slack/monitor/events/pins.ts @@ -1,18 +1,18 @@ import type { SlackEventMiddlewareArgs } from "@slack/bolt"; - -import { danger } from "../../../globals.js"; -import { enqueueSystemEvent } from "../../../infra/system-events.js"; - -import { resolveSlackChannelLabel } from "../channel-config.js"; import type { SlackMonitorContext } from "../context.js"; import type { SlackPinEvent } from "../types.js"; +import { danger } from "../../../globals.js"; +import { enqueueSystemEvent } from "../../../infra/system-events.js"; +import { resolveSlackChannelLabel } from "../channel-config.js"; export function registerSlackPinEvents(params: { ctx: SlackMonitorContext }) { const { ctx } = params; ctx.app.event("pin_added", async ({ event, body }: SlackEventMiddlewareArgs<"pin_added">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const payload = event as SlackPinEvent; const channelId = payload.channel_id; @@ -49,7 +49,9 @@ export function registerSlackPinEvents(params: { ctx: SlackMonitorContext }) { ctx.app.event("pin_removed", async ({ event, body }: SlackEventMiddlewareArgs<"pin_removed">) => { try { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } const payload = event as SlackPinEvent; const channelId = payload.channel_id; diff --git a/src/slack/monitor/events/reactions.ts b/src/slack/monitor/events/reactions.ts index ec859051f..0844fddd8 100644 --- a/src/slack/monitor/events/reactions.ts +++ b/src/slack/monitor/events/reactions.ts @@ -1,11 +1,9 @@ import type { SlackEventMiddlewareArgs } from "@slack/bolt"; - +import type { SlackMonitorContext } from "../context.js"; +import type { SlackReactionEvent } from "../types.js"; import { danger } from "../../../globals.js"; import { enqueueSystemEvent } from "../../../infra/system-events.js"; - import { resolveSlackChannelLabel } from "../channel-config.js"; -import type { SlackMonitorContext } from "../context.js"; -import type { SlackMessageEvent, SlackReactionEvent } from "../types.js"; export function registerSlackReactionEvents(params: { ctx: SlackMonitorContext }) { const { ctx } = params; @@ -13,10 +11,12 @@ export function registerSlackReactionEvents(params: { ctx: SlackMonitorContext } const handleReactionEvent = async (event: SlackReactionEvent, action: string) => { try { const item = event.item; - if (!item || item.type !== "message") return; + if (!item || item.type !== "message") { + return; + } const channelInfo = item.channel ? await ctx.resolveChannelName(item.channel) : {}; - const channelType = channelInfo?.type as SlackMessageEvent["channel_type"]; + const channelType = channelInfo?.type; if ( !ctx.isChannelAllowed({ channelId: item.channel, @@ -54,7 +54,9 @@ export function registerSlackReactionEvents(params: { ctx: SlackMonitorContext } ctx.app.event( "reaction_added", async ({ event, body }: SlackEventMiddlewareArgs<"reaction_added">) => { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } await handleReactionEvent(event as SlackReactionEvent, "added"); }, ); @@ -62,7 +64,9 @@ export function registerSlackReactionEvents(params: { ctx: SlackMonitorContext } ctx.app.event( "reaction_removed", async ({ event, body }: SlackEventMiddlewareArgs<"reaction_removed">) => { - if (ctx.shouldDropMismatchedSlackEvent(body)) return; + if (ctx.shouldDropMismatchedSlackEvent(body)) { + return; + } await handleReactionEvent(event as SlackReactionEvent, "removed"); }, ); diff --git a/src/slack/monitor/media.ts b/src/slack/monitor/media.ts index 2674e2d50..161237edc 100644 --- a/src/slack/monitor/media.ts +++ b/src/slack/monitor/media.ts @@ -1,9 +1,8 @@ import type { WebClient as SlackWebClient } from "@slack/web-api"; - import type { FetchLike } from "../../media/fetch.js"; +import type { SlackFile } from "../types.js"; import { fetchRemoteMedia } from "../../media/fetch.js"; import { saveMediaBuffer } from "../../media/store.js"; -import type { SlackFile } from "../types.js"; /** * Fetches a URL with Authorization header, handling cross-origin redirects. @@ -49,7 +48,9 @@ export async function resolveSlackMedia(params: { const files = params.files ?? []; for (const file of files) { const url = file.url_private_download ?? file.url_private; - if (!url) continue; + if (!url) { + continue; + } try { // Note: We ignore init options because fetchWithSlackAuth handles // redirect behavior specially. fetchRemoteMedia only passes the URL. @@ -63,7 +64,9 @@ export async function resolveSlackMedia(params: { fetchImpl, filePathHint: file.name, }); - if (fetched.buffer.byteLength > params.maxBytes) continue; + if (fetched.buffer.byteLength > params.maxBytes) { + continue; + } const saved = await saveMediaBuffer( fetched.buffer, fetched.contentType ?? file.mimetype, @@ -99,7 +102,9 @@ export async function resolveSlackThreadStarter(params: { }): Promise { const cacheKey = `${params.channelId}:${params.threadTs}`; const cached = THREAD_STARTER_CACHE.get(cacheKey); - if (cached) return cached; + if (cached) { + return cached; + } try { const response = (await params.client.conversations.replies({ channel: params.channelId, @@ -109,7 +114,9 @@ export async function resolveSlackThreadStarter(params: { })) as { messages?: Array<{ text?: string; user?: string; ts?: string; files?: SlackFile[] }> }; const message = response?.messages?.[0]; const text = (message?.text ?? "").trim(); - if (!message || !text) return null; + if (!message || !text) { + return null; + } const starter: SlackThreadStarter = { text, userId: message.user, diff --git a/src/slack/monitor/message-handler.ts b/src/slack/monitor/message-handler.ts index 1ee736496..f87c14ccc 100644 --- a/src/slack/monitor/message-handler.ts +++ b/src/slack/monitor/message-handler.ts @@ -1,11 +1,11 @@ +import type { ResolvedSlackAccount } from "../accounts.js"; +import type { SlackMessageEvent } from "../types.js"; +import type { SlackMonitorContext } from "./context.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js"; -import type { ResolvedSlackAccount } from "../accounts.js"; -import type { SlackMessageEvent } from "../types.js"; -import type { SlackMonitorContext } from "./context.js"; import { dispatchPreparedSlackMessage } from "./message-handler/dispatch.js"; import { prepareSlackMessage } from "./message-handler/prepare.js"; import { createSlackThreadTsResolver } from "./thread-resolution.js"; @@ -30,7 +30,9 @@ export function createSlackMessageHandler(params: { debounceMs, buildKey: (entry) => { const senderId = entry.message.user ?? entry.message.bot_id; - if (!senderId) return null; + if (!senderId) { + return null; + } const messageTs = entry.message.ts ?? entry.message.event_ts; // If Slack flags a thread reply but omits thread_ts, isolate it from root debouncing. const threadKey = entry.message.thread_ts @@ -42,13 +44,19 @@ export function createSlackMessageHandler(params: { }, shouldDebounce: (entry) => { const text = entry.message.text ?? ""; - if (!text.trim()) return false; - if (entry.message.files && entry.message.files.length > 0) return false; + if (!text.trim()) { + return false; + } + if (entry.message.files && entry.message.files.length > 0) { + return false; + } return !hasControlCommand(text, ctx.cfg); }, onFlush: async (entries) => { const last = entries.at(-1); - if (!last) return; + if (!last) { + return; + } const combinedText = entries.length === 1 ? (last.message.text ?? "") @@ -70,7 +78,9 @@ export function createSlackMessageHandler(params: { wasMentioned: combinedMentioned || last.opts.wasMentioned, }, }); - if (!prepared) return; + if (!prepared) { + return; + } if (entries.length > 1) { const ids = entries.map((entry) => entry.message.ts).filter(Boolean) as string[]; if (ids.length > 0) { @@ -87,7 +97,9 @@ export function createSlackMessageHandler(params: { }); return async (message, opts) => { - if (opts.source === "message" && message.type !== "message") return; + if (opts.source === "message" && message.type !== "message") { + return; + } if ( opts.source === "message" && message.subtype && @@ -96,7 +108,9 @@ export function createSlackMessageHandler(params: { ) { return; } - if (ctx.markMessageSeen(message.channel, message.ts)) return; + if (ctx.markMessageSeen(message.channel, message.ts)) { + return; + } const resolvedMessage = await threadTsResolver.resolve({ message, source: opts.source }); await debouncer.enqueue({ message: resolvedMessage, opts }); }; diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index 38b69f049..0028b1c3b 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -1,20 +1,18 @@ +import type { PreparedSlackMessage } from "./types.js"; import { resolveHumanDelayConfig } from "../../../agents/identity.js"; import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js"; import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js"; +import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js"; import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js"; import { logAckFailure, logTypingFailure } from "../../../channels/logging.js"; import { createReplyPrefixContext } from "../../../channels/reply-prefix.js"; import { createTypingCallbacks } from "../../../channels/typing.js"; -import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js"; import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js"; import { removeSlackReaction } from "../../actions.js"; import { resolveSlackThreadTargets } from "../../threading.js"; - import { createSlackReplyDeliveryPlan, deliverReplies } from "../replies.js"; -import type { PreparedSlackMessage } from "./types.js"; - export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessage) { const { ctx, account, message, route } = prepared; const cfg = ctx.cfg; @@ -67,7 +65,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }); }, stop: async () => { - if (!didSetStatus) return; + if (!didSetStatus) { + return; + } didSetStatus = false; await ctx.setSlackThreadStatus({ channelId: message.channel, diff --git a/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts b/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts index 7141dec72..178f12d4d 100644 --- a/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts +++ b/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts @@ -1,11 +1,10 @@ import type { App } from "@slack/bolt"; import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../../../config/config.js"; +import type { OpenClawConfig } from "../../../config/config.js"; import type { RuntimeEnv } from "../../../runtime.js"; -import { expectInboundContextContract } from "../../../../test/helpers/inbound-contract.js"; import type { ResolvedSlackAccount } from "../../accounts.js"; import type { SlackMessageEvent } from "../../types.js"; +import { expectInboundContextContract } from "../../../../test/helpers/inbound-contract.js"; import { createSlackMonitorContext } from "../context.js"; import { prepareSlackMessage } from "./prepare.js"; @@ -14,7 +13,7 @@ describe("slack prepareSlackMessage inbound contract", () => { const slackCtx = createSlackMonitorContext({ cfg: { channels: { slack: { enabled: true } }, - } as MoltbotConfig, + } as OpenClawConfig, accountId: "default", botToken: "token", app: { client: {} } as App, @@ -40,7 +39,7 @@ describe("slack prepareSlackMessage inbound contract", () => { threadInheritParent: false, slashCommand: { enabled: false, - name: "clawd", + name: "openclaw", sessionPrefix: "slack:slash", ephemeral: true, }, @@ -82,7 +81,7 @@ describe("slack prepareSlackMessage inbound contract", () => { const slackCtx = createSlackMonitorContext({ cfg: { channels: { slack: { enabled: true, replyToMode: "all" } }, - } as MoltbotConfig, + } as OpenClawConfig, accountId: "default", botToken: "token", app: { client: {} } as App, @@ -108,7 +107,7 @@ describe("slack prepareSlackMessage inbound contract", () => { threadInheritParent: false, slashCommand: { enabled: false, - name: "clawd", + name: "openclaw", sessionPrefix: "slack:slash", ephemeral: true, }, diff --git a/src/slack/monitor/message-handler/prepare.sender-prefix.test.ts b/src/slack/monitor/message-handler/prepare.sender-prefix.test.ts index e0f51f447..79983a7c8 100644 --- a/src/slack/monitor/message-handler/prepare.sender-prefix.test.ts +++ b/src/slack/monitor/message-handler/prepare.sender-prefix.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import type { SlackMonitorContext } from "../context.js"; import { prepareSlackMessage } from "./prepare.js"; @@ -7,7 +6,7 @@ describe("prepareSlackMessage sender prefix", () => { it("prefixes channel bodies with sender label", async () => { const ctx = { cfg: { - agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } }, + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, channels: { slack: {} }, }, accountId: "default", @@ -40,7 +39,7 @@ describe("prepareSlackMessage sender prefix", () => { replyToMode: "off", threadHistoryScope: "channel", threadInheritParent: false, - slashCommand: { command: "/clawd", enabled: true }, + slashCommand: { command: "/openclaw", enabled: true }, textLimit: 2000, ackReactionScope: "off", mediaMaxBytes: 1000, diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 8a2a9e111..2a9eceea6 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -1,7 +1,10 @@ +import type { FinalizedMsgContext } from "../../../auto-reply/templating.js"; +import type { ResolvedSlackAccount } from "../../accounts.js"; +import type { SlackMessageEvent } from "../../types.js"; +import type { PreparedSlackMessage } from "./types.js"; import { resolveAckReaction } from "../../../agents/identity.js"; import { hasControlCommand } from "../../../auto-reply/command-detection.js"; import { shouldHandleTextCommands } from "../../../auto-reply/commands-registry.js"; -import type { FinalizedMsgContext } from "../../../auto-reply/templating.js"; import { formatInboundEnvelope, formatThreadStarterEnvelope, @@ -16,38 +19,32 @@ import { buildMentionRegexes, matchesMentionWithExplicit, } from "../../../auto-reply/reply/mentions.js"; +import { + shouldAckReaction as shouldAckReactionGate, + type AckReactionScope, +} from "../../../channels/ack-reactions.js"; +import { formatAllowlistMatchMeta } from "../../../channels/allowlist-match.js"; +import { resolveControlCommandGate } from "../../../channels/command-gating.js"; +import { resolveConversationLabel } from "../../../channels/conversation-label.js"; +import { logInboundDrop } from "../../../channels/logging.js"; +import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js"; +import { recordInboundSession } from "../../../channels/session.js"; +import { readSessionUpdatedAt, resolveStorePath } from "../../../config/sessions.js"; import { logVerbose, shouldLogVerbose } from "../../../globals.js"; import { enqueueSystemEvent } from "../../../infra/system-events.js"; import { buildPairingReply } from "../../../pairing/pairing-messages.js"; import { upsertChannelPairingRequest } from "../../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../../../routing/session-key.js"; -import { - shouldAckReaction as shouldAckReactionGate, - type AckReactionScope, -} from "../../../channels/ack-reactions.js"; -import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js"; -import { resolveConversationLabel } from "../../../channels/conversation-label.js"; -import { resolveControlCommandGate } from "../../../channels/command-gating.js"; -import { logInboundDrop } from "../../../channels/logging.js"; -import { formatAllowlistMatchMeta } from "../../../channels/allowlist-match.js"; -import { recordInboundSession } from "../../../channels/session.js"; -import { readSessionUpdatedAt, resolveStorePath } from "../../../config/sessions.js"; - -import type { ResolvedSlackAccount } from "../../accounts.js"; import { reactSlackMessage } from "../../actions.js"; import { sendMessageSlack } from "../../send.js"; -import type { SlackMessageEvent } from "../../types.js"; import { resolveSlackThreadContext } from "../../threading.js"; - import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js"; import { resolveSlackEffectiveAllowFrom } from "../auth.js"; import { resolveSlackChannelConfig } from "../channel-config.js"; import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js"; import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js"; -import type { PreparedSlackMessage } from "./types.js"; - export async function prepareSlackMessage(params: { ctx: SlackMonitorContext; account: ResolvedSlackAccount; @@ -92,7 +89,9 @@ export async function prepareSlackMessage(params: { const isBotMessage = Boolean(message.bot_id); if (isBotMessage) { - if (message.user && ctx.botUserId && message.user === ctx.botUserId) return null; + if (message.user && ctx.botUserId && message.user === ctx.botUserId) { + return null; + } if (!allowBots) { logVerbose(`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`); return null; @@ -337,7 +336,9 @@ export async function prepareSlackMessage(params: { maxBytes: ctx.mediaMaxBytes, }); const rawBody = (message.text ?? "").trim() || media?.placeholder || ""; - if (!rawBody) return null; + if (!rawBody) { + return null; + } const ackReaction = resolveAckReaction(cfg, route.agentId); const ackReactionValue = ackReaction ?? ""; @@ -552,7 +553,9 @@ export async function prepareSlackMessage(params: { }); const replyTarget = ctxPayload.To ?? undefined; - if (!replyTarget) return null; + if (!replyTarget) { + return null; + } if (shouldLogVerbose()) { logVerbose(`slack inbound: channel=${message.channel} from=${slackFrom} preview="${preview}"`); diff --git a/src/slack/monitor/message-handler/types.ts b/src/slack/monitor/message-handler/types.ts index e7b4b0807..8fbf4a939 100644 --- a/src/slack/monitor/message-handler/types.ts +++ b/src/slack/monitor/message-handler/types.ts @@ -1,5 +1,5 @@ -import type { ResolvedAgentRoute } from "../../../routing/resolve-route.js"; import type { FinalizedMsgContext } from "../../../auto-reply/templating.js"; +import type { ResolvedAgentRoute } from "../../../routing/resolve-route.js"; import type { ResolvedSlackAccount } from "../../accounts.js"; import type { SlackMessageEvent } from "../../types.js"; import type { SlackChannelConfigResolved } from "../channel-config.js"; diff --git a/src/slack/monitor/policy.ts b/src/slack/monitor/policy.ts index a4ac37917..fbf1d3a73 100644 --- a/src/slack/monitor/policy.ts +++ b/src/slack/monitor/policy.ts @@ -4,8 +4,14 @@ export function isSlackChannelAllowedByPolicy(params: { channelAllowed: boolean; }): boolean { const { groupPolicy, channelAllowlistConfigured, channelAllowed } = params; - if (groupPolicy === "disabled") return false; - if (groupPolicy === "open") return true; - if (!channelAllowlistConfigured) return false; + if (groupPolicy === "disabled") { + return false; + } + if (groupPolicy === "open") { + return true; + } + if (!channelAllowlistConfigured) { + return false; + } return channelAllowed; } diff --git a/src/slack/monitor/provider.ts b/src/slack/monitor/provider.ts index 366a32a34..ee440d565 100644 --- a/src/slack/monitor/provider.ts +++ b/src/slack/monitor/provider.ts @@ -1,31 +1,26 @@ import type { IncomingMessage, ServerResponse } from "node:http"; - import SlackBolt from "@slack/bolt"; - +import type { SessionScope } from "../../config/sessions.js"; +import type { RuntimeEnv } from "../../runtime.js"; +import type { MonitorSlackOpts } from "./types.js"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.js"; import { mergeAllowlist, summarizeMapping } from "../../channels/allowlists/resolve-utils.js"; import { loadConfig } from "../../config/config.js"; -import type { SessionScope } from "../../config/sessions.js"; -import type { DmPolicy, GroupPolicy } from "../../config/types.js"; import { warn } from "../../globals.js"; import { normalizeMainKey } from "../../routing/session-key.js"; -import type { RuntimeEnv } from "../../runtime.js"; - import { resolveSlackAccount } from "../accounts.js"; +import { resolveSlackWebClientOptions } from "../client.js"; +import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js"; import { resolveSlackChannelAllowlist } from "../resolve-channels.js"; import { resolveSlackUserAllowlist } from "../resolve-users.js"; import { resolveSlackAppToken, resolveSlackBotToken } from "../token.js"; -import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js"; -import { resolveSlackWebClientOptions } from "../client.js"; +import { normalizeAllowList } from "./allow-list.js"; import { resolveSlackSlashCommandConfig } from "./commands.js"; import { createSlackMonitorContext } from "./context.js"; import { registerSlackMonitorEvents } from "./events.js"; import { createSlackMessageHandler } from "./message-handler.js"; import { registerSlackMonitorSlashCommands } from "./slash.js"; -import { normalizeAllowList } from "./allow-list.js"; - -import type { MonitorSlackOpts } from "./types.js"; const slackBoltModule = SlackBolt as typeof import("@slack/bolt") & { default?: typeof import("@slack/bolt"); @@ -37,7 +32,9 @@ const slackBolt = const { App, HTTPReceiver } = slackBolt; function parseApiAppIdFromAppToken(raw?: string) { const token = raw?.trim(); - if (!token) return undefined; + if (!token) { + return undefined; + } const match = /^xapp-\d-([a-z0-9]+)-/i.exec(token); return match?.[1]?.toUpperCase(); } @@ -91,13 +88,13 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const dmConfig = slackCfg.dm; const dmEnabled = dmConfig?.enabled ?? true; - const dmPolicy = (dmConfig?.policy ?? "pairing") as DmPolicy; + const dmPolicy = dmConfig?.policy ?? "pairing"; let allowFrom = dmConfig?.allowFrom; const groupDmEnabled = dmConfig?.groupEnabled ?? false; const groupDmChannels = dmConfig?.groupChannels; let channelsConfig = slackCfg.channels; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const groupPolicy = (slackCfg.groupPolicy ?? defaultGroupPolicy ?? "open") as GroupPolicy; + const groupPolicy = slackCfg.groupPolicy ?? defaultGroupPolicy ?? "open"; if ( slackCfg.groupPolicy === undefined && slackCfg.channels === undefined && @@ -221,7 +218,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { if (resolveToken) { void (async () => { - if (opts.abortSignal?.aborted) return; + if (opts.abortSignal?.aborted) { + return; + } if (channelsConfig && Object.keys(channelsConfig).length > 0) { try { @@ -236,7 +235,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const unresolved: string[] = []; for (const entry of resolved) { const source = channelsConfig?.[entry.input]; - if (!source) continue; + if (!source) { + continue; + } if (!entry.resolved || !entry.id) { unresolved.push(entry.input); continue; @@ -285,12 +286,18 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { if (channelsConfig && Object.keys(channelsConfig).length > 0) { const userEntries = new Set(); for (const channel of Object.values(channelsConfig)) { - if (!channel || typeof channel !== "object") continue; + if (!channel || typeof channel !== "object") { + continue; + } const channelUsers = (channel as { users?: Array }).users; - if (!Array.isArray(channelUsers)) continue; + if (!Array.isArray(channelUsers)) { + continue; + } for (const entry of channelUsers) { const trimmed = String(entry).trim(); - if (trimmed && trimmed !== "*") userEntries.add(trimmed); + if (trimmed && trimmed !== "*") { + userEntries.add(trimmed); + } } } @@ -310,14 +317,20 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const nextChannels = { ...channelsConfig }; for (const [channelKey, channelConfig] of Object.entries(channelsConfig)) { - if (!channelConfig || typeof channelConfig !== "object") continue; + if (!channelConfig || typeof channelConfig !== "object") { + continue; + } const channelUsers = (channelConfig as { users?: Array }).users; - if (!Array.isArray(channelUsers) || channelUsers.length === 0) continue; + if (!Array.isArray(channelUsers) || channelUsers.length === 0) { + continue; + } const additions: string[] = []; for (const entry of channelUsers) { const trimmed = String(entry).trim(); const resolved = resolvedMap.get(trimmed); - if (resolved?.resolved && resolved.id) additions.push(resolved.id); + if (resolved?.resolved && resolved.id) { + additions.push(resolved.id); + } } nextChannels[channelKey] = { ...channelConfig, @@ -338,7 +351,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { } const stopOnAbort = () => { - if (opts.abortSignal?.aborted && slackMode === "socket") void app.stop(); + if (opts.abortSignal?.aborted && slackMode === "socket") { + void app.stop(); + } }; opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true }); @@ -349,7 +364,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { } else { runtime.log?.(`slack http mode listening at ${slackWebhookPath}`); } - if (opts.abortSignal?.aborted) return; + if (opts.abortSignal?.aborted) { + return; + } await new Promise((resolve) => { opts.abortSignal?.addEventListener("abort", () => resolve(), { once: true, diff --git a/src/slack/monitor/replies.ts b/src/slack/monitor/replies.ts index 314be285f..c759ca0b5 100644 --- a/src/slack/monitor/replies.ts +++ b/src/slack/monitor/replies.ts @@ -1,10 +1,10 @@ -import { createReplyReferencePlanner } from "../../auto-reply/reply/reply-reference.js"; -import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js"; import type { ChunkMode } from "../../auto-reply/chunk.js"; -import { chunkMarkdownTextWithMode } from "../../auto-reply/chunk.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import type { MarkdownTableMode } from "../../config/types.base.js"; import type { RuntimeEnv } from "../../runtime.js"; +import { chunkMarkdownTextWithMode } from "../../auto-reply/chunk.js"; +import { createReplyReferencePlanner } from "../../auto-reply/reply/reply-reference.js"; +import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js"; import { markdownToSlackMrkdwnChunks } from "../format.js"; import { sendMessageSlack } from "../send.js"; @@ -21,11 +21,15 @@ export async function deliverReplies(params: { const threadTs = payload.replyToId ?? params.replyThreadTs; const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const text = payload.text ?? ""; - if (!text && mediaList.length === 0) continue; + if (!text && mediaList.length === 0) { + continue; + } if (mediaList.length === 0) { const trimmed = text.trim(); - if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) continue; + if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) { + continue; + } await sendMessageSlack(params.target, trimmed, { token: params.token, threadTs, @@ -131,7 +135,9 @@ export async function deliverSlackSlashReplies(params: { const combined = [text ?? "", ...mediaList.map((url) => url.trim()).filter(Boolean)] .filter(Boolean) .join("\n"); - if (!combined) continue; + if (!combined) { + continue; + } const chunkMode = params.chunkMode ?? "length"; const markdownChunks = chunkMode === "newline" @@ -140,13 +146,17 @@ export async function deliverSlackSlashReplies(params: { const chunks = markdownChunks.flatMap((markdown) => markdownToSlackMrkdwnChunks(markdown, chunkLimit, { tableMode: params.tableMode }), ); - if (!chunks.length && combined) chunks.push(combined); + if (!chunks.length && combined) { + chunks.push(combined); + } for (const chunk of chunks) { messages.push(chunk); } } - if (messages.length === 0) return; + if (messages.length === 0) { + return; + } // Slack slash command responses can be multi-part by sending follow-ups via response_url. const responseType = params.ephemeral ? "ephemeral" : "in_channel"; diff --git a/src/slack/monitor/slash.command-arg-menus.test.ts b/src/slack/monitor/slash.command-arg-menus.test.ts index b7b5011f0..ebf40aeca 100644 --- a/src/slack/monitor/slash.command-arg-menus.test.ts +++ b/src/slack/monitor/slash.command-arg-menus.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { registerSlackMonitorSlashCommands } from "./slash.js"; const dispatchMock = vi.fn(); @@ -68,7 +67,12 @@ function createHarness() { groupPolicy: "open", useAccessGroups: false, channelsConfig: undefined, - slashCommand: { enabled: true, name: "clawd", ephemeral: true, sessionPrefix: "slack:slash" }, + slashCommand: { + enabled: true, + name: "openclaw", + ephemeral: true, + sessionPrefix: "slack:slash", + }, textLimit: 4000, app, isChannelAllowed: () => true, @@ -98,7 +102,9 @@ describe("Slack native command argument menus", () => { registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); const handler = commands.get("/usage"); - if (!handler) throw new Error("Missing /usage handler"); + if (!handler) { + throw new Error("Missing /usage handler"); + } const respond = vi.fn().mockResolvedValue(undefined); const ack = vi.fn().mockResolvedValue(undefined); @@ -126,8 +132,10 @@ describe("Slack native command argument menus", () => { const { actions, ctx, account } = createHarness(); registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); - const handler = actions.get("moltbot_cmdarg"); - if (!handler) throw new Error("Missing arg-menu action handler"); + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } const respond = vi.fn().mockResolvedValue(undefined); await handler({ @@ -152,8 +160,10 @@ describe("Slack native command argument menus", () => { const { actions, ctx, account } = createHarness(); registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); - const handler = actions.get("moltbot_cmdarg"); - if (!handler) throw new Error("Missing arg-menu action handler"); + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } const respond = vi.fn().mockResolvedValue(undefined); await handler({ @@ -180,8 +190,10 @@ describe("Slack native command argument menus", () => { const { actions, postEphemeral, ctx, account } = createHarness(); registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); - const handler = actions.get("moltbot_cmdarg"); - if (!handler) throw new Error("Missing arg-menu action handler"); + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } await handler({ ack: vi.fn().mockResolvedValue(undefined), @@ -202,8 +214,10 @@ describe("Slack native command argument menus", () => { const { actions, postEphemeral, ctx, account } = createHarness(); registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); - const handler = actions.get("moltbot_cmdarg"); - if (!handler) throw new Error("Missing arg-menu action handler"); + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } await handler({ ack: vi.fn().mockResolvedValue(undefined), diff --git a/src/slack/monitor/slash.policy.test.ts b/src/slack/monitor/slash.policy.test.ts index 182e7a015..72606e755 100644 --- a/src/slack/monitor/slash.policy.test.ts +++ b/src/slack/monitor/slash.policy.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { registerSlackMonitorSlashCommands } from "./slash.js"; const dispatchMock = vi.fn(); @@ -61,7 +60,12 @@ function createHarness(overrides?: { groupPolicy: overrides?.groupPolicy ?? "open", useAccessGroups: true, channelsConfig: overrides?.channelsConfig, - slashCommand: { enabled: true, name: "clawd", ephemeral: true, sessionPrefix: "slack:slash" }, + slashCommand: { + enabled: true, + name: "openclaw", + ephemeral: true, + sessionPrefix: "slack:slash", + }, textLimit: 4000, app, isChannelAllowed: () => true, @@ -96,7 +100,9 @@ describe("slack slash commands channel policy", () => { registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); const handler = [...commands.values()][0]; - if (!handler) throw new Error("Missing slash handler"); + if (!handler) { + throw new Error("Missing slash handler"); + } const respond = vi.fn().mockResolvedValue(undefined); await handler({ @@ -128,7 +134,9 @@ describe("slack slash commands channel policy", () => { registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); const handler = [...commands.values()][0]; - if (!handler) throw new Error("Missing slash handler"); + if (!handler) { + throw new Error("Missing slash handler"); + } const respond = vi.fn().mockResolvedValue(undefined); await handler({ @@ -161,7 +169,9 @@ describe("slack slash commands channel policy", () => { registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); const handler = [...commands.values()][0]; - if (!handler) throw new Error("Missing slash handler"); + if (!handler) { + throw new Error("Missing slash handler"); + } const respond = vi.fn().mockResolvedValue(undefined); await handler({ diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index 3534561ab..4519ce638 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -1,7 +1,9 @@ import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt"; import type { ChatCommandDefinition, CommandArgs } from "../../auto-reply/commands-registry.js"; -import { resolveChunkMode } from "../../auto-reply/chunk.js"; +import type { ResolvedSlackAccount } from "../accounts.js"; +import type { SlackMonitorContext } from "./context.js"; import { resolveEffectiveMessagesConfig } from "../../agents/identity.js"; +import { resolveChunkMode } from "../../auto-reply/chunk.js"; import { buildCommandTextFromArgs, findCommandByNativeName, @@ -9,9 +11,12 @@ import { parseCommandArgs, resolveCommandArgMenu, } from "../../auto-reply/commands-registry.js"; -import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js"; -import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js"; import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; +import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js"; +import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js"; +import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; +import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; +import { resolveConversationLabel } from "../../channels/conversation-label.js"; import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js"; import { resolveMarkdownTableMode } from "../../config/markdown-tables.js"; import { danger, logVerbose } from "../../globals.js"; @@ -21,12 +26,6 @@ import { upsertChannelPairingRequest, } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; -import { resolveConversationLabel } from "../../channels/conversation-label.js"; -import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; -import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; - -import type { ResolvedSlackAccount } from "../accounts.js"; - import { normalizeAllowList, normalizeAllowListLower, @@ -35,17 +34,18 @@ import { } from "./allow-list.js"; import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./channel-config.js"; import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js"; -import type { SlackMonitorContext } from "./context.js"; import { isSlackChannelAllowedByPolicy } from "./policy.js"; import { deliverSlackSlashReplies } from "./replies.js"; type SlackBlock = { type: string; [key: string]: unknown }; -const SLACK_COMMAND_ARG_ACTION_ID = "moltbot_cmdarg"; +const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg"; const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg"; function chunkItems(items: T[], size: number): T[][] { - if (size <= 0) return [items]; + if (size <= 0) { + return [items]; + } const rows: T[][] = []; for (let i = 0; i < items.length; i += size) { rows.push(items.slice(i, i + size)); @@ -74,11 +74,17 @@ function parseSlackCommandArgValue(raw?: string | null): { value: string; userId: string; } | null { - if (!raw) return null; + if (!raw) { + return null; + } const parts = raw.split("|"); - if (parts.length !== 5 || parts[0] !== SLACK_COMMAND_ARG_VALUE_PREFIX) return null; + if (parts.length !== 5 || parts[0] !== SLACK_COMMAND_ARG_VALUE_PREFIX) { + return null; + } const [, command, arg, value, userId] = parts; - if (!command || !arg || !value || !userId) return null; + if (!command || !arg || !value || !userId) { + return null; + } const decode = (text: string) => { try { return decodeURIComponent(text); @@ -90,7 +96,9 @@ function parseSlackCommandArgValue(raw?: string | null): { const decodedArg = decode(arg); const decodedValue = decode(value); const decodedUserId = decode(userId); - if (!decodedCommand || !decodedArg || !decodedValue || !decodedUserId) return null; + if (!decodedCommand || !decodedArg || !decodedValue || !decodedUserId) { + return null; + } return { command: decodedCommand, arg: decodedArg, @@ -163,7 +171,9 @@ export function registerSlackMonitorSlashCommands(params: { } await ack(); - if (ctx.botUserId && command.user_id === ctx.botUserId) return; + if (ctx.botUserId && command.user_id === ctx.botUserId) { + return; + } const channelInfo = await ctx.resolveChannelName(command.channel_id); const channelType = @@ -526,70 +536,79 @@ export function registerSlackMonitorSlashCommands(params: { logVerbose("slack: slash commands disabled"); } - if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) return; + if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) { + return; + } - ( - ctx.app as unknown as { action: NonNullable<(typeof ctx.app & { action?: unknown })["action"]> } - ).action(SLACK_COMMAND_ARG_ACTION_ID, async (args: SlackActionMiddlewareArgs) => { - const { ack, body, respond } = args; - const action = args.action as { value?: string }; - await ack(); - const respondFn = - respond ?? - (async (payload: { text: string; blocks?: SlackBlock[]; response_type?: string }) => { - if (!body.channel?.id || !body.user?.id) return; - await ctx.app.client.chat.postEphemeral({ - token: ctx.botToken, - channel: body.channel.id, - user: body.user.id, - text: payload.text, - blocks: payload.blocks, + const registerArgAction = (actionId: string) => { + ( + ctx.app as unknown as { + action: NonNullable<(typeof ctx.app & { action?: unknown })["action"]>; + } + ).action(actionId, async (args: SlackActionMiddlewareArgs) => { + const { ack, body, respond } = args; + const action = args.action as { value?: string }; + await ack(); + const respondFn = + respond ?? + (async (payload: { text: string; blocks?: SlackBlock[]; response_type?: string }) => { + if (!body.channel?.id || !body.user?.id) { + return; + } + await ctx.app.client.chat.postEphemeral({ + token: ctx.botToken, + channel: body.channel.id, + user: body.user.id, + text: payload.text, + blocks: payload.blocks, + }); }); + const parsed = parseSlackCommandArgValue(action?.value); + if (!parsed) { + await respondFn({ + text: "Sorry, that button is no longer valid.", + response_type: "ephemeral", + }); + return; + } + if (body.user?.id && parsed.userId !== body.user.id) { + await respondFn({ + text: "That menu is for another user.", + response_type: "ephemeral", + }); + return; + } + const commandDefinition = findCommandByNativeName(parsed.command, "slack"); + const commandArgs: CommandArgs = { + values: { [parsed.arg]: parsed.value }, + }; + const prompt = commandDefinition + ? buildCommandTextFromArgs(commandDefinition, commandArgs) + : `/${parsed.command} ${parsed.value}`; + const user = body.user; + const userName = + user && "name" in user && user.name + ? user.name + : user && "username" in user && user.username + ? user.username + : (user?.id ?? ""); + const triggerId = "trigger_id" in body ? body.trigger_id : undefined; + const commandPayload = { + user_id: user?.id ?? "", + user_name: userName, + channel_id: body.channel?.id ?? "", + channel_name: body.channel?.name ?? body.channel?.id ?? "", + trigger_id: triggerId ?? String(Date.now()), + } as SlackCommandMiddlewareArgs["command"]; + await handleSlashCommand({ + command: commandPayload, + ack: async () => {}, + respond: respondFn, + prompt, + commandArgs, + commandDefinition: commandDefinition ?? undefined, }); - const parsed = parseSlackCommandArgValue(action?.value); - if (!parsed) { - await respondFn({ - text: "Sorry, that button is no longer valid.", - response_type: "ephemeral", - }); - return; - } - if (body.user?.id && parsed.userId !== body.user.id) { - await respondFn({ - text: "That menu is for another user.", - response_type: "ephemeral", - }); - return; - } - const commandDefinition = findCommandByNativeName(parsed.command, "slack"); - const commandArgs: CommandArgs = { - values: { [parsed.arg]: parsed.value }, - }; - const prompt = commandDefinition - ? buildCommandTextFromArgs(commandDefinition, commandArgs) - : `/${parsed.command} ${parsed.value}`; - const user = body.user; - const userName = - user && "name" in user && user.name - ? user.name - : user && "username" in user && user.username - ? user.username - : (user?.id ?? ""); - const triggerId = "trigger_id" in body ? body.trigger_id : undefined; - const commandPayload = { - user_id: user?.id ?? "", - user_name: userName, - channel_id: body.channel?.id ?? "", - channel_name: body.channel?.name ?? body.channel?.id ?? "", - trigger_id: triggerId ?? String(Date.now()), - } as SlackCommandMiddlewareArgs["command"]; - await handleSlashCommand({ - command: commandPayload, - ack: async () => {}, - respond: respondFn as SlackCommandMiddlewareArgs["respond"], - prompt, - commandArgs, - commandDefinition: commandDefinition ?? undefined, }); - }); + }; + registerArgAction(SLACK_COMMAND_ARG_ACTION_ID); } diff --git a/src/slack/monitor/thread-resolution.test.ts b/src/slack/monitor/thread-resolution.test.ts index e670f1ee6..3016f82d9 100644 --- a/src/slack/monitor/thread-resolution.test.ts +++ b/src/slack/monitor/thread-resolution.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import type { SlackMessageEvent } from "../types.js"; import { createSlackThreadTsResolver } from "./thread-resolution.js"; diff --git a/src/slack/monitor/thread-resolution.ts b/src/slack/monitor/thread-resolution.ts index 3a9306144..87e9978f0 100644 --- a/src/slack/monitor/thread-resolution.ts +++ b/src/slack/monitor/thread-resolution.ts @@ -1,7 +1,6 @@ import type { WebClient as SlackWebClient } from "@slack/web-api"; - -import { logVerbose, shouldLogVerbose } from "../../globals.js"; import type { SlackMessageEvent } from "../types.js"; +import { logVerbose, shouldLogVerbose } from "../../globals.js"; type ThreadTsCacheEntry = { threadTs: string | null; @@ -54,7 +53,9 @@ export function createSlackThreadTsResolver(params: { const getCached = (key: string, now: number) => { const entry = cache.get(key); - if (!entry) return undefined; + if (!entry) { + return undefined; + } if (ttlMs > 0 && now - entry.updatedAt > ttlMs) { cache.delete(key); return undefined; @@ -72,8 +73,10 @@ export function createSlackThreadTsResolver(params: { return; } while (cache.size > maxSize) { - const oldestKey = cache.keys().next().value as string | undefined; - if (!oldestKey) break; + const oldestKey = cache.keys().next().value; + if (!oldestKey) { + break; + } cache.delete(oldestKey); } }; diff --git a/src/slack/monitor/types.ts b/src/slack/monitor/types.ts index 06f72b306..c77bd53f9 100644 --- a/src/slack/monitor/types.ts +++ b/src/slack/monitor/types.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig, SlackSlashCommandConfig } from "../../config/config.js"; +import type { OpenClawConfig, SlackSlashCommandConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { SlackFile, SlackMessageEvent } from "../types.js"; @@ -7,7 +7,7 @@ export type MonitorSlackOpts = { appToken?: string; accountId?: string; mode?: "socket" | "http"; - config?: MoltbotConfig; + config?: OpenClawConfig; runtime?: RuntimeEnv; abortSignal?: AbortSignal; mediaMaxMb?: number; diff --git a/src/slack/probe.ts b/src/slack/probe.ts index a4e880838..cde5e5157 100644 --- a/src/slack/probe.ts +++ b/src/slack/probe.ts @@ -10,13 +10,17 @@ export type SlackProbe = { }; function withTimeout(promise: Promise, timeoutMs: number): Promise { - if (!timeoutMs || timeoutMs <= 0) return promise; + if (!timeoutMs || timeoutMs <= 0) { + return promise; + } let timer: NodeJS.Timeout | null = null; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("timeout")), timeoutMs); }); return Promise.race([promise, timeout]).finally(() => { - if (timer) clearTimeout(timer); + if (timer) { + clearTimeout(timer); + } }); } diff --git a/src/slack/resolve-channels.test.ts b/src/slack/resolve-channels.test.ts index 27ea0f4ed..17e04d80a 100644 --- a/src/slack/resolve-channels.test.ts +++ b/src/slack/resolve-channels.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { resolveSlackChannelAllowlist } from "./resolve-channels.js"; describe("resolveSlackChannelAllowlist", () => { diff --git a/src/slack/resolve-channels.ts b/src/slack/resolve-channels.ts index b9f14bcf0..2112a2a3c 100644 --- a/src/slack/resolve-channels.ts +++ b/src/slack/resolve-channels.ts @@ -1,5 +1,4 @@ import type { WebClient } from "@slack/web-api"; - import { createSlackWebClient } from "./client.js"; export type SlackChannelLookup = { @@ -29,7 +28,9 @@ type SlackListResponse = { function parseSlackChannelMention(raw: string): { id?: string; name?: string } { const trimmed = raw.trim(); - if (!trimmed) return {}; + if (!trimmed) { + return {}; + } const mention = trimmed.match(/^<#([A-Z0-9]+)(?:\|([^>]+))?>$/i); if (mention) { const id = mention[1]?.toUpperCase(); @@ -37,7 +38,9 @@ function parseSlackChannelMention(raw: string): { id?: string; name?: string } { return { id, name }; } const prefixed = trimmed.replace(/^(slack:|channel:)/i, ""); - if (/^[CG][A-Z0-9]+$/i.test(prefixed)) return { id: prefixed.toUpperCase() }; + if (/^[CG][A-Z0-9]+$/i.test(prefixed)) { + return { id: prefixed.toUpperCase() }; + } const name = prefixed.replace(/^#/, "").trim(); return name ? { name } : {}; } @@ -55,7 +58,9 @@ async function listSlackChannels(client: WebClient): Promise channel.name.toLowerCase() === target); - if (matches.length === 0) return undefined; + if (matches.length === 0) { + return undefined; + } const active = matches.find((channel) => !channel.archived); return active ?? matches[0]; } diff --git a/src/slack/resolve-users.ts b/src/slack/resolve-users.ts index a87057b5c..66f101d32 100644 --- a/src/slack/resolve-users.ts +++ b/src/slack/resolve-users.ts @@ -1,5 +1,4 @@ import type { WebClient } from "@slack/web-api"; - import { createSlackWebClient } from "./client.js"; export type SlackUserLookup = { @@ -43,12 +42,20 @@ type SlackListUsersResponse = { function parseSlackUserInput(raw: string): { id?: string; name?: string; email?: string } { const trimmed = raw.trim(); - if (!trimmed) return {}; + if (!trimmed) { + return {}; + } const mention = trimmed.match(/^<@([A-Z0-9]+)>$/i); - if (mention) return { id: mention[1]?.toUpperCase() }; + if (mention) { + return { id: mention[1]?.toUpperCase() }; + } const prefixed = trimmed.replace(/^(slack:|user:)/i, ""); - if (/^[A-Z][A-Z0-9]+$/i.test(prefixed)) return { id: prefixed.toUpperCase() }; - if (trimmed.includes("@") && !trimmed.startsWith("@")) return { email: trimmed.toLowerCase() }; + if (/^[A-Z][A-Z0-9]+$/i.test(prefixed)) { + return { id: prefixed.toUpperCase() }; + } + if (trimmed.includes("@") && !trimmed.startsWith("@")) { + return { email: trimmed.toLowerCase() }; + } const name = trimmed.replace(/^@/, "").trim(); return name ? { name } : {}; } @@ -64,7 +71,9 @@ async function listSlackUsers(client: WebClient): Promise { for (const member of res.members ?? []) { const id = member.id?.trim(); const name = member.name?.trim(); - if (!id || !name) continue; + if (!id || !name) { + continue; + } const profile = member.profile ?? {}; users.push({ id, @@ -85,15 +94,23 @@ async function listSlackUsers(client: WebClient): Promise { function scoreSlackUser(user: SlackUserLookup, match: { name?: string; email?: string }): number { let score = 0; - if (!user.deleted) score += 3; - if (!user.isBot && !user.isAppUser) score += 2; - if (match.email && user.email === match.email) score += 5; + if (!user.deleted) { + score += 3; + } + if (!user.isBot && !user.isAppUser) { + score += 2; + } + if (match.email && user.email === match.email) { + score += 5; + } if (match.name) { const target = match.name.toLowerCase(); const candidates = [user.name, user.displayName, user.realName] .map((value) => value?.toLowerCase()) .filter(Boolean) as string[]; - if (candidates.some((value) => value === target)) score += 2; + if (candidates.some((value) => value === target)) { + score += 2; + } } return score; } @@ -127,7 +144,7 @@ export async function resolveSlackUserAllowlist(params: { if (matches.length > 0) { const scored = matches .map((user) => ({ user, score: scoreSlackUser(user, parsed) })) - .sort((a, b) => b.score - a.score); + .toSorted((a, b) => b.score - a.score); const best = scored[0]?.user ?? matches[0]; results.push({ input, @@ -153,7 +170,7 @@ export async function resolveSlackUserAllowlist(params: { if (matches.length > 0) { const scored = matches .map((user) => ({ user, score: scoreSlackUser(user, parsed) })) - .sort((a, b) => b.score - a.score); + .toSorted((a, b) => b.score - a.score); const best = scored[0]?.user ?? matches[0]; results.push({ input, diff --git a/src/slack/scopes.ts b/src/slack/scopes.ts index 911f0f5b9..7c49ff305 100644 --- a/src/slack/scopes.ts +++ b/src/slack/scopes.ts @@ -1,5 +1,4 @@ import type { WebClient } from "@slack/web-api"; - import { createSlackWebClient } from "./client.js"; export type SlackScopesResult = { @@ -16,23 +15,33 @@ function isRecord(value: unknown): value is Record { } function collectScopes(value: unknown, into: string[]) { - if (!value) return; + if (!value) { + return; + } if (Array.isArray(value)) { for (const entry of value) { - if (typeof entry === "string" && entry.trim()) into.push(entry.trim()); + if (typeof entry === "string" && entry.trim()) { + into.push(entry.trim()); + } } return; } if (typeof value === "string") { const raw = value.trim(); - if (!raw) return; + if (!raw) { + return; + } const parts = raw.split(/[,\s]+/).map((part) => part.trim()); for (const part of parts) { - if (part) into.push(part); + if (part) { + into.push(part); + } } return; } - if (!isRecord(value)) return; + if (!isRecord(value)) { + return; + } for (const entry of Object.values(value)) { if (Array.isArray(entry) || typeof entry === "string") { collectScopes(entry, into); @@ -41,11 +50,13 @@ function collectScopes(value: unknown, into: string[]) { } function normalizeScopes(scopes: string[]) { - return Array.from(new Set(scopes.map((scope) => scope.trim()).filter(Boolean))).sort(); + return Array.from(new Set(scopes.map((scope) => scope.trim()).filter(Boolean))).toSorted(); } function extractScopes(payload: unknown): string[] { - if (!isRecord(payload)) return []; + if (!isRecord(payload)) { + return []; + } const scopes: string[] = []; collectScopes(payload.scopes, scopes); collectScopes(payload.scope, scopes); @@ -59,7 +70,9 @@ function extractScopes(payload: unknown): string[] { } function readError(payload: unknown): string | undefined { - if (!isRecord(payload)) return undefined; + if (!isRecord(payload)) { + return undefined; + } const error = payload.error; return typeof error === "string" && error.trim() ? error.trim() : undefined; } @@ -94,7 +107,9 @@ export async function fetchSlackScopes( return { ok: true, scopes, source: method }; } const error = readError(result); - if (error) errors.push(`${method}: ${error}`); + if (error) { + errors.push(`${method}: ${error}`); + } } return { diff --git a/src/slack/send.ts b/src/slack/send.ts index 31677278a..6bdf4ab2f 100644 --- a/src/slack/send.ts +++ b/src/slack/send.ts @@ -1,18 +1,17 @@ import { type FilesUploadV2Arguments, type WebClient } from "@slack/web-api"; - +import type { SlackTokenSource } from "./accounts.js"; import { chunkMarkdownTextWithMode, resolveChunkMode, resolveTextChunkLimit, } from "../auto-reply/chunk.js"; import { loadConfig } from "../config/config.js"; +import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { logVerbose } from "../globals.js"; import { loadWebMedia } from "../web/media.js"; -import type { SlackTokenSource } from "./accounts.js"; import { resolveSlackAccount } from "./accounts.js"; import { createSlackWebClient } from "./client.js"; import { markdownToSlackMrkdwnChunks } from "./format.js"; -import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { parseSlackTarget } from "./targets.js"; import { resolveSlackBotToken } from "./token.js"; @@ -48,7 +47,9 @@ function resolveToken(params: { fallbackSource?: SlackTokenSource; }) { const explicit = resolveSlackBotToken(params.explicit); - if (explicit) return explicit; + if (explicit) { + return explicit; + } const fallback = resolveSlackBotToken(params.fallbackToken); if (!fallback) { logVerbose( @@ -161,7 +162,9 @@ export async function sendMessageSlack( const chunks = markdownChunks.flatMap((markdown) => markdownToSlackMrkdwnChunks(markdown, chunkLimit, { tableMode }), ); - if (!chunks.length && trimmedMessage) chunks.push(trimmedMessage); + if (!chunks.length && trimmedMessage) { + chunks.push(trimmedMessage); + } const mediaMaxBytes = typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 diff --git a/src/slack/targets.test.ts b/src/slack/targets.test.ts index 5b5cfe849..a15906884 100644 --- a/src/slack/targets.test.ts +++ b/src/slack/targets.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { normalizeSlackMessagingTarget } from "../channels/plugins/normalize/slack.js"; import { parseSlackTarget, resolveSlackChannelId } from "./targets.js"; diff --git a/src/slack/targets.ts b/src/slack/targets.ts index 5701a16e2..7f66a1d5c 100644 --- a/src/slack/targets.ts +++ b/src/slack/targets.ts @@ -18,7 +18,9 @@ export function parseSlackTarget( options: SlackTargetParseOptions = {}, ): SlackTarget | undefined { const trimmed = raw.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i); if (mentionMatch) { return buildMessagingTarget("user", mentionMatch[1], trimmed); diff --git a/src/slack/threading-tool-context.test.ts b/src/slack/threading-tool-context.test.ts index 6adab5dc9..9975a818c 100644 --- a/src/slack/threading-tool-context.test.ts +++ b/src/slack/threading-tool-context.test.ts @@ -1,9 +1,8 @@ import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { buildSlackThreadingToolContext } from "./threading-tool-context.js"; -const emptyCfg = {} as MoltbotConfig; +const emptyCfg = {} as OpenClawConfig; describe("buildSlackThreadingToolContext", () => { it("uses top-level replyToMode by default", () => { @@ -11,7 +10,7 @@ describe("buildSlackThreadingToolContext", () => { channels: { slack: { replyToMode: "first" }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = buildSlackThreadingToolContext({ cfg, accountId: null, @@ -28,7 +27,7 @@ describe("buildSlackThreadingToolContext", () => { replyToModeByChatType: { direct: "all" }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = buildSlackThreadingToolContext({ cfg, accountId: null, @@ -45,7 +44,7 @@ describe("buildSlackThreadingToolContext", () => { replyToModeByChatType: { direct: "all" }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = buildSlackThreadingToolContext({ cfg, accountId: null, @@ -61,7 +60,7 @@ describe("buildSlackThreadingToolContext", () => { replyToMode: "first", }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = buildSlackThreadingToolContext({ cfg, accountId: null, @@ -78,7 +77,7 @@ describe("buildSlackThreadingToolContext", () => { dm: { replyToMode: "all" }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = buildSlackThreadingToolContext({ cfg, accountId: null, @@ -92,7 +91,7 @@ describe("buildSlackThreadingToolContext", () => { channels: { slack: { replyToMode: "off" }, }, - } as MoltbotConfig; + } as OpenClawConfig; const result = buildSlackThreadingToolContext({ cfg, accountId: null, diff --git a/src/slack/threading-tool-context.ts b/src/slack/threading-tool-context.ts index a54d2c3a6..6a8e1b57d 100644 --- a/src/slack/threading-tool-context.ts +++ b/src/slack/threading-tool-context.ts @@ -2,11 +2,11 @@ import type { ChannelThreadingContext, ChannelThreadingToolContext, } from "../channels/plugins/types.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveSlackAccount, resolveSlackReplyToMode } from "./accounts.js"; export function buildSlackThreadingToolContext(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; context: ChannelThreadingContext; hasRepliedRef?: { value: boolean }; diff --git a/src/slack/threading.test.ts b/src/slack/threading.test.ts index 837d3ddbc..a9f107254 100644 --- a/src/slack/threading.test.ts +++ b/src/slack/threading.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { resolveSlackThreadContext, resolveSlackThreadTargets } from "./threading.js"; describe("resolveSlackThreadTargets", () => { diff --git a/src/telegram/accounts.test.ts b/src/telegram/accounts.test.ts index cb756aafb..e04284ca8 100644 --- a/src/telegram/accounts.test.ts +++ b/src/telegram/accounts.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveTelegramAccount } from "./accounts.js"; describe("resolveTelegramAccount", () => { @@ -8,7 +7,7 @@ describe("resolveTelegramAccount", () => { const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = ""; try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { accounts: { work: { botToken: "tok-work" } } }, }, @@ -31,7 +30,7 @@ describe("resolveTelegramAccount", () => { const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = "tok-env"; try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { accounts: { work: { botToken: "tok-work" } } }, }, @@ -54,7 +53,7 @@ describe("resolveTelegramAccount", () => { const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = "tok-env"; try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { botToken: "tok-config" }, }, @@ -77,7 +76,7 @@ describe("resolveTelegramAccount", () => { const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = ""; try { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { accounts: { work: { botToken: "tok-work" } } }, }, diff --git a/src/telegram/accounts.ts b/src/telegram/accounts.ts index 80eb535a3..e985e67c6 100644 --- a/src/telegram/accounts.ts +++ b/src/telegram/accounts.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { TelegramAccountConfig } from "../config/types.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { listBoundAccountIds, resolveDefaultAgentBoundAccountId } from "../routing/bindings.js"; @@ -6,7 +6,7 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.j import { resolveTelegramToken } from "./token.js"; const debugAccounts = (...args: unknown[]) => { - if (isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_TELEGRAM_ACCOUNTS)) { + if (isTruthyEnvValue(process.env.OPENCLAW_DEBUG_TELEGRAM_ACCOUNTS)) { console.warn("[telegram:accounts]", ...args); } }; @@ -20,48 +20,62 @@ export type ResolvedTelegramAccount = { config: TelegramAccountConfig; }; -function listConfiguredAccountIds(cfg: MoltbotConfig): string[] { +function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = cfg.channels?.telegram?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } const ids = new Set(); for (const key of Object.keys(accounts)) { - if (!key) continue; + if (!key) { + continue; + } ids.add(normalizeAccountId(key)); } return [...ids]; } -export function listTelegramAccountIds(cfg: MoltbotConfig): string[] { +export function listTelegramAccountIds(cfg: OpenClawConfig): string[] { const ids = Array.from( new Set([...listConfiguredAccountIds(cfg), ...listBoundAccountIds(cfg, "telegram")]), ); debugAccounts("listTelegramAccountIds", ids); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } -export function resolveDefaultTelegramAccountId(cfg: MoltbotConfig): string { +export function resolveDefaultTelegramAccountId(cfg: OpenClawConfig): string { const boundDefault = resolveDefaultAgentBoundAccountId(cfg, "telegram"); - if (boundDefault) return boundDefault; + if (boundDefault) { + return boundDefault; + } const ids = listTelegramAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } function resolveAccountConfig( - cfg: MoltbotConfig, + cfg: OpenClawConfig, accountId: string, ): TelegramAccountConfig | undefined { const accounts = cfg.channels?.telegram?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } const direct = accounts[accountId] as TelegramAccountConfig | undefined; - if (direct) return direct; + if (direct) { + return direct; + } const normalized = normalizeAccountId(accountId); const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized); return matchKey ? (accounts[matchKey] as TelegramAccountConfig | undefined) : undefined; } -function mergeTelegramAccountConfig(cfg: MoltbotConfig, accountId: string): TelegramAccountConfig { +function mergeTelegramAccountConfig(cfg: OpenClawConfig, accountId: string): TelegramAccountConfig { const { accounts: _ignored, ...base } = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & { accounts?: unknown }; const account = resolveAccountConfig(cfg, accountId) ?? {}; @@ -69,7 +83,7 @@ function mergeTelegramAccountConfig(cfg: MoltbotConfig, accountId: string): Tele } export function resolveTelegramAccount(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; }): ResolvedTelegramAccount { const hasExplicitAccountId = Boolean(params.accountId?.trim()); @@ -97,20 +111,28 @@ export function resolveTelegramAccount(params: { const normalized = normalizeAccountId(params.accountId); const primary = resolve(normalized); - if (hasExplicitAccountId) return primary; - if (primary.tokenSource !== "none") return primary; + if (hasExplicitAccountId) { + return primary; + } + if (primary.tokenSource !== "none") { + return primary; + } // If accountId is omitted, prefer a configured account token over failing on // the implicit "default" account. This keeps env-based setups working while // making config-only tokens work for things like heartbeats. const fallbackId = resolveDefaultTelegramAccountId(params.cfg); - if (fallbackId === primary.accountId) return primary; + if (fallbackId === primary.accountId) { + return primary; + } const fallback = resolve(fallbackId); - if (fallback.tokenSource === "none") return primary; + if (fallback.tokenSource === "none") { + return primary; + } return fallback; } -export function listEnabledTelegramAccounts(cfg: MoltbotConfig): ResolvedTelegramAccount[] { +export function listEnabledTelegramAccounts(cfg: OpenClawConfig): ResolvedTelegramAccount[] { return listTelegramAccountIds(cfg) .map((accountId) => resolveTelegramAccount({ cfg, accountId })) .filter((account) => account.enabled); diff --git a/src/telegram/api-logging.ts b/src/telegram/api-logging.ts index 110fd4e34..6dc2776c2 100644 --- a/src/telegram/api-logging.ts +++ b/src/telegram/api-logging.ts @@ -1,7 +1,7 @@ +import type { RuntimeEnv } from "../runtime.js"; import { danger } from "../globals.js"; import { formatErrorMessage } from "../infra/errors.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import type { RuntimeEnv } from "../runtime.js"; export type TelegramApiLogger = (message: string) => void; @@ -16,8 +16,12 @@ type TelegramApiLoggingParams = { const fallbackLogger = createSubsystemLogger("telegram/api"); function resolveTelegramApiLogger(runtime?: RuntimeEnv, logger?: TelegramApiLogger) { - if (logger) return logger; - if (runtime?.error) return runtime.error; + if (logger) { + return logger; + } + if (runtime?.error) { + return runtime.error; + } return (message: string) => fallbackLogger.error(message); } diff --git a/src/telegram/audit.ts b/src/telegram/audit.ts index eb49f2d99..54a51c6b2 100644 --- a/src/telegram/audit.ts +++ b/src/telegram/audit.ts @@ -57,12 +57,22 @@ export function collectTelegramUnmentionedGroupIds( const groupIds: string[] = []; let unresolvedGroups = 0; for (const [key, value] of Object.entries(groups)) { - if (key === "*") continue; - if (!value || typeof value !== "object") continue; - if ((value as TelegramGroupConfig).enabled === false) continue; - if ((value as TelegramGroupConfig).requireMention !== false) continue; + if (key === "*") { + continue; + } + if (!value || typeof value !== "object") { + continue; + } + if (value.enabled === false) { + continue; + } + if (value.requireMention !== false) { + continue; + } const id = String(key).trim(); - if (!id) continue; + if (!id) { + continue; + } if (/^-?\d+$/.test(id)) { groupIds.push(id); } else { @@ -102,9 +112,9 @@ export async function auditTelegramGroupMembership(params: { const url = `${base}/getChatMember?chat_id=${encodeURIComponent(chatId)}&user_id=${encodeURIComponent(String(params.botId))}`; const res = await fetchWithTimeout(url, params.timeoutMs, fetcher); const json = (await res.json()) as TelegramApiOk<{ status?: string }> | TelegramApiErr; - if (!res.ok || !isRecord(json) || json.ok !== true) { + if (!res.ok || !isRecord(json) || !json.ok) { const desc = - isRecord(json) && json.ok === false && typeof json.description === "string" + isRecord(json) && !json.ok && typeof json.description === "string" ? json.description : `getChatMember failed (${res.status})`; groups.push({ diff --git a/src/telegram/bot-access.ts b/src/telegram/bot-access.ts index 2f55c1f6b..f3ac93b4c 100644 --- a/src/telegram/bot-access.ts +++ b/src/telegram/bot-access.ts @@ -36,7 +36,9 @@ export const normalizeAllowFromWithStore = (params: { export const firstDefined = (...values: Array) => { for (const value of values) { - if (typeof value !== "undefined") return value; + if (typeof value !== "undefined") { + return value; + } } return undefined; }; @@ -47,11 +49,19 @@ export const isSenderAllowed = (params: { senderUsername?: string; }) => { const { allow, senderId, senderUsername } = params; - if (!allow.hasEntries) return true; - if (allow.hasWildcard) return true; - if (senderId && allow.entries.includes(senderId)) return true; + if (!allow.hasEntries) { + return true; + } + if (allow.hasWildcard) { + return true; + } + if (senderId && allow.entries.includes(senderId)) { + return true; + } const username = senderUsername?.toLowerCase(); - if (!username) return false; + if (!username) { + return false; + } return allow.entriesLower.some((entry) => entry === username || entry === `@${username}`); }; @@ -64,12 +74,16 @@ export const resolveSenderAllowMatch = (params: { if (allow.hasWildcard) { return { allowed: true, matchKey: "*", matchSource: "wildcard" }; } - if (!allow.hasEntries) return { allowed: false }; + if (!allow.hasEntries) { + return { allowed: false }; + } if (senderId && allow.entries.includes(senderId)) { return { allowed: true, matchKey: senderId, matchSource: "id" }; } const username = senderUsername?.toLowerCase(); - if (!username) return { allowed: false }; + if (!username) { + return { allowed: false }; + } const entry = allow.entriesLower.find( (candidate) => candidate === username || candidate === `@${username}`, ); diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index 477b98280..a5fcd2d21 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -1,3 +1,5 @@ +import type { TelegramMessage } from "./bot/types.js"; +import { resolveDefaultAgentId } from "../agents/agent-scope.js"; // @ts-nocheck import { hasControlCommand } from "../auto-reply/command-detection.js"; import { @@ -5,22 +7,21 @@ import { resolveInboundDebounceMs, } from "../auto-reply/inbound-debounce.js"; import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js"; -import { buildCommandsMessagePaginated } from "../auto-reply/status.js"; import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; -import { resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { buildCommandsMessagePaginated } from "../auto-reply/status.js"; +import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js"; import { loadConfig } from "../config/config.js"; import { writeConfigFile } from "../config/io.js"; import { danger, logVerbose, warn } from "../globals.js"; -import { resolveMedia } from "./bot/delivery.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { resolveTelegramForumThreadId } from "./bot/helpers.js"; -import type { TelegramMessage } from "./bot/types.js"; import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; +import { RegisterTelegramHandlerParams } from "./bot-native-commands.js"; import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js"; +import { resolveMedia } from "./bot/delivery.js"; +import { resolveTelegramForumThreadId } from "./bot/helpers.js"; import { migrateTelegramGroupConfig } from "./group-migration.js"; import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js"; import { readTelegramAllowFromStore } from "./pairing-store.js"; -import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js"; import { buildInlineKeyboard } from "./send.js"; export const registerTelegramHandlers = ({ @@ -37,7 +38,7 @@ export const registerTelegramHandlers = ({ shouldSkipUpdate, processMessage, logger, -}) => { +}: RegisterTelegramHandlerParams) => { const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000; const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS = 1500; const TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP = 1; @@ -68,14 +69,20 @@ export const registerTelegramHandlers = ({ debounceMs, buildKey: (entry) => entry.debounceKey, shouldDebounce: (entry) => { - if (entry.allMedia.length > 0) return false; + if (entry.allMedia.length > 0) { + return false; + } const text = entry.msg.text ?? entry.msg.caption ?? ""; - if (!text.trim()) return false; + if (!text.trim()) { + return false; + } return !hasControlCommand(text, cfg, { botUsername: entry.botUsername }); }, onFlush: async (entries) => { const last = entries.at(-1); - if (!last) return; + if (!last) { + return; + } if (entries.length === 1) { await processMessage(last.ctx, last.allMedia, last.storeAllowFrom); return; @@ -84,7 +91,9 @@ export const registerTelegramHandlers = ({ .map((entry) => entry.msg.text ?? entry.msg.caption ?? "") .filter(Boolean) .join("\n"); - if (!combinedText.trim()) return; + if (!combinedText.trim()) { + return; + } const first = entries[0]; const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record; const getFile = @@ -146,10 +155,14 @@ export const registerTelegramHandlers = ({ const first = entry.messages[0]; const last = entry.messages.at(-1); - if (!first || !last) return; + if (!first || !last) { + return; + } const combinedText = entry.messages.map((m) => m.msg.text ?? "").join(""); - if (!combinedText.trim()) return; + if (!combinedText.trim()) { + return; + } const syntheticMessage: TelegramMessage = { ...first.msg, @@ -191,8 +204,12 @@ export const registerTelegramHandlers = ({ bot.on("callback_query", async (ctx) => { const callback = ctx.callbackQuery; - if (!callback) return; - if (shouldSkipUpdate(ctx)) return; + if (!callback) { + return; + } + if (shouldSkipUpdate(ctx)) { + return; + } // Answer immediately to prevent Telegram from retrying while we process await withTelegramApiErrorLogging({ operation: "answerCallbackQuery", @@ -202,19 +219,27 @@ export const registerTelegramHandlers = ({ try { const data = (callback.data ?? "").trim(); const callbackMessage = callback.message; - if (!data || !callbackMessage) return; + if (!data || !callbackMessage) { + return; + } const inlineButtonsScope = resolveTelegramInlineButtonsScope({ cfg, accountId, }); - if (inlineButtonsScope === "off") return; + if (inlineButtonsScope === "off") { + return; + } const chatId = callbackMessage.chat.id; const isGroup = callbackMessage.chat.type === "group" || callbackMessage.chat.type === "supergroup"; - if (inlineButtonsScope === "dm" && isGroup) return; - if (inlineButtonsScope === "group" && !isGroup) return; + if (inlineButtonsScope === "dm" && isGroup) { + return; + } + if (inlineButtonsScope === "group" && !isGroup) { + return; + } const messageThreadId = (callbackMessage as { message_thread_id?: number }).message_thread_id; const isForum = (callbackMessage.chat as { is_forum?: boolean }).is_forum === true; @@ -303,7 +328,9 @@ export const registerTelegramHandlers = ({ if (inlineButtonsScope === "allowlist") { if (!isGroup) { - if (dmPolicy === "disabled") return; + if (dmPolicy === "disabled") { + return; + } if (dmPolicy !== "open") { const allowed = effectiveDmAllow.hasWildcard || @@ -313,7 +340,9 @@ export const registerTelegramHandlers = ({ senderId, senderUsername, })); - if (!allowed) return; + if (!allowed) { + return; + } } } else { const allowed = @@ -324,17 +353,23 @@ export const registerTelegramHandlers = ({ senderId, senderUsername, })); - if (!allowed) return; + if (!allowed) { + return; + } } } const paginationMatch = data.match(/^commands_page_(\d+|noop)(?::(.+))?$/); if (paginationMatch) { const pageValue = paginationMatch[1]; - if (pageValue === "noop") return; + if (pageValue === "noop") { + return; + } const page = Number.parseInt(pageValue, 10); - if (Number.isNaN(page) || page < 1) return; + if (Number.isNaN(page) || page < 1) { + return; + } const agentId = paginationMatch[2]?.trim() || resolveDefaultAgentId(cfg) || undefined; const skillCommands = listSkillCommandsForAgents({ @@ -391,8 +426,12 @@ export const registerTelegramHandlers = ({ bot.on("message:migrate_to_chat_id", async (ctx) => { try { const msg = ctx.message; - if (!msg?.migrate_to_chat_id) return; - if (shouldSkipUpdate(ctx)) return; + if (!msg?.migrate_to_chat_id) { + return; + } + if (shouldSkipUpdate(ctx)) { + return; + } const oldChatId = String(msg.chat.id); const newChatId = String(msg.migrate_to_chat_id); @@ -438,8 +477,12 @@ export const registerTelegramHandlers = ({ bot.on("message", async (ctx) => { try { const msg = ctx.message; - if (!msg) return; - if (shouldSkipUpdate(ctx)) return; + if (!msg) { + return; + } + if (shouldSkipUpdate(ctx)) { + return; + } const chatId = msg.chat.id; const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup"; diff --git a/src/telegram/bot-message-context.dm-threads.test.ts b/src/telegram/bot-message-context.dm-threads.test.ts index d710e0b1b..24dc73ad7 100644 --- a/src/telegram/bot-message-context.dm-threads.test.ts +++ b/src/telegram/bot-message-context.dm-threads.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it, vi } from "vitest"; - import { buildTelegramMessageContext } from "./bot-message-context.js"; describe("buildTelegramMessageContext dm thread sessions", () => { const baseConfig = { - agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } }, + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, channels: { telegram: {} }, messages: { groupChat: { mentionPatterns: [] } }, } as never; @@ -73,7 +72,7 @@ describe("buildTelegramMessageContext dm thread sessions", () => { describe("buildTelegramMessageContext group sessions without forum", () => { const baseConfig = { - agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } }, + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, channels: { telegram: {} }, messages: { groupChat: { mentionPatterns: [] } }, } as never; diff --git a/src/telegram/bot-message-context.sender-prefix.test.ts b/src/telegram/bot-message-context.sender-prefix.test.ts index c7f0e5de9..c93e8df89 100644 --- a/src/telegram/bot-message-context.sender-prefix.test.ts +++ b/src/telegram/bot-message-context.sender-prefix.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { buildTelegramMessageContext } from "./bot-message-context.js"; describe("buildTelegramMessageContext sender prefix", () => { @@ -25,7 +24,7 @@ describe("buildTelegramMessageContext sender prefix", () => { }, } as never, cfg: { - agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } }, + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, channels: { telegram: {} }, messages: { groupChat: { mentionPatterns: [] } }, } as never, @@ -72,7 +71,7 @@ describe("buildTelegramMessageContext sender prefix", () => { }, } as never, cfg: { - agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } }, + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, channels: { telegram: {} }, messages: { groupChat: { mentionPatterns: [] } }, } as never, @@ -118,7 +117,7 @@ describe("buildTelegramMessageContext sender prefix", () => { }, } as never, cfg: { - agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } }, + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, channels: { telegram: {} }, messages: { groupChat: { mentionPatterns: [] } }, } as never, diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 832a4413d..1427e6ec5 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -1,5 +1,7 @@ import type { Bot } from "grammy"; - +import type { OpenClawConfig } from "../config/config.js"; +import type { DmPolicy, TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js"; +import type { TelegramContext } from "./bot/types.js"; import { resolveAckReaction } from "../agents/identity.js"; import { findModelInCatalog, @@ -17,21 +19,25 @@ import { } from "../auto-reply/reply/history.js"; import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; import { buildMentionRegexes, matchesMentionWithExplicit } from "../auto-reply/reply/mentions.js"; +import { shouldAckReaction as shouldAckReactionGate } from "../channels/ack-reactions.js"; +import { resolveControlCommandGate } from "../channels/command-gating.js"; import { formatLocationText, toLocationContext } from "../channels/location.js"; +import { logInboundDrop } from "../channels/logging.js"; +import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; import { recordInboundSession } from "../channels/session.js"; import { formatCliCommand } from "../cli/command-format.js"; import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js"; -import type { MoltbotConfig } from "../config/config.js"; -import type { DmPolicy, TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js"; import { logVerbose, shouldLogVerbose } from "../globals.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../routing/session-key.js"; -import { shouldAckReaction as shouldAckReactionGate } from "../channels/ack-reactions.js"; -import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; -import { resolveControlCommandGate } from "../channels/command-gating.js"; -import { logInboundDrop } from "../channels/logging.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { + firstDefined, + isSenderAllowed, + normalizeAllowFromWithStore, + resolveSenderAllowMatch, +} from "./bot-access.js"; import { buildGroupLabel, buildSenderLabel, @@ -46,14 +52,7 @@ import { hasBotMention, resolveTelegramForumThreadId, } from "./bot/helpers.js"; -import { - firstDefined, - isSenderAllowed, - normalizeAllowFromWithStore, - resolveSenderAllowMatch, -} from "./bot-access.js"; import { upsertTelegramPairingRequest } from "./pairing-store.js"; -import type { TelegramContext } from "./bot/types.js"; type TelegramMediaRef = { path: string; @@ -96,7 +95,7 @@ type BuildTelegramMessageContextParams = { storeAllowFrom: string[]; options?: TelegramMessageContextOptions; bot: Bot; - cfg: MoltbotConfig; + cfg: OpenClawConfig; account: { accountId: string }; historyLimit: number; groupHistories: Map; @@ -111,7 +110,7 @@ type BuildTelegramMessageContextParams = { }; async function resolveStickerVisionSupport(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentId?: string; }): Promise { try { @@ -121,7 +120,9 @@ async function resolveStickerVisionSupport(params: { agentId: params.agentId, }); const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); - if (!entry) return false; + if (!entry) { + return false; + } return modelSupportsVision(entry); } catch { return false; @@ -161,6 +162,7 @@ export const buildTelegramMessageContext = async ({ isForum, messageThreadId, }); + const replyThreadId = isGroup ? resolvedThreadId : messageThreadId; const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); const route = resolveAgentRoute({ @@ -203,7 +205,7 @@ export const buildTelegramMessageContext = async ({ const sendTyping = async () => { await withTelegramApiErrorLogging({ operation: "sendChatAction", - fn: () => bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId)), + fn: () => bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(replyThreadId)), }); }; @@ -212,7 +214,7 @@ export const buildTelegramMessageContext = async ({ await withTelegramApiErrorLogging({ operation: "sendChatAction", fn: () => - bot.api.sendChatAction(chatId, "record_voice", buildTypingThreadParams(resolvedThreadId)), + bot.api.sendChatAction(chatId, "record_voice", buildTypingThreadParams(replyThreadId)), }); } catch (err) { logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`); @@ -221,7 +223,9 @@ export const buildTelegramMessageContext = async ({ // DM access control (secure defaults): "pairing" (default) / "allowlist" / "open" / "disabled" if (!isGroup) { - if (dmPolicy === "disabled") return null; + if (dmPolicy === "disabled") { + return null; + } if (dmPolicy !== "open") { const candidate = String(chatId); @@ -272,14 +276,14 @@ export const buildTelegramMessageContext = async ({ bot.api.sendMessage( chatId, [ - "Moltbot: access not configured.", + "OpenClaw: access not configured.", "", `Your Telegram user id: ${telegramUserId}`, "", `Pairing code: ${code}`, "", "Ask the bot owner to approve with:", - formatCliCommand("moltbot pairing approve telegram "), + formatCliCommand("openclaw pairing approve telegram "), ].join("\n"), ), }); @@ -333,11 +337,19 @@ export const buildTelegramMessageContext = async ({ const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined; let placeholder = ""; - if (msg.photo) placeholder = ""; - else if (msg.video) placeholder = ""; - else if (msg.audio || msg.voice) placeholder = ""; - else if (msg.document) placeholder = ""; - else if (msg.sticker) placeholder = ""; + if (msg.photo) { + placeholder = ""; + } else if (msg.video) { + placeholder = ""; + } else if (msg.video_note) { + placeholder = ""; + } else if (msg.audio || msg.voice) { + placeholder = ""; + } else if (msg.document) { + placeholder = ""; + } else if (msg.sticker) { + placeholder = ""; + } // Check if sticker has a cached description - if so, use it instead of sending the image const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription; @@ -358,8 +370,12 @@ export const buildTelegramMessageContext = async ({ const rawTextSource = msg.text ?? msg.caption ?? ""; const rawText = expandTextLinks(rawTextSource, msg.entities ?? msg.caption_entities).trim(); let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim(); - if (!rawBody) rawBody = placeholder; - if (!rawBody && allMedia.length === 0) return null; + if (!rawBody) { + rawBody = placeholder; + } + if (!rawBody && allMedia.length === 0) { + return null; + } let bodyText = rawBody; if (!bodyText && allMedia.length > 0) { @@ -656,6 +672,7 @@ export const buildTelegramMessageContext = async ({ chatId, isGroup, resolvedThreadId, + replyThreadId, isForum, historyKey, historyLimit, diff --git a/src/telegram/bot-message-dispatch.test.ts b/src/telegram/bot-message-dispatch.test.ts new file mode 100644 index 000000000..2916ca21b --- /dev/null +++ b/src/telegram/bot-message-dispatch.test.ts @@ -0,0 +1,101 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const createTelegramDraftStream = vi.hoisted(() => vi.fn()); +const dispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() => vi.fn()); +const deliverReplies = vi.hoisted(() => vi.fn()); + +vi.mock("./draft-stream.js", () => ({ + createTelegramDraftStream, +})); + +vi.mock("../auto-reply/reply/provider-dispatcher.js", () => ({ + dispatchReplyWithBufferedBlockDispatcher, +})); + +vi.mock("./bot/delivery.js", () => ({ + deliverReplies, +})); + +vi.mock("./sticker-cache.js", () => ({ + cacheSticker: vi.fn(), + describeStickerImage: vi.fn(), +})); + +import { dispatchTelegramMessage } from "./bot-message-dispatch.js"; + +describe("dispatchTelegramMessage draft streaming", () => { + beforeEach(() => { + createTelegramDraftStream.mockReset(); + dispatchReplyWithBufferedBlockDispatcher.mockReset(); + deliverReplies.mockReset(); + }); + + it("streams drafts in private threads and forwards thread id", async () => { + const draftStream = { + update: vi.fn(), + flush: vi.fn().mockResolvedValue(undefined), + stop: vi.fn(), + }; + createTelegramDraftStream.mockReturnValue(draftStream); + dispatchReplyWithBufferedBlockDispatcher.mockImplementation( + async ({ dispatcherOptions, replyOptions }) => { + await replyOptions?.onPartialReply?.({ text: "Hello" }); + await dispatcherOptions.deliver({ text: "Hello" }, { kind: "final" }); + return { queuedFinal: true }; + }, + ); + deliverReplies.mockResolvedValue({ delivered: true }); + + const resolveBotTopicsEnabled = vi.fn().mockResolvedValue(true); + const context = { + ctxPayload: {}, + primaryCtx: { message: { chat: { id: 123, type: "private" } } }, + msg: { + chat: { id: 123, type: "private" }, + message_id: 456, + message_thread_id: 777, + }, + chatId: 123, + isGroup: false, + resolvedThreadId: undefined, + replyThreadId: 777, + historyKey: undefined, + historyLimit: 0, + groupHistories: new Map(), + route: { agentId: "default", accountId: "default" }, + skillFilter: undefined, + sendTyping: vi.fn(), + sendRecordVoice: vi.fn(), + ackReactionPromise: null, + reactionApi: null, + removeAckAfterReply: false, + }; + + await dispatchTelegramMessage({ + context, + bot: { api: {} }, + cfg: {}, + runtime: {}, + replyToMode: "first", + streamMode: "partial", + textLimit: 4096, + telegramCfg: {}, + opts: {}, + resolveBotTopicsEnabled, + }); + + expect(resolveBotTopicsEnabled).toHaveBeenCalledWith(context.primaryCtx); + expect(createTelegramDraftStream).toHaveBeenCalledWith( + expect.objectContaining({ + chatId: 123, + messageThreadId: 777, + }), + ); + expect(draftStream.update).toHaveBeenCalledWith("Hello"); + expect(deliverReplies).toHaveBeenCalledWith( + expect.objectContaining({ + messageThreadId: 777, + }), + ); + }); +}); diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index cead0628a..13d02341e 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -1,11 +1,12 @@ +import { resolveAgentDir } from "../agents/agent-scope.js"; // @ts-nocheck -import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; import { findModelInCatalog, loadModelCatalog, modelSupportsVision, } from "../agents/model-catalog.js"; import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; +import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; import { resolveChunkMode } from "../auto-reply/chunk.js"; import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; @@ -13,20 +14,24 @@ import { removeAckReactionAfterReply } from "../channels/ack-reactions.js"; import { logAckFailure, logTypingFailure } from "../channels/logging.js"; import { createReplyPrefixContext } from "../channels/reply-prefix.js"; import { createTypingCallbacks } from "../channels/typing.js"; -import { danger, logVerbose } from "../globals.js"; +import { OpenClawConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; +import { danger, logVerbose } from "../globals.js"; import { deliverReplies } from "./bot/delivery.js"; import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js"; import { createTelegramDraftStream } from "./draft-stream.js"; import { cacheSticker, describeStickerImage } from "./sticker-cache.js"; -import { resolveAgentDir } from "../agents/agent-scope.js"; -async function resolveStickerVisionSupport(cfg, agentId) { +const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again."; + +async function resolveStickerVisionSupport(cfg: OpenClawConfig, agentId: string) { try { const catalog = await loadModelCatalog({ config: cfg }); const defaultModel = resolveDefaultModelForAgent({ cfg, agentId }); const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); - if (!entry) return false; + if (!entry) { + return false; + } return modelSupportsVision(entry); } catch { return false; @@ -44,14 +49,14 @@ export const dispatchTelegramMessage = async ({ telegramCfg, opts, resolveBotTopicsEnabled, -}) => { +}: any) => { const { ctxPayload, primaryCtx, msg, chatId, isGroup, - resolvedThreadId, + replyThreadId, historyKey, historyLimit, groupHistories, @@ -65,11 +70,13 @@ export const dispatchTelegramMessage = async ({ } = context; const isPrivateChat = msg.chat.type === "private"; + const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id; + const draftThreadId = replyThreadId ?? messageThreadId; const draftMaxChars = Math.min(textLimit, 4096); const canStreamDraft = streamMode !== "off" && isPrivateChat && - typeof resolvedThreadId === "number" && + typeof draftThreadId === "number" && (await resolveBotTopicsEnabled(primaryCtx)); const draftStream = canStreamDraft ? createTelegramDraftStream({ @@ -77,7 +84,7 @@ export const dispatchTelegramMessage = async ({ chatId, draftId: msg.message_id || Date.now(), maxChars: draftMaxChars, - messageThreadId: resolvedThreadId, + messageThreadId: draftThreadId, log: logVerbose, warn: logVerbose, }) @@ -90,8 +97,12 @@ export const dispatchTelegramMessage = async ({ let lastPartialText = ""; let draftText = ""; const updateDraftFromPartial = (text?: string) => { - if (!draftStream || !text) return; - if (text === lastPartialText) return; + if (!draftStream || !text) { + return; + } + if (text === lastPartialText) { + return; + } if (streamMode === "partial") { lastPartialText = text; draftStream.update(text); @@ -106,7 +117,9 @@ export const dispatchTelegramMessage = async ({ draftText = ""; } lastPartialText = text; - if (!delta) return; + if (!delta) { + return; + } if (!draftChunker) { draftText = text; draftStream.update(draftText); @@ -122,7 +135,9 @@ export const dispatchTelegramMessage = async ({ }); }; const flushDraft = async () => { - if (!draftStream) return; + if (!draftStream) { + return; + } if (draftChunker?.hasBuffered()) { draftChunker.drain({ force: true, @@ -131,7 +146,9 @@ export const dispatchTelegramMessage = async ({ }, }); draftChunker.reset(); - if (draftText) draftStream.update(draftText); + if (draftText) { + draftStream.update(draftText); + } } await draftStream.flush(); }; @@ -198,6 +215,15 @@ export const dispatchTelegramMessage = async ({ } } + const replyQuoteText = + ctxPayload.ReplyToIsQuote && ctxPayload.ReplyToBody + ? ctxPayload.ReplyToBody.trim() || undefined + : undefined; + const deliveryState = { + delivered: false, + skippedNonSilent: 0, + }; + const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg, @@ -209,12 +235,7 @@ export const dispatchTelegramMessage = async ({ await flushDraft(); draftStream?.stop(); } - - const replyQuoteText = - ctxPayload.ReplyToIsQuote && ctxPayload.ReplyToBody - ? ctxPayload.ReplyToBody.trim() || undefined - : undefined; - await deliverReplies({ + const result = await deliverReplies({ replies: [payload], chatId: String(chatId), token: opts.token, @@ -222,13 +243,21 @@ export const dispatchTelegramMessage = async ({ bot, replyToMode, textLimit, - messageThreadId: resolvedThreadId, + messageThreadId: replyThreadId, tableMode, chunkMode, onVoiceRecording: sendRecordVoice, linkPreview: telegramCfg.linkPreview, replyQuoteText, }); + if (result.delivered) { + deliveryState.delivered = true; + } + }, + onSkip: (_payload, info) => { + if (info.reason !== "silent") { + deliveryState.skippedNonSilent += 1; + } }, onError: (err, info) => { runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`)); @@ -247,20 +276,35 @@ export const dispatchTelegramMessage = async ({ }, replyOptions: { skillFilter, - onPartialReply: draftStream ? (payload) => updateDraftFromPartial(payload.text) : undefined, - onReasoningStream: draftStream - ? (payload) => { - if (payload.text) draftStream.update(payload.text); - } - : undefined, disableBlockStreaming, + onPartialReply: draftStream ? (payload) => updateDraftFromPartial(payload.text) : undefined, onModelSelected: (ctx) => { prefixContext.onModelSelected(ctx); }, }, }); draftStream?.stop(); - if (!queuedFinal) { + let sentFallback = false; + if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) { + const result = await deliverReplies({ + replies: [{ text: EMPTY_RESPONSE_FALLBACK }], + chatId: String(chatId), + token: opts.token, + runtime, + bot, + replyToMode, + textLimit, + messageThreadId: replyThreadId, + tableMode, + chunkMode, + linkPreview: telegramCfg.linkPreview, + replyQuoteText, + }); + sentFallback = result.delivered; + } + + const hasFinalResponse = queuedFinal || sentFallback; + if (!hasFinalResponse) { if (isGroup && historyKey) { clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit }); } @@ -272,7 +316,9 @@ export const dispatchTelegramMessage = async ({ ackReactionValue: ackReactionPromise ? "ack" : null, remove: () => reactionApi?.(chatId, msg.message_id ?? 0, []) ?? Promise.resolve(), onError: (err) => { - if (!msg.message_id) return; + if (!msg.message_id) { + return; + } logAckFailure({ log: logVerbose, channel: "telegram", diff --git a/src/telegram/bot-message.ts b/src/telegram/bot-message.ts index 313296b1d..ffaa00be5 100644 --- a/src/telegram/bot-message.ts +++ b/src/telegram/bot-message.ts @@ -46,7 +46,9 @@ export const createTelegramMessageProcessor = (deps) => { resolveGroupRequireMention, resolveTelegramGroupConfig, }); - if (!context) return; + if (!context) { + return; + } await dispatchTelegramMessage({ context, bot, diff --git a/src/telegram/bot-native-commands.plugin-auth.test.ts b/src/telegram/bot-native-commands.plugin-auth.test.ts index 03f99e1fe..60e315e8d 100644 --- a/src/telegram/bot-native-commands.plugin-auth.test.ts +++ b/src/telegram/bot-native-commands.plugin-auth.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from "vitest"; - +import type { OpenClawConfig } from "../config/config.js"; import type { ChannelGroupPolicy } from "../config/group-policy.js"; -import type { MoltbotConfig } from "../config/config.js"; import type { TelegramAccountConfig } from "../config/types.js"; import type { RuntimeEnv } from "../runtime.js"; import { registerTelegramNativeCommands } from "./bot-native-commands.js"; @@ -47,7 +46,7 @@ describe("registerTelegramNativeCommands (plugin auth)", () => { }, } as const; - const cfg = {} as MoltbotConfig; + const cfg = {} as OpenClawConfig; const telegramCfg = {} as TelegramAccountConfig; const resolveGroupPolicy = () => ({ diff --git a/src/telegram/bot-native-commands.test.ts b/src/telegram/bot-native-commands.test.ts new file mode 100644 index 000000000..1226ec701 --- /dev/null +++ b/src/telegram/bot-native-commands.test.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import type { TelegramAccountConfig } from "../config/types.js"; +import type { RuntimeEnv } from "../runtime.js"; +import { registerTelegramNativeCommands } from "./bot-native-commands.js"; + +const { listSkillCommandsForAgents } = vi.hoisted(() => ({ + listSkillCommandsForAgents: vi.fn(() => []), +})); + +vi.mock("../auto-reply/skill-commands.js", () => ({ + listSkillCommandsForAgents, +})); + +describe("registerTelegramNativeCommands", () => { + beforeEach(() => { + listSkillCommandsForAgents.mockReset(); + }); + + const buildParams = (cfg: OpenClawConfig, accountId = "default") => ({ + bot: { + api: { + setMyCommands: vi.fn().mockResolvedValue(undefined), + sendMessage: vi.fn().mockResolvedValue(undefined), + }, + command: vi.fn(), + } as unknown as Parameters[0]["bot"], + cfg, + runtime: {} as RuntimeEnv, + accountId, + telegramCfg: {} as TelegramAccountConfig, + allowFrom: [], + groupAllowFrom: [], + replyToMode: "off" as const, + textLimit: 4096, + useAccessGroups: false, + nativeEnabled: true, + nativeSkillsEnabled: true, + nativeDisabledExplicit: false, + resolveGroupPolicy: () => ({ allowlistEnabled: false, allowed: true }), + resolveTelegramGroupConfig: () => ({ + groupConfig: undefined, + topicConfig: undefined, + }), + shouldSkipUpdate: () => false, + opts: { token: "token" }, + }); + + it("scopes skill commands when account binding exists", () => { + const cfg: OpenClawConfig = { + agents: { + list: [{ id: "main", default: true }, { id: "butler" }], + }, + bindings: [ + { + agentId: "butler", + match: { channel: "telegram", accountId: "bot-a" }, + }, + ], + }; + + registerTelegramNativeCommands(buildParams(cfg, "bot-a")); + + expect(listSkillCommandsForAgents).toHaveBeenCalledWith({ + cfg, + agentIds: ["butler"], + }); + }); + + it("keeps skill commands unscoped without a matching binding", () => { + const cfg: OpenClawConfig = { + agents: { + list: [{ id: "main", default: true }, { id: "butler" }], + }, + }; + + registerTelegramNativeCommands(buildParams(cfg, "bot-a")); + + expect(listSkillCommandsForAgents).toHaveBeenCalledWith({ cfg }); + }); +}); diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index 3415ea927..c34d59436 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -1,5 +1,14 @@ import type { Bot, Context } from "grammy"; - +import type { CommandArgs } from "../auto-reply/commands-registry.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { ChannelGroupPolicy } from "../config/group-policy.js"; +import type { + ReplyToMode, + TelegramAccountConfig, + TelegramGroupConfig, + TelegramTopicConfig, +} from "../config/types.js"; +import type { RuntimeEnv } from "../runtime.js"; import { resolveEffectiveMessagesConfig } from "../agents/identity.js"; import { resolveChunkMode } from "../auto-reply/chunk.js"; import { @@ -10,45 +19,40 @@ import { parseCommandArgs, resolveCommandArgMenu, } from "../auto-reply/commands-registry.js"; -import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; -import type { CommandArgs } from "../auto-reply/commands-registry.js"; -import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js"; -import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; -import { danger, logVerbose } from "../globals.js"; +import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; +import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; +import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; -import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js"; import { normalizeTelegramCommandName, TELEGRAM_COMMAND_NAME_PATTERN, } from "../config/telegram-custom-commands.js"; -import { resolveAgentRoute } from "../routing/resolve-route.js"; -import { resolveThreadSessionKeys } from "../routing/session-key.js"; -import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js"; +import { danger, logVerbose } from "../globals.js"; +import { getChildLogger } from "../logging.js"; import { executePluginCommand, getPluginCommandSpecs, matchPluginCommand, } from "../plugins/commands.js"; -import type { ChannelGroupPolicy } from "../config/group-policy.js"; -import type { - ReplyToMode, - TelegramAccountConfig, - TelegramGroupConfig, - TelegramTopicConfig, -} from "../config/types.js"; -import type { MoltbotConfig } from "../config/config.js"; -import type { RuntimeEnv } from "../runtime.js"; +import { resolveAgentRoute } from "../routing/resolve-route.js"; +import { resolveThreadSessionKeys } from "../routing/session-key.js"; +import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; +import { TelegramUpdateKeyContext } from "./bot-updates.js"; +import { TelegramBotOptions } from "./bot.js"; import { deliverReplies } from "./bot/delivery.js"; -import { buildInlineKeyboard } from "./send.js"; import { buildSenderName, buildTelegramGroupFrom, buildTelegramGroupPeerId, resolveTelegramForumThreadId, } from "./bot/helpers.js"; -import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; import { readTelegramAllowFromStore } from "./pairing-store.js"; +import { buildInlineKeyboard } from "./send.js"; + +const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again."; type TelegramNativeCommandContext = Context & { match?: string }; @@ -64,9 +68,36 @@ type TelegramCommandAuthResult = { commandAuthorized: boolean; }; +export type RegisterTelegramHandlerParams = { + cfg: OpenClawConfig; + accountId: string; + bot: Bot; + mediaMaxBytes: number; + opts: TelegramBotOptions; + runtime: RuntimeEnv; + telegramCfg: TelegramAccountConfig; + groupAllowFrom?: Array; + resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy; + resolveTelegramGroupConfig: ( + chatId: string | number, + messageThreadId?: number, + ) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig }; + shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean; + processMessage: ( + ctx: unknown, + allMedia: Array<{ path: string; contentType?: string }>, + storeAllowFrom: string[], + options?: { + messageIdOverride?: string; + forceWasMentioned?: boolean; + }, + ) => Promise; + logger: ReturnType; +}; + type RegisterTelegramNativeCommandsParams = { bot: Bot; - cfg: MoltbotConfig; + cfg: OpenClawConfig; runtime: RuntimeEnv; accountId: string; telegramCfg: TelegramAccountConfig; @@ -83,14 +114,14 @@ type RegisterTelegramNativeCommandsParams = { chatId: string | number, messageThreadId?: number, ) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig }; - shouldSkipUpdate: (ctx: unknown) => boolean; + shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean; opts: { token: string }; }; async function resolveTelegramCommandAuth(params: { msg: NonNullable; bot: Bot; - cfg: MoltbotConfig; + cfg: OpenClawConfig; telegramCfg: TelegramAccountConfig; allowFrom?: Array; groupAllowFrom?: Array; @@ -255,10 +286,21 @@ export const registerTelegramNativeCommands = ({ shouldSkipUpdate, opts, }: RegisterTelegramNativeCommandsParams) => { + const boundRoute = + nativeEnabled && nativeSkillsEnabled + ? resolveAgentRoute({ cfg, channel: "telegram", accountId }) + : null; + const boundAgentIds = + boundRoute && boundRoute.matchedBy.startsWith("binding.") ? [boundRoute.agentId] : null; const skillCommands = - nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : []; + nativeEnabled && nativeSkillsEnabled + ? listSkillCommandsForAgents(boundAgentIds ? { cfg, agentIds: boundAgentIds } : { cfg }) + : []; const nativeCommands = nativeEnabled - ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "telegram" }) + ? listNativeCommandSpecsForConfig(cfg, { + skillCommands, + provider: "telegram", + }) : []; const reservedCommands = new Set( listNativeCommandSpecs().map((command) => command.name.toLowerCase()), @@ -334,8 +376,12 @@ export const registerTelegramNativeCommands = ({ for (const command of nativeCommands) { bot.command(command.name, async (ctx: TelegramNativeCommandContext) => { const msg = ctx.message; - if (!msg) return; - if (shouldSkipUpdate(ctx)) return; + if (!msg) { + return; + } + if (shouldSkipUpdate(ctx)) { + return; + } const auth = await resolveTelegramCommandAuth({ msg, bot, @@ -348,7 +394,9 @@ export const registerTelegramNativeCommands = ({ resolveTelegramGroupConfig, requireAuth: true, }); - if (!auth) return; + if (!auth) { + return; + } const { chatId, isGroup, @@ -427,7 +475,10 @@ export const registerTelegramNativeCommands = ({ const dmThreadId = !isGroup ? messageThreadId : undefined; const threadKeys = dmThreadId != null - ? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) }) + ? resolveThreadSessionKeys({ + baseSessionKey, + threadId: String(dmThreadId), + }) : null; const sessionKey = threadKeys?.sessionKey ?? baseSessionKey; const tableMode = resolveMarkdownTableMode({ @@ -468,6 +519,7 @@ export const registerTelegramNativeCommands = ({ CommandAuthorized: commandAuthorized, CommandSource: "native" as const, SessionKey: `telegram:slash:${senderId || chatId}`, + AccountId: route.accountId, CommandTargetSessionKey: sessionKey, MessageThreadId: threadIdForSend, IsForum: isForum, @@ -482,13 +534,18 @@ export const registerTelegramNativeCommands = ({ : undefined; const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId); + const deliveryState = { + delivered: false, + skippedNonSilent: 0, + }; + await dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg, dispatcherOptions: { responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, - deliver: async (payload) => { - await deliverReplies({ + deliver: async (payload, _info) => { + const result = await deliverReplies({ replies: [payload], chatId: String(chatId), token: opts.token, @@ -501,6 +558,14 @@ export const registerTelegramNativeCommands = ({ chunkMode, linkPreview: telegramCfg.linkPreview, }); + if (result.delivered) { + deliveryState.delivered = true; + } + }, + onSkip: (_payload, info) => { + if (info.reason !== "silent") { + deliveryState.skippedNonSilent += 1; + } }, onError: (err, info) => { runtime.error?.(danger(`telegram slash ${info.kind} reply failed: ${String(err)}`)); @@ -511,14 +576,33 @@ export const registerTelegramNativeCommands = ({ disableBlockStreaming, }, }); + if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) { + await deliverReplies({ + replies: [{ text: EMPTY_RESPONSE_FALLBACK }], + chatId: String(chatId), + token: opts.token, + runtime, + bot, + replyToMode, + textLimit, + messageThreadId: threadIdForSend, + tableMode, + chunkMode, + linkPreview: telegramCfg.linkPreview, + }); + } }); } for (const pluginCommand of pluginCommands) { bot.command(pluginCommand.command, async (ctx: TelegramNativeCommandContext) => { const msg = ctx.message; - if (!msg) return; - if (shouldSkipUpdate(ctx)) return; + if (!msg) { + return; + } + if (shouldSkipUpdate(ctx)) { + return; + } const chatId = msg.chat.id; const rawText = ctx.match?.trim() ?? ""; const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`; @@ -543,7 +627,9 @@ export const registerTelegramNativeCommands = ({ resolveTelegramGroupConfig, requireAuth: match.command.requireAuth !== false, }); - if (!auth) return; + if (!auth) { + return; + } const { resolvedThreadId, senderId, commandAuthorized, isGroup } = auth; const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id; const threadIdForSend = isGroup ? resolvedThreadId : messageThreadId; diff --git a/src/telegram/bot-updates.ts b/src/telegram/bot-updates.ts index e63658eb4..c59e9ac21 100644 --- a/src/telegram/bot-updates.ts +++ b/src/telegram/bot-updates.ts @@ -1,5 +1,5 @@ -import { createDedupeCache } from "../infra/dedupe.js"; import type { TelegramContext, TelegramMessage } from "./bot/types.js"; +import { createDedupeCache } from "../infra/dedupe.js"; const MEDIA_GROUP_TIMEOUT_MS = 500; const RECENT_TELEGRAM_UPDATE_TTL_MS = 5 * 60_000; @@ -29,9 +29,13 @@ export const resolveTelegramUpdateId = (ctx: TelegramUpdateKeyContext) => export const buildTelegramUpdateKey = (ctx: TelegramUpdateKeyContext) => { const updateId = resolveTelegramUpdateId(ctx); - if (typeof updateId === "number") return `update:${updateId}`; + if (typeof updateId === "number") { + return `update:${updateId}`; + } const callbackId = ctx.callbackQuery?.id; - if (callbackId) return `callback:${callbackId}`; + if (callbackId) { + return `callback:${callbackId}`; + } const msg = ctx.message ?? ctx.update?.message ?? ctx.update?.edited_message ?? ctx.callbackQuery?.message; const chatId = msg?.chat?.id; diff --git a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts index 8bfe1fdd3..765d0d2f8 100644 --- a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts +++ b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts @@ -5,7 +5,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ @@ -129,7 +129,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -196,7 +198,7 @@ describe("createTelegramBot", () => { message_id: 1, from: { id: 9, first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -212,7 +214,7 @@ describe("createTelegramBot", () => { ); }); - it("skips group messages when another user is explicitly mentioned", async () => { + it("accepts group messages when mentionPatterns match even if another user is mentioned", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); @@ -245,11 +247,12 @@ describe("createTelegramBot", () => { message_id: 3, from: { id: 9, first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); - expect(replySpy).not.toHaveBeenCalled(); + expect(replySpy).toHaveBeenCalledTimes(1); + expect(replySpy.mock.calls[0][0].WasMentioned).toBe(true); }); it("keeps group envelope headers stable (sender identity is separate)", async () => { @@ -287,7 +290,7 @@ describe("createTelegramBot", () => { username: "ada", }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -333,7 +336,7 @@ describe("createTelegramBot", () => { message_id: 123, from: { id: 9, first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -374,7 +377,7 @@ describe("createTelegramBot", () => { message_id: 2, from: { id: 9, first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -434,7 +437,7 @@ describe("createTelegramBot", () => { from: { first_name: "Ada" }, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); diff --git a/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts b/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts index bb61ceebb..4b0c852d9 100644 --- a/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts +++ b/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts @@ -4,7 +4,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ @@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -201,7 +203,7 @@ describe("createTelegramBot", () => { message_id: 42, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -245,7 +247,7 @@ describe("createTelegramBot", () => { message_id: 42, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -326,7 +328,9 @@ describe("createTelegramBot", () => { const verboseHandler = commandSpy.mock.calls.find((call) => call[0] === "verbose")?.[1] as | ((ctx: Record) => Promise) | undefined; - if (!verboseHandler) throw new Error("verbose command handler missing"); + if (!verboseHandler) { + throw new Error("verbose command handler missing"); + } await verboseHandler({ message: { @@ -365,7 +369,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }; diff --git a/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts b/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts index a9f5214c8..7cc27f7f7 100644 --- a/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts +++ b/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts @@ -4,7 +4,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ @@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -178,10 +180,10 @@ describe("createTelegramBot", () => { message: { chat: { id: -100123456789, type: "group", title: "Test Group" }, from: { id: 123456789, username: "testuser" }, - text: "@moltbot_bot hello", + text: "@openclaw_bot hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -208,10 +210,10 @@ describe("createTelegramBot", () => { message: { chat: { id: -100123456789, type: "group", title: "Test Group" }, from: { id: 999999, username: "notallowed" }, // Not in allowFrom - text: "@moltbot_bot hello", + text: "@openclaw_bot hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -241,7 +243,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -271,7 +273,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -301,7 +303,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -331,7 +333,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -360,7 +362,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); diff --git a/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts b/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts index ebd1ff0a9..7feecf57d 100644 --- a/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts +++ b/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts @@ -4,7 +4,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ @@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -186,7 +188,7 @@ describe("createTelegramBot", () => { message_id: 9001, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }; @@ -222,7 +224,7 @@ describe("createTelegramBot", () => { message_id: 9001, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }); @@ -237,7 +239,7 @@ describe("createTelegramBot", () => { message_id: 9001, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }); diff --git a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts index c3844ac88..c1b78ea24 100644 --- a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts +++ b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts @@ -7,7 +7,7 @@ let getTelegramSequentialKey: typeof import("./bot.js").getTelegramSequentialKey let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-throttler-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-throttler-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ loadWebMedia: vi.fn(), @@ -132,7 +132,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -294,7 +296,7 @@ describe("createTelegramBot", () => { message_id: 10, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -328,7 +330,7 @@ describe("createTelegramBot", () => { }; await handler({ message, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -371,7 +373,7 @@ describe("createTelegramBot", () => { date: 1736380800, from: { id: 999, username: "random" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -408,12 +410,12 @@ describe("createTelegramBot", () => { await handler({ message, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); await handler({ message: { ...message, text: "hello again" }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -428,7 +430,7 @@ describe("createTelegramBot", () => { const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 42, type: "private" }, text: "hi" }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); diff --git a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts index 72ee9de0b..365e9e1a2 100644 --- a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts +++ b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts @@ -4,7 +4,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ @@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -182,7 +184,7 @@ describe("createTelegramBot", () => { text: "hello from prefixed user", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -212,7 +214,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -242,7 +244,7 @@ describe("createTelegramBot", () => { text: "/status", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -280,7 +282,7 @@ describe("createTelegramBot", () => { message_id: 42, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -325,7 +327,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -366,7 +368,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); diff --git a/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts b/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts index d46fe663d..d32496cdc 100644 --- a/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts +++ b/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts @@ -4,7 +4,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ @@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -182,7 +184,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -211,7 +213,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -239,7 +241,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -267,7 +269,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -297,7 +299,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -326,7 +328,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -356,7 +358,7 @@ describe("createTelegramBot", () => { text: "hello from prefixed user", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); diff --git a/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts b/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts index e854d1962..929395fe6 100644 --- a/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts +++ b/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts @@ -4,7 +4,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ @@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -193,7 +195,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -227,7 +229,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -269,7 +271,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -297,7 +299,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -357,7 +359,7 @@ describe("createTelegramBot", () => { message_id: 5, from: { first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); diff --git a/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts b/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts index 61cf4def6..1d482c0a7 100644 --- a/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts +++ b/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts @@ -7,7 +7,7 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-reply-threading-${Math.random() + sessionStorePath: `/tmp/openclaw-telegram-reply-threading-${Math.random() .toString(16) .slice(2)}.json`, })); @@ -133,7 +133,9 @@ let replyModule: typeof import("../auto-reply/reply.js"); const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -179,7 +181,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 101, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -207,7 +209,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 101, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -239,7 +241,7 @@ describe("createTelegramBot", () => { text: "hi", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -265,7 +267,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 101, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -294,10 +296,10 @@ describe("createTelegramBot", () => { await handler({ message: { chat: { id: 456, type: "group", title: "Ops" }, - text: "@moltbot_bot hello", + text: "@openclaw_bot hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -322,7 +324,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -332,7 +334,7 @@ describe("createTelegramBot", () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); - const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-telegram-")); + const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-")); const storePath = path.join(storeDir, "sessions.json"); fs.writeFileSync( storePath, @@ -369,7 +371,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); diff --git a/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts b/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts index 79c8aab28..bd3b73499 100644 --- a/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts +++ b/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts @@ -151,7 +151,7 @@ describe("telegram inbound media", () => { photo: [{ file_id: "fid" }], date: 1736380800, // 2025-01-09T00:00:00Z }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "photos/1.jpg" }), }); @@ -206,7 +206,7 @@ describe("telegram inbound media", () => { chat: { id: 1234, type: "private" }, photo: [{ file_id: "fid" }], }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "photos/2.jpg" }), }); @@ -249,7 +249,7 @@ describe("telegram inbound media", () => { chat: { id: 1234, type: "private" }, photo: [{ file_id: "fid" }], }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }); @@ -319,7 +319,7 @@ describe("telegram media groups", () => { media_group_id: "album123", photo: [{ file_id: "photo1" }], }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "photos/photo1.jpg" }), }); @@ -331,7 +331,7 @@ describe("telegram media groups", () => { media_group_id: "album123", photo: [{ file_id: "photo2" }], }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "photos/photo2.jpg" }), }); @@ -385,7 +385,7 @@ describe("telegram media groups", () => { media_group_id: "albumA", photo: [{ file_id: "photoA1" }], }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "photos/photoA1.jpg" }), }); @@ -398,7 +398,7 @@ describe("telegram media groups", () => { media_group_id: "albumB", photo: [{ file_id: "photoB1" }], }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "photos/photoB1.jpg" }), }); @@ -477,7 +477,7 @@ describe("telegram stickers", () => { }, date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "stickers/sticker.webp" }), }); @@ -558,7 +558,7 @@ describe("telegram stickers", () => { }, date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "stickers/sticker.webp" }), }); @@ -624,7 +624,7 @@ describe("telegram stickers", () => { }, date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "stickers/animated.tgs" }), }); @@ -684,7 +684,7 @@ describe("telegram stickers", () => { }, date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "stickers/video.webm" }), }); @@ -737,7 +737,7 @@ describe("telegram text fragments", () => { date: 1736380800, text: part1, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }); @@ -748,7 +748,7 @@ describe("telegram text fragments", () => { date: 1736380801, text: part2, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }); diff --git a/src/telegram/bot.media.includes-location-text-ctx-fields-pins.test.ts b/src/telegram/bot.media.includes-location-text-ctx-fields-pins.test.ts index e991e6aec..5a4f1b362 100644 --- a/src/telegram/bot.media.includes-location-text-ctx-fields-pins.test.ts +++ b/src/telegram/bot.media.includes-location-text-ctx-fields-pins.test.ts @@ -123,7 +123,7 @@ describe("telegram inbound media", () => { horizontal_accuracy: 12, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "unused" }), }); @@ -166,7 +166,7 @@ describe("telegram inbound media", () => { location: { latitude: 48.858844, longitude: 2.294351 }, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ file_path: "unused" }), }); diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index c075174fb..ab79c7ada 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -2,12 +2,12 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; +import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js"; import { listNativeCommandSpecs, listNativeCommandSpecsForConfig, } from "../auto-reply/commands-registry.js"; -import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; -import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js"; import { resolveTelegramFetch } from "./fetch.js"; let createTelegramBot: typeof import("./bot.js").createTelegramBot; @@ -22,7 +22,7 @@ vi.mock("../auto-reply/skill-commands.js", () => ({ })); const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/moltbot-telegram-bot-${Math.random().toString(16).slice(2)}.json`, + sessionStorePath: `/tmp/openclaw-telegram-bot-${Math.random().toString(16).slice(2)}.json`, })); function resolveSkillCommands(config: Parameters[0]) { @@ -167,7 +167,9 @@ vi.mock("../auto-reply/reply.js", () => { const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) throw new Error(`Missing handler for event: ${event}`); + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } return handler as (ctx: Record) => Promise; }; @@ -310,8 +312,8 @@ describe("createTelegramBot", () => { { command: "custom_backup", description: "Git backup" }, { command: "custom_generate", description: "Create an image" }, ]); - const reserved = listNativeCommandSpecs().map((command) => command.name); - expect(registered.some((command) => reserved.includes(command.command))).toBe(false); + const reserved = new Set(listNativeCommandSpecs().map((command) => command.name)); + expect(registered.some((command) => reserved.has(command.command))).toBe(false); }); it("uses wrapped fetch when global fetch is available", () => { @@ -382,7 +384,7 @@ describe("createTelegramBot", () => { message_id: 10, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -425,7 +427,7 @@ describe("createTelegramBot", () => { message_id: 11, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -454,7 +456,7 @@ describe("createTelegramBot", () => { message_id: 12, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -506,7 +508,7 @@ describe("createTelegramBot", () => { message_id: 13, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -539,7 +541,7 @@ describe("createTelegramBot", () => { }; await handler({ message, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -583,7 +585,7 @@ describe("createTelegramBot", () => { date: 1736380800, from: { id: 999, username: "random" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -621,12 +623,12 @@ describe("createTelegramBot", () => { await handler({ message, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); await handler({ message: { ...message, text: "hello again" }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -642,7 +644,7 @@ describe("createTelegramBot", () => { const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 42, type: "private" }, text: "hi" }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -681,7 +683,7 @@ describe("createTelegramBot", () => { message_id: 1, from: { id: 9, first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -733,7 +735,7 @@ describe("createTelegramBot", () => { username: "ada", }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -781,7 +783,7 @@ describe("createTelegramBot", () => { message_id: 123, from: { id: 9, first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -824,7 +826,7 @@ describe("createTelegramBot", () => { message_id: 2, from: { id: 9, first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -886,7 +888,7 @@ describe("createTelegramBot", () => { from: { first_name: "Ada" }, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -922,7 +924,7 @@ describe("createTelegramBot", () => { text: "summarize this", }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -953,7 +955,7 @@ describe("createTelegramBot", () => { text: "summarize this", }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -982,7 +984,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 101, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1011,7 +1013,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 101, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1044,7 +1046,7 @@ describe("createTelegramBot", () => { text: "hi", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1071,7 +1073,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 101, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1101,10 +1103,10 @@ describe("createTelegramBot", () => { await handler({ message: { chat: { id: 456, type: "group", title: "Ops" }, - text: "@moltbot_bot hello", + text: "@openclaw_bot hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1130,7 +1132,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1158,10 +1160,10 @@ describe("createTelegramBot", () => { reply_to_message: { message_id: 42, text: "original reply", - from: { id: 999, first_name: "Moltbot" }, + from: { id: 999, first_name: "OpenClaw" }, }, }, - me: { id: 999, username: "moltbot_bot" }, + me: { id: 999, username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1174,7 +1176,7 @@ describe("createTelegramBot", () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); - const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-telegram-")); + const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-")); const storePath = path.join(storeDir, "sessions.json"); fs.writeFileSync( storePath, @@ -1211,7 +1213,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1253,7 +1255,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1288,7 +1290,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1331,7 +1333,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1375,7 +1377,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1418,7 +1420,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1447,7 +1449,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1509,7 +1511,7 @@ describe("createTelegramBot", () => { message_id: 5, from: { first_name: "Ada" }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1543,10 +1545,10 @@ describe("createTelegramBot", () => { message: { chat: { id: -100123456789, type: "group", title: "Test Group" }, from: { id: 123456789, username: "testuser" }, - text: "@moltbot_bot hello", + text: "@openclaw_bot hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1574,10 +1576,10 @@ describe("createTelegramBot", () => { message: { chat: { id: -100123456789, type: "group", title: "Test Group" }, from: { id: 999999, username: "notallowed" }, // Not in allowFrom - text: "@moltbot_bot hello", + text: "@openclaw_bot hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1608,7 +1610,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1639,7 +1641,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1670,7 +1672,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1701,7 +1703,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1731,7 +1733,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1762,7 +1764,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1792,7 +1794,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1821,7 +1823,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1850,7 +1852,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1881,7 +1883,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1911,7 +1913,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1942,7 +1944,7 @@ describe("createTelegramBot", () => { text: "hello from prefixed user", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -1974,7 +1976,7 @@ describe("createTelegramBot", () => { text: "hello from prefixed user", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2005,7 +2007,7 @@ describe("createTelegramBot", () => { text: "hello", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2036,7 +2038,7 @@ describe("createTelegramBot", () => { text: "/status", date: 1736380800, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2075,7 +2077,7 @@ describe("createTelegramBot", () => { message_id: 42, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2121,7 +2123,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2163,7 +2165,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2215,7 +2217,7 @@ describe("createTelegramBot", () => { message_id: 42, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2260,7 +2262,7 @@ describe("createTelegramBot", () => { message_id: 42, message_thread_id: 99, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); @@ -2339,7 +2341,9 @@ describe("createTelegramBot", () => { const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as | ((ctx: Record) => Promise) | undefined; - if (!handler) throw new Error("status command handler missing"); + if (!handler) { + throw new Error("status command handler missing"); + } await handler({ message: { @@ -2380,7 +2384,9 @@ describe("createTelegramBot", () => { const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as | ((ctx: Record) => Promise) | undefined; - if (!handler) throw new Error("status command handler missing"); + if (!handler) { + throw new Error("status command handler missing"); + } await handler({ message: { @@ -2422,7 +2428,9 @@ describe("createTelegramBot", () => { const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as | ((ctx: Record) => Promise) | undefined; - if (!handler) throw new Error("status command handler missing"); + if (!handler) { + throw new Error("status command handler missing"); + } await handler({ message: { @@ -2467,7 +2475,9 @@ describe("createTelegramBot", () => { const verboseHandler = commandSpy.mock.calls.find((call) => call[0] === "verbose")?.[1] as | ((ctx: Record) => Promise) | undefined; - if (!verboseHandler) throw new Error("verbose command handler missing"); + if (!verboseHandler) { + throw new Error("verbose command handler missing"); + } await verboseHandler({ message: { @@ -2507,7 +2517,7 @@ describe("createTelegramBot", () => { date: 1736380800, message_id: 42, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }; @@ -2545,7 +2555,7 @@ describe("createTelegramBot", () => { message_id: 9001, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }; @@ -2582,7 +2592,7 @@ describe("createTelegramBot", () => { message_id: 9001, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }); @@ -2597,7 +2607,7 @@ describe("createTelegramBot", () => { message_id: 9001, }, }, - me: { username: "moltbot_bot" }, + me: { username: "openclaw_bot" }, getFile: async () => ({}), }); diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index ae21d10da..3d44bcba0 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -1,18 +1,21 @@ +import type { ApiClientOptions } from "grammy"; // @ts-nocheck import { sequentialize } from "@grammyjs/runner"; import { apiThrottler } from "@grammyjs/transformer-throttler"; -import type { ApiClientOptions } from "grammy"; +import { ReactionTypeEmoji } from "@grammyjs/types"; import { Bot, webhookCallback } from "grammy"; +import type { OpenClawConfig, ReplyToMode } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { TelegramContext, TelegramMessage } from "./bot/types.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { isControlCommandMessage } from "../auto-reply/command-detection.js"; import { resolveTextChunkLimit } from "../auto-reply/chunk.js"; +import { isControlCommandMessage } from "../auto-reply/command-detection.js"; import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "../auto-reply/reply/history.js"; import { isNativeCommandsExplicitlyDisabled, resolveNativeCommandsEnabled, resolveNativeSkillsEnabled, } from "../config/commands.js"; -import type { MoltbotConfig, ReplyToMode } from "../config/config.js"; import { loadConfig } from "../config/config.js"; import { resolveChannelGroupPolicy, @@ -20,21 +23,14 @@ import { } from "../config/group-policy.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { danger, logVerbose, shouldLogVerbose } from "../globals.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; import { formatUncaughtError } from "../infra/errors.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; import { getChildLogger } from "../logging.js"; -import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../routing/session-key.js"; -import type { RuntimeEnv } from "../runtime.js"; import { resolveTelegramAccount } from "./accounts.js"; -import { - buildTelegramGroupPeerId, - resolveTelegramForumThreadId, - resolveTelegramStreamMode, -} from "./bot/helpers.js"; -import type { TelegramContext, TelegramMessage } from "./bot/types.js"; +import { withTelegramApiErrorLogging } from "./api-logging.js"; import { registerTelegramHandlers } from "./bot-handlers.js"; import { createTelegramMessageProcessor } from "./bot-message.js"; import { registerTelegramNativeCommands } from "./bot-native-commands.js"; @@ -44,6 +40,11 @@ import { resolveTelegramUpdateId, type TelegramUpdateKeyContext, } from "./bot-updates.js"; +import { + buildTelegramGroupPeerId, + resolveTelegramForumThreadId, + resolveTelegramStreamMode, +} from "./bot/helpers.js"; import { resolveTelegramFetch } from "./fetch.js"; import { wasSentByBot } from "./sent-message-cache.js"; @@ -57,7 +58,7 @@ export type TelegramBotOptions = { mediaMaxMb?: number; replyToMode?: ReplyToMode; proxyFetch?: typeof fetch; - config?: MoltbotConfig; + config?: OpenClawConfig; updateOffset?: { lastUpdateId?: number | null; onUpdateId?: (updateId: number) => void | Promise; @@ -91,7 +92,9 @@ export function getTelegramSequentialKey(ctx: { rawText && isControlCommandMessage(rawText, undefined, botUsername ? { botUsername } : undefined) ) { - if (typeof chatId === "number") return `telegram:${chatId}:control`; + if (typeof chatId === "number") { + return `telegram:${chatId}:control`; + } return "telegram:control"; } const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup"; @@ -158,8 +161,12 @@ export function createTelegramBot(opts: TelegramBotOptions) { const recordUpdateId = (ctx: TelegramUpdateKeyContext) => { const updateId = resolveTelegramUpdateId(ctx); - if (typeof updateId !== "number") return; - if (lastUpdateId !== null && updateId <= lastUpdateId) return; + if (typeof updateId !== "number") { + return; + } + if (lastUpdateId !== null && updateId <= lastUpdateId) { + return; + } lastUpdateId = updateId; void opts.updateOffset?.onUpdateId?.(updateId); }; @@ -167,7 +174,9 @@ export function createTelegramBot(opts: TelegramBotOptions) { const shouldSkipUpdate = (ctx: TelegramUpdateKeyContext) => { const updateId = resolveTelegramUpdateId(ctx); if (typeof updateId === "number" && lastUpdateId !== null) { - if (updateId <= lastUpdateId) return true; + if (updateId <= lastUpdateId) { + return true; + } } const key = buildTelegramUpdateKey(ctx); const skipped = recentUpdates.check(key); @@ -182,7 +191,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { const MAX_RAW_UPDATE_STRING = 500; const MAX_RAW_UPDATE_ARRAY = 20; const stringifyUpdate = (update: unknown) => { - const seen = new WeakSet(); + const seen = new WeakSet(); return JSON.stringify(update ?? null, (key, value) => { if (typeof value === "string" && value.length > MAX_RAW_UPDATE_STRING) { return `${value.slice(0, MAX_RAW_UPDATE_STRING)}...`; @@ -195,7 +204,9 @@ export function createTelegramBot(opts: TelegramBotOptions) { } if (value && typeof value === "object") { const obj = value as object; - if (seen.has(obj)) return "[Circular]"; + if (seen.has(obj)) { + return "[Circular]"; + } seen.add(obj); } return value; @@ -235,7 +246,6 @@ export function createTelegramBot(opts: TelegramBotOptions) { : undefined) ?? (opts.allowFrom && opts.allowFrom.length > 0 ? opts.allowFrom : undefined); const replyToMode = opts.replyToMode ?? telegramCfg.replyToMode ?? "first"; - const streamMode = resolveTelegramStreamMode(telegramCfg); const nativeEnabled = resolveNativeCommandsEnabled({ providerId: "telegram", providerSetting: telegramCfg.commands?.native, @@ -254,6 +264,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions"; const mediaMaxBytes = (opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 5) * 1024 * 1024; const logger = getChildLogger({ module: "telegram-auto-reply" }); + const streamMode = resolveTelegramStreamMode(telegramCfg); let botHasTopicsEnabled: boolean | undefined; const resolveBotTopicsEnabled = async (ctx?: TelegramContext) => { const fromCtx = ctx?.me as { has_topics_enabled?: boolean } | undefined; @@ -261,7 +272,13 @@ export function createTelegramBot(opts: TelegramBotOptions) { botHasTopicsEnabled = fromCtx.has_topics_enabled; return botHasTopicsEnabled; } - if (typeof botHasTopicsEnabled === "boolean") return botHasTopicsEnabled; + if (typeof botHasTopicsEnabled === "boolean") { + return botHasTopicsEnabled; + } + if (typeof bot.api.getMe !== "function") { + botHasTopicsEnabled = false; + return botHasTopicsEnabled; + } try { const me = (await withTelegramApiErrorLogging({ operation: "getMe", @@ -296,8 +313,12 @@ export function createTelegramBot(opts: TelegramBotOptions) { try { const store = loadSessionStore(storePath); const entry = store[sessionKey]; - if (entry?.groupActivation === "always") return false; - if (entry?.groupActivation === "mention") return true; + if (entry?.groupActivation === "always") { + return false; + } + if (entry?.groupActivation === "mention") { + return true; + } } catch (err) { logVerbose(`Failed to load session for activation check: ${String(err)}`); } @@ -314,7 +335,9 @@ export function createTelegramBot(opts: TelegramBotOptions) { }); const resolveTelegramGroupConfig = (chatId: string | number, messageThreadId?: number) => { const groups = telegramCfg.groups; - if (!groups) return { groupConfig: undefined, topicConfig: undefined }; + if (!groups) { + return { groupConfig: undefined, topicConfig: undefined }; + } const groupKey = String(chatId); const groupConfig = groups[groupKey] ?? groups["*"]; const topicConfig = @@ -369,8 +392,12 @@ export function createTelegramBot(opts: TelegramBotOptions) { bot.on("message_reaction", async (ctx) => { try { const reaction = ctx.messageReaction; - if (!reaction) return; - if (shouldSkipUpdate(ctx)) return; + if (!reaction) { + return; + } + if (shouldSkipUpdate(ctx)) { + return; + } const chatId = reaction.chat.id; const messageId = reaction.message_id; @@ -378,21 +405,29 @@ export function createTelegramBot(opts: TelegramBotOptions) { // Resolve reaction notification mode (default: "own") const reactionMode = telegramCfg.reactionNotifications ?? "own"; - if (reactionMode === "off") return; - if (user?.is_bot) return; - if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) return; + if (reactionMode === "off") { + return; + } + if (user?.is_bot) { + return; + } + if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) { + return; + } // Detect added reactions const oldEmojis = new Set( reaction.old_reaction - .filter((r): r is { type: "emoji"; emoji: string } => r.type === "emoji") + .filter((r): r is ReactionTypeEmoji => r.type === "emoji") .map((r) => r.emoji), ); const addedReactions = reaction.new_reaction - .filter((r): r is { type: "emoji"; emoji: string } => r.type === "emoji") + .filter((r): r is ReactionTypeEmoji => r.type === "emoji") .filter((r) => !oldEmojis.has(r.emoji)); - if (addedReactions.length === 0) return; + if (addedReactions.length === 0) { + return; + } // Build sender label const senderName = user diff --git a/src/telegram/bot/delivery.test.ts b/src/telegram/bot/delivery.test.ts index 3cf1b2534..0fb388a35 100644 --- a/src/telegram/bot/delivery.test.ts +++ b/src/telegram/bot/delivery.test.ts @@ -1,7 +1,5 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - import type { Bot } from "grammy"; - +import { beforeEach, describe, expect, it, vi } from "vitest"; import { deliverReplies } from "./delivery.js"; const loadWebMedia = vi.fn(); diff --git a/src/telegram/bot/delivery.ts b/src/telegram/bot/delivery.ts index 4f45f9997..5583fec54 100644 --- a/src/telegram/bot/delivery.ts +++ b/src/telegram/bot/delivery.ts @@ -1,28 +1,28 @@ import { type Bot, GrammyError, InputFile } from "grammy"; -import { - markdownToTelegramChunks, - markdownToTelegramHtml, - renderTelegramHtmlText, -} from "../format.js"; -import { withTelegramApiErrorLogging } from "../api-logging.js"; -import { chunkMarkdownTextWithMode, type ChunkMode } from "../../auto-reply/chunk.js"; -import { splitTelegramCaption } from "../caption.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import type { ReplyToMode } from "../../config/config.js"; import type { MarkdownTableMode } from "../../config/types.base.js"; +import type { RuntimeEnv } from "../../runtime.js"; +import type { StickerMetadata, TelegramContext } from "./types.js"; +import { chunkMarkdownTextWithMode, type ChunkMode } from "../../auto-reply/chunk.js"; import { danger, logVerbose } from "../../globals.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { mediaKindFromMime } from "../../media/constants.js"; import { fetchRemoteMedia } from "../../media/fetch.js"; import { isGifMedia } from "../../media/mime.js"; import { saveMediaBuffer } from "../../media/store.js"; -import type { RuntimeEnv } from "../../runtime.js"; import { loadWebMedia } from "../../web/media.js"; +import { withTelegramApiErrorLogging } from "../api-logging.js"; +import { splitTelegramCaption } from "../caption.js"; +import { + markdownToTelegramChunks, + markdownToTelegramHtml, + renderTelegramHtmlText, +} from "../format.js"; import { buildInlineKeyboard } from "../send.js"; +import { cacheSticker, getCachedSticker } from "../sticker-cache.js"; import { resolveTelegramVoiceSend } from "../voice.js"; import { buildTelegramThreadParams, resolveTelegramReplyId } from "./helpers.js"; -import type { StickerMetadata, TelegramContext } from "./types.js"; -import { cacheSticker, getCachedSticker } from "../sticker-cache.js"; const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i; const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/; @@ -44,7 +44,7 @@ export async function deliverReplies(params: { linkPreview?: boolean; /** Optional quote text for Telegram reply_parameters. */ replyQuoteText?: string; -}) { +}): Promise<{ delivered: boolean }> { const { replies, chatId, @@ -58,6 +58,10 @@ export async function deliverReplies(params: { } = params; const chunkMode = params.chunkMode ?? "length"; let hasReplied = false; + let hasDelivered = false; + const markDelivered = () => { + hasDelivered = true; + }; const chunkText = (markdown: string) => { const markdownChunks = chunkMode === "newline" @@ -101,7 +105,9 @@ export async function deliverReplies(params: { const chunks = chunkText(reply.text || ""); for (let i = 0; i < chunks.length; i += 1) { const chunk = chunks[i]; - if (!chunk) continue; + if (!chunk) { + continue; + } // Only attach buttons to the first chunk. const shouldAttachButtons = i === 0 && replyMarkup; await sendTelegramText(bot, chatId, chunk.html, runtime, { @@ -114,6 +120,7 @@ export async function deliverReplies(params: { linkPreview, replyMarkup: shouldAttachButtons ? replyMarkup : undefined, }); + markDelivered(); if (replyToId && !hasReplied) { hasReplied = true; } @@ -165,18 +172,21 @@ export async function deliverReplies(params: { runtime, fn: () => bot.api.sendAnimation(chatId, file, { ...mediaParams }), }); + markDelivered(); } else if (kind === "image") { await withTelegramApiErrorLogging({ operation: "sendPhoto", runtime, fn: () => bot.api.sendPhoto(chatId, file, { ...mediaParams }), }); + markDelivered(); } else if (kind === "video") { await withTelegramApiErrorLogging({ operation: "sendVideo", runtime, fn: () => bot.api.sendVideo(chatId, file, { ...mediaParams }), }); + markDelivered(); } else if (kind === "audio") { const { useVoice } = resolveTelegramVoiceSend({ wantsVoice: reply.audioAsVoice === true, // default false (backward compatible) @@ -195,6 +205,7 @@ export async function deliverReplies(params: { shouldLog: (err) => !isVoiceMessagesForbidden(err), fn: () => bot.api.sendVoice(chatId, file, { ...mediaParams }), }); + markDelivered(); } catch (voiceErr) { // Fall back to text if voice messages are forbidden in this chat. // This happens when the recipient has Telegram Premium privacy settings @@ -221,6 +232,7 @@ export async function deliverReplies(params: { replyMarkup, replyQuoteText, }); + markDelivered(); // Skip this media item; continue with next. continue; } @@ -233,6 +245,7 @@ export async function deliverReplies(params: { runtime, fn: () => bot.api.sendAudio(chatId, file, { ...mediaParams }), }); + markDelivered(); } } else { await withTelegramApiErrorLogging({ @@ -240,6 +253,7 @@ export async function deliverReplies(params: { runtime, fn: () => bot.api.sendDocument(chatId, file, { ...mediaParams }), }); + markDelivered(); } if (replyToId && !hasReplied) { hasReplied = true; @@ -260,6 +274,7 @@ export async function deliverReplies(params: { linkPreview, replyMarkup: i === 0 ? replyMarkup : undefined, }); + markDelivered(); if (replyToId && !hasReplied) { hasReplied = true; } @@ -268,6 +283,8 @@ export async function deliverReplies(params: { } } } + + return { delivered: hasDelivered }; } export async function resolveMedia( @@ -291,7 +308,9 @@ export async function resolveMedia( logVerbose("telegram: skipping animated/video sticker (only static stickers supported)"); return null; } - if (!sticker.file_id) return null; + if (!sticker.file_id) { + return null; + } try { const file = await ctx.getFile(); @@ -310,7 +329,14 @@ export async function resolveMedia( fetchImpl, filePathHint: file.file_path, }); - const saved = await saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes); + const originalName = fetched.fileName ?? file.file_path; + const saved = await saveMediaBuffer( + fetched.buffer, + fetched.contentType, + "inbound", + maxBytes, + originalName, + ); // Check sticker cache for existing description const cached = sticker.file_unique_id ? getCachedSticker(sticker.file_unique_id) : null; @@ -361,8 +387,15 @@ export async function resolveMedia( } const m = - msg.photo?.[msg.photo.length - 1] ?? msg.video ?? msg.document ?? msg.audio ?? msg.voice; - if (!m?.file_id) return null; + msg.photo?.[msg.photo.length - 1] ?? + msg.video ?? + msg.video_note ?? + msg.document ?? + msg.audio ?? + msg.voice; + if (!m?.file_id) { + return null; + } const file = await ctx.getFile(); if (!file.file_path) { throw new Error("Telegram getFile returned no file_path"); @@ -377,11 +410,24 @@ export async function resolveMedia( fetchImpl, filePathHint: file.file_path, }); - const saved = await saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes); + const originalName = fetched.fileName ?? file.file_path; + const saved = await saveMediaBuffer( + fetched.buffer, + fetched.contentType, + "inbound", + maxBytes, + originalName, + ); let placeholder = ""; - if (msg.photo) placeholder = ""; - else if (msg.video) placeholder = ""; - else if (msg.audio || msg.voice) placeholder = ""; + if (msg.photo) { + placeholder = ""; + } else if (msg.video) { + placeholder = ""; + } else if (msg.video_note) { + placeholder = ""; + } else if (msg.audio || msg.voice) { + placeholder = ""; + } return { path: saved.path, contentType: saved.contentType, placeholder }; } diff --git a/src/telegram/bot/helpers.expand-text-links.test.ts b/src/telegram/bot/helpers.expand-text-links.test.ts index aed680682..7035a670a 100644 --- a/src/telegram/bot/helpers.expand-text-links.test.ts +++ b/src/telegram/bot/helpers.expand-text-links.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { expandTextLinks } from "./helpers.js"; describe("expandTextLinks", () => { diff --git a/src/telegram/bot/helpers.test.ts b/src/telegram/bot/helpers.test.ts index 8e90bb520..1f0a58132 100644 --- a/src/telegram/bot/helpers.test.ts +++ b/src/telegram/bot/helpers.test.ts @@ -91,8 +91,8 @@ describe("normalizeForwardedContext", () => { it("handles legacy forwards with signatures", () => { const ctx = normalizeForwardedContext({ forward_from_chat: { - title: "Moltbot Updates", - username: "moltbot", + title: "OpenClaw Updates", + username: "openclaw", id: 99, type: "channel", }, @@ -100,11 +100,11 @@ describe("normalizeForwardedContext", () => { forward_date: 789, } as any); expect(ctx).not.toBeNull(); - expect(ctx?.from).toBe("Moltbot Updates (Stan)"); + expect(ctx?.from).toBe("OpenClaw Updates (Stan)"); expect(ctx?.fromType).toBe("legacy_channel"); expect(ctx?.fromId).toBe("99"); - expect(ctx?.fromUsername).toBe("moltbot"); - expect(ctx?.fromTitle).toBe("Moltbot Updates"); + expect(ctx?.fromUsername).toBe("openclaw"); + expect(ctx?.fromTitle).toBe("OpenClaw Updates"); expect(ctx?.fromSignature).toBe("Stan"); expect(ctx?.date).toBe(789); }); diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index cd57392c0..4e059c879 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -1,5 +1,3 @@ -import { formatLocationText, type NormalizedLocation } from "../../channels/location.js"; -import type { TelegramAccountConfig } from "../../config/types.telegram.js"; import type { TelegramForwardChat, TelegramForwardOrigin, @@ -10,6 +8,7 @@ import type { TelegramStreamMode, TelegramVenue, } from "./types.js"; +import { formatLocationText, type NormalizedLocation } from "../../channels/location.js"; const TELEGRAM_GENERAL_TOPIC_ID = 1; @@ -61,11 +60,13 @@ export function buildTypingThreadParams(messageThreadId?: number) { return { message_thread_id: Math.trunc(messageThreadId) }; } -export function resolveTelegramStreamMode( - telegramCfg: Pick | undefined, -): TelegramStreamMode { +export function resolveTelegramStreamMode(telegramCfg?: { + streamMode?: TelegramStreamMode; +}): TelegramStreamMode { const raw = telegramCfg?.streamMode?.trim().toLowerCase(); - if (raw === "off" || raw === "partial" || raw === "block") return raw; + if (raw === "off" || raw === "partial" || raw === "block") { + return raw; + } return "partial"; } @@ -97,8 +98,12 @@ export function buildSenderLabel(msg: TelegramMessage, senderId?: number | strin senderId != null && `${senderId}`.trim() ? `${senderId}`.trim() : undefined; const fallbackId = normalizedSenderId ?? (msg.from?.id != null ? String(msg.from.id) : undefined); const idPart = fallbackId ? `id:${fallbackId}` : undefined; - if (label && idPart) return `${label} ${idPart}`; - if (label) return label; + if (label && idPart) { + return `${label} ${idPart}`; + } + if (label) { + return label; + } return idPart ?? "id:unknown"; } @@ -109,18 +114,26 @@ export function buildGroupLabel( ) { const title = msg.chat?.title; const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : ""; - if (title) return `${title} id:${chatId}${topicSuffix}`; + if (title) { + return `${title} id:${chatId}${topicSuffix}`; + } return `group:${chatId}${topicSuffix}`; } export function hasBotMention(msg: TelegramMessage, botUsername: string) { const text = (msg.text ?? msg.caption ?? "").toLowerCase(); - if (text.includes(`@${botUsername}`)) return true; + if (text.includes(`@${botUsername}`)) { + return true; + } const entities = msg.entities ?? msg.caption_entities ?? []; for (const ent of entities) { - if (ent.type !== "mention") continue; + if (ent.type !== "mention") { + continue; + } const slice = (msg.text ?? msg.caption ?? "").slice(ent.offset, ent.offset + ent.length); - if (slice.toLowerCase() === `@${botUsername}`) return true; + if (slice.toLowerCase() === `@${botUsername}`) { + return true; + } } return false; } @@ -133,16 +146,20 @@ type TelegramTextLinkEntity = { }; export function expandTextLinks(text: string, entities?: TelegramTextLinkEntity[] | null): string { - if (!text || !entities?.length) return text; + if (!text || !entities?.length) { + return text; + } const textLinks = entities .filter( (entity): entity is TelegramTextLinkEntity & { url: string } => entity.type === "text_link" && Boolean(entity.url), ) - .sort((a, b) => b.offset - a.offset); + .toSorted((a, b) => b.offset - a.offset); - if (textLinks.length === 0) return text; + if (textLinks.length === 0) { + return text; + } let result = text; for (const entity of textLinks) { @@ -155,9 +172,13 @@ export function expandTextLinks(text: string, entities?: TelegramTextLinkEntity[ } export function resolveTelegramReplyId(raw?: string): number | undefined { - if (!raw) return undefined; + if (!raw) { + return undefined; + } const parsed = Number(raw); - if (!Number.isFinite(parsed)) return undefined; + if (!Number.isFinite(parsed)) { + return undefined; + } return parsed; } @@ -185,17 +206,25 @@ export function describeReplyTarget(msg: TelegramMessage): TelegramReplyTarget | const replyBody = (reply.text ?? reply.caption ?? "").trim(); body = replyBody; if (!body) { - if (reply.photo) body = ""; - else if (reply.video) body = ""; - else if (reply.audio || reply.voice) body = ""; - else if (reply.document) body = ""; - else { + if (reply.photo) { + body = ""; + } else if (reply.video) { + body = ""; + } else if (reply.audio || reply.voice) { + body = ""; + } else if (reply.document) { + body = ""; + } else { const locationData = extractTelegramLocation(reply); - if (locationData) body = formatLocationText(locationData); + if (locationData) { + body = formatLocationText(locationData); + } } } } - if (!body) return null; + if (!body) { + return null; + } const sender = reply ? buildSenderName(reply) : undefined; const senderLabel = sender ? `${sender}` : "unknown sender"; @@ -243,7 +272,9 @@ function buildForwardedContextFromUser(params: { type: string; }): TelegramForwardedContext | null { const { display, name, username, id } = normalizeForwardedUserLabel(params.user); - if (!display) return null; + if (!display) { + return null; + } return { from: display, date: params.date, @@ -260,7 +291,9 @@ function buildForwardedContextFromHiddenName(params: { type: string; }): TelegramForwardedContext | null { const trimmed = params.name?.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } return { from: trimmed, date: params.date, @@ -278,7 +311,9 @@ function buildForwardedContextFromChat(params: { const fallbackKind = params.type === "channel" || params.type === "legacy_channel" ? "channel" : "chat"; const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind); - if (!display) return null; + if (!display) { + return null; + } const signature = params.signature?.trim() || undefined; const from = signature ? `${display} (${signature})` : display; return { @@ -339,7 +374,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward if (forwardMsg.forward_origin) { const originContext = resolveForwardOrigin(forwardMsg.forward_origin, signature); - if (originContext) return originContext; + if (originContext) { + return originContext; + } } if (forwardMsg.forward_from_chat) { @@ -351,7 +388,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward type: legacyType, signature, }); - if (legacyContext) return legacyContext; + if (legacyContext) { + return legacyContext; + } } if (forwardMsg.forward_from) { @@ -360,7 +399,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward date: forwardMsg.forward_date, type: "legacy_user", }); - if (legacyContext) return legacyContext; + if (legacyContext) { + return legacyContext; + } } const hiddenContext = buildForwardedContextFromHiddenName({ @@ -368,7 +409,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward date: forwardMsg.forward_date, type: "legacy_hidden_user", }); - if (hiddenContext) return hiddenContext; + if (hiddenContext) { + return hiddenContext; + } return null; } diff --git a/src/telegram/download.test.ts b/src/telegram/download.test.ts index 6625cbcf5..5738877ca 100644 --- a/src/telegram/download.test.ts +++ b/src/telegram/download.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { downloadTelegramFile, getTelegramFile, type TelegramFileInfo } from "./download.js"; describe("telegram download", () => { diff --git a/src/telegram/download.ts b/src/telegram/download.ts index 1b3c61e22..748749c4c 100644 --- a/src/telegram/download.ts +++ b/src/telegram/download.ts @@ -27,7 +27,9 @@ export async function downloadTelegramFile( info: TelegramFileInfo, maxBytes?: number, ): Promise { - if (!info.file_path) throw new Error("file_path missing"); + if (!info.file_path) { + throw new Error("file_path missing"); + } const url = `https://api.telegram.org/file/bot${token}/${info.file_path}`; const res = await fetch(url); if (!res.ok || !res.body) { @@ -40,8 +42,10 @@ export async function downloadTelegramFile( filePath: info.file_path, }); // save with inbound subdir - const saved = await saveMediaBuffer(array, mime, "inbound", maxBytes); + const saved = await saveMediaBuffer(array, mime, "inbound", maxBytes, info.file_path); // Ensure extension matches mime if possible - if (!saved.contentType && mime) saved.contentType = mime; + if (!saved.contentType && mime) { + saved.contentType = mime; + } return saved; } diff --git a/src/telegram/draft-chunking.test.ts b/src/telegram/draft-chunking.test.ts index d2ebb75f9..1885efd94 100644 --- a/src/telegram/draft-chunking.test.ts +++ b/src/telegram/draft-chunking.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js"; describe("resolveTelegramDraftStreamingChunking", () => { @@ -14,7 +13,7 @@ describe("resolveTelegramDraftStreamingChunking", () => { }); it("clamps to telegram.textChunkLimit", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { allowFrom: ["*"], textChunkLimit: 150 } }, }; const chunking = resolveTelegramDraftStreamingChunking(cfg, "default"); @@ -26,7 +25,7 @@ describe("resolveTelegramDraftStreamingChunking", () => { }); it("supports per-account overrides", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { allowFrom: ["*"], diff --git a/src/telegram/draft-chunking.ts b/src/telegram/draft-chunking.ts index fd0d33e15..8c594cb65 100644 --- a/src/telegram/draft-chunking.ts +++ b/src/telegram/draft-chunking.ts @@ -1,13 +1,13 @@ +import type { OpenClawConfig } from "../config/config.js"; import { resolveTextChunkLimit } from "../auto-reply/chunk.js"; import { getChannelDock } from "../channels/dock.js"; -import type { MoltbotConfig } from "../config/config.js"; import { normalizeAccountId } from "../routing/session-key.js"; const DEFAULT_TELEGRAM_DRAFT_STREAM_MIN = 200; const DEFAULT_TELEGRAM_DRAFT_STREAM_MAX = 800; export function resolveTelegramDraftStreamingChunking( - cfg: MoltbotConfig | undefined, + cfg: OpenClawConfig | undefined, accountId?: string | null, ): { minChars: number; diff --git a/src/telegram/draft-stream.test.ts b/src/telegram/draft-stream.test.ts new file mode 100644 index 000000000..b67e13fca --- /dev/null +++ b/src/telegram/draft-stream.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; +import { createTelegramDraftStream } from "./draft-stream.js"; + +describe("createTelegramDraftStream", () => { + it("passes message_thread_id when provided", () => { + const api = { sendMessageDraft: vi.fn().mockResolvedValue(true) }; + const stream = createTelegramDraftStream({ + api: api as any, + chatId: 123, + draftId: 42, + messageThreadId: 99, + }); + + stream.update("Hello"); + + expect(api.sendMessageDraft).toHaveBeenCalledWith(123, 42, "Hello", { + message_thread_id: 99, + }); + }); + + it("omits message_thread_id for general topic id", () => { + const api = { sendMessageDraft: vi.fn().mockResolvedValue(true) }; + const stream = createTelegramDraftStream({ + api: api as any, + chatId: 123, + draftId: 42, + messageThreadId: 1, + }); + + stream.update("Hello"); + + expect(api.sendMessageDraft).toHaveBeenCalledWith(123, 42, "Hello", undefined); + }); +}); diff --git a/src/telegram/draft-stream.ts b/src/telegram/draft-stream.ts index 74c2fa26d..194db7170 100644 --- a/src/telegram/draft-stream.ts +++ b/src/telegram/draft-stream.ts @@ -1,4 +1,5 @@ import type { Bot } from "grammy"; +import { buildTelegramThreadParams } from "./bot/helpers.js"; const TELEGRAM_DRAFT_MAX_CHARS = 4096; const DEFAULT_THROTTLE_MS = 300; @@ -24,10 +25,7 @@ export function createTelegramDraftStream(params: { const rawDraftId = Number.isFinite(params.draftId) ? Math.trunc(params.draftId) : 1; const draftId = rawDraftId === 0 ? 1 : Math.abs(rawDraftId); const chatId = params.chatId; - const threadParams = - typeof params.messageThreadId === "number" - ? { message_thread_id: Math.trunc(params.messageThreadId) } - : undefined; + const threadParams = buildTelegramThreadParams(params.messageThreadId); let lastSentText = ""; let lastSentAt = 0; @@ -37,9 +35,13 @@ export function createTelegramDraftStream(params: { let stopped = false; const sendDraft = async (text: string) => { - if (stopped) return; + if (stopped) { + return; + } const trimmed = text.trimEnd(); - if (!trimmed) return; + if (!trimmed) { + return; + } if (trimmed.length > maxChars) { // Drafts are capped at 4096 chars. Stop streaming once we exceed the cap // so we don't keep sending failing updates or a truncated preview. @@ -47,7 +49,9 @@ export function createTelegramDraftStream(params: { params.warn?.(`telegram draft stream stopped (draft length ${trimmed.length} > ${maxChars})`); return; } - if (trimmed === lastSentText) return; + if (trimmed === lastSentText) { + return; + } lastSentText = trimmed; lastSentAt = Date.now(); try { @@ -70,22 +74,32 @@ export function createTelegramDraftStream(params: { return; } const text = pendingText; - pendingText = ""; - if (!text.trim()) { - if (pendingText) schedule(); + const trimmed = text.trim(); + if (!trimmed) { + if (pendingText === text) { + pendingText = ""; + } + if (pendingText) { + schedule(); + } return; } + pendingText = ""; inFlight = true; try { await sendDraft(text); } finally { inFlight = false; } - if (pendingText) schedule(); + if (pendingText) { + schedule(); + } }; const schedule = () => { - if (timer) return; + if (timer) { + return; + } const delay = Math.max(0, throttleMs - (Date.now() - lastSentAt)); timer = setTimeout(() => { void flush(); @@ -93,7 +107,9 @@ export function createTelegramDraftStream(params: { }; const update = (text: string) => { - if (stopped) return; + if (stopped) { + return; + } pendingText = text; if (inFlight) { schedule(); diff --git a/src/telegram/fetch.test.ts b/src/telegram/fetch.test.ts index 17cda1d00..437359286 100644 --- a/src/telegram/fetch.test.ts +++ b/src/telegram/fetch.test.ts @@ -39,7 +39,7 @@ describe("resolveTelegramFetch", () => { }); it("honors env enable override", async () => { - vi.stubEnv("CLAWDBOT_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "1"); + vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "1"); globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule(); resolveTelegramFetch(); @@ -54,7 +54,7 @@ describe("resolveTelegramFetch", () => { }); it("env disable override wins over config", async () => { - vi.stubEnv("CLAWDBOT_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", "1"); + vi.stubEnv("OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", "1"); globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule(); resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } }); diff --git a/src/telegram/fetch.ts b/src/telegram/fetch.ts index ebed468c9..96cb09277 100644 --- a/src/telegram/fetch.ts +++ b/src/telegram/fetch.ts @@ -1,6 +1,6 @@ import * as net from "node:net"; -import { resolveFetch } from "../infra/fetch.js"; import type { TelegramNetworkConfig } from "../config/types.telegram.js"; +import { resolveFetch } from "../infra/fetch.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveTelegramAutoSelectFamilyDecision } from "./network-config.js"; @@ -11,7 +11,9 @@ const log = createSubsystemLogger("telegram/network"); // See: https://github.com/nodejs/node/issues/54359 function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void { const decision = resolveTelegramAutoSelectFamilyDecision({ network }); - if (decision.value === null || decision.value === appliedAutoSelectFamily) return; + if (decision.value === null || decision.value === appliedAutoSelectFamily) { + return; + } appliedAutoSelectFamily = decision.value; if (typeof net.setDefaultAutoSelectFamily === "function") { @@ -31,7 +33,9 @@ export function resolveTelegramFetch( options?: { network?: TelegramNetworkConfig }, ): typeof fetch | undefined { applyTelegramNetworkWorkarounds(options?.network); - if (proxyFetch) return resolveFetch(proxyFetch); + if (proxyFetch) { + return resolveFetch(proxyFetch); + } const fetchImpl = resolveFetch(); if (!fetchImpl) { throw new Error("fetch is not available; set channels.telegram.proxy in config"); diff --git a/src/telegram/format.test.ts b/src/telegram/format.test.ts index 831782815..7dedc2c6f 100644 --- a/src/telegram/format.test.ts +++ b/src/telegram/format.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { markdownToTelegramHtml } from "./format.js"; describe("markdownToTelegramHtml", () => { @@ -47,4 +46,26 @@ describe("markdownToTelegramHtml", () => { const res = markdownToTelegramHtml("```js\nconst x = 1;\n```"); expect(res).toBe("
const x = 1;\n
"); }); + + it("properly nests overlapping bold and autolink (#4071)", () => { + const res = markdownToTelegramHtml("**start https://example.com** end"); + expect(res).toMatch( + /start https:\/\/example\.com<\/a><\/b> end/, + ); + }); + + it("properly nests link inside bold", () => { + const res = markdownToTelegramHtml("**bold [link](https://example.com) text**"); + expect(res).toBe('bold link text'); + }); + + it("properly nests bold wrapping a link with trailing text", () => { + const res = markdownToTelegramHtml("**[link](https://example.com) rest**"); + expect(res).toBe('link rest'); + }); + + it("properly nests bold inside a link", () => { + const res = markdownToTelegramHtml("[**bold**](https://example.com)"); + expect(res).toBe('bold'); + }); }); diff --git a/src/telegram/format.ts b/src/telegram/format.ts index 472fc1f43..e3d7e4c43 100644 --- a/src/telegram/format.ts +++ b/src/telegram/format.ts @@ -1,3 +1,4 @@ +import type { MarkdownTableMode } from "../config/types.base.js"; import { chunkMarkdownIR, markdownToIR, @@ -5,7 +6,6 @@ import { type MarkdownIR, } from "../markdown/ir.js"; import { renderMarkdownWithMarkers } from "../markdown/render.js"; -import type { MarkdownTableMode } from "../config/types.base.js"; export type TelegramFormattedChunk = { html: string; @@ -22,8 +22,12 @@ function escapeHtmlAttr(text: string): string { function buildTelegramLink(link: MarkdownLinkSpan, _text: string) { const href = link.href.trim(); - if (!href) return null; - if (link.start === link.end) return null; + if (!href) { + return null; + } + if (link.start === link.end) { + return null; + } const safeHref = escapeHtmlAttr(href); return { start: link.start, @@ -65,7 +69,9 @@ export function renderTelegramHtmlText( options: { textMode?: "markdown" | "html"; tableMode?: MarkdownTableMode } = {}, ): string { const textMode = options.textMode ?? "markdown"; - if (textMode === "html") return text; + if (textMode === "html") { + return text; + } return markdownToTelegramHtml(text, { tableMode: options.tableMode }); } diff --git a/src/telegram/group-migration.test.ts b/src/telegram/group-migration.test.ts index f6cc03360..4d4ca9758 100644 --- a/src/telegram/group-migration.test.ts +++ b/src/telegram/group-migration.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { migrateTelegramGroupConfig } from "./group-migration.js"; describe("migrateTelegramGroupConfig", () => { diff --git a/src/telegram/group-migration.ts b/src/telegram/group-migration.ts index 588fb0c53..085aeabaf 100644 --- a/src/telegram/group-migration.ts +++ b/src/telegram/group-migration.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { TelegramGroupConfig } from "../config/types.telegram.js"; import { normalizeAccountId } from "../routing/session-key.js"; @@ -13,15 +13,21 @@ export type TelegramGroupMigrationResult = { }; function resolveAccountGroups( - cfg: MoltbotConfig, + cfg: OpenClawConfig, accountId?: string | null, ): { groups?: TelegramGroups } { - if (!accountId) return {}; + if (!accountId) { + return {}; + } const normalized = normalizeAccountId(accountId); const accounts = cfg.channels?.telegram?.accounts; - if (!accounts || typeof accounts !== "object") return {}; + if (!accounts || typeof accounts !== "object") { + return {}; + } const exact = accounts[normalized]; - if (exact?.groups) return { groups: exact.groups }; + if (exact?.groups) { + return { groups: exact.groups }; + } const matchKey = Object.keys(accounts).find( (key) => key.toLowerCase() === normalized.toLowerCase(), ); @@ -33,17 +39,25 @@ export function migrateTelegramGroupsInPlace( oldChatId: string, newChatId: string, ): { migrated: boolean; skippedExisting: boolean } { - if (!groups) return { migrated: false, skippedExisting: false }; - if (oldChatId === newChatId) return { migrated: false, skippedExisting: false }; - if (!Object.hasOwn(groups, oldChatId)) return { migrated: false, skippedExisting: false }; - if (Object.hasOwn(groups, newChatId)) return { migrated: false, skippedExisting: true }; + if (!groups) { + return { migrated: false, skippedExisting: false }; + } + if (oldChatId === newChatId) { + return { migrated: false, skippedExisting: false }; + } + if (!Object.hasOwn(groups, oldChatId)) { + return { migrated: false, skippedExisting: false }; + } + if (Object.hasOwn(groups, newChatId)) { + return { migrated: false, skippedExisting: true }; + } groups[newChatId] = groups[oldChatId]; delete groups[oldChatId]; return { migrated: true, skippedExisting: false }; } export function migrateTelegramGroupConfig(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; oldChatId: string; newChatId: string; @@ -59,7 +73,9 @@ export function migrateTelegramGroupConfig(params: { migrated = true; scopes.push("account"); } - if (result.skippedExisting) skippedExisting = true; + if (result.skippedExisting) { + skippedExisting = true; + } } const globalGroups = params.cfg.channels?.telegram?.groups; @@ -69,7 +85,9 @@ export function migrateTelegramGroupConfig(params: { migrated = true; scopes.push("global"); } - if (result.skippedExisting) skippedExisting = true; + if (result.skippedExisting) { + skippedExisting = true; + } } return { migrated, skippedExisting, scopes }; diff --git a/src/telegram/inline-buttons.test.ts b/src/telegram/inline-buttons.test.ts index 687d29bdd..5828e3d1e 100644 --- a/src/telegram/inline-buttons.test.ts +++ b/src/telegram/inline-buttons.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { resolveTelegramTargetChatType } from "./inline-buttons.js"; describe("resolveTelegramTargetChatType", () => { diff --git a/src/telegram/inline-buttons.ts b/src/telegram/inline-buttons.ts index 72f5c8232..1cf770d0a 100644 --- a/src/telegram/inline-buttons.ts +++ b/src/telegram/inline-buttons.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { TelegramInlineButtonsScope } from "../config/types.telegram.js"; import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js"; import { parseTelegramTarget } from "./targets.js"; @@ -6,7 +6,9 @@ import { parseTelegramTarget } from "./targets.js"; const DEFAULT_INLINE_BUTTONS_SCOPE: TelegramInlineButtonsScope = "allowlist"; function normalizeInlineButtonsScope(value: unknown): TelegramInlineButtonsScope | undefined { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const trimmed = value.trim().toLowerCase(); if ( trimmed === "off" || @@ -23,7 +25,9 @@ function normalizeInlineButtonsScope(value: unknown): TelegramInlineButtonsScope function resolveInlineButtonsScopeFromCapabilities( capabilities: unknown, ): TelegramInlineButtonsScope { - if (!capabilities) return DEFAULT_INLINE_BUTTONS_SCOPE; + if (!capabilities) { + return DEFAULT_INLINE_BUTTONS_SCOPE; + } if (Array.isArray(capabilities)) { const enabled = capabilities.some( (entry) => String(entry).trim().toLowerCase() === "inlinebuttons", @@ -38,7 +42,7 @@ function resolveInlineButtonsScopeFromCapabilities( } export function resolveTelegramInlineButtonsScope(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; }): TelegramInlineButtonsScope { const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId }); @@ -46,7 +50,7 @@ export function resolveTelegramInlineButtonsScope(params: { } export function isTelegramInlineButtonsEnabled(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; }): boolean { if (params.accountId) { @@ -62,10 +66,14 @@ export function isTelegramInlineButtonsEnabled(params: { } export function resolveTelegramTargetChatType(target: string): "direct" | "group" | "unknown" { - if (!target.trim()) return "unknown"; + if (!target.trim()) { + return "unknown"; + } const parsed = parseTelegramTarget(target); const chatId = parsed.chatId.trim(); - if (!chatId) return "unknown"; + if (!chatId) { + return "unknown"; + } if (/^-?\d+$/.test(chatId)) { return chatId.startsWith("-") ? "group" : "direct"; } diff --git a/src/telegram/monitor.test.ts b/src/telegram/monitor.test.ts index 2fc46827b..20ffd4e1b 100644 --- a/src/telegram/monitor.test.ts +++ b/src/telegram/monitor.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { monitorTelegramProvider } from "./monitor.js"; type MockCtx = { @@ -54,8 +53,12 @@ vi.mock("./bot.js", () => ({ const chatId = ctx.message.chat.id; const isGroup = ctx.message.chat.type !== "private"; const text = ctx.message.text ?? ctx.message.caption ?? ""; - if (isGroup && !text.includes("@mybot")) return; - if (!text.trim()) return; + if (isGroup && !text.includes("@mybot")) { + return; + } + if (!text.trim()) { + return; + } await api.sendMessage(chatId, `echo:${text}`, { parse_mode: "HTML" }); }; return { diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index c3b3a5a2f..7a3a796b5 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -1,11 +1,11 @@ import { type RunOptions, run } from "@grammyjs/runner"; -import type { MoltbotConfig } from "../config/config.js"; -import { loadConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; import { resolveAgentMaxConcurrent } from "../config/agent-limits.js"; +import { loadConfig } from "../config/config.js"; import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; import { formatErrorMessage } from "../infra/errors.js"; import { formatDurationMs } from "../infra/format-duration.js"; -import type { RuntimeEnv } from "../runtime.js"; import { resolveTelegramAccount } from "./accounts.js"; import { resolveTelegramAllowedUpdates } from "./allowed-updates.js"; import { createTelegramBot } from "./bot.js"; @@ -17,7 +17,7 @@ import { startTelegramWebhook } from "./webhook.js"; export type MonitorTelegramOpts = { token?: string; accountId?: string; - config?: MoltbotConfig; + config?: OpenClawConfig; runtime?: RuntimeEnv; abortSignal?: AbortSignal; useWebhook?: boolean; @@ -28,7 +28,7 @@ export type MonitorTelegramOpts = { webhookUrl?: string; }; -export function createTelegramRunnerOptions(cfg: MoltbotConfig): RunOptions { +export function createTelegramRunnerOptions(cfg: OpenClawConfig): RunOptions { return { sink: { concurrency: resolveAgentMaxConcurrent(cfg), @@ -57,7 +57,9 @@ const TELEGRAM_POLL_RESTART_POLICY = { }; const isGetUpdatesConflict = (err: unknown) => { - if (!err || typeof err !== "object") return false; + if (!err || typeof err !== "object") { + return false; + } const typed = err as { error_code?: number; errorCode?: number; @@ -66,7 +68,9 @@ const isGetUpdatesConflict = (err: unknown) => { message?: string; }; const errorCode = typed.error_code ?? typed.errorCode; - if (errorCode !== 409) return false; + if (errorCode !== 409) { + return false; + } const haystack = [typed.method, typed.description, typed.message] .filter((value): value is string => typeof value === "string") .join(" ") @@ -85,9 +89,13 @@ const NETWORK_ERROR_SNIPPETS = [ ]; const isNetworkRelatedError = (err: unknown) => { - if (!err) return false; + if (!err) { + return false; + } const message = formatErrorMessage(err).toLowerCase(); - if (!message) return false; + if (!message) { + return false; + } return NETWORK_ERROR_SNIPPETS.some((snippet) => message.includes(snippet)); }; @@ -105,14 +113,15 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { } const proxyFetch = - opts.proxyFetch ?? - (account.config.proxy ? makeProxyFetch(account.config.proxy as string) : undefined); + opts.proxyFetch ?? (account.config.proxy ? makeProxyFetch(account.config.proxy) : undefined); let lastUpdateId = await readTelegramUpdateOffset({ accountId: account.accountId, }); const persistUpdateId = async (updateId: number) => { - if (lastUpdateId !== null && updateId <= lastUpdateId) return; + if (lastUpdateId !== null && updateId <= lastUpdateId) { + return; + } lastUpdateId = updateId; try { await writeTelegramUpdateOffset({ @@ -189,7 +198,9 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { try { await sleepWithAbort(delayMs, opts.abortSignal); } catch (sleepErr) { - if (opts.abortSignal?.aborted) return; + if (opts.abortSignal?.aborted) { + return; + } throw sleepErr; } } finally { diff --git a/src/telegram/network-config.test.ts b/src/telegram/network-config.test.ts index cb4bc4c6e..ed4aa8a01 100644 --- a/src/telegram/network-config.test.ts +++ b/src/telegram/network-config.test.ts @@ -1,30 +1,29 @@ import { describe, expect, it } from "vitest"; - import { resolveTelegramAutoSelectFamilyDecision } from "./network-config.js"; describe("resolveTelegramAutoSelectFamilyDecision", () => { it("prefers env enable over env disable", () => { const decision = resolveTelegramAutoSelectFamilyDecision({ env: { - CLAWDBOT_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY: "1", - CLAWDBOT_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY: "1", + OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY: "1", + OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY: "1", }, nodeMajor: 22, }); expect(decision).toEqual({ value: true, - source: "env:CLAWDBOT_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", + source: "env:OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", }); }); it("uses env disable when set", () => { const decision = resolveTelegramAutoSelectFamilyDecision({ - env: { CLAWDBOT_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY: "1" }, + env: { OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY: "1" }, nodeMajor: 22, }); expect(decision).toEqual({ value: false, - source: "env:CLAWDBOT_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", + source: "env:OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", }); }); diff --git a/src/telegram/network-config.ts b/src/telegram/network-config.ts index ac5dd05a7..4a8fb1ef1 100644 --- a/src/telegram/network-config.ts +++ b/src/telegram/network-config.ts @@ -1,11 +1,10 @@ import process from "node:process"; - -import { isTruthyEnvValue } from "../infra/env.js"; import type { TelegramNetworkConfig } from "../config/types.telegram.js"; +import { isTruthyEnvValue } from "../infra/env.js"; export const TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV = - "CLAWDBOT_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY"; -export const TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV = "CLAWDBOT_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY"; + "OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY"; +export const TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV = "OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY"; export type TelegramAutoSelectFamilyDecision = { value: boolean | null; diff --git a/src/telegram/network-errors.test.ts b/src/telegram/network-errors.test.ts index db582355f..462932bd2 100644 --- a/src/telegram/network-errors.test.ts +++ b/src/telegram/network-errors.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { isRecoverableTelegramNetworkError } from "./network-errors.js"; describe("isRecoverableTelegramNetworkError", () => { diff --git a/src/telegram/network-errors.ts b/src/telegram/network-errors.ts index bb3432432..0b658f0c9 100644 --- a/src/telegram/network-errors.ts +++ b/src/telegram/network-errors.ts @@ -43,17 +43,27 @@ function normalizeCode(code?: string): string { } function getErrorName(err: unknown): string { - if (!err || typeof err !== "object") return ""; + if (!err || typeof err !== "object") { + return ""; + } return "name" in err ? String(err.name) : ""; } function getErrorCode(err: unknown): string | undefined { const direct = extractErrorCode(err); - if (direct) return direct; - if (!err || typeof err !== "object") return undefined; + if (direct) { + return direct; + } + if (!err || typeof err !== "object") { + return undefined; + } const errno = (err as { errno?: unknown }).errno; - if (typeof errno === "string") return errno; - if (typeof errno === "number") return String(errno); + if (typeof errno === "string") { + return errno; + } + if (typeof errno === "number") { + return String(errno); + } return undefined; } @@ -64,19 +74,27 @@ function collectErrorCandidates(err: unknown): unknown[] { while (queue.length > 0) { const current = queue.shift(); - if (current == null || seen.has(current)) continue; + if (current == null || seen.has(current)) { + continue; + } seen.add(current); candidates.push(current); if (typeof current === "object") { const cause = (current as { cause?: unknown }).cause; - if (cause && !seen.has(cause)) queue.push(cause); + if (cause && !seen.has(cause)) { + queue.push(cause); + } const reason = (current as { reason?: unknown }).reason; - if (reason && !seen.has(reason)) queue.push(reason); + if (reason && !seen.has(reason)) { + queue.push(reason); + } const errors = (current as { errors?: unknown }).errors; if (Array.isArray(errors)) { for (const nested of errors) { - if (nested && !seen.has(nested)) queue.push(nested); + if (nested && !seen.has(nested)) { + queue.push(nested); + } } } } @@ -91,7 +109,9 @@ export function isRecoverableTelegramNetworkError( err: unknown, options: { context?: TelegramNetworkErrorContext; allowMessageMatch?: boolean } = {}, ): boolean { - if (!err) return false; + if (!err) { + return false; + } const allowMessageMatch = typeof options.allowMessageMatch === "boolean" ? options.allowMessageMatch @@ -99,10 +119,14 @@ export function isRecoverableTelegramNetworkError( for (const candidate of collectErrorCandidates(err)) { const code = normalizeCode(getErrorCode(candidate)); - if (code && RECOVERABLE_ERROR_CODES.has(code)) return true; + if (code && RECOVERABLE_ERROR_CODES.has(code)) { + return true; + } const name = getErrorName(candidate); - if (name && RECOVERABLE_ERROR_NAMES.has(name)) return true; + if (name && RECOVERABLE_ERROR_NAMES.has(name)) { + return true; + } if (allowMessageMatch) { const message = formatErrorMessage(candidate).toLowerCase(); diff --git a/src/telegram/pairing-store.test.ts b/src/telegram/pairing-store.test.ts index 5a13e4658..08ef7bdb2 100644 --- a/src/telegram/pairing-store.test.ts +++ b/src/telegram/pairing-store.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { approveTelegramPairingCode, listTelegramPairingRequests, @@ -12,14 +10,17 @@ import { } from "./pairing-store.js"; async function withTempStateDir(fn: (stateDir: string) => Promise) { - const previous = process.env.CLAWDBOT_STATE_DIR; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-pairing-")); - process.env.CLAWDBOT_STATE_DIR = dir; + const previous = process.env.OPENCLAW_STATE_DIR; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pairing-")); + process.env.OPENCLAW_STATE_DIR = dir; try { return await fn(dir); } finally { - if (previous === undefined) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = previous; + if (previous === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previous; + } await fs.rm(dir, { recursive: true, force: true }); } } diff --git a/src/telegram/pairing-store.ts b/src/telegram/pairing-store.ts index 087fa1e16..74223fb57 100644 --- a/src/telegram/pairing-store.ts +++ b/src/telegram/pairing-store.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { addChannelAllowFromStoreEntry, approveChannelPairingCode, @@ -79,7 +79,9 @@ export async function approveTelegramPairingCode(params: { code: params.code, env: params.env, }); - if (!res) return null; + if (!res) { + return null; + } const entry = res.entry ? { chatId: res.entry.id, @@ -95,7 +97,7 @@ export async function approveTelegramPairingCode(params: { } export async function resolveTelegramEffectiveAllowFrom(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; }): Promise<{ dm: string[]; group: string[] }> { const env = params.env ?? process.env; diff --git a/src/telegram/proxy.test.ts b/src/telegram/proxy.test.ts new file mode 100644 index 000000000..71fd5f88e --- /dev/null +++ b/src/telegram/proxy.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from "vitest"; + +const { ProxyAgent, undiciFetch, proxyAgentSpy, getLastAgent } = vi.hoisted(() => { + const undiciFetch = vi.fn(); + const proxyAgentSpy = vi.fn(); + class ProxyAgent { + static lastCreated: ProxyAgent | undefined; + proxyUrl: string; + constructor(proxyUrl: string) { + this.proxyUrl = proxyUrl; + ProxyAgent.lastCreated = this; + proxyAgentSpy(proxyUrl); + } + } + + return { + ProxyAgent, + undiciFetch, + proxyAgentSpy, + getLastAgent: () => ProxyAgent.lastCreated, + }; +}); + +vi.mock("undici", () => ({ + ProxyAgent, + fetch: undiciFetch, +})); + +import { makeProxyFetch } from "./proxy.js"; + +describe("makeProxyFetch", () => { + it("uses undici fetch with ProxyAgent dispatcher", async () => { + const proxyUrl = "http://proxy.test:8080"; + undiciFetch.mockResolvedValue({ ok: true }); + + const proxyFetch = makeProxyFetch(proxyUrl); + await proxyFetch("https://api.telegram.org/bot123/getMe"); + + expect(proxyAgentSpy).toHaveBeenCalledWith(proxyUrl); + expect(undiciFetch).toHaveBeenCalledWith( + "https://api.telegram.org/bot123/getMe", + expect.objectContaining({ dispatcher: getLastAgent() }), + ); + }); +}); diff --git a/src/telegram/proxy.ts b/src/telegram/proxy.ts index 19d53d569..84251d7fe 100644 --- a/src/telegram/proxy.ts +++ b/src/telegram/proxy.ts @@ -1,11 +1,11 @@ // @ts-nocheck -import { ProxyAgent } from "undici"; +import { ProxyAgent, fetch as undiciFetch } from "undici"; import { wrapFetchWithAbortSignal } from "../infra/fetch.js"; export function makeProxyFetch(proxyUrl: string): typeof fetch { const agent = new ProxyAgent(proxyUrl); return wrapFetchWithAbortSignal((input: RequestInfo | URL, init?: RequestInit) => { const base = init ? { ...init } : {}; - return fetch(input, { ...base, dispatcher: agent }); + return undiciFetch(input, { ...base, dispatcher: agent }); }); } diff --git a/src/telegram/reaction-level.test.ts b/src/telegram/reaction-level.test.ts index 1f077ff9d..a90f49f20 100644 --- a/src/telegram/reaction-level.test.ts +++ b/src/telegram/reaction-level.test.ts @@ -1,6 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveTelegramReactionLevel } from "./reaction-level.js"; describe("resolveTelegramReactionLevel", () => { @@ -19,7 +18,7 @@ describe("resolveTelegramReactionLevel", () => { }); it("defaults to minimal level when reactionLevel is not set", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: {} }, }; @@ -31,7 +30,7 @@ describe("resolveTelegramReactionLevel", () => { }); it("returns off level with no reactions enabled", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { reactionLevel: "off" } }, }; @@ -43,7 +42,7 @@ describe("resolveTelegramReactionLevel", () => { }); it("returns ack level with only ackEnabled", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { reactionLevel: "ack" } }, }; @@ -55,7 +54,7 @@ describe("resolveTelegramReactionLevel", () => { }); it("returns minimal level with agent reactions enabled and minimal guidance", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { reactionLevel: "minimal" } }, }; @@ -67,7 +66,7 @@ describe("resolveTelegramReactionLevel", () => { }); it("returns extensive level with agent reactions enabled and extensive guidance", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { reactionLevel: "extensive" } }, }; @@ -79,7 +78,7 @@ describe("resolveTelegramReactionLevel", () => { }); it("resolves reaction level from a specific account", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { reactionLevel: "ack", @@ -98,7 +97,7 @@ describe("resolveTelegramReactionLevel", () => { }); it("falls back to global level when account has no reactionLevel", () => { - const cfg: MoltbotConfig = { + const cfg: OpenClawConfig = { channels: { telegram: { reactionLevel: "minimal", diff --git a/src/telegram/reaction-level.ts b/src/telegram/reaction-level.ts index 0105f0fb3..2a88c573a 100644 --- a/src/telegram/reaction-level.ts +++ b/src/telegram/reaction-level.ts @@ -1,4 +1,4 @@ -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveTelegramAccount } from "./accounts.js"; export type TelegramReactionLevel = "off" | "ack" | "minimal" | "extensive"; @@ -17,7 +17,7 @@ export type ResolvedReactionLevel = { * Resolve the effective reaction level and its implications. */ export function resolveTelegramReactionLevel(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string; }): ResolvedReactionLevel { const account = resolveTelegramAccount({ diff --git a/src/telegram/send.ts b/src/telegram/send.ts index e3f3ac30e..cf5f80298 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -5,13 +5,13 @@ import type { ReactionTypeEmoji, } from "@grammyjs/types"; import { type ApiClientOptions, Bot, HttpError, InputFile } from "grammy"; +import type { RetryConfig } from "../infra/retry.js"; import { loadConfig } from "../config/config.js"; +import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { logVerbose } from "../globals.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; -import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js"; import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js"; -import type { RetryConfig } from "../infra/retry.js"; +import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js"; import { createTelegramRetryRunner } from "../infra/retry-policy.js"; import { redactSensitiveText } from "../logging/redact.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -19,16 +19,16 @@ import { mediaKindFromMime } from "../media/constants.js"; import { isGifMedia } from "../media/mime.js"; import { loadWebMedia } from "../web/media.js"; import { type ResolvedTelegramAccount, resolveTelegramAccount } from "./accounts.js"; -import { resolveTelegramFetch } from "./fetch.js"; -import { makeProxyFetch } from "./proxy.js"; -import { renderTelegramHtmlText } from "./format.js"; -import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; -import { isRecoverableTelegramNetworkError } from "./network-errors.js"; +import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { buildTelegramThreadParams } from "./bot/helpers.js"; import { splitTelegramCaption } from "./caption.js"; +import { resolveTelegramFetch } from "./fetch.js"; +import { renderTelegramHtmlText } from "./format.js"; +import { isRecoverableTelegramNetworkError } from "./network-errors.js"; +import { makeProxyFetch } from "./proxy.js"; import { recordSentMessage } from "./sent-message-cache.js"; import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js"; import { resolveTelegramVoiceSend } from "./voice.js"; -import { buildTelegramThreadParams } from "./bot/helpers.js"; type TelegramSendOpts = { token?: string; @@ -77,7 +77,9 @@ function createTelegramHttpLogger(cfg: ReturnType) { return () => {}; } return (label: string, err: unknown) => { - if (!(err instanceof HttpError)) return; + if (!(err instanceof HttpError)) { + return; + } const detail = redactSensitiveText(formatUncaughtError(err.error ?? err)); diagLogger.warn(`telegram http error (${label}): ${detail}`); }; @@ -105,7 +107,9 @@ function resolveTelegramClientOptions( } function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) { - if (explicit?.trim()) return explicit.trim(); + if (explicit?.trim()) { + return explicit.trim(); + } if (!params.token) { throw new Error( `Telegram bot token missing for account "${params.accountId}" (set channels.telegram.accounts.${params.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`, @@ -116,7 +120,9 @@ function resolveToken(explicit: string | undefined, params: { accountId: string; function normalizeChatId(to: string): string { const trimmed = to.trim(); - if (!trimmed) throw new Error("Recipient is required for Telegram sends"); + if (!trimmed) { + throw new Error("Recipient is required for Telegram sends"); + } // Common internal prefixes that sometimes leak into outbound sends. // - ctx.To uses `telegram:` @@ -128,14 +134,24 @@ function normalizeChatId(to: string): string { const m = /^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ?? /^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized); - if (m?.[1]) normalized = `@${m[1]}`; + if (m?.[1]) { + normalized = `@${m[1]}`; + } - if (!normalized) throw new Error("Recipient is required for Telegram sends"); - if (normalized.startsWith("@")) return normalized; - if (/^-?\d+$/.test(normalized)) return normalized; + if (!normalized) { + throw new Error("Recipient is required for Telegram sends"); + } + if (normalized.startsWith("@")) { + return normalized; + } + if (/^-?\d+$/.test(normalized)) { + return normalized; + } // If the user passed a username without `@`, assume they meant a public chat/channel. - if (/^[A-Za-z0-9_]{5,}$/i.test(normalized)) return `@${normalized}`; + if (/^[A-Za-z0-9_]{5,}$/i.test(normalized)) { + return `@${normalized}`; + } return normalized; } @@ -150,7 +166,9 @@ function normalizeMessageId(raw: string | number): number { throw new Error("Message id is required for Telegram actions"); } const parsed = Number.parseInt(value, 10); - if (Number.isFinite(parsed)) return parsed; + if (Number.isFinite(parsed)) { + return parsed; + } } throw new Error("Message id is required for Telegram actions"); } @@ -158,7 +176,9 @@ function normalizeMessageId(raw: string | number): number { export function buildInlineKeyboard( buttons?: TelegramSendOpts["buttons"], ): InlineKeyboardMarkup | undefined { - if (!buttons?.length) return undefined; + if (!buttons?.length) { + return undefined; + } const rows = buttons .map((row) => row @@ -171,7 +191,9 @@ export function buildInlineKeyboard( ), ) .filter((row) => row.length > 0); - if (rows.length === 0) return undefined; + if (rows.length === 0) { + return undefined; + } return { inline_keyboard: rows }; } @@ -229,7 +251,9 @@ export async function sendMessageTelegram( throw err; }); const wrapChatNotFound = (err: unknown) => { - if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err; + if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) { + return err; + } return new Error( [ `Telegram send failed: chat not found (chat_id=${chatId}).`, @@ -690,7 +714,9 @@ export async function sendStickerTelegram( }); const wrapChatNotFound = (err: unknown) => { - if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err; + if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) { + return err; + } return new Error( [ `Telegram send failed: chat not found (chat_id=${chatId}).`, diff --git a/src/telegram/sent-message-cache.ts b/src/telegram/sent-message-cache.ts index 05c12ddf7..039d38a4c 100644 --- a/src/telegram/sent-message-cache.ts +++ b/src/telegram/sent-message-cache.ts @@ -50,7 +50,9 @@ export function recordSentMessage(chatId: number | string, messageId: number): v export function wasSentByBot(chatId: number | string, messageId: number): boolean { const key = getChatKey(chatId); const entry = sentMessages.get(key); - if (!entry) return false; + if (!entry) { + return false; + } // Clean up expired entries on read cleanupExpired(entry); return entry.messageIds.has(messageId); diff --git a/src/telegram/sticker-cache.test.ts b/src/telegram/sticker-cache.test.ts index ddb280ac6..7d82c7651 100644 --- a/src/telegram/sticker-cache.test.ts +++ b/src/telegram/sticker-cache.test.ts @@ -11,10 +11,10 @@ import { // Mock the state directory to use a temp location vi.mock("../config/paths.js", () => ({ - STATE_DIR: "/tmp/moltbot-test-sticker-cache", + STATE_DIR: "/tmp/openclaw-test-sticker-cache", })); -const TEST_CACHE_DIR = "/tmp/moltbot-test-sticker-cache/telegram"; +const TEST_CACHE_DIR = "/tmp/openclaw-test-sticker-cache/telegram"; const TEST_CACHE_FILE = path.join(TEST_CACHE_DIR, "sticker-cache.json"); describe("sticker-cache", () => { diff --git a/src/telegram/sticker-cache.ts b/src/telegram/sticker-cache.ts index fc2d7c97a..d49877b60 100644 --- a/src/telegram/sticker-cache.ts +++ b/src/telegram/sticker-cache.ts @@ -1,17 +1,17 @@ import fs from "node:fs/promises"; import path from "node:path"; -import type { MoltbotConfig } from "../config/config.js"; -import { STATE_DIR } from "../config/paths.js"; -import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; -import { logVerbose } from "../globals.js"; import type { ModelCatalogEntry } from "../agents/model-catalog.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveApiKeyForProvider } from "../agents/model-auth.js"; import { findModelInCatalog, loadModelCatalog, modelSupportsVision, } from "../agents/model-catalog.js"; -import { resolveApiKeyForProvider } from "../agents/model-auth.js"; import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; +import { STATE_DIR } from "../config/paths.js"; +import { logVerbose } from "../globals.js"; +import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; import { resolveAutoImageModel } from "../media-understanding/runner.js"; const CACHE_FILE = path.join(STATE_DIR, "telegram", "sticker-cache.json"); @@ -108,7 +108,7 @@ export function searchStickers(query: string, limit = 10): CachedSticker[] { } return results - .sort((a, b) => b.score - a.score) + .toSorted((a, b) => b.score - a.score) .slice(0, limit) .map((r) => r.sticker); } @@ -130,7 +130,7 @@ export function getCacheStats(): { count: number; oldestAt?: string; newestAt?: if (stickers.length === 0) { return { count: 0 }; } - const sorted = [...stickers].sort( + const sorted = [...stickers].toSorted( (a, b) => new Date(a.cachedAt).getTime() - new Date(b.cachedAt).getTime(), ); return { @@ -146,7 +146,7 @@ const VISION_PROVIDERS = ["openai", "anthropic", "google", "minimax"] as const; export interface DescribeStickerParams { imagePath: string; - cfg: MoltbotConfig; + cfg: OpenClawConfig; agentDir?: string; agentId?: string; } @@ -187,7 +187,9 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi (entry) => entry.provider.toLowerCase() === provider.toLowerCase() && modelSupportsVision(entry), ); - if (entries.length === 0) return undefined; + if (entries.length === 0) { + return undefined; + } const defaultId = provider === "openai" ? "gpt-5-mini" @@ -211,7 +213,9 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi if (!resolved) { for (const provider of VISION_PROVIDERS) { - if (!(await hasProviderKey(provider))) continue; + if (!(await hasProviderKey(provider))) { + continue; + } const entry = selectCatalogModel(provider); if (entry) { resolved = { provider, model: entry.id }; diff --git a/src/telegram/targets.test.ts b/src/telegram/targets.test.ts index f0b28fef1..e25e38b2c 100644 --- a/src/telegram/targets.test.ts +++ b/src/telegram/targets.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js"; describe("stripTelegramInternalPrefixes", () => { diff --git a/src/telegram/targets.ts b/src/telegram/targets.ts index 2bf139a14..cb26c0d06 100644 --- a/src/telegram/targets.ts +++ b/src/telegram/targets.ts @@ -18,7 +18,9 @@ export function stripTelegramInternalPrefixes(to: string): string { } return trimmed; })(); - if (next === trimmed) return trimmed; + if (next === trimmed) { + return trimmed; + } trimmed = next; } } diff --git a/src/telegram/token.test.ts b/src/telegram/token.test.ts index 08e17519a..ad5a389dd 100644 --- a/src/telegram/token.test.ts +++ b/src/telegram/token.test.ts @@ -1,14 +1,12 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - import { afterEach, describe, expect, it, vi } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveTelegramToken } from "./token.js"; function withTempDir(): string { - return fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-telegram-token-")); + return fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-token-")); } describe("resolveTelegramToken", () => { @@ -20,7 +18,7 @@ describe("resolveTelegramToken", () => { vi.stubEnv("TELEGRAM_BOT_TOKEN", "env-token"); const cfg = { channels: { telegram: { botToken: "cfg-token" } }, - } as MoltbotConfig; + } as OpenClawConfig; const res = resolveTelegramToken(cfg); expect(res.token).toBe("cfg-token"); expect(res.source).toBe("config"); @@ -30,7 +28,7 @@ describe("resolveTelegramToken", () => { vi.stubEnv("TELEGRAM_BOT_TOKEN", "env-token"); const cfg = { channels: { telegram: {} }, - } as MoltbotConfig; + } as OpenClawConfig; const res = resolveTelegramToken(cfg); expect(res.token).toBe("env-token"); expect(res.source).toBe("env"); @@ -41,7 +39,7 @@ describe("resolveTelegramToken", () => { const dir = withTempDir(); const tokenFile = path.join(dir, "token.txt"); fs.writeFileSync(tokenFile, "file-token\n", "utf-8"); - const cfg = { channels: { telegram: { tokenFile } } } as MoltbotConfig; + const cfg = { channels: { telegram: { tokenFile } } } as OpenClawConfig; const res = resolveTelegramToken(cfg); expect(res.token).toBe("file-token"); expect(res.source).toBe("tokenFile"); @@ -52,7 +50,7 @@ describe("resolveTelegramToken", () => { vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); const cfg = { channels: { telegram: { botToken: "cfg-token" } }, - } as MoltbotConfig; + } as OpenClawConfig; const res = resolveTelegramToken(cfg); expect(res.token).toBe("cfg-token"); expect(res.source).toBe("config"); @@ -64,10 +62,28 @@ describe("resolveTelegramToken", () => { const tokenFile = path.join(dir, "missing-token.txt"); const cfg = { channels: { telegram: { tokenFile, botToken: "cfg-token" } }, - } as MoltbotConfig; + } as OpenClawConfig; const res = resolveTelegramToken(cfg); expect(res.token).toBe(""); expect(res.source).toBe("none"); fs.rmSync(dir, { recursive: true, force: true }); }); + + it("resolves per-account tokens when the config account key casing doesn't match routing normalization", () => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); + const cfg = { + channels: { + telegram: { + accounts: { + // Note the mixed-case key; runtime accountId is normalized. + careyNotifications: { botToken: "acct-token" }, + }, + }, + }, + } as OpenClawConfig; + + const res = resolveTelegramToken(cfg, { accountId: "careynotifications" }); + expect(res.token).toBe("acct-token"); + expect(res.source).toBe("config"); + }); }); diff --git a/src/telegram/token.ts b/src/telegram/token.ts index cf6ba5f62..ed11d3f74 100644 --- a/src/telegram/token.ts +++ b/src/telegram/token.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { TelegramAccountConfig } from "../config/types.telegram.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; export type TelegramTokenSource = "env" | "tokenFile" | "config" | "none"; @@ -17,15 +17,32 @@ type ResolveTelegramTokenOpts = { }; export function resolveTelegramToken( - cfg?: MoltbotConfig, + cfg?: OpenClawConfig, opts: ResolveTelegramTokenOpts = {}, ): TelegramTokenResolution { const accountId = normalizeAccountId(opts.accountId); const telegramCfg = cfg?.channels?.telegram; - const accountCfg = - accountId !== DEFAULT_ACCOUNT_ID - ? telegramCfg?.accounts?.[accountId] - : telegramCfg?.accounts?.[DEFAULT_ACCOUNT_ID]; + + // Account IDs are normalized for routing (e.g. lowercased). Config keys may not + // be normalized, so resolve per-account config by matching normalized IDs. + const resolveAccountCfg = (id: string): TelegramAccountConfig | undefined => { + const accounts = telegramCfg?.accounts; + if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) { + return undefined; + } + // Direct hit (already normalized key) + const direct = accounts[id]; + if (direct) { + return direct; + } + // Fallback: match by normalized key + const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === id); + return matchKey ? accounts[matchKey] : undefined; + }; + + const accountCfg = resolveAccountCfg( + accountId !== DEFAULT_ACCOUNT_ID ? accountId : DEFAULT_ACCOUNT_ID, + ); const accountTokenFile = accountCfg?.tokenFile?.trim(); if (accountTokenFile) { if (!fs.existsSync(accountTokenFile)) { diff --git a/src/telegram/update-offset-store.test.ts b/src/telegram/update-offset-store.test.ts index cab586173..4e3f5d9a3 100644 --- a/src/telegram/update-offset-store.test.ts +++ b/src/telegram/update-offset-store.test.ts @@ -1,20 +1,21 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { readTelegramUpdateOffset, writeTelegramUpdateOffset } from "./update-offset-store.js"; async function withTempStateDir(fn: (dir: string) => Promise) { - const previous = process.env.CLAWDBOT_STATE_DIR; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-telegram-")); - process.env.CLAWDBOT_STATE_DIR = dir; + const previous = process.env.OPENCLAW_STATE_DIR; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-telegram-")); + process.env.OPENCLAW_STATE_DIR = dir; try { return await fn(dir); } finally { - if (previous === undefined) delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = previous; + if (previous === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previous; + } await fs.rm(dir, { recursive: true, force: true }); } } diff --git a/src/telegram/update-offset-store.ts b/src/telegram/update-offset-store.ts index 303db8c9c..6597fa25c 100644 --- a/src/telegram/update-offset-store.ts +++ b/src/telegram/update-offset-store.ts @@ -2,7 +2,6 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { resolveStateDir } from "../config/paths.js"; const STORE_VERSION = 1; @@ -14,7 +13,9 @@ type TelegramUpdateOffsetState = { function normalizeAccountId(accountId?: string) { const trimmed = accountId?.trim(); - if (!trimmed) return "default"; + if (!trimmed) { + return "default"; + } return trimmed.replace(/[^a-z0-9._-]+/gi, "_"); } @@ -30,7 +31,9 @@ function resolveTelegramUpdateOffsetPath( function safeParseState(raw: string): TelegramUpdateOffsetState | null { try { const parsed = JSON.parse(raw) as TelegramUpdateOffsetState; - if (parsed?.version !== STORE_VERSION) return null; + if (parsed?.version !== STORE_VERSION) { + return null; + } if (parsed.lastUpdateId !== null && typeof parsed.lastUpdateId !== "number") { return null; } @@ -51,7 +54,9 @@ export async function readTelegramUpdateOffset(params: { return parsed?.lastUpdateId ?? null; } catch (err) { const code = (err as { code?: string }).code; - if (code === "ENOENT") return null; + if (code === "ENOENT") { + return null; + } return null; } } diff --git a/src/telegram/voice.test.ts b/src/telegram/voice.test.ts index e1e74caeb..e2d96a971 100644 --- a/src/telegram/voice.test.ts +++ b/src/telegram/voice.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { resolveTelegramVoiceSend } from "./voice.js"; describe("resolveTelegramVoiceSend", () => { diff --git a/src/telegram/voice.ts b/src/telegram/voice.ts index bf296c8f1..39da4e500 100644 --- a/src/telegram/voice.ts +++ b/src/telegram/voice.ts @@ -12,8 +12,12 @@ export function resolveTelegramVoiceDecision(opts: { contentType?: string | null; fileName?: string | null; }): { useVoice: boolean; reason?: string } { - if (!opts.wantsVoice) return { useVoice: false }; - if (isTelegramVoiceCompatible(opts)) return { useVoice: true }; + if (!opts.wantsVoice) { + return { useVoice: false }; + } + if (isTelegramVoiceCompatible(opts)) { + return { useVoice: true }; + } const contentType = opts.contentType ?? "unknown"; const fileName = opts.fileName ?? "unknown"; return { diff --git a/src/telegram/webhook-set.ts b/src/telegram/webhook-set.ts index 0d2e815fc..1bee52485 100644 --- a/src/telegram/webhook-set.ts +++ b/src/telegram/webhook-set.ts @@ -1,7 +1,7 @@ import { type ApiClientOptions, Bot } from "grammy"; import type { TelegramNetworkConfig } from "../config/types.telegram.js"; -import { resolveTelegramFetch } from "./fetch.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { resolveTelegramFetch } from "./fetch.js"; export async function setTelegramWebhook(opts: { token: string; diff --git a/src/telegram/webhook.test.ts b/src/telegram/webhook.test.ts index 04bccfe07..5d9efe610 100644 --- a/src/telegram/webhook.test.ts +++ b/src/telegram/webhook.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { startTelegramWebhook } from "./webhook.js"; const handlerSpy = vi.fn( @@ -44,7 +43,9 @@ describe("startTelegramWebhook", () => { }), ); const address = server.address(); - if (!address || typeof address === "string") throw new Error("no address"); + if (!address || typeof address === "string") { + throw new Error("no address"); + } const url = `http://127.0.0.1:${address.port}`; const health = await fetch(`${url}/healthz`); @@ -74,7 +75,9 @@ describe("startTelegramWebhook", () => { }), ); const addr = server.address(); - if (!addr || typeof addr === "string") throw new Error("no addr"); + if (!addr || typeof addr === "string") { + throw new Error("no addr"); + } await fetch(`http://127.0.0.1:${addr.port}/hook`, { method: "POST" }); expect(handlerSpy).toHaveBeenCalled(); abort.abort(); diff --git a/src/telegram/webhook.ts b/src/telegram/webhook.ts index e8c224eb2..b9dc070d1 100644 --- a/src/telegram/webhook.ts +++ b/src/telegram/webhook.ts @@ -1,11 +1,9 @@ -import { createServer } from "node:http"; - import { webhookCallback } from "grammy"; -import type { MoltbotConfig } from "../config/config.js"; +import { createServer } from "node:http"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js"; import { formatErrorMessage } from "../infra/errors.js"; -import type { RuntimeEnv } from "../runtime.js"; -import { defaultRuntime } from "../runtime.js"; import { logWebhookError, logWebhookProcessed, @@ -13,14 +11,15 @@ import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat, } from "../logging/diagnostic.js"; +import { defaultRuntime } from "../runtime.js"; import { resolveTelegramAllowedUpdates } from "./allowed-updates.js"; -import { createTelegramBot } from "./bot.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { createTelegramBot } from "./bot.js"; export async function startTelegramWebhook(opts: { token: string; accountId?: string; - config?: MoltbotConfig; + config?: OpenClawConfig; path?: string; port?: number; host?: string; @@ -68,8 +67,8 @@ export async function startTelegramWebhook(opts: { logWebhookReceived({ channel: "telegram", updateType: "telegram-post" }); } const handled = handler(req, res); - if (handled && typeof (handled as Promise).catch === "function") { - void (handled as Promise) + if (handled && typeof handled.catch === "function") { + void handled .then(() => { if (diagnosticsEnabled) { logWebhookProcessed({ @@ -89,7 +88,9 @@ export async function startTelegramWebhook(opts: { }); } runtime.log?.(`webhook handler failed: ${errMsg}`); - if (!res.headersSent) res.writeHead(500); + if (!res.headersSent) { + res.writeHead(500); + } res.end(); }); } diff --git a/src/terminal/links.ts b/src/terminal/links.ts index 0d965633a..879c27937 100644 --- a/src/terminal/links.ts +++ b/src/terminal/links.ts @@ -1,6 +1,6 @@ import { formatTerminalLink } from "../utils.js"; -export const DOCS_ROOT = "https://docs.molt.bot"; +export const DOCS_ROOT = "https://docs.openclaw.ai"; export function formatDocsLink( path: string, diff --git a/src/terminal/note.ts b/src/terminal/note.ts index 7a35cf069..48bca06fe 100644 --- a/src/terminal/note.ts +++ b/src/terminal/note.ts @@ -3,7 +3,9 @@ import { visibleWidth } from "./ansi.js"; import { stylePromptTitle } from "./prompt-style.js"; function splitLongWord(word: string, maxLen: number): string[] { - if (maxLen <= 0) return [word]; + if (maxLen <= 0) { + return [word]; + } const chars = Array.from(word); const parts: string[] = []; for (let i = 0; i < chars.length; i += maxLen) { @@ -13,7 +15,9 @@ function splitLongWord(word: string, maxLen: number): string[] { } function wrapLine(line: string, maxWidth: number): string[] { - if (line.trim().length === 0) return [line]; + if (line.trim().length === 0) { + return [line]; + } const match = line.match(/^(\s*)([-*\u2022]\s+)?(.*)$/); const indent = match?.[1] ?? ""; const bullet = match?.[2] ?? ""; @@ -37,7 +41,9 @@ function wrapLine(line: string, maxWidth: number): string[] { lines.push(prefix + first); prefix = nextPrefix; available = nextWidth; - for (const part of parts) lines.push(prefix + part); + for (const part of parts) { + lines.push(prefix + part); + } continue; } current = word; @@ -58,7 +64,9 @@ function wrapLine(line: string, maxWidth: number): string[] { const parts = splitLongWord(word, available); const first = parts.shift() ?? ""; lines.push(prefix + first); - for (const part of parts) lines.push(prefix + part); + for (const part of parts) { + lines.push(prefix + part); + } current = ""; continue; } diff --git a/src/terminal/progress-line.ts b/src/terminal/progress-line.ts index 1ee94baab..818c63714 100644 --- a/src/terminal/progress-line.ts +++ b/src/terminal/progress-line.ts @@ -1,17 +1,25 @@ let activeStream: NodeJS.WriteStream | null = null; export function registerActiveProgressLine(stream: NodeJS.WriteStream): void { - if (!stream.isTTY) return; + if (!stream.isTTY) { + return; + } activeStream = stream; } export function clearActiveProgressLine(): void { - if (!activeStream?.isTTY) return; + if (!activeStream?.isTTY) { + return; + } activeStream.write("\r\x1b[2K"); } export function unregisterActiveProgressLine(stream?: NodeJS.WriteStream): void { - if (!activeStream) return; - if (stream && activeStream !== stream) return; + if (!activeStream) { + return; + } + if (stream && activeStream !== stream) { + return; + } activeStream = null; } diff --git a/src/terminal/stream-writer.test.ts b/src/terminal/stream-writer.test.ts index 429199a83..5355ac59f 100644 --- a/src/terminal/stream-writer.test.ts +++ b/src/terminal/stream-writer.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { createSafeStreamWriter } from "./stream-writer.js"; describe("createSafeStreamWriter", () => { diff --git a/src/terminal/stream-writer.ts b/src/terminal/stream-writer.ts index cd471ea5f..2c7ab2178 100644 --- a/src/terminal/stream-writer.ts +++ b/src/terminal/stream-writer.ts @@ -20,7 +20,9 @@ export function createSafeStreamWriter(options: SafeStreamWriterOptions = {}): S let notified = false; const noteBrokenPipe = (err: NodeJS.ErrnoException, stream: NodeJS.WriteStream) => { - if (notified) return; + if (notified) { + return; + } notified = true; options.onBrokenPipe?.(err, stream); }; @@ -35,7 +37,9 @@ export function createSafeStreamWriter(options: SafeStreamWriterOptions = {}): S }; const write = (stream: NodeJS.WriteStream, text: string): boolean => { - if (closed) return false; + if (closed) { + return false; + } try { options.beforeWrite?.(); } catch (err) { diff --git a/src/terminal/table.test.ts b/src/terminal/table.test.ts index 1e7b24b76..3c0d22b35 100644 --- a/src/terminal/table.test.ts +++ b/src/terminal/table.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { visibleWidth } from "./ansi.js"; import { renderTable } from "./table.js"; @@ -50,14 +49,18 @@ describe("renderTable", () => { const ESC = "\u001b"; for (let i = 0; i < out.length; i += 1) { - if (out[i] !== ESC) continue; + if (out[i] !== ESC) { + continue; + } // SGR: ESC [ ... m if (out[i + 1] === "[") { let j = i + 2; while (j < out.length) { const ch = out[j]; - if (ch === "m") break; + if (ch === "m") { + break; + } if (ch && ch >= "0" && ch <= "9") { j += 1; continue; diff --git a/src/terminal/table.ts b/src/terminal/table.ts index 5a735a749..34d7b15dd 100644 --- a/src/terminal/table.ts +++ b/src/terminal/table.ts @@ -1,5 +1,5 @@ -import { visibleWidth } from "./ansi.js"; import { displayString } from "../utils.js"; +import { visibleWidth } from "./ansi.js"; type Align = "left" | "right" | "center"; @@ -21,15 +21,21 @@ export type RenderTableOptions = { }; function repeat(ch: string, n: number): string { - if (n <= 0) return ""; + if (n <= 0) { + return ""; + } return ch.repeat(n); } function padCell(text: string, width: number, align: Align): string { const w = visibleWidth(text); - if (w >= width) return text; + if (w >= width) { + return text; + } const pad = width - w; - if (align === "right") return `${repeat(" ", pad)}${text}`; + if (align === "right") { + return `${repeat(" ", pad)}${text}`; + } if (align === "center") { const left = Math.floor(pad / 2); const right = pad - left; @@ -39,7 +45,9 @@ function padCell(text: string, width: number, align: Align): string { } function wrapLine(text: string, width: number): string[] { - if (width <= 0) return [text]; + if (width <= 0) { + return [text]; + } // ANSI-aware wrapping: never split inside ANSI SGR/OSC-8 sequences. // We don't attempt to re-open styling per line; terminals keep SGR state @@ -55,7 +63,9 @@ function wrapLine(text: string, width: number): string[] { let j = i + 2; while (j < text.length) { const ch = text[j]; - if (ch === "m") break; + if (ch === "m") { + break; + } if (ch && ch >= "0" && ch <= "9") { j += 1; continue; @@ -85,14 +95,18 @@ function wrapLine(text: string, width: number): string[] { } const cp = text.codePointAt(i); - if (!cp) break; + if (!cp) { + break; + } const ch = String.fromCodePoint(cp); tokens.push({ kind: "char", value: ch }); i += ch.length; } const firstCharIndex = tokens.findIndex((t) => t.kind === "char"); - if (firstCharIndex < 0) return [text]; + if (firstCharIndex < 0) { + return [text]; + } let lastCharIndex = -1; for (let i = tokens.length - 1; i >= 0; i -= 1) { if (tokens[i]?.kind === "char") { @@ -129,12 +143,16 @@ function wrapLine(text: string, width: number): string[] { const pushLine = (value: string) => { const cleaned = value.replace(/\s+$/, ""); - if (cleaned.trim().length === 0) return; + if (cleaned.trim().length === 0) { + return; + } lines.push(cleaned); }; const flushAt = (breakAt: number | null) => { - if (buf.length === 0) return; + if (buf.length === 0) { + return; + } if (breakAt == null || breakAt <= 0) { pushLine(bufToString()); buf.length = 0; @@ -166,11 +184,15 @@ function wrapLine(text: string, width: number): string[] { const ch = token.value; if (skipNextLf) { skipNextLf = false; - if (ch === "\n") continue; + if (ch === "\n") { + continue; + } } if (ch === "\n" || ch === "\r") { flushAt(buf.length); - if (ch === "\r") skipNextLf = true; + if (ch === "\r") { + skipNextLf = true; + } continue; } if (bufVisible + 1 > width && bufVisible > 0) { @@ -179,21 +201,33 @@ function wrapLine(text: string, width: number): string[] { buf.push(token); bufVisible += 1; - if (isBreakChar(ch)) lastBreakIndex = buf.length; + if (isBreakChar(ch)) { + lastBreakIndex = buf.length; + } } flushAt(buf.length); - if (!lines.length) return [""]; - if (!prefixAnsi && !suffixAnsi) return lines; + if (!lines.length) { + return [""]; + } + if (!prefixAnsi && !suffixAnsi) { + return lines; + } return lines.map((line) => { - if (!line) return line; + if (!line) { + return line; + } return `${prefixAnsi}${line}${suffixAnsi}`; }); } function normalizeWidth(n: number | undefined): number | undefined { - if (n == null) return undefined; - if (!Number.isFinite(n) || n <= 0) return undefined; + if (n == null) { + return undefined; + } + if (!Number.isFinite(n) || n <= 0) { + return undefined; + } return Math.floor(n); } @@ -246,26 +280,32 @@ export function renderTable(opts: RenderTableOptions): string { const flexOrder = columns .map((_c, i) => ({ i, w: widths[i] ?? 0 })) .filter(({ i }) => Boolean(columns[i]?.flex)) - .sort((a, b) => b.w - a.w) + .toSorted((a, b) => b.w - a.w) .map((x) => x.i); const nonFlexOrder = columns .map((_c, i) => ({ i, w: widths[i] ?? 0 })) .filter(({ i }) => !columns[i]?.flex) - .sort((a, b) => b.w - a.w) + .toSorted((a, b) => b.w - a.w) .map((x) => x.i); const shrink = (order: number[], minWidths: number[]) => { while (over > 0) { let progressed = false; for (const i of order) { - if ((widths[i] ?? 0) <= (minWidths[i] ?? 0)) continue; + if ((widths[i] ?? 0) <= (minWidths[i] ?? 0)) { + continue; + } widths[i] = (widths[i] ?? 0) - 1; over -= 1; progressed = true; - if (over <= 0) break; + if (over <= 0) { + break; + } + } + if (!progressed) { + break; } - if (!progressed) break; } }; @@ -298,13 +338,19 @@ export function renderTable(opts: RenderTableOptions): string { while (extra > 0) { let progressed = false; for (const i of flexCols) { - if ((widths[i] ?? 0) >= (caps[i] ?? Number.POSITIVE_INFINITY)) continue; + if ((widths[i] ?? 0) >= (caps[i] ?? Number.POSITIVE_INFINITY)) { + continue; + } widths[i] = (widths[i] ?? 0) + 1; extra -= 1; progressed = true; - if (extra <= 0) break; + if (extra <= 0) { + break; + } + } + if (!progressed) { + break; } - if (!progressed) break; } } } diff --git a/src/terminal/theme.ts b/src/terminal/theme.ts index 966f289d7..e5a771ace 100644 --- a/src/terminal/theme.ts +++ b/src/terminal/theme.ts @@ -1,5 +1,4 @@ import chalk, { Chalk } from "chalk"; - import { LOBSTER_PALETTE } from "./palette.js"; const hasForceColor = diff --git a/src/test-helpers/workspace.ts b/src/test-helpers/workspace.ts index 73b00629f..6106e492d 100644 --- a/src/test-helpers/workspace.ts +++ b/src/test-helpers/workspace.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -export async function makeTempWorkspace(prefix = "moltbot-workspace-"): Promise { +export async function makeTempWorkspace(prefix = "openclaw-workspace-"): Promise { return fs.mkdtemp(path.join(os.tmpdir(), prefix)); } diff --git a/src/test-utils/channel-plugins.ts b/src/test-utils/channel-plugins.ts index 3e6bf2112..783582b20 100644 --- a/src/test-utils/channel-plugins.ts +++ b/src/test-utils/channel-plugins.ts @@ -1,4 +1,3 @@ -import { imessageOutbound } from "../channels/plugins/outbound/imessage.js"; import type { ChannelCapabilities, ChannelId, @@ -6,6 +5,7 @@ import type { ChannelPlugin, } from "../channels/plugins/types.js"; import type { PluginRegistry } from "../plugins/registry.js"; +import { imessageOutbound } from "../channels/plugins/outbound/imessage.js"; import { normalizeIMessageHandle } from "../imessage/targets.js"; export const createTestRegistry = (channels: PluginRegistry["channels"] = []): PluginRegistry => ({ @@ -45,7 +45,9 @@ export const createIMessageTestPlugin = (params?: { collectStatusIssues: (accounts) => accounts.flatMap((account) => { const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; - if (!lastError) return []; + if (!lastError) { + return []; + } return [ { channel: "imessage", @@ -61,11 +63,15 @@ export const createIMessageTestPlugin = (params?: { targetResolver: { looksLikeId: (raw) => { const trimmed = raw.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } if (/^(imessage:|sms:|auto:|chat_id:|chat_guid:|chat_identifier:)/i.test(trimmed)) { return true; } - if (trimmed.includes("@")) return true; + if (trimmed.includes("@")) { + return true; + } return /^\+?\d{3,}$/.test(trimmed); }, hint: "", diff --git a/src/test-utils/ports.ts b/src/test-utils/ports.ts index 428eb3816..214f9ba8f 100644 --- a/src/test-utils/ports.ts +++ b/src/test-utils/ports.ts @@ -1,8 +1,10 @@ -import { type AddressInfo, createServer } from "node:net"; +import { createServer } from "node:net"; import { isMainThread, threadId } from "node:worker_threads"; async function isPortFree(port: number): Promise { - if (!Number.isFinite(port) || port <= 0 || port > 65535) return false; + if (!Number.isFinite(port) || port <= 0 || port > 65535) { + return false; + } return await new Promise((resolve) => { const server = createServer(); server.once("error", () => resolve(false)); @@ -23,7 +25,7 @@ async function getOsFreePort(): Promise { reject(new Error("failed to acquire free port")); return; } - const port = (addr as AddressInfo).port; + const port = addr.port; server.close((err) => (err ? reject(err) : resolve(port))); }); }); @@ -66,7 +68,9 @@ export async function getDeterministicFreePortBlock(params?: { const ok = (await Promise.all(offsets.map((offset) => isPortFree(start + offset)))).every( Boolean, ); - if (!ok) continue; + if (!ok) { + continue; + } nextTestPortOffset = (nextTestPortOffset + attempt + blockSize) % usable; return start; } @@ -79,7 +83,9 @@ export async function getDeterministicFreePortBlock(params?: { const ok = (await Promise.all(offsets.map((offset) => isPortFree(port + offset)))).every( Boolean, ); - if (ok) return port; + if (ok) { + return port; + } } throw new Error("failed to acquire a free port block"); diff --git a/src/tts/tts.test.ts b/src/tts/tts.test.ts index 8462cba01..0e94d5d8c 100644 --- a/src/tts/tts.test.ts +++ b/src/tts/tts.test.ts @@ -1,7 +1,5 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; - import { completeSimple } from "@mariozechner/pi-ai"; - +import { describe, expect, it, vi, beforeEach } from "vitest"; import { getApiKeyForModel } from "../agents/model-auth.js"; import { resolveModel } from "../agents/pi-embedded-runner/model.js"; import * as tts from "./tts.js"; @@ -449,8 +447,8 @@ describe("tts", () => { }; it("skips auto-TTS when inbound audio gating is on and the message is not audio", async () => { - const prevPrefs = process.env.CLAWDBOT_TTS_PREFS; - process.env.CLAWDBOT_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; + const prevPrefs = process.env.OPENCLAW_TTS_PREFS; + process.env.OPENCLAW_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; const originalFetch = globalThis.fetch; const fetchMock = vi.fn(async () => ({ ok: true, @@ -470,12 +468,12 @@ describe("tts", () => { expect(fetchMock).not.toHaveBeenCalled(); globalThis.fetch = originalFetch; - process.env.CLAWDBOT_TTS_PREFS = prevPrefs; + process.env.OPENCLAW_TTS_PREFS = prevPrefs; }); it("attempts auto-TTS when inbound audio gating is on and the message is audio", async () => { - const prevPrefs = process.env.CLAWDBOT_TTS_PREFS; - process.env.CLAWDBOT_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; + const prevPrefs = process.env.OPENCLAW_TTS_PREFS; + process.env.OPENCLAW_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; const originalFetch = globalThis.fetch; const fetchMock = vi.fn(async () => ({ ok: true, @@ -494,12 +492,12 @@ describe("tts", () => { expect(fetchMock).toHaveBeenCalledTimes(1); globalThis.fetch = originalFetch; - process.env.CLAWDBOT_TTS_PREFS = prevPrefs; + process.env.OPENCLAW_TTS_PREFS = prevPrefs; }); it("skips auto-TTS in tagged mode unless a tts tag is present", async () => { - const prevPrefs = process.env.CLAWDBOT_TTS_PREFS; - process.env.CLAWDBOT_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; + const prevPrefs = process.env.OPENCLAW_TTS_PREFS; + process.env.OPENCLAW_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; const originalFetch = globalThis.fetch; const fetchMock = vi.fn(async () => ({ ok: true, @@ -526,12 +524,12 @@ describe("tts", () => { expect(fetchMock).not.toHaveBeenCalled(); globalThis.fetch = originalFetch; - process.env.CLAWDBOT_TTS_PREFS = prevPrefs; + process.env.OPENCLAW_TTS_PREFS = prevPrefs; }); it("runs auto-TTS in tagged mode when tags are present", async () => { - const prevPrefs = process.env.CLAWDBOT_TTS_PREFS; - process.env.CLAWDBOT_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; + const prevPrefs = process.env.OPENCLAW_TTS_PREFS; + process.env.OPENCLAW_TTS_PREFS = `/tmp/tts-test-${Date.now()}.json`; const originalFetch = globalThis.fetch; const fetchMock = vi.fn(async () => ({ ok: true, @@ -557,7 +555,7 @@ describe("tts", () => { expect(fetchMock).toHaveBeenCalledTimes(1); globalThis.fetch = originalFetch; - process.env.CLAWDBOT_TTS_PREFS = prevPrefs; + process.env.OPENCLAW_TTS_PREFS = prevPrefs; }); }); }); diff --git a/src/tts/tts.ts b/src/tts/tts.ts index faa83d3a6..0f47c02a9 100644 --- a/src/tts/tts.ts +++ b/src/tts/tts.ts @@ -1,3 +1,5 @@ +import { completeSimple, type TextContent } from "@mariozechner/pi-ai"; +import { EdgeTTS } from "node-edge-tts"; import { existsSync, mkdirSync, @@ -10,14 +12,9 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; - -import { completeSimple, type TextContent } from "@mariozechner/pi-ai"; -import { EdgeTTS } from "node-edge-tts"; - import type { ReplyPayload } from "../auto-reply/types.js"; -import { normalizeChannelId } from "../channels/plugins/index.js"; import type { ChannelId } from "../channels/plugins/types.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { TtsConfig, TtsAutoMode, @@ -25,9 +22,6 @@ import type { TtsProvider, TtsModelOverrideConfig, } from "../config/types.tts.js"; -import { logVerbose } from "../globals.js"; -import { isVoiceCompatibleAudio } from "../media/audio.js"; -import { CONFIG_DIR, resolveUserPath } from "../utils.js"; import { getApiKeyForModel, requireApiKey } from "../agents/model-auth.js"; import { buildModelAliasIndex, @@ -36,6 +30,10 @@ import { type ModelRef, } from "../agents/model-selection.js"; import { resolveModel } from "../agents/pi-embedded-runner/model.js"; +import { normalizeChannelId } from "../channels/plugins/index.js"; +import { logVerbose } from "../globals.js"; +import { isVoiceCompatibleAudio } from "../media/audio.js"; +import { CONFIG_DIR, resolveUserPath } from "../utils.js"; const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_TTS_MAX_LENGTH = 1500; @@ -208,7 +206,9 @@ type TtsStatusEntry = { let lastTtsAttempt: TtsStatusEntry | undefined; export function normalizeTtsAutoMode(value: unknown): TtsAutoMode | undefined { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const normalized = value.trim().toLowerCase(); if (TTS_AUTO_MODES.has(normalized as TtsAutoMode)) { return normalized as TtsAutoMode; @@ -245,7 +245,7 @@ function resolveModelOverridePolicy( }; } -export function resolveTtsConfig(cfg: MoltbotConfig): ResolvedTtsConfig { +export function resolveTtsConfig(cfg: OpenClawConfig): ResolvedTtsConfig { const raw: TtsConfig = cfg.messages?.tts ?? {}; const providerSource = raw.provider ? "config" : "default"; const edgeOutputFormat = raw.edge?.outputFormat?.trim(); @@ -303,15 +303,21 @@ export function resolveTtsConfig(cfg: MoltbotConfig): ResolvedTtsConfig { } export function resolveTtsPrefsPath(config: ResolvedTtsConfig): string { - if (config.prefsPath?.trim()) return resolveUserPath(config.prefsPath.trim()); - const envPath = process.env.CLAWDBOT_TTS_PREFS?.trim(); - if (envPath) return resolveUserPath(envPath); + if (config.prefsPath?.trim()) { + return resolveUserPath(config.prefsPath.trim()); + } + const envPath = process.env.OPENCLAW_TTS_PREFS?.trim(); + if (envPath) { + return resolveUserPath(envPath); + } return path.join(CONFIG_DIR, "settings", "tts.json"); } function resolveTtsAutoModeFromPrefs(prefs: TtsUserPrefs): TtsAutoMode | undefined { const auto = normalizeTtsAutoMode(prefs.tts?.auto); - if (auto) return auto; + if (auto) { + return auto; + } if (typeof prefs.tts?.enabled === "boolean") { return prefs.tts.enabled ? "always" : "off"; } @@ -324,17 +330,23 @@ export function resolveTtsAutoMode(params: { sessionAuto?: string; }): TtsAutoMode { const sessionAuto = normalizeTtsAutoMode(params.sessionAuto); - if (sessionAuto) return sessionAuto; + if (sessionAuto) { + return sessionAuto; + } const prefsAuto = resolveTtsAutoModeFromPrefs(readPrefs(params.prefsPath)); - if (prefsAuto) return prefsAuto; + if (prefsAuto) { + return prefsAuto; + } return params.config.auto; } -export function buildTtsSystemPromptHint(cfg: MoltbotConfig): string | undefined { +export function buildTtsSystemPromptHint(cfg: OpenClawConfig): string | undefined { const config = resolveTtsConfig(cfg); const prefsPath = resolveTtsPrefsPath(config); const autoMode = resolveTtsAutoMode({ config, prefsPath }); - if (autoMode === "off") return undefined; + if (autoMode === "off") { + return undefined; + } const maxLength = getTtsMaxLength(prefsPath); const summarize = isSummarizationEnabled(prefsPath) ? "on" : "off"; const autoHint = @@ -355,7 +367,9 @@ export function buildTtsSystemPromptHint(cfg: MoltbotConfig): string | undefined function readPrefs(prefsPath: string): TtsUserPrefs { try { - if (!existsSync(prefsPath)) return {}; + if (!existsSync(prefsPath)) { + return {}; + } return JSON.parse(readFileSync(prefsPath, "utf8")) as TtsUserPrefs; } catch { return {}; @@ -407,11 +421,19 @@ export function setTtsEnabled(prefsPath: string, enabled: boolean): void { export function getTtsProvider(config: ResolvedTtsConfig, prefsPath: string): TtsProvider { const prefs = readPrefs(prefsPath); - if (prefs.tts?.provider) return prefs.tts.provider; - if (config.providerSource === "config") return config.provider; + if (prefs.tts?.provider) { + return prefs.tts.provider; + } + if (config.providerSource === "config") { + return config.provider; + } - if (resolveTtsApiKey(config, "openai")) return "openai"; - if (resolveTtsApiKey(config, "elevenlabs")) return "elevenlabs"; + if (resolveTtsApiKey(config, "openai")) { + return "openai"; + } + if (resolveTtsApiKey(config, "elevenlabs")) { + return "elevenlabs"; + } return "edge"; } @@ -452,7 +474,9 @@ export function setLastTtsAttempt(entry: TtsStatusEntry | undefined): void { } function resolveOutputFormat(channelId?: string | null) { - if (channelId === "telegram") return TELEGRAM_OUTPUT; + if (channelId === "telegram") { + return TELEGRAM_OUTPUT; + } return DEFAULT_OUTPUT; } @@ -484,7 +508,9 @@ export function resolveTtsProviderOrder(primary: TtsProvider): TtsProvider[] { } export function isTtsProviderConfigured(config: ResolvedTtsConfig, provider: TtsProvider): boolean { - if (provider === "edge") return config.edge.enabled; + if (provider === "edge") { + return config.edge.enabled; + } return Boolean(resolveTtsApiKey(config, provider)); } @@ -494,7 +520,9 @@ function isValidVoiceId(voiceId: string): boolean { function normalizeElevenLabsBaseUrl(baseUrl: string): string { const trimmed = baseUrl.trim(); - if (!trimmed) return DEFAULT_ELEVENLABS_BASE_URL; + if (!trimmed) { + return DEFAULT_ELEVENLABS_BASE_URL; + } return trimmed.replace(/\/+$/, ""); } @@ -513,7 +541,9 @@ function assertElevenLabsVoiceSettings(settings: ResolvedTtsConfig["elevenlabs"] function normalizeLanguageCode(code?: string): string | undefined { const trimmed = code?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const normalized = trimmed.toLowerCase(); if (!/^[a-z]{2}$/.test(normalized)) { throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)"); @@ -523,14 +553,20 @@ function normalizeLanguageCode(code?: string): string | undefined { function normalizeApplyTextNormalization(mode?: string): "auto" | "on" | "off" | undefined { const trimmed = mode?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const normalized = trimmed.toLowerCase(); - if (normalized === "auto" || normalized === "on" || normalized === "off") return normalized; + if (normalized === "auto" || normalized === "on" || normalized === "off") { + return normalized; + } throw new Error("applyTextNormalization must be one of: auto, on, off"); } function normalizeSeed(seed?: number): number | undefined { - if (seed == null) return undefined; + if (seed == null) { + return undefined; + } const next = Math.floor(seed); if (!Number.isFinite(next) || next < 0 || next > 4_294_967_295) { throw new Error("seed must be between 0 and 4294967295"); @@ -540,8 +576,12 @@ function normalizeSeed(seed?: number): number | undefined { function parseBooleanValue(value: string): boolean | undefined { const normalized = value.trim().toLowerCase(); - if (["true", "1", "yes", "on"].includes(normalized)) return true; - if (["false", "0", "no", "off"].includes(normalized)) return false; + if (["true", "1", "yes", "on"].includes(normalized)) { + return true; + } + if (["false", "0", "no", "off"].includes(normalized)) { + return false; + } return undefined; } @@ -578,15 +618,21 @@ function parseTtsDirectives( const tokens = body.split(/\s+/).filter(Boolean); for (const token of tokens) { const eqIndex = token.indexOf("="); - if (eqIndex === -1) continue; + if (eqIndex === -1) { + continue; + } const rawKey = token.slice(0, eqIndex).trim(); const rawValue = token.slice(eqIndex + 1).trim(); - if (!rawKey || !rawValue) continue; + if (!rawKey || !rawValue) { + continue; + } const key = rawKey.toLowerCase(); try { switch (key) { case "provider": - if (!policy.allowProvider) break; + if (!policy.allowProvider) { + break; + } if (rawValue === "openai" || rawValue === "elevenlabs" || rawValue === "edge") { overrides.provider = rawValue; } else { @@ -596,7 +642,9 @@ function parseTtsDirectives( case "voice": case "openai_voice": case "openaivoice": - if (!policy.allowVoice) break; + if (!policy.allowVoice) { + break; + } if (isValidOpenAIVoice(rawValue)) { overrides.openai = { ...overrides.openai, voice: rawValue }; } else { @@ -607,7 +655,9 @@ function parseTtsDirectives( case "voice_id": case "elevenlabs_voice": case "elevenlabsvoice": - if (!policy.allowVoice) break; + if (!policy.allowVoice) { + break; + } if (isValidVoiceId(rawValue)) { overrides.elevenlabs = { ...overrides.elevenlabs, voiceId: rawValue }; } else { @@ -621,7 +671,9 @@ function parseTtsDirectives( case "elevenlabsmodel": case "openai_model": case "openaimodel": - if (!policy.allowModelId) break; + if (!policy.allowModelId) { + break; + } if (isValidOpenAIModel(rawValue)) { overrides.openai = { ...overrides.openai, model: rawValue }; } else { @@ -629,7 +681,9 @@ function parseTtsDirectives( } break; case "stability": - if (!policy.allowVoiceSettings) break; + if (!policy.allowVoiceSettings) { + break; + } { const value = parseNumberValue(rawValue); if (value == null) { @@ -646,7 +700,9 @@ function parseTtsDirectives( case "similarity": case "similarityboost": case "similarity_boost": - if (!policy.allowVoiceSettings) break; + if (!policy.allowVoiceSettings) { + break; + } { const value = parseNumberValue(rawValue); if (value == null) { @@ -661,7 +717,9 @@ function parseTtsDirectives( } break; case "style": - if (!policy.allowVoiceSettings) break; + if (!policy.allowVoiceSettings) { + break; + } { const value = parseNumberValue(rawValue); if (value == null) { @@ -676,7 +734,9 @@ function parseTtsDirectives( } break; case "speed": - if (!policy.allowVoiceSettings) break; + if (!policy.allowVoiceSettings) { + break; + } { const value = parseNumberValue(rawValue); if (value == null) { @@ -694,7 +754,9 @@ function parseTtsDirectives( case "speaker_boost": case "usespeakerboost": case "use_speaker_boost": - if (!policy.allowVoiceSettings) break; + if (!policy.allowVoiceSettings) { + break; + } { const value = parseBooleanValue(rawValue); if (value == null) { @@ -710,7 +772,9 @@ function parseTtsDirectives( case "normalize": case "applytextnormalization": case "apply_text_normalization": - if (!policy.allowNormalization) break; + if (!policy.allowNormalization) { + break; + } overrides.elevenlabs = { ...overrides.elevenlabs, applyTextNormalization: normalizeApplyTextNormalization(rawValue), @@ -719,14 +783,18 @@ function parseTtsDirectives( case "language": case "languagecode": case "language_code": - if (!policy.allowNormalization) break; + if (!policy.allowNormalization) { + break; + } overrides.elevenlabs = { ...overrides.elevenlabs, languageCode: normalizeLanguageCode(rawValue), }; break; case "seed": - if (!policy.allowSeed) break; + if (!policy.allowSeed) { + break; + } overrides.elevenlabs = { ...overrides.elevenlabs, seed: normalizeSeed(Number.parseInt(rawValue, 10)), @@ -786,13 +854,17 @@ type OpenAiTtsVoice = (typeof OPENAI_TTS_VOICES)[number]; function isValidOpenAIModel(model: string): boolean { // Allow any model when using custom endpoint (e.g., Kokoro, LocalAI) - if (isCustomOpenAIEndpoint()) return true; + if (isCustomOpenAIEndpoint()) { + return true; + } return OPENAI_TTS_MODELS.includes(model as (typeof OPENAI_TTS_MODELS)[number]); } function isValidOpenAIVoice(voice: string): voice is OpenAiTtsVoice { // Allow any voice when using custom endpoint (e.g., Kokoro Chinese voices) - if (isCustomOpenAIEndpoint()) return true; + if (isCustomOpenAIEndpoint()) { + return true; + } return OPENAI_TTS_VOICES.includes(voice as OpenAiTtsVoice); } @@ -809,12 +881,14 @@ type SummaryModelSelection = { }; function resolveSummaryModelRef( - cfg: MoltbotConfig, + cfg: OpenClawConfig, config: ResolvedTtsConfig, ): SummaryModelSelection { const defaultRef = resolveDefaultModelForAgent({ cfg }); const override = config.summaryModel?.trim(); - if (!override) return { ref: defaultRef, source: "default" }; + if (!override) { + return { ref: defaultRef, source: "default" }; + } const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: defaultRef.provider }); const resolved = resolveModelRefFromString({ @@ -822,7 +896,9 @@ function resolveSummaryModelRef( defaultProvider: defaultRef.provider, aliasIndex, }); - if (!resolved) return { ref: defaultRef, source: "default" }; + if (!resolved) { + return { ref: defaultRef, source: "default" }; + } return { ref: resolved.ref, source: "summaryModel" }; } @@ -833,7 +909,7 @@ function isTextContentBlock(block: { type: string }): block is TextContent { async function summarizeText(params: { text: string; targetLength: number; - cfg: MoltbotConfig; + cfg: OpenClawConfig; config: ResolvedTtsConfig; timeoutMs: number; }): Promise { @@ -904,7 +980,7 @@ async function summarizeText(params: { } catch (err) { const error = err as Error; if (error.name === "AbortError") { - throw new Error("Summarization timed out"); + throw new Error("Summarization timed out", { cause: err }); } throw err; } @@ -1046,9 +1122,15 @@ async function openaiTTS(params: { function inferEdgeExtension(outputFormat: string): string { const normalized = outputFormat.toLowerCase(); - if (normalized.includes("webm")) return ".webm"; - if (normalized.includes("ogg")) return ".ogg"; - if (normalized.includes("opus")) return ".opus"; + if (normalized.includes("webm")) { + return ".webm"; + } + if (normalized.includes("ogg")) { + return ".ogg"; + } + if (normalized.includes("opus")) { + return ".opus"; + } if (normalized.includes("wav") || normalized.includes("riff") || normalized.includes("pcm")) { return ".wav"; } @@ -1078,7 +1160,7 @@ async function edgeTTS(params: { export async function textToSpeech(params: { text: string; - cfg: MoltbotConfig; + cfg: OpenClawConfig; prefsPath?: string; channel?: string; overrides?: TtsDirectiveOverrides; @@ -1249,7 +1331,7 @@ export async function textToSpeech(params: { export async function textToSpeechTelephony(params: { text: string; - cfg: MoltbotConfig; + cfg: OpenClawConfig; prefsPath?: string; }): Promise { const config = resolveTtsConfig(params.cfg); @@ -1343,7 +1425,7 @@ export async function textToSpeechTelephony(params: { export async function maybeApplyTtsToPayload(params: { payload: ReplyPayload; - cfg: MoltbotConfig; + cfg: OpenClawConfig; channel?: string; kind?: "tool" | "block" | "final"; inboundAudio?: boolean; @@ -1356,7 +1438,9 @@ export async function maybeApplyTtsToPayload(params: { prefsPath, sessionAuto: params.ttsAuto, }); - if (autoMode === "off") return params.payload; + if (autoMode === "off") { + return params.payload; + } const text = params.payload.text ?? ""; const directives = parseTtsDirectives(text, config.modelOverrides); @@ -1377,16 +1461,30 @@ export async function maybeApplyTtsToPayload(params: { text: visibleText.length > 0 ? visibleText : undefined, }; - if (autoMode === "tagged" && !directives.hasDirective) return nextPayload; - if (autoMode === "inbound" && params.inboundAudio !== true) return nextPayload; + if (autoMode === "tagged" && !directives.hasDirective) { + return nextPayload; + } + if (autoMode === "inbound" && params.inboundAudio !== true) { + return nextPayload; + } const mode = config.mode ?? "final"; - if (mode === "final" && params.kind && params.kind !== "final") return nextPayload; + if (mode === "final" && params.kind && params.kind !== "final") { + return nextPayload; + } - if (!ttsText.trim()) return nextPayload; - if (params.payload.mediaUrl || (params.payload.mediaUrls?.length ?? 0) > 0) return nextPayload; - if (text.includes("MEDIA:")) return nextPayload; - if (ttsText.trim().length < 10) return nextPayload; + if (!ttsText.trim()) { + return nextPayload; + } + if (params.payload.mediaUrl || (params.payload.mediaUrls?.length ?? 0) > 0) { + return nextPayload; + } + if (text.includes("MEDIA:")) { + return nextPayload; + } + if (ttsText.trim().length < 10) { + return nextPayload; + } const maxLength = getTtsMaxLength(prefsPath); let textForAudio = ttsText.trim(); diff --git a/src/tui/commands.test.ts b/src/tui/commands.test.ts index 43be20733..5bd02b87d 100644 --- a/src/tui/commands.test.ts +++ b/src/tui/commands.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { getSlashCommands, parseCommand } from "./commands.js"; describe("tui slash commands", () => { diff --git a/src/tui/commands.ts b/src/tui/commands.ts index dfc419632..66260d6a1 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -1,7 +1,7 @@ import type { SlashCommand } from "@mariozechner/pi-tui"; +import type { OpenClawConfig } from "../config/types.js"; import { listChatCommands, listChatCommandsForConfig } from "../auto-reply/commands-registry.js"; import { formatThinkingLevels, listThinkingLevelLabels } from "../auto-reply/thinking.js"; -import type { MoltbotConfig } from "../config/types.js"; const VERBOSE_LEVELS = ["on", "off"]; const REASONING_LEVELS = ["on", "off"]; @@ -15,7 +15,7 @@ export type ParsedCommand = { }; export type SlashCommandOptions = { - cfg?: MoltbotConfig; + cfg?: OpenClawConfig; provider?: string; model?: string; }; @@ -26,7 +26,9 @@ const COMMAND_ALIASES: Record = { export function parseCommand(input: string): ParsedCommand { const trimmed = input.replace(/^\//, "").trim(); - if (!trimmed) return { name: "", args: "" }; + if (!trimmed) { + return { name: "", args: "" }; + } const [name, ...rest] = trimmed.split(/\s+/); const normalized = name.toLowerCase(); return { @@ -125,7 +127,9 @@ export function getSlashCommands(options: SlashCommandOptions = {}): SlashComman const aliases = command.textAliases.length > 0 ? command.textAliases : [`/${command.key}`]; for (const alias of aliases) { const name = alias.replace(/^\//, "").trim(); - if (!name || seen.has(name)) continue; + if (!name || seen.has(name)) { + continue; + } seen.add(name); commands.push({ name, description: command.description }); } diff --git a/src/tui/components/chat-log.ts b/src/tui/components/chat-log.ts index 25061245b..e9c696397 100644 --- a/src/tui/components/chat-log.ts +++ b/src/tui/components/chat-log.ts @@ -71,7 +71,9 @@ export class ChatLog extends Container { updateToolArgs(toolCallId: string, args: unknown) { const existing = this.toolById.get(toolCallId); - if (!existing) return; + if (!existing) { + return; + } existing.setArgs(args); } @@ -81,7 +83,9 @@ export class ChatLog extends Container { opts?: { isError?: boolean; partial?: boolean }, ) { const existing = this.toolById.get(toolCallId); - if (!existing) return; + if (!existing) { + return; + } if (opts?.partial) { existing.setPartialResult(result as Record); return; diff --git a/src/tui/components/custom-editor.ts b/src/tui/components/custom-editor.ts index 0abede6a0..4dc42391f 100644 --- a/src/tui/components/custom-editor.ts +++ b/src/tui/components/custom-editor.ts @@ -1,11 +1,4 @@ -import { - Editor, - type EditorOptions, - type EditorTheme, - type TUI, - Key, - matchesKey, -} from "@mariozechner/pi-tui"; +import { Editor, Key, matchesKey } from "@mariozechner/pi-tui"; export class CustomEditor extends Editor { onEscape?: () => void; @@ -19,9 +12,6 @@ export class CustomEditor extends Editor { onShiftTab?: () => void; onAltEnter?: () => void; - constructor(tui: TUI, theme: EditorTheme, options?: EditorOptions) { - super(tui, theme, options); - } handleInput(data: string): void { if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) { this.onAltEnter(); diff --git a/src/tui/components/filterable-select-list.ts b/src/tui/components/filterable-select-list.ts index a7b197bf5..7a2834872 100644 --- a/src/tui/components/filterable-select-list.ts +++ b/src/tui/components/filterable-select-list.ts @@ -1,3 +1,4 @@ +import type { Component } from "@mariozechner/pi-tui"; import { Input, matchesKey, @@ -6,7 +7,6 @@ import { type SelectListTheme, getEditorKeybindings, } from "@mariozechner/pi-tui"; -import type { Component } from "@mariozechner/pi-tui"; import chalk from "chalk"; import { fuzzyFilterLower, prepareSearchItems } from "./fuzzy-filter.js"; diff --git a/src/tui/components/fuzzy-filter.ts b/src/tui/components/fuzzy-filter.ts index fb6e2acf2..7fea77422 100644 --- a/src/tui/components/fuzzy-filter.ts +++ b/src/tui/components/fuzzy-filter.ts @@ -19,11 +19,15 @@ export function isWordBoundary(text: string, index: number): boolean { * Returns null if no match. */ export function findWordBoundaryIndex(text: string, query: string): number | null { - if (!query) return null; + if (!query) { + return null; + } const textLower = text.toLowerCase(); const queryLower = query.toLowerCase(); const maxIndex = textLower.length - queryLower.length; - if (maxIndex < 0) return null; + if (maxIndex < 0) { + return null; + } for (let i = 0; i <= maxIndex; i++) { if (textLower.startsWith(queryLower, i) && isWordBoundary(textLower, i)) { return i; @@ -37,8 +41,12 @@ export function findWordBoundaryIndex(text: string, query: string): number | nul * Returns score (lower = better) or null if no match. */ export function fuzzyMatchLower(queryLower: string, textLower: string): number | null { - if (queryLower.length === 0) return 0; - if (queryLower.length > textLower.length) return null; + if (queryLower.length === 0) { + return 0; + } + if (queryLower.length > textLower.length) { + return null; + } let queryIndex = 0; let score = 0; @@ -53,9 +61,13 @@ export function fuzzyMatchLower(queryLower: string, textLower: string): number | score -= consecutiveMatches * 5; // Reward consecutive matches } else { consecutiveMatches = 0; - if (lastMatchIndex >= 0) score += (i - lastMatchIndex - 1) * 2; // Penalize gaps + if (lastMatchIndex >= 0) { + score += (i - lastMatchIndex - 1) * 2; + } // Penalize gaps } - if (isAtWordBoundary) score -= 10; // Reward word boundary matches + if (isAtWordBoundary) { + score -= 10; + } // Reward word boundary matches score += i * 0.1; // Slight penalty for later matches lastMatchIndex = i; queryIndex++; @@ -73,10 +85,14 @@ export function fuzzyFilterLower( queryLower: string, ): T[] { const trimmed = queryLower.trim(); - if (!trimmed) return items; + if (!trimmed) { + return items; + } const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0); - if (tokens.length === 0) return items; + if (tokens.length === 0) { + return items; + } const results: { item: T; score: number }[] = []; for (const item of items) { @@ -92,7 +108,9 @@ export function fuzzyFilterLower( break; } } - if (allMatch) results.push({ item, score: totalScore }); + if (allMatch) { + results.push({ item, score: totalScore }); + } } results.sort((a, b) => a.score - b.score); return results.map((r) => r.item); @@ -106,9 +124,15 @@ export function prepareSearchItems< >(items: T[]): (T & { searchTextLower: string })[] { return items.map((item) => { const parts: string[] = []; - if (item.label) parts.push(item.label); - if (item.description) parts.push(item.description); - if (item.searchText) parts.push(item.searchText); + if (item.label) { + parts.push(item.label); + } + if (item.description) { + parts.push(item.description); + } + if (item.searchText) { + parts.push(item.searchText); + } return { ...item, searchTextLower: parts.join(" ").toLowerCase() }; }); } diff --git a/src/tui/components/searchable-select-list.ts b/src/tui/components/searchable-select-list.ts index 54fc34918..046cc138c 100644 --- a/src/tui/components/searchable-select-list.ts +++ b/src/tui/components/searchable-select-list.ts @@ -119,8 +119,12 @@ export class SearchableSelectList implements Component { a: { item: SelectItem; tier: number; score: number }, b: { item: SelectItem; tier: number; score: number }, ) => { - if (a.tier !== b.tier) return a.tier - b.tier; - if (a.score !== b.score) return a.score - b.score; + if (a.tier !== b.tier) { + return a.tier - b.tier; + } + if (a.score !== b.score) { + return a.score - b.score; + } return this.getItemLabel(a.item).localeCompare(this.getItemLabel(b.item)); }; @@ -134,9 +138,11 @@ export class SearchableSelectList implements Component { .split(/\s+/) .map((token) => token.toLowerCase()) .filter((token) => token.length > 0); - if (tokens.length === 0) return text; + if (tokens.length === 0) { + return text; + } - const uniqueTokens = Array.from(new Set(tokens)).sort((a, b) => b.length - a.length); + const uniqueTokens = Array.from(new Set(tokens)).toSorted((a, b) => b.length - a.length); let result = text; for (const token of uniqueTokens) { const regex = this.getCachedRegex(token); @@ -186,7 +192,9 @@ export class SearchableSelectList implements Component { // Render visible items for (let i = startIndex; i < endIndex; i++) { const item = this.filteredItems[i]; - if (!item) continue; + if (!item) { + continue; + } const isSelected = i === this.selectedIndex; lines.push(this.renderItemLine(item, isSelected, width, query)); } @@ -236,7 +244,9 @@ export class SearchableSelectList implements Component { } handleInput(keyData: string): void { - if (isKeyRelease(keyData)) return; + if (isKeyRelease(keyData)) { + return; + } const allowVimNav = !this.searchInput.getValue().trim(); diff --git a/src/tui/components/tool-execution.ts b/src/tui/components/tool-execution.ts index da3aabfb7..e5d15fec2 100644 --- a/src/tui/components/tool-execution.ts +++ b/src/tui/components/tool-execution.ts @@ -20,8 +20,12 @@ const PREVIEW_LINES = 12; function formatArgs(toolName: string, args: unknown): string { const display = resolveToolDisplay({ name: toolName, args }); const detail = formatToolDetail(display); - if (detail) return detail; - if (!args || typeof args !== "object") return ""; + if (detail) { + return detail; + } + if (!args || typeof args !== "object") { + return ""; + } try { return JSON.stringify(args); } catch { @@ -30,7 +34,9 @@ function formatArgs(toolName: string, args: unknown): string { } function extractText(result?: ToolResult): string { - if (!result?.content) return ""; + if (!result?.content) { + return ""; + } const lines: string[] = []; for (const entry of result.content) { if (entry.type === "text" && entry.text) { diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index c13d48ccf..0dc08e9c2 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -112,7 +112,7 @@ export class GatewayChatClient { token: resolved.token, password: resolved.password, clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, - clientDisplayName: "moltbot-tui", + clientDisplayName: "openclaw-tui", clientVersion: VERSION, platform: process.platform, mode: GATEWAY_CLIENT_MODES.UI, @@ -235,7 +235,7 @@ export function resolveGatewayConnection(opts: GatewayConnectionOptions) { ? typeof remote?.token === "string" && remote.token.trim().length > 0 ? remote.token.trim() : undefined - : process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() || + : process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || (typeof authToken === "string" && authToken.trim().length > 0 ? authToken.trim() : undefined)); @@ -244,7 +244,7 @@ export function resolveGatewayConnection(opts: GatewayConnectionOptions) { (typeof opts.password === "string" && opts.password.trim().length > 0 ? opts.password.trim() : undefined) || - process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || + process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || (typeof remote?.password === "string" && remote.password.trim().length > 0 ? remote.password.trim() : undefined); diff --git a/src/tui/tui-command-handlers.test.ts b/src/tui/tui-command-handlers.test.ts index fc2ac4fa6..5ca9f9745 100644 --- a/src/tui/tui-command-handlers.test.ts +++ b/src/tui/tui-command-handlers.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { createCommandHandlers } from "./tui-command-handlers.js"; describe("tui command handlers", () => { diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index a14172809..136885b2a 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -1,4 +1,12 @@ import type { Component, TUI } from "@mariozechner/pi-tui"; +import type { ChatLog } from "./components/chat-log.js"; +import type { GatewayChatClient } from "./gateway-chat.js"; +import type { + AgentSummary, + GatewayStatusSummary, + TuiOptions, + TuiStateAccess, +} from "./tui-types.js"; import { formatThinkingLevels, normalizeUsageDisplay, @@ -7,20 +15,12 @@ import { import { normalizeAgentId } from "../routing/session-key.js"; import { formatRelativeTime } from "../utils/time-format.js"; import { helpText, parseCommand } from "./commands.js"; -import type { ChatLog } from "./components/chat-log.js"; import { createFilterableSelectList, createSearchableSelectList, createSettingsList, } from "./components/selectors.js"; -import type { GatewayChatClient } from "./gateway-chat.js"; import { formatStatusSummary } from "./tui-status-summary.js"; -import type { - AgentSummary, - GatewayStatusSummary, - TuiOptions, - TuiStateAccess, -} from "./tui-types.js"; type CommandHandlerContext = { client: GatewayChatClient; @@ -228,7 +228,9 @@ export function createCommandHandlers(context: CommandHandlerContext) { const handleCommand = async (raw: string) => { const { name, args } = parseCommand(raw); - if (!name) return; + if (!name) { + return; + } switch (name) { case "help": chatLog.addSystem( @@ -247,7 +249,9 @@ export function createCommandHandlers(context: CommandHandlerContext) { } if (status && typeof status === "object") { const lines = formatStatusSummary(status as GatewayStatusSummary); - for (const line of lines) chatLog.addSystem(line); + for (const line of lines) { + chatLog.addSystem(line); + } break; } chatLog.addSystem("status: unknown response"); diff --git a/src/tui/tui-event-handlers.test.ts b/src/tui/tui-event-handlers.test.ts index ee661da39..3549cf4bb 100644 --- a/src/tui/tui-event-handlers.test.ts +++ b/src/tui/tui-event-handlers.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from "vitest"; - -import { createEventHandlers } from "./tui-event-handlers.js"; import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js"; +import { createEventHandlers } from "./tui-event-handlers.js"; type MockChatLog = { startTool: ReturnType; diff --git a/src/tui/tui-event-handlers.ts b/src/tui/tui-event-handlers.ts index bc857c704..111f1fafb 100644 --- a/src/tui/tui-event-handlers.ts +++ b/src/tui/tui-event-handlers.ts @@ -1,8 +1,8 @@ import type { TUI } from "@mariozechner/pi-tui"; import type { ChatLog } from "./components/chat-log.js"; +import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js"; import { asString, extractTextFromMessage, isCommandMessage } from "./tui-formatters.js"; import { TuiStreamAssembler } from "./tui-stream-assembler.js"; -import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js"; type EventHandlerContext = { chatLog: ChatLog; @@ -20,22 +20,32 @@ export function createEventHandlers(context: EventHandlerContext) { let lastSessionKey = state.currentSessionKey; const pruneRunMap = (runs: Map) => { - if (runs.size <= 200) return; + if (runs.size <= 200) { + return; + } const keepUntil = Date.now() - 10 * 60 * 1000; for (const [key, ts] of runs) { - if (runs.size <= 150) break; - if (ts < keepUntil) runs.delete(key); + if (runs.size <= 150) { + break; + } + if (ts < keepUntil) { + runs.delete(key); + } } if (runs.size > 200) { for (const key of runs.keys()) { runs.delete(key); - if (runs.size <= 150) break; + if (runs.size <= 150) { + break; + } } } }; const syncSessionKey = () => { - if (state.currentSessionKey === lastSessionKey) return; + if (state.currentSessionKey === lastSessionKey) { + return; + } lastSessionKey = state.currentSessionKey; finalizedRuns.clear(); sessionRuns.clear(); @@ -55,13 +65,21 @@ export function createEventHandlers(context: EventHandlerContext) { }; const handleChatEvent = (payload: unknown) => { - if (!payload || typeof payload !== "object") return; + if (!payload || typeof payload !== "object") { + return; + } const evt = payload as ChatEvent; syncSessionKey(); - if (evt.sessionKey !== state.currentSessionKey) return; + if (evt.sessionKey !== state.currentSessionKey) { + return; + } if (finalizedRuns.has(evt.runId)) { - if (evt.state === "delta") return; - if (evt.state === "final") return; + if (evt.state === "delta") { + return; + } + if (evt.state === "final") { + return; + } } noteSessionRun(evt.runId); if (!state.activeChatRunId) { @@ -69,14 +87,18 @@ export function createEventHandlers(context: EventHandlerContext) { } if (evt.state === "delta") { const displayText = streamAssembler.ingestDelta(evt.runId, evt.message, state.showThinking); - if (!displayText) return; + if (!displayText) { + return; + } chatLog.updateAssistant(displayText, evt.runId); setActivityStatus("streaming"); } if (evt.state === "final") { if (isCommandMessage(evt.message)) { const text = extractTextFromMessage(evt.message); - if (text) chatLog.addSystem(text); + if (text) { + chatLog.addSystem(text); + } streamAssembler.drop(evt.runId); noteFinalizedRun(evt.runId); state.activeChatRunId = null; @@ -120,19 +142,25 @@ export function createEventHandlers(context: EventHandlerContext) { }; const handleAgentEvent = (payload: unknown) => { - if (!payload || typeof payload !== "object") return; + if (!payload || typeof payload !== "object") { + return; + } const evt = payload as AgentEvent; syncSessionKey(); // Agent events (tool streaming, lifecycle) are emitted per-run. Filter against the // active chat run id, not the session id. const isActiveRun = evt.runId === state.activeChatRunId; - if (!isActiveRun && !sessionRuns.has(evt.runId)) return; + if (!isActiveRun && !sessionRuns.has(evt.runId)) { + return; + } if (evt.stream === "tool") { const data = evt.data ?? {}; const phase = asString(data.phase, ""); const toolCallId = asString(data.toolCallId, ""); const toolName = asString(data.name, "tool"); - if (!toolCallId) return; + if (!toolCallId) { + return; + } if (phase === "start") { chatLog.startTool(toolCallId, toolName, data.args); } else if (phase === "update") { @@ -148,11 +176,19 @@ export function createEventHandlers(context: EventHandlerContext) { return; } if (evt.stream === "lifecycle") { - if (!isActiveRun) return; + if (!isActiveRun) { + return; + } const phase = typeof evt.data?.phase === "string" ? evt.data.phase : ""; - if (phase === "start") setActivityStatus("running"); - if (phase === "end") setActivityStatus("idle"); - if (phase === "error") setActivityStatus("error"); + if (phase === "start") { + setActivityStatus("running"); + } + if (phase === "end") { + setActivityStatus("idle"); + } + if (phase === "error") { + setActivityStatus("error"); + } tui.requestRender(); } }; diff --git a/src/tui/tui-formatters.test.ts b/src/tui/tui-formatters.test.ts index 3200b237a..74d574c51 100644 --- a/src/tui/tui-formatters.test.ts +++ b/src/tui/tui-formatters.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { extractContentFromMessage, extractTextFromMessage, diff --git a/src/tui/tui-formatters.ts b/src/tui/tui-formatters.ts index f77eb9ff1..4c6693a6b 100644 --- a/src/tui/tui-formatters.ts +++ b/src/tui/tui-formatters.ts @@ -1,14 +1,18 @@ -import { formatTokenCount } from "../utils/usage-format.js"; import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js"; +import { formatTokenCount } from "../utils/usage-format.js"; export function resolveFinalAssistantText(params: { finalText?: string | null; streamedText?: string | null; }) { const finalText = params.finalText ?? ""; - if (finalText.trim()) return finalText; + if (finalText.trim()) { + return finalText; + } const streamedText = params.streamedText ?? ""; - if (streamedText.trim()) return streamedText; + if (streamedText.trim()) { + return streamedText; + } return "(no output)"; } @@ -36,15 +40,23 @@ export function composeThinkingAndContent(params: { * Model-agnostic: returns empty string if no thinking blocks exist. */ export function extractThinkingFromMessage(message: unknown): string { - if (!message || typeof message !== "object") return ""; + if (!message || typeof message !== "object") { + return ""; + } const record = message as Record; const content = record.content; - if (typeof content === "string") return ""; - if (!Array.isArray(content)) return ""; + if (typeof content === "string") { + return ""; + } + if (!Array.isArray(content)) { + return ""; + } const parts: string[] = []; for (const block of content) { - if (!block || typeof block !== "object") continue; + if (!block || typeof block !== "object") { + continue; + } const rec = block as Record; if (rec.type === "thinking" && typeof rec.thinking === "string") { parts.push(rec.thinking); @@ -58,11 +70,15 @@ export function extractThinkingFromMessage(message: unknown): string { * Model-agnostic: works for any model with text content blocks. */ export function extractContentFromMessage(message: unknown): string { - if (!message || typeof message !== "object") return ""; + if (!message || typeof message !== "object") { + return ""; + } const record = message as Record; const content = record.content; - if (typeof content === "string") return content.trim(); + if (typeof content === "string") { + return content.trim(); + } // Check for error BEFORE returning empty for non-array content if (!Array.isArray(content)) { @@ -76,7 +92,9 @@ export function extractContentFromMessage(message: unknown): string { const parts: string[] = []; for (const block of content) { - if (!block || typeof block !== "object") continue; + if (!block || typeof block !== "object") { + continue; + } const rec = block as Record; if (rec.type === "text" && typeof rec.text === "string") { parts.push(rec.text); @@ -96,14 +114,20 @@ export function extractContentFromMessage(message: unknown): string { } function extractTextBlocks(content: unknown, opts?: { includeThinking?: boolean }): string { - if (typeof content === "string") return content.trim(); - if (!Array.isArray(content)) return ""; + if (typeof content === "string") { + return content.trim(); + } + if (!Array.isArray(content)) { + return ""; + } const thinkingParts: string[] = []; const textParts: string[] = []; for (const block of content) { - if (!block || typeof block !== "object") continue; + if (!block || typeof block !== "object") { + continue; + } const record = block as Record; if (record.type === "text" && typeof record.text === "string") { textParts.push(record.text); @@ -128,27 +152,39 @@ export function extractTextFromMessage( message: unknown, opts?: { includeThinking?: boolean }, ): string { - if (!message || typeof message !== "object") return ""; + if (!message || typeof message !== "object") { + return ""; + } const record = message as Record; const text = extractTextBlocks(record.content, opts); - if (text) return text; + if (text) { + return text; + } const stopReason = typeof record.stopReason === "string" ? record.stopReason : ""; - if (stopReason !== "error") return ""; + if (stopReason !== "error") { + return ""; + } const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : ""; return formatRawAssistantErrorForUi(errorMessage); } export function isCommandMessage(message: unknown): boolean { - if (!message || typeof message !== "object") return false; + if (!message || typeof message !== "object") { + return false; + } return (message as Record).command === true; } export function formatTokens(total?: number | null, context?: number | null) { - if (total == null && context == null) return "tokens ?"; + if (total == null && context == null) { + return "tokens ?"; + } const totalLabel = total == null ? "?" : formatTokenCount(total); - if (context == null) return `tokens ${totalLabel}`; + if (context == null) { + return `tokens ${totalLabel}`; + } const pct = typeof total === "number" && context > 0 ? Math.min(999, Math.round((total / context) * 100)) @@ -173,7 +209,9 @@ export function formatContextUsageLine(params: { } export function asString(value: unknown, fallback = ""): string { - if (typeof value === "string") return value; + if (typeof value === "string") { + return value; + } if (typeof value === "number" || typeof value === "boolean") { return String(value); } diff --git a/src/tui/tui-input-history.test.ts b/src/tui/tui-input-history.test.ts index 858e599a0..5bcdbe547 100644 --- a/src/tui/tui-input-history.test.ts +++ b/src/tui/tui-input-history.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { createEditorSubmitHandler } from "./tui.js"; describe("createEditorSubmitHandler", () => { diff --git a/src/tui/tui-local-shell.test.ts b/src/tui/tui-local-shell.test.ts index 1e600ef6a..7728478e7 100644 --- a/src/tui/tui-local-shell.test.ts +++ b/src/tui/tui-local-shell.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { createLocalShellRunner } from "./tui-local-shell.js"; const createSelector = () => { diff --git a/src/tui/tui-local-shell.ts b/src/tui/tui-local-shell.ts index 296862c30..0ff12a546 100644 --- a/src/tui/tui-local-shell.ts +++ b/src/tui/tui-local-shell.ts @@ -34,8 +34,12 @@ export function createLocalShellRunner(deps: LocalShellDeps) { const maxChars = deps.maxOutputChars ?? 40_000; const ensureLocalExecAllowed = async (): Promise => { - if (localExecAllowed) return true; - if (localExecAsked) return false; + if (localExecAllowed) { + return true; + } + if (localExecAsked) { + return false; + } localExecAsked = true; return await new Promise((resolve) => { @@ -78,7 +82,9 @@ export function createLocalShellRunner(deps: LocalShellDeps) { const cmd = line.slice(1); // NOTE: A lone '!' is handled by the submit handler as a normal message. // Keep this guard anyway in case this is called directly. - if (cmd === "") return; + if (cmd === "") { + return; + } if (localExecAsked && !localExecAllowed) { deps.chatLog.addSystem("local shell: not enabled for this session"); @@ -87,7 +93,9 @@ export function createLocalShellRunner(deps: LocalShellDeps) { } const allowed = await ensureLocalExecAllowed(); - if (!allowed) return; + if (!allowed) { + return; + } deps.chatLog.addSystem(`[local] $ ${cmd}`); deps.tui.requestRender(); diff --git a/src/tui/tui-overlays.test.ts b/src/tui/tui-overlays.test.ts index a612c8c76..c3842bd70 100644 --- a/src/tui/tui-overlays.test.ts +++ b/src/tui/tui-overlays.test.ts @@ -1,6 +1,5 @@ import type { Component } from "@mariozechner/pi-tui"; import { describe, expect, it, vi } from "vitest"; - import { createOverlayHandlers } from "./tui-overlays.js"; class DummyComponent implements Component { diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index 5dc6696ad..310acc741 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -1,13 +1,13 @@ import type { TUI } from "@mariozechner/pi-tui"; +import type { ChatLog } from "./components/chat-log.js"; +import type { GatewayAgentsList, GatewayChatClient } from "./gateway-chat.js"; +import type { TuiOptions, TuiStateAccess } from "./tui-types.js"; import { normalizeAgentId, normalizeMainKey, parseAgentSessionKey, } from "../routing/session-key.js"; -import type { ChatLog } from "./components/chat-log.js"; -import type { GatewayAgentsList, GatewayChatClient } from "./gateway-chat.js"; import { asString, extractTextFromMessage, isCommandMessage } from "./tui-formatters.js"; -import type { TuiOptions, TuiStateAccess } from "./tui-types.js"; type SessionActionContext = { client: GatewayChatClient; @@ -53,7 +53,9 @@ export function createSessionActions(context: SessionActionContext) { })); agentNames.clear(); for (const agent of state.agents) { - if (agent.name) agentNames.set(agent.id, agent.name); + if (agent.name) { + agentNames.set(agent.id, agent.name); + } } if (!state.initialSessionApplied) { if (initialSessionAgentId) { @@ -88,7 +90,9 @@ export function createSessionActions(context: SessionActionContext) { const updateAgentFromSessionKey = (key: string) => { const parsed = parseAgentSessionKey(key); - if (!parsed) return; + if (!parsed) { + return; + } const next = normalizeAgentId(parsed.agentId); if (next !== state.currentAgentId) { state.currentAgentId = next; @@ -96,7 +100,9 @@ export function createSessionActions(context: SessionActionContext) { }; const refreshSessionInfo = async () => { - if (refreshSessionInfoPromise) return refreshSessionInfoPromise; + if (refreshSessionInfoPromise) { + return refreshSessionInfoPromise; + } refreshSessionInfoPromise = (async () => { try { const listAgentId = @@ -110,7 +116,9 @@ export function createSessionActions(context: SessionActionContext) { }); const entry = result.sessions.find((row) => { // Exact match - if (row.key === state.currentSessionKey) return true; + if (row.key === state.currentSessionKey) { + return true; + } // Also match canonical keys like "agent:default:main" against "main" const parsed = parseAgentSessionKey(row.key); return parsed?.rest === state.currentSessionKey; @@ -159,23 +167,31 @@ export function createSessionActions(context: SessionActionContext) { chatLog.clearAll(); chatLog.addSystem(`session ${state.currentSessionKey}`); for (const entry of record.messages ?? []) { - if (!entry || typeof entry !== "object") continue; + if (!entry || typeof entry !== "object") { + continue; + } const message = entry as Record; if (isCommandMessage(message)) { const text = extractTextFromMessage(message); - if (text) chatLog.addSystem(text); + if (text) { + chatLog.addSystem(text); + } continue; } if (message.role === "user") { const text = extractTextFromMessage(message); - if (text) chatLog.addUser(text); + if (text) { + chatLog.addUser(text); + } continue; } if (message.role === "assistant") { const text = extractTextFromMessage(message, { includeThinking: state.showThinking, }); - if (text) chatLog.finalizeAssistant(text); + if (text) { + chatLog.finalizeAssistant(text); + } continue; } if (message.role === "toolResult") { diff --git a/src/tui/tui-status-summary.ts b/src/tui/tui-status-summary.ts index 2c0402fac..bda1b1b76 100644 --- a/src/tui/tui-status-summary.ts +++ b/src/tui/tui-status-summary.ts @@ -1,7 +1,7 @@ +import type { GatewayStatusSummary } from "./tui-types.js"; import { formatAge } from "../infra/channel-summary.js"; import { formatTokenCount } from "../utils/usage-format.js"; import { formatContextUsageLine } from "./tui-formatters.js"; -import type { GatewayStatusSummary } from "./tui-types.js"; export function formatStatusSummary(summary: GatewayStatusSummary) { const lines: string[] = []; @@ -32,7 +32,9 @@ export function formatStatusSummary(summary: GatewayStatusSummary) { if (heartbeatAgents.length > 0) { const heartbeatParts = heartbeatAgents.map((agent) => { const agentId = agent.agentId ?? "unknown"; - if (!agent.enabled || !agent.everyMs) return `disabled (${agentId})`; + if (!agent.enabled || !agent.everyMs) { + return `disabled (${agentId})`; + } return `${agent.every ?? "unknown"} (${agentId})`; }); lines.push(""); diff --git a/src/tui/tui-stream-assembler.test.ts b/src/tui/tui-stream-assembler.test.ts index 4a180a0d8..e56eb5699 100644 --- a/src/tui/tui-stream-assembler.test.ts +++ b/src/tui/tui-stream-assembler.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { TuiStreamAssembler } from "./tui-stream-assembler.js"; describe("TuiStreamAssembler", () => { diff --git a/src/tui/tui-stream-assembler.ts b/src/tui/tui-stream-assembler.ts index d6f5d817a..f94483461 100644 --- a/src/tui/tui-stream-assembler.ts +++ b/src/tui/tui-stream-assembler.ts @@ -52,7 +52,9 @@ export class TuiStreamAssembler { const previousDisplayText = state.displayText; this.updateRunState(state, message, showThinking); - if (!state.displayText || state.displayText === previousDisplayText) return null; + if (!state.displayText || state.displayText === previousDisplayText) { + return null; + } return state.displayText; } diff --git a/src/tui/tui-waiting.test.ts b/src/tui/tui-waiting.test.ts index 12a3bc6c9..d2a7aee87 100644 --- a/src/tui/tui-waiting.test.ts +++ b/src/tui/tui-waiting.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { buildWaitingStatusMessage, pickWaitingPhrase } from "./tui-waiting.js"; const theme = { diff --git a/src/tui/tui.submit-handler.test.ts b/src/tui/tui.submit-handler.test.ts index 799f382e2..a12d9f114 100644 --- a/src/tui/tui.submit-handler.test.ts +++ b/src/tui/tui.submit-handler.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { createEditorSubmitHandler } from "./tui.js"; describe("createEditorSubmitHandler", () => { diff --git a/src/tui/tui.test.ts b/src/tui/tui.test.ts index ade4239d3..789b95009 100644 --- a/src/tui/tui.test.ts +++ b/src/tui/tui.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { resolveFinalAssistantText } from "./tui.js"; describe("resolveFinalAssistantText", () => { diff --git a/src/tui/tui.ts b/src/tui/tui.ts index a7e72f179..a2250746e 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -6,6 +6,13 @@ import { Text, TUI, } from "@mariozechner/pi-tui"; +import type { + AgentSummary, + SessionInfo, + SessionScope, + TuiOptions, + TuiStateAccess, +} from "./tui-types.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { loadConfig } from "../config/config.js"; import { @@ -23,16 +30,9 @@ import { createCommandHandlers } from "./tui-command-handlers.js"; import { createEventHandlers } from "./tui-event-handlers.js"; import { formatTokens } from "./tui-formatters.js"; import { createLocalShellRunner } from "./tui-local-shell.js"; -import { buildWaitingStatusMessage, defaultWaitingPhrases } from "./tui-waiting.js"; import { createOverlayHandlers } from "./tui-overlays.js"; import { createSessionActions } from "./tui-session-actions.js"; -import type { - AgentSummary, - SessionInfo, - SessionScope, - TuiOptions, - TuiStateAccess, -} from "./tui-types.js"; +import { buildWaitingStatusMessage, defaultWaitingPhrases } from "./tui-waiting.js"; export { resolveFinalAssistantText } from "./tui-formatters.js"; export type { TuiOptions } from "./tui-types.js"; @@ -52,7 +52,9 @@ export function createEditorSubmitHandler(params: { params.editor.setText(""); // Keep previous behavior: ignore empty/whitespace-only submissions. - if (!value) return; + if (!value) { + return; + } // Bash mode: only if the very first character is '!' and it's not just '!'. // IMPORTANT: use the raw (untrimmed) text so leading spaces do NOT trigger. @@ -259,7 +261,9 @@ export async function runTui(opts: TuiOptions) { tui.setFocus(editor); const formatSessionKey = (key: string) => { - if (key === "global" || key === "unknown") return key; + if (key === "global" || key === "unknown") { + return key; + } const parsed = parseAgentSessionKey(key); return parsed?.rest ?? key; }; @@ -271,15 +275,21 @@ export async function runTui(opts: TuiOptions) { const resolveSessionKey = (raw?: string) => { const trimmed = (raw ?? "").trim(); - if (sessionScope === "global") return "global"; + if (sessionScope === "global") { + return "global"; + } if (!trimmed) { return buildAgentMainSessionKey({ agentId: currentAgentId, mainKey: sessionMainKey, }); } - if (trimmed === "global" || trimmed === "unknown") return trimmed; - if (trimmed.startsWith("agent:")) return trimmed; + if (trimmed === "global" || trimmed === "unknown") { + return trimmed; + } + if (trimmed.startsWith("agent:")) { + return trimmed; + } return `agent:${currentAgentId}:${trimmed}`; }; @@ -290,7 +300,7 @@ export async function runTui(opts: TuiOptions) { const agentLabel = formatAgentLabel(currentAgentId); header.setText( theme.header( - `moltbot tui - ${client.connection.url} - agent ${agentLabel} - session ${sessionLabel}`, + `openclaw tui - ${client.connection.url} - agent ${agentLabel} - session ${sessionLabel}`, ), ); }; @@ -301,14 +311,18 @@ export async function runTui(opts: TuiOptions) { const formatElapsed = (startMs: number) => { const totalSeconds = Math.max(0, Math.floor((Date.now() - startMs) / 1000)); - if (totalSeconds < 60) return `${totalSeconds}s`; + if (totalSeconds < 60) { + return `${totalSeconds}s`; + } const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}m ${seconds}s`; }; const ensureStatusText = () => { - if (statusText) return; + if (statusText) { + return; + } statusContainer.clear(); statusLoader?.stop(); statusLoader = null; @@ -317,7 +331,9 @@ export async function runTui(opts: TuiOptions) { }; const ensureStatusLoader = () => { - if (statusLoader) return; + if (statusLoader) { + return; + } statusContainer.clear(); statusText = null; statusLoader = new Loader( @@ -334,7 +350,9 @@ export async function runTui(opts: TuiOptions) { let waitingPhrase: string | null = null; const updateBusyStatusMessage = () => { - if (!statusLoader || !statusStartedAt) return; + if (!statusLoader || !statusStartedAt) { + return; + } const elapsed = formatElapsed(statusStartedAt); if (activityStatus === "waiting") { @@ -355,21 +373,29 @@ export async function runTui(opts: TuiOptions) { }; const startStatusTimer = () => { - if (statusTimer) return; + if (statusTimer) { + return; + } statusTimer = setInterval(() => { - if (!busyStates.has(activityStatus)) return; + if (!busyStates.has(activityStatus)) { + return; + } updateBusyStatusMessage(); }, 1000); }; const stopStatusTimer = () => { - if (!statusTimer) return; + if (!statusTimer) { + return; + } clearInterval(statusTimer); statusTimer = null; }; const startWaitingTimer = () => { - if (waitingTimer) return; + if (waitingTimer) { + return; + } // Pick a phrase once per waiting session. if (!waitingPhrase) { @@ -380,13 +406,17 @@ export async function runTui(opts: TuiOptions) { waitingTick = 0; waitingTimer = setInterval(() => { - if (activityStatus !== "waiting") return; + if (activityStatus !== "waiting") { + return; + } updateBusyStatusMessage(); }, 120); }; const stopWaitingTimer = () => { - if (!waitingTimer) return; + if (!waitingTimer) { + return; + } clearInterval(waitingTimer); waitingTimer = null; waitingPhrase = null; @@ -423,7 +453,9 @@ export async function runTui(opts: TuiOptions) { const setConnectionStatus = (text: string, ttlMs?: number) => { connectionStatus = text; renderStatus(); - if (statusTimeout) clearTimeout(statusTimeout); + if (statusTimeout) { + clearTimeout(statusTimeout); + } if (ttlMs && ttlMs > 0) { statusTimeout = setTimeout(() => { connectionStatus = isConnected ? "connected" : "disconnected"; @@ -469,7 +501,9 @@ export async function runTui(opts: TuiOptions) { const { openOverlay, closeOverlay } = createOverlayHandlers(tui, editor); const initialSessionAgentId = (() => { - if (!initialSessionInput) return null; + if (!initialSessionInput) { + return null; + } const parsed = parseAgentSessionKey(initialSessionInput); return parsed ? normalizeAgentId(parsed.agentId) : null; })(); @@ -579,8 +613,12 @@ export async function runTui(opts: TuiOptions) { }; client.onEvent = (evt) => { - if (evt.event === "chat") handleChatEvent(evt.payload); - if (evt.event === "agent") handleAgentEvent(evt.payload); + if (evt.event === "chat") { + handleChatEvent(evt.payload); + } + if (evt.event === "agent") { + handleAgentEvent(evt.payload); + } }; client.onConnected = () => { diff --git a/src/types/pi-coding-agent.d.ts b/src/types/pi-coding-agent.d.ts new file mode 100644 index 000000000..b455056e6 --- /dev/null +++ b/src/types/pi-coding-agent.d.ts @@ -0,0 +1,8 @@ +import "@mariozechner/pi-coding-agent"; + +declare module "@mariozechner/pi-coding-agent" { + interface CreateAgentSessionOptions { + /** Extra extension paths merged with settings-based discovery. */ + additionalExtensionPaths?: string[]; + } +} diff --git a/src/utils.test.ts b/src/utils.test.ts index 769c98a4f..03aef5341 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -39,7 +39,7 @@ describe("withWhatsAppPrefix", () => { describe("ensureDir", () => { it("creates nested directory", async () => { - const tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), "moltbot-test-")); + const tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); const target = path.join(tmp, "nested", "dir"); await ensureDir(target); expect(fs.existsSync(target)).toBe(true); @@ -83,7 +83,9 @@ describe("jidToE164", () => { .spyOn(fs, "readFileSync") // biome-ignore lint/suspicious/noExplicitAny: forwarding to native signature .mockImplementation((path: any, encoding?: any) => { - if (path === mappingPath) return `"5551234"`; + if (path === mappingPath) { + return `"5551234"`; + } return original(path, encoding); }); expect(jidToE164("123@lid")).toBe("+5551234"); @@ -91,7 +93,7 @@ describe("jidToE164", () => { }); it("maps @lid from authDir mapping files", () => { - const authDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-")); + const authDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-")); const mappingPath = path.join(authDir, "lid-mapping-456_reverse.json"); fs.writeFileSync(mappingPath, JSON.stringify("5559876")); expect(jidToE164("456@lid", { authDir })).toBe("+5559876"); @@ -99,7 +101,7 @@ describe("jidToE164", () => { }); it("maps @hosted.lid from authDir mapping files", () => { - const authDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-")); + const authDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-")); const mappingPath = path.join(authDir, "lid-mapping-789_reverse.json"); fs.writeFileSync(mappingPath, JSON.stringify(4440001)); expect(jidToE164("789@hosted.lid", { authDir })).toBe("+4440001"); @@ -111,8 +113,8 @@ describe("jidToE164", () => { }); it("falls back through lidMappingDirs in order", () => { - const first = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-lid-a-")); - const second = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-lid-b-")); + const first = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-lid-a-")); + const second = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-lid-b-")); const mappingPath = path.join(second, "lid-mapping-321_reverse.json"); fs.writeFileSync(mappingPath, JSON.stringify("123321")); expect(jidToE164("321@lid", { lidMappingDirs: [first, second] })).toBe("+123321"); @@ -122,10 +124,10 @@ describe("jidToE164", () => { }); describe("resolveConfigDir", () => { - it("prefers ~/.moltbot when legacy dir is missing", async () => { - const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "moltbot-config-dir-")); + it("prefers ~/.openclaw when legacy dir is missing", async () => { + const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-config-dir-")); try { - const newDir = path.join(root, ".moltbot"); + const newDir = path.join(root, ".openclaw"); await fs.promises.mkdir(newDir, { recursive: true }); const resolved = resolveConfigDir({} as NodeJS.ProcessEnv, () => root); expect(resolved).toBe(newDir); @@ -159,7 +161,7 @@ describe("resolveUserPath", () => { }); it("expands ~/ to home dir", () => { - expect(resolveUserPath("~/clawd")).toBe(path.resolve(os.homedir(), "clawd")); + expect(resolveUserPath("~/openclaw")).toBe(path.resolve(os.homedir(), "openclaw")); }); it("resolves relative paths", () => { diff --git a/src/utils.ts b/src/utils.ts index 7c441f4f1..e8a6ac725 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,7 +25,9 @@ export function assertWebChannel(input: string): asserts input is WebChannel { } export function normalizePath(p: string): string { - if (!p.startsWith("/")) return `/${p}`; + if (!p.startsWith("/")) { + return `/${p}`; + } return p; } @@ -36,7 +38,9 @@ export function withWhatsAppPrefix(number: string): string { export function normalizeE164(number: string): string { const withoutPrefix = number.replace(/^whatsapp:/, "").trim(); const digits = withoutPrefix.replace(/[^\d+]/g, ""); - if (digits.startsWith("+")) return `+${digits.slice(1)}`; + if (digits.startsWith("+")) { + return `+${digits.slice(1)}`; + } return `+${digits}`; } @@ -49,11 +53,17 @@ export function isSelfChatMode( selfE164: string | null | undefined, allowFrom?: Array | null, ): boolean { - if (!selfE164) return false; - if (!Array.isArray(allowFrom) || allowFrom.length === 0) return false; + if (!selfE164) { + return false; + } + if (!Array.isArray(allowFrom) || allowFrom.length === 0) { + return false; + } const normalizedSelf = normalizeE164(selfE164); return allowFrom.some((n) => { - if (n === "*") return false; + if (n === "*") { + return false; + } try { return normalizeE164(String(n)) === normalizedSelf; } catch { @@ -64,7 +74,9 @@ export function isSelfChatMode( export function toWhatsappJid(number: string): string { const withoutPrefix = number.replace(/^whatsapp:/, "").trim(); - if (withoutPrefix.includes("@")) return withoutPrefix; + if (withoutPrefix.includes("@")) { + return withoutPrefix; + } const e164 = normalizeE164(withoutPrefix); const digits = e164.replace(/\D/g, ""); return `${digits}@s.whatsapp.net`; @@ -83,11 +95,15 @@ type LidLookup = { function resolveLidMappingDirs(opts?: JidToE164Options): string[] { const dirs = new Set(); const addDir = (dir?: string | null) => { - if (!dir) return; + if (!dir) { + return; + } dirs.add(resolveUserPath(dir)); }; addDir(opts?.authDir); - for (const dir of opts?.lidMappingDirs ?? []) addDir(dir); + for (const dir of opts?.lidMappingDirs ?? []) { + addDir(dir); + } addDir(resolveOAuthDir()); addDir(path.join(CONFIG_DIR, "credentials")); return [...dirs]; @@ -101,7 +117,9 @@ function readLidReverseMapping(lid: string, opts?: JidToE164Options): string | n try { const data = fs.readFileSync(mappingPath, "utf8"); const phone = JSON.parse(data) as string | number | null; - if (phone === null || phone === undefined) continue; + if (phone === null || phone === undefined) { + continue; + } return normalizeE164(String(phone)); } catch { // Try the next location. @@ -123,7 +141,9 @@ export function jidToE164(jid: string, opts?: JidToE164Options): string | null { if (lidMatch) { const lid = lidMatch[1]; const phone = readLidReverseMapping(lid, opts); - if (phone) return phone; + if (phone) { + return phone; + } const shouldLog = opts?.logMissing ?? shouldLogVerbose(); if (shouldLog) { logVerbose(`LID mapping not found for ${lid}; skipping inbound message`); @@ -137,14 +157,24 @@ export async function resolveJidToE164( jid: string | null | undefined, opts?: JidToE164Options & { lidLookup?: LidLookup }, ): Promise { - if (!jid) return null; + if (!jid) { + return null; + } const direct = jidToE164(jid, opts); - if (direct) return direct; - if (!/(@lid|@hosted\.lid)$/.test(jid)) return null; - if (!opts?.lidLookup?.getPNForLID) return null; + if (direct) { + return direct; + } + if (!/(@lid|@hosted\.lid)$/.test(jid)) { + return null; + } + if (!opts?.lidLookup?.getPNForLID) { + return null; + } try { const pnJid = await opts.lidLookup.getPNForLID(jid); - if (!pnJid) return null; + if (!pnJid) { + return null; + } return jidToE164(pnJid, opts); } catch (err) { if (shouldLogVerbose()) { @@ -197,13 +227,17 @@ export function sliceUtf16Safe(input: string, start: number, end?: number): stri export function truncateUtf16Safe(input: string, maxLen: number): string { const limit = Math.max(0, Math.floor(maxLen)); - if (input.length <= limit) return input; + if (input.length <= limit) { + return input; + } return sliceUtf16Safe(input, 0, limit); } export function resolveUserPath(input: string): string { const trimmed = input.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } if (trimmed.startsWith("~")) { const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir()); return path.resolve(expanded); @@ -215,25 +249,31 @@ export function resolveConfigDir( env: NodeJS.ProcessEnv = process.env, homedir: () => string = os.homedir, ): string { - const override = env.MOLTBOT_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim(); - if (override) return resolveUserPath(override); - const legacyDir = path.join(homedir(), ".clawdbot"); - const newDir = path.join(homedir(), ".moltbot"); + const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim(); + if (override) { + return resolveUserPath(override); + } + const newDir = path.join(homedir(), ".openclaw"); try { - const hasLegacy = fs.existsSync(legacyDir); const hasNew = fs.existsSync(newDir); - if (!hasLegacy && hasNew) return newDir; + if (hasNew) { + return newDir; + } } catch { // best-effort } - return legacyDir; + return newDir; } export function resolveHomeDir(): string | undefined { const envHome = process.env.HOME?.trim(); - if (envHome) return envHome; + if (envHome) { + return envHome; + } const envProfile = process.env.USERPROFILE?.trim(); - if (envProfile) return envProfile; + if (envProfile) { + return envProfile; + } try { const home = os.homedir(); return home?.trim() ? home : undefined; @@ -243,18 +283,30 @@ export function resolveHomeDir(): string | undefined { } export function shortenHomePath(input: string): string { - if (!input) return input; + if (!input) { + return input; + } const home = resolveHomeDir(); - if (!home) return input; - if (input === home) return "~"; - if (input.startsWith(`${home}/`)) return `~${input.slice(home.length)}`; + if (!home) { + return input; + } + if (input === home) { + return "~"; + } + if (input.startsWith(`${home}/`)) { + return `~${input.slice(home.length)}`; + } return input; } export function shortenHomeInString(input: string): string { - if (!input) return input; + if (!input) { + return input; + } const home = resolveHomeDir(); - if (!home) return input; + if (!home) { + return input; + } return input.split(home).join("~"); } @@ -282,5 +334,5 @@ export function formatTerminalLink( return `\u001b]8;;${safeUrl}\u0007${safeLabel}\u001b]8;;\u0007`; } -// Configuration root; can be overridden via CLAWDBOT_STATE_DIR. +// Configuration root; can be overridden via OPENCLAW_STATE_DIR. export const CONFIG_DIR = resolveConfigDir(); diff --git a/src/utils/account-id.ts b/src/utils/account-id.ts index e8992cfd3..4827fc214 100644 --- a/src/utils/account-id.ts +++ b/src/utils/account-id.ts @@ -1,5 +1,7 @@ export function normalizeAccountId(value?: string): string | undefined { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const trimmed = value.trim(); return trimmed || undefined; } diff --git a/src/utils/boolean.test.ts b/src/utils/boolean.test.ts index 00a2a66c3..04c1dd52f 100644 --- a/src/utils/boolean.test.ts +++ b/src/utils/boolean.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { parseBooleanValue } from "./boolean.js"; describe("parseBooleanValue", () => { diff --git a/src/utils/boolean.ts b/src/utils/boolean.ts index 8847950ef..60e303f90 100644 --- a/src/utils/boolean.ts +++ b/src/utils/boolean.ts @@ -12,15 +12,25 @@ export function parseBooleanValue( value: unknown, options: BooleanParseOptions = {}, ): boolean | undefined { - if (typeof value === "boolean") return value; - if (typeof value !== "string") return undefined; + if (typeof value === "boolean") { + return value; + } + if (typeof value !== "string") { + return undefined; + } const normalized = value.trim().toLowerCase(); - if (!normalized) return undefined; + if (!normalized) { + return undefined; + } const truthy = options.truthy ?? DEFAULT_TRUTHY; const falsy = options.falsy ?? DEFAULT_FALSY; const truthySet = truthy === DEFAULT_TRUTHY ? DEFAULT_TRUTHY_SET : new Set(truthy); const falsySet = falsy === DEFAULT_FALSY ? DEFAULT_FALSY_SET : new Set(falsy); - if (truthySet.has(normalized)) return true; - if (falsySet.has(normalized)) return false; + if (truthySet.has(normalized)) { + return true; + } + if (falsySet.has(normalized)) { + return false; + } return undefined; } diff --git a/src/utils/delivery-context.test.ts b/src/utils/delivery-context.test.ts index 705e6d27f..6ab1abfce 100644 --- a/src/utils/delivery-context.test.ts +++ b/src/utils/delivery-context.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { deliveryContextKey, deliveryContextFromSession, @@ -75,6 +74,33 @@ describe("delivery context helpers", () => { accountId: undefined, threadId: "999", }); + + expect( + deliveryContextFromSession({ + channel: "telegram", + lastTo: " -1001 ", + origin: { threadId: 42 }, + }), + ).toEqual({ + channel: "telegram", + to: "-1001", + accountId: undefined, + threadId: 42, + }); + + expect( + deliveryContextFromSession({ + channel: "telegram", + lastTo: " -1001 ", + deliveryContext: { threadId: " 777 " }, + origin: { threadId: 42 }, + }), + ).toEqual({ + channel: "telegram", + to: "-1001", + accountId: undefined, + threadId: "777", + }); }); it("normalizes delivery fields and mirrors them on session entries", () => { diff --git a/src/utils/delivery-context.ts b/src/utils/delivery-context.ts index 9f5803e17..97e88e9a8 100644 --- a/src/utils/delivery-context.ts +++ b/src/utils/delivery-context.ts @@ -18,7 +18,9 @@ export type DeliveryContextSessionSource = { }; export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryContext | undefined { - if (!context) return undefined; + if (!context) { + return undefined; + } const channel = typeof context.channel === "string" ? (normalizeMessageChannel(context.channel) ?? context.channel.trim()) @@ -33,13 +35,17 @@ export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryCon : undefined; const normalizedThreadId = typeof threadId === "string" ? (threadId ? threadId : undefined) : threadId; - if (!channel && !to && !accountId && normalizedThreadId == null) return undefined; + if (!channel && !to && !accountId && normalizedThreadId == null) { + return undefined; + } const normalized: DeliveryContext = { channel: channel || undefined, to: to || undefined, accountId, }; - if (normalizedThreadId != null) normalized.threadId = normalizedThreadId; + if (normalizedThreadId != null) { + normalized.threadId = normalizedThreadId; + } return normalized; } @@ -90,10 +96,20 @@ export function normalizeSessionDeliveryFields(source?: DeliveryContextSessionSo } export function deliveryContextFromSession( - entry?: DeliveryContextSessionSource, + entry?: DeliveryContextSessionSource & { origin?: { threadId?: string | number } }, ): DeliveryContext | undefined { - if (!entry) return undefined; - return normalizeSessionDeliveryFields(entry).deliveryContext; + if (!entry) { + return undefined; + } + const source: DeliveryContextSessionSource = { + channel: entry.channel, + lastChannel: entry.lastChannel, + lastTo: entry.lastTo, + lastAccountId: entry.lastAccountId, + lastThreadId: entry.lastThreadId ?? entry.deliveryContext?.threadId ?? entry.origin?.threadId, + deliveryContext: entry.deliveryContext, + }; + return normalizeSessionDeliveryFields(source).deliveryContext; } export function mergeDeliveryContext( @@ -102,7 +118,9 @@ export function mergeDeliveryContext( ): DeliveryContext | undefined { const normalizedPrimary = normalizeDeliveryContext(primary); const normalizedFallback = normalizeDeliveryContext(fallback); - if (!normalizedPrimary && !normalizedFallback) return undefined; + if (!normalizedPrimary && !normalizedFallback) { + return undefined; + } return normalizeDeliveryContext({ channel: normalizedPrimary?.channel ?? normalizedFallback?.channel, to: normalizedPrimary?.to ?? normalizedFallback?.to, @@ -113,7 +131,9 @@ export function mergeDeliveryContext( export function deliveryContextKey(context?: DeliveryContext): string | undefined { const normalized = normalizeDeliveryContext(context); - if (!normalized?.channel || !normalized?.to) return undefined; + if (!normalized?.channel || !normalized?.to) { + return undefined; + } const threadId = normalized.threadId != null && normalized.threadId !== "" ? String(normalized.threadId) : ""; return `${normalized.channel}|${normalized.to}|${normalized.accountId ?? ""}|${threadId}`; diff --git a/src/utils/directive-tags.ts b/src/utils/directive-tags.ts index a58b143dc..1260b8aa5 100644 --- a/src/utils/directive-tags.ts +++ b/src/utils/directive-tags.ts @@ -58,7 +58,9 @@ export function parseInlineDirectives( sawCurrent = true; } else { const id = idRaw.trim(); - if (id) lastExplicitId = id; + if (id) { + lastExplicitId = id; + } } return stripReplyTags ? " " : match; }); diff --git a/src/utils/message-channel.test.ts b/src/utils/message-channel.test.ts index 5651b97a3..460b51feb 100644 --- a/src/utils/message-channel.test.ts +++ b/src/utils/message-channel.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; - import type { ChannelPlugin } from "../channels/plugins/types.js"; import type { PluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; diff --git a/src/utils/message-channel.ts b/src/utils/message-channel.ts index 1b33b7209..ed580960a 100644 --- a/src/utils/message-channel.ts +++ b/src/utils/message-channel.ts @@ -1,9 +1,9 @@ +import type { ChannelId } from "../channels/plugins/types.js"; import { CHANNEL_IDS, listChatChannelAliases, normalizeChatChannelId, } from "../channels/registry.js"; -import type { ChannelId } from "../channels/plugins/types.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, @@ -46,19 +46,29 @@ export function isInternalMessageChannel(raw?: string | null): raw is InternalMe export function isWebchatClient(client?: GatewayClientInfoLike | null): boolean { const mode = normalizeGatewayClientMode(client?.mode); - if (mode === GATEWAY_CLIENT_MODES.WEBCHAT) return true; + if (mode === GATEWAY_CLIENT_MODES.WEBCHAT) { + return true; + } return normalizeGatewayClientName(client?.id) === GATEWAY_CLIENT_NAMES.WEBCHAT_UI; } export function normalizeMessageChannel(raw?: string | null): string | undefined { const normalized = raw?.trim().toLowerCase(); - if (!normalized) return undefined; - if (normalized === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL; + if (!normalized) { + return undefined; + } + if (normalized === INTERNAL_MESSAGE_CHANNEL) { + return INTERNAL_MESSAGE_CHANNEL; + } const builtIn = normalizeChatChannelId(normalized); - if (builtIn) return builtIn; + if (builtIn) { + return builtIn; + } const registry = getActivePluginRegistry(); const pluginMatch = registry?.channels.find((entry) => { - if (entry.plugin.id.toLowerCase() === normalized) return true; + if (entry.plugin.id.toLowerCase() === normalized) { + return true; + } return (entry.plugin.meta.aliases ?? []).some( (alias) => alias.trim().toLowerCase() === normalized, ); @@ -68,13 +78,17 @@ export function normalizeMessageChannel(raw?: string | null): string | undefined const listPluginChannelIds = (): string[] => { const registry = getActivePluginRegistry(); - if (!registry) return []; + if (!registry) { + return []; + } return registry.channels.map((entry) => entry.plugin.id); }; const listPluginChannelAliases = (): string[] => { const registry = getActivePluginRegistry(); - if (!registry) return []; + if (!registry) { + return []; + } return registry.channels.flatMap((entry) => entry.plugin.meta.aliases ?? []); }; @@ -112,7 +126,9 @@ export function resolveGatewayMessageChannel( raw?: string | null, ): GatewayMessageChannel | undefined { const normalized = normalizeMessageChannel(raw); - if (!normalized) return undefined; + if (!normalized) { + return undefined; + } return isGatewayMessageChannel(normalized) ? normalized : undefined; } @@ -125,6 +141,8 @@ export function resolveMessageChannel( export function isMarkdownCapableMessageChannel(raw?: string | null): boolean { const channel = normalizeMessageChannel(raw); - if (!channel) return false; + if (!channel) { + return false; + } return MARKDOWN_CAPABLE_CHANNELS.has(channel); } diff --git a/src/utils/provider-utils.ts b/src/utils/provider-utils.ts index e29ffe1ab..046a23c78 100644 --- a/src/utils/provider-utils.ts +++ b/src/utils/provider-utils.ts @@ -8,7 +8,9 @@ * API fields for reasoning/thinking. */ export function isReasoningTagProvider(provider: string | undefined | null): boolean { - if (!provider) return false; + if (!provider) { + return false; + } const normalized = provider.trim().toLowerCase(); // Check for exact matches or known prefixes/substrings for reasoning providers diff --git a/src/utils/queue-helpers.ts b/src/utils/queue-helpers.ts index e4bac1fe0..990b52bb5 100644 --- a/src/utils/queue-helpers.ts +++ b/src/utils/queue-helpers.ts @@ -12,7 +12,9 @@ export type QueueState = QueueSummaryState & { }; export function elideQueueText(text: string, limit = 140): string { - if (text.length <= limit) return text; + if (text.length <= limit) { + return text; + } return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…`; } @@ -26,7 +28,9 @@ export function shouldSkipQueueItem(params: { items: T[]; dedupe?: (item: T, items: T[]) => boolean; }): boolean { - if (!params.dedupe) return false; + if (!params.dedupe) { + return false; + } return params.dedupe(params.item, params.items); } @@ -36,8 +40,12 @@ export function applyQueueDropPolicy(params: { summaryLimit?: number; }): boolean { const cap = params.queue.cap; - if (cap <= 0 || params.queue.items.length < cap) return true; - if (params.queue.dropPolicy === "new") return false; + if (cap <= 0 || params.queue.items.length < cap) { + return true; + } + if (params.queue.dropPolicy === "new") { + return false; + } const dropCount = params.queue.items.length - cap + 1; const dropped = params.queue.items.splice(0, dropCount); if (params.queue.dropPolicy === "summarize") { @@ -46,7 +54,9 @@ export function applyQueueDropPolicy(params: { params.queue.summaryLines.push(buildQueueSummaryLine(params.summarize(item))); } const limit = Math.max(0, params.summaryLimit ?? cap); - while (params.queue.summaryLines.length > limit) params.queue.summaryLines.shift(); + while (params.queue.summaryLines.length > limit) { + params.queue.summaryLines.shift(); + } } return true; } @@ -56,7 +66,9 @@ export function waitForQueueDebounce(queue: { lastEnqueuedAt: number; }): Promise { const debounceMs = Math.max(0, queue.debounceMs); - if (debounceMs <= 0) return Promise.resolve(); + if (debounceMs <= 0) { + return Promise.resolve(); + } return new Promise((resolve) => { const check = () => { const since = Date.now() - queue.lastEnqueuedAt; @@ -101,7 +113,9 @@ export function buildCollectPrompt(params: { renderItem: (item: T, index: number) => string; }): string { const blocks: string[] = [params.title]; - if (params.summary) blocks.push(params.summary); + if (params.summary) { + blocks.push(params.summary); + } params.items.forEach((item, idx) => { blocks.push(params.renderItem(item, idx)); }); @@ -117,7 +131,9 @@ export function hasCrossChannelItems( for (const item of items) { const resolved = resolveKey(item); - if (resolved.cross) return true; + if (resolved.cross) { + return true; + } if (!resolved.key) { hasUnkeyed = true; continue; @@ -125,7 +141,11 @@ export function hasCrossChannelItems( keys.add(resolved.key); } - if (keys.size === 0) return false; - if (hasUnkeyed) return true; + if (keys.size === 0) { + return false; + } + if (hasUnkeyed) { + return true; + } return keys.size > 1; } diff --git a/src/utils/time-format.ts b/src/utils/time-format.ts index bd473e4f6..188cec4c7 100644 --- a/src/utils/time-format.ts +++ b/src/utils/time-format.ts @@ -6,10 +6,20 @@ export function formatRelativeTime(timestamp: number): string { const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); - if (seconds < 60) return "just now"; - if (minutes < 60) return `${minutes}m ago`; - if (hours < 24) return `${hours}h ago`; - if (days === 1) return "Yesterday"; - if (days < 7) return `${days}d ago`; + if (seconds < 60) { + return "just now"; + } + if (minutes < 60) { + return `${minutes}m ago`; + } + if (hours < 24) { + return `${hours}h ago`; + } + if (days === 1) { + return "Yesterday"; + } + if (days < 7) { + return `${days}d ago`; + } return new Date(timestamp).toLocaleDateString(undefined, { month: "short", day: "numeric" }); } diff --git a/src/utils/usage-format.test.ts b/src/utils/usage-format.test.ts index 12da3b182..8d7985f4d 100644 --- a/src/utils/usage-format.test.ts +++ b/src/utils/usage-format.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { estimateUsageCost, formatTokenCount, @@ -35,7 +35,7 @@ describe("usage-format", () => { }, }, }, - } as MoltbotConfig; + } as OpenClawConfig; const cost = resolveModelCostConfig({ provider: "test", diff --git a/src/utils/usage-format.ts b/src/utils/usage-format.ts index 962e55bc4..f8182f5db 100644 --- a/src/utils/usage-format.ts +++ b/src/utils/usage-format.ts @@ -1,5 +1,5 @@ import type { NormalizedUsage } from "../agents/usage.js"; -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; export type ModelCostConfig = { input: number; @@ -17,28 +17,42 @@ export type UsageTotals = { }; export function formatTokenCount(value?: number): string { - if (value === undefined || !Number.isFinite(value)) return "0"; + if (value === undefined || !Number.isFinite(value)) { + return "0"; + } const safe = Math.max(0, value); - if (safe >= 1_000_000) return `${(safe / 1_000_000).toFixed(1)}m`; - if (safe >= 1_000) return `${(safe / 1_000).toFixed(safe >= 10_000 ? 0 : 1)}k`; + if (safe >= 1_000_000) { + return `${(safe / 1_000_000).toFixed(1)}m`; + } + if (safe >= 1_000) { + return `${(safe / 1_000).toFixed(safe >= 10_000 ? 0 : 1)}k`; + } return String(Math.round(safe)); } export function formatUsd(value?: number): string | undefined { - if (value === undefined || !Number.isFinite(value)) return undefined; - if (value >= 1) return `$${value.toFixed(2)}`; - if (value >= 0.01) return `$${value.toFixed(2)}`; + if (value === undefined || !Number.isFinite(value)) { + return undefined; + } + if (value >= 1) { + return `$${value.toFixed(2)}`; + } + if (value >= 0.01) { + return `$${value.toFixed(2)}`; + } return `$${value.toFixed(4)}`; } export function resolveModelCostConfig(params: { provider?: string; model?: string; - config?: MoltbotConfig; + config?: OpenClawConfig; }): ModelCostConfig | undefined { const provider = params.provider?.trim(); const model = params.model?.trim(); - if (!provider || !model) return undefined; + if (!provider || !model) { + return undefined; + } const providers = params.config?.models?.providers ?? {}; const entry = providers[provider]?.models?.find((item) => item.id === model); return entry?.cost; @@ -53,7 +67,9 @@ export function estimateUsageCost(params: { }): number | undefined { const usage = params.usage; const cost = params.cost; - if (!usage || !cost) return undefined; + if (!usage || !cost) { + return undefined; + } const input = toNumber(usage.input); const output = toNumber(usage.output); const cacheRead = toNumber(usage.cacheRead); @@ -63,6 +79,8 @@ export function estimateUsageCost(params: { output * cost.output + cacheRead * cost.cacheRead + cacheWrite * cost.cacheWrite; - if (!Number.isFinite(total)) return undefined; + if (!Number.isFinite(total)) { + return undefined; + } return total / 1_000_000; } diff --git a/src/version.ts b/src/version.ts index 18c3a3b73..e254dd91f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; -declare const __CLAWDBOT_VERSION__: string | undefined; +declare const __OPENCLAW_VERSION__: string | undefined; function readVersionFromPackageJson(): string | null { try { @@ -12,11 +12,11 @@ function readVersionFromPackageJson(): string | null { } } -// Single source of truth for the current moltbot version. +// Single source of truth for the current OpenClaw version. // - Embedded/bundled builds: injected define or env var. // - Dev/npm builds: package.json. export const VERSION = - (typeof __CLAWDBOT_VERSION__ === "string" && __CLAWDBOT_VERSION__) || - process.env.CLAWDBOT_BUNDLED_VERSION || + (typeof __OPENCLAW_VERSION__ === "string" && __OPENCLAW_VERSION__) || + process.env.OPENCLAW_BUNDLED_VERSION || readVersionFromPackageJson() || "0.0.0"; diff --git a/src/web/accounts.ts b/src/web/accounts.ts index 79b6ea642..88754b2a4 100644 --- a/src/web/accounts.ts +++ b/src/web/accounts.ts @@ -1,9 +1,8 @@ import fs from "node:fs"; import path from "node:path"; - -import type { MoltbotConfig } from "../config/config.js"; -import { resolveOAuthDir } from "../config/paths.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { DmPolicy, GroupPolicy, WhatsAppAccountConfig } from "../config/types.js"; +import { resolveOAuthDir } from "../config/paths.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { resolveUserPath } from "../utils.js"; import { hasWebCredsSync } from "./auth-store.js"; @@ -30,13 +29,15 @@ export type ResolvedWhatsAppAccount = { debounceMs?: number; }; -function listConfiguredAccountIds(cfg: MoltbotConfig): string[] { +function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = cfg.channels?.whatsapp?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } -export function listWhatsAppAuthDirs(cfg: MoltbotConfig): string[] { +export function listWhatsAppAuthDirs(cfg: OpenClawConfig): string[] { const oauthDir = resolveOAuthDir(); const whatsappDir = path.join(oauthDir, "whatsapp"); const authDirs = new Set([oauthDir, path.join(whatsappDir, DEFAULT_ACCOUNT_ID)]); @@ -49,7 +50,9 @@ export function listWhatsAppAuthDirs(cfg: MoltbotConfig): string[] { try { const entries = fs.readdirSync(whatsappDir, { withFileTypes: true }); for (const entry of entries) { - if (!entry.isDirectory()) continue; + if (!entry.isDirectory()) { + continue; + } authDirs.add(path.join(whatsappDir, entry.name)); } } catch { @@ -59,28 +62,34 @@ export function listWhatsAppAuthDirs(cfg: MoltbotConfig): string[] { return Array.from(authDirs); } -export function hasAnyWhatsAppAuth(cfg: MoltbotConfig): boolean { +export function hasAnyWhatsAppAuth(cfg: OpenClawConfig): boolean { return listWhatsAppAuthDirs(cfg).some((authDir) => hasWebCredsSync(authDir)); } -export function listWhatsAppAccountIds(cfg: MoltbotConfig): string[] { +export function listWhatsAppAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } -export function resolveDefaultWhatsAppAccountId(cfg: MoltbotConfig): string { +export function resolveDefaultWhatsAppAccountId(cfg: OpenClawConfig): string { const ids = listWhatsAppAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } function resolveAccountConfig( - cfg: MoltbotConfig, + cfg: OpenClawConfig, accountId: string, ): WhatsAppAccountConfig | undefined { const accounts = cfg.channels?.whatsapp?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } const entry = accounts[accountId] as WhatsAppAccountConfig | undefined; return entry; } @@ -102,7 +111,7 @@ function legacyAuthExists(authDir: string): boolean { } } -export function resolveWhatsAppAuthDir(params: { cfg: MoltbotConfig; accountId: string }): { +export function resolveWhatsAppAuthDir(params: { cfg: OpenClawConfig; accountId: string }): { authDir: string; isLegacy: boolean; } { @@ -125,7 +134,7 @@ export function resolveWhatsAppAuthDir(params: { cfg: MoltbotConfig; accountId: } export function resolveWhatsAppAccount(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; }): ResolvedWhatsAppAccount { const rootCfg = params.cfg.channels?.whatsapp; @@ -160,7 +169,7 @@ export function resolveWhatsAppAccount(params: { }; } -export function listEnabledWhatsAppAccounts(cfg: MoltbotConfig): ResolvedWhatsAppAccount[] { +export function listEnabledWhatsAppAccounts(cfg: OpenClawConfig): ResolvedWhatsAppAccount[] { return listWhatsAppAccountIds(cfg) .map((accountId) => resolveWhatsAppAccount({ cfg, accountId })) .filter((account) => account.enabled); diff --git a/src/web/accounts.whatsapp-auth.test.ts b/src/web/accounts.whatsapp-auth.test.ts index b754a8982..c63ae12e5 100644 --- a/src/web/accounts.whatsapp-auth.test.ts +++ b/src/web/accounts.whatsapp-auth.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; - import { hasAnyWhatsAppAuth, listWhatsAppAuthDirs } from "./accounts.js"; describe("hasAnyWhatsAppAuth", () => { @@ -15,16 +14,16 @@ describe("hasAnyWhatsAppAuth", () => { }; beforeEach(() => { - previousOauthDir = process.env.CLAWDBOT_OAUTH_DIR; - tempOauthDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-oauth-")); - process.env.CLAWDBOT_OAUTH_DIR = tempOauthDir; + previousOauthDir = process.env.OPENCLAW_OAUTH_DIR; + tempOauthDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-oauth-")); + process.env.OPENCLAW_OAUTH_DIR = tempOauthDir; }); afterEach(() => { if (previousOauthDir === undefined) { - delete process.env.CLAWDBOT_OAUTH_DIR; + delete process.env.OPENCLAW_OAUTH_DIR; } else { - process.env.CLAWDBOT_OAUTH_DIR = previousOauthDir; + process.env.OPENCLAW_OAUTH_DIR = previousOauthDir; } if (tempOauthDir) { fs.rmSync(tempOauthDir, { recursive: true, force: true }); @@ -47,7 +46,7 @@ describe("hasAnyWhatsAppAuth", () => { }); it("includes authDir overrides", () => { - const customDir = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-wa-auth-")); + const customDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-wa-auth-")); try { writeCreds(customDir); const cfg = { diff --git a/src/web/active-listener.ts b/src/web/active-listener.ts index 233f11a8f..81170d308 100644 --- a/src/web/active-listener.ts +++ b/src/web/active-listener.ts @@ -1,5 +1,5 @@ -import { formatCliCommand } from "../cli/command-format.js"; import type { PollInput } from "../polls.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; export type ActiveWebSendOptions = { @@ -43,7 +43,7 @@ export function requireActiveWebListener(accountId?: string | null): { const listener = listeners.get(id) ?? null; if (!listener) { throw new Error( - `No active WhatsApp Web listener (account: ${id}). Start the gateway, then link WhatsApp with: ${formatCliCommand(`moltbot channels login --channel whatsapp --account ${id}`)}.`, + `No active WhatsApp Web listener (account: ${id}). Start the gateway, then link WhatsApp with: ${formatCliCommand(`openclaw channels login --channel whatsapp --account ${id}`)}.`, ); } return { accountId: id, listener }; diff --git a/src/web/auth-store.ts b/src/web/auth-store.ts index 3e22fd22b..3b535eb82 100644 --- a/src/web/auth-store.ts +++ b/src/web/auth-store.ts @@ -1,14 +1,13 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; - +import type { WebChannel } from "../utils.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { resolveOAuthDir } from "../config/paths.js"; import { info, success } from "../globals.js"; import { getChildLogger } from "../logging.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { formatCliCommand } from "../cli/command-format.js"; -import type { WebChannel } from "../utils.js"; import { jidToE164, resolveUserPath } from "../utils.js"; export function resolveDefaultWebAuthDir(): string { @@ -36,9 +35,13 @@ export function hasWebCredsSync(authDir: string): boolean { function readCredsJsonRaw(filePath: string): string | null { try { - if (!fsSync.existsSync(filePath)) return null; + if (!fsSync.existsSync(filePath)) { + return null; + } const stats = fsSync.statSync(filePath); - if (!stats.isFile() || stats.size <= 1) return null; + if (!stats.isFile() || stats.size <= 1) { + return null; + } return fsSync.readFileSync(filePath, "utf-8"); } catch { return null; @@ -58,7 +61,9 @@ export function maybeRestoreCredsFromBackup(authDir: string): void { } const backupRaw = readCredsJsonRaw(backupPath); - if (!backupRaw) return; + if (!backupRaw) { + return; + } // Ensure backup is parseable before restoring. JSON.parse(backupRaw); @@ -80,7 +85,9 @@ export async function webAuthExists(authDir: string = resolveDefaultWebAuthDir() } try { const stats = await fs.stat(credsPath); - if (!stats.isFile() || stats.size <= 1) return false; + if (!stats.isFile() || stats.size <= 1) { + return false; + } const raw = await fs.readFile(credsPath, "utf-8"); JSON.parse(raw); return true; @@ -92,15 +99,25 @@ export async function webAuthExists(authDir: string = resolveDefaultWebAuthDir() async function clearLegacyBaileysAuthState(authDir: string) { const entries = await fs.readdir(authDir, { withFileTypes: true }); const shouldDelete = (name: string) => { - if (name === "oauth.json") return false; - if (name === "creds.json" || name === "creds.json.bak") return true; - if (!name.endsWith(".json")) return false; + if (name === "oauth.json") { + return false; + } + if (name === "creds.json" || name === "creds.json.bak") { + return true; + } + if (!name.endsWith(".json")) { + return false; + } return /^(app-state-sync|session|sender-key|pre-key)-/.test(name); }; await Promise.all( entries.map(async (entry) => { - if (!entry.isFile()) return; - if (!shouldDelete(entry.name)) return; + if (!entry.isFile()) { + return; + } + if (!shouldDelete(entry.name)) { + return; + } await fs.rm(path.join(authDir, entry.name), { force: true }); }), ); @@ -177,7 +194,7 @@ export async function pickWebChannel( const hasWeb = await webAuthExists(authDir); if (!hasWeb) { throw new Error( - `No WhatsApp Web session found. Run \`${formatCliCommand("moltbot channels login --channel whatsapp --verbose")}\` to link.`, + `No WhatsApp Web session found. Run \`${formatCliCommand("openclaw channels login --channel whatsapp --verbose")}\` to link.`, ); } return choice; diff --git a/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts b/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts index b8e19e6aa..c3c2e26a1 100644 --- a/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts +++ b/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts @@ -13,8 +13,8 @@ vi.mock("../agents/pi-embedded.js", () => ({ resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); +import type { OpenClawConfig } from "../config/config.js"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import type { MoltbotConfig } from "../config/config.js"; import { monitorWebChannel } from "./auto-reply.js"; import { resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; @@ -46,7 +46,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -61,7 +61,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -104,7 +104,7 @@ describe("broadcast groups", () => { strategy: "sequential", "+1000": ["alfred", "baerbel"], }, - } satisfies MoltbotConfig); + } satisfies OpenClawConfig); const sendMedia = vi.fn(); const reply = vi.fn().mockResolvedValue(undefined); @@ -158,7 +158,7 @@ describe("broadcast groups", () => { strategy: "sequential", "123@g.us": ["alfred", "baerbel"], }, - } satisfies MoltbotConfig); + } satisfies OpenClawConfig); const sendMedia = vi.fn(); const reply = vi.fn().mockResolvedValue(undefined); @@ -269,7 +269,7 @@ describe("broadcast groups", () => { strategy: "parallel", "+1000": ["alfred", "baerbel"], }, - } satisfies MoltbotConfig); + } satisfies OpenClawConfig); const sendMedia = vi.fn(); const reply = vi.fn().mockResolvedValue(undefined); diff --git a/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts b/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts index 8c2f9045e..b7f47d6e4 100644 --- a/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts +++ b/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts @@ -13,8 +13,8 @@ vi.mock("../agents/pi-embedded.js", () => ({ resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); +import type { OpenClawConfig } from "../config/config.js"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import type { MoltbotConfig } from "../config/config.js"; import { monitorWebChannel } from "./auto-reply.js"; import { resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; @@ -46,7 +46,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -61,7 +61,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -103,7 +103,7 @@ describe("broadcast groups", () => { broadcast: { "+1000": ["alfred", "missing"], }, - } satisfies MoltbotConfig); + } satisfies OpenClawConfig); const sendMedia = vi.fn(); const reply = vi.fn().mockResolvedValue(undefined); diff --git a/src/web/auto-reply.partial-reply-gating.test.ts b/src/web/auto-reply.partial-reply-gating.test.ts index 43bb22db1..30ecf3e62 100644 --- a/src/web/auto-reply.partial-reply-gating.test.ts +++ b/src/web/auto-reply.partial-reply-gating.test.ts @@ -13,10 +13,10 @@ vi.mock("../agents/pi-embedded.js", () => ({ resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); +import type { OpenClawConfig } from "../config/config.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; -import type { MoltbotConfig } from "../config/config.js"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { monitorWebChannel } from "./auto-reply.js"; import { resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; @@ -48,7 +48,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -63,7 +63,7 @@ afterEach(async () => { const makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -102,7 +102,7 @@ describe("partial reply gating", () => { const replyResolver = vi.fn().mockResolvedValue({ text: "final reply" }); - const mockConfig: MoltbotConfig = { + const mockConfig: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["*"] } }, }; @@ -145,7 +145,7 @@ describe("partial reply gating", () => { const replyResolver = vi.fn().mockResolvedValue({ text: "final reply" }); - const mockConfig: MoltbotConfig = { + const mockConfig: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["*"], @@ -195,7 +195,7 @@ describe("partial reply gating", () => { const replyResolver = vi.fn().mockResolvedValue(undefined); - const mockConfig: MoltbotConfig = { + const mockConfig: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["*"] } }, session: { store: store.storePath }, }; @@ -230,10 +230,14 @@ describe("partial reply gating", () => { string, { lastChannel?: string; lastTo?: string } >; - if (stored[mainSessionKey]?.lastChannel && stored[mainSessionKey]?.lastTo) break; + if (stored[mainSessionKey]?.lastChannel && stored[mainSessionKey]?.lastTo) { + break; + } await new Promise((resolve) => setTimeout(resolve, 5)); } - if (!stored) throw new Error("store not loaded"); + if (!stored) { + throw new Error("store not loaded"); + } expect(stored[mainSessionKey]?.lastChannel).toBe("whatsapp"); expect(stored[mainSessionKey]?.lastTo).toBe("+1000"); @@ -249,7 +253,7 @@ describe("partial reply gating", () => { const replyResolver = vi.fn().mockResolvedValue(undefined); - const mockConfig: MoltbotConfig = { + const mockConfig: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["*"] } }, session: { store: store.storePath }, }; @@ -295,11 +299,14 @@ describe("partial reply gating", () => { stored[groupSessionKey]?.lastChannel && stored[groupSessionKey]?.lastTo && stored[groupSessionKey]?.lastAccountId - ) + ) { break; + } await new Promise((resolve) => setTimeout(resolve, 5)); } - if (!stored) throw new Error("store not loaded"); + if (!stored) { + throw new Error("store not loaded"); + } expect(stored[groupSessionKey]?.lastChannel).toBe("whatsapp"); expect(stored[groupSessionKey]?.lastTo).toBe("123@g.us"); expect(stored[groupSessionKey]?.lastAccountId).toBe("work"); diff --git a/src/web/auto-reply.typing-controller-idle.test.ts b/src/web/auto-reply.typing-controller-idle.test.ts index b805832c9..9df5e7e4d 100644 --- a/src/web/auto-reply.typing-controller-idle.test.ts +++ b/src/web/auto-reply.typing-controller-idle.test.ts @@ -13,8 +13,8 @@ vi.mock("../agents/pi-embedded.js", () => ({ resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); +import type { OpenClawConfig } from "../config/config.js"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import type { MoltbotConfig } from "../config/config.js"; import { monitorWebChannel } from "./auto-reply.js"; import { resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; @@ -46,7 +46,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -61,7 +61,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -114,7 +114,7 @@ describe("typing controller idle", () => { return { text: "final reply" }; }); - const mockConfig: MoltbotConfig = { + const mockConfig: OpenClawConfig = { channels: { whatsapp: { allowFrom: ["*"] } }, }; diff --git a/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts b/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts index 7ec81e7ff..60d9fb83e 100644 --- a/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +++ b/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts @@ -48,7 +48,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -63,7 +63,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { diff --git a/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts b/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts index 2c16df2cc..c1bc72d6f 100644 --- a/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts +++ b/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts @@ -47,7 +47,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -62,7 +62,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { diff --git a/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts b/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts index dd27bad8a..2616c98c7 100644 --- a/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts +++ b/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts @@ -46,7 +46,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -61,7 +61,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { diff --git a/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts b/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts index aa5bc8ba0..9c6bcd37e 100644 --- a/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts +++ b/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts @@ -47,7 +47,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -62,7 +62,7 @@ afterEach(async () => { const makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -339,11 +339,11 @@ describe("web auto-reply", () => { const firstPattern = escapeRegExp(firstTimestamp); const secondPattern = escapeRegExp(secondTimestamp); expect(firstArgs.Body).toMatch( - new RegExp(`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${firstPattern}\\] \\[moltbot\\] first`), + new RegExp(`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${firstPattern}\\] \\[openclaw\\] first`), ); expect(firstArgs.Body).not.toContain("second"); expect(secondArgs.Body).toMatch( - new RegExp(`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${secondPattern}\\] \\[moltbot\\] second`), + new RegExp(`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${secondPattern}\\] \\[openclaw\\] second`), ); expect(secondArgs.Body).not.toContain("first"); diff --git a/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts b/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts index d8b3a38e7..11ee7ce48 100644 --- a/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts +++ b/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts @@ -46,7 +46,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -61,7 +61,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -366,7 +366,7 @@ describe("web auto-reply", () => { return { close: vi.fn() }; }; - const authDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-wa-auth-")); + const authDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wa-auth-")); try { await fs.writeFile( @@ -444,7 +444,7 @@ describe("web auto-reply", () => { return { close: vi.fn() }; }; - const authDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-wa-auth-")); + const authDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wa-auth-")); try { await fs.writeFile( diff --git a/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts b/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts index 823224186..f689b7a33 100644 --- a/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts +++ b/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts @@ -46,7 +46,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -61,7 +61,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -200,7 +200,7 @@ describe("web auto-reply", () => { expect(resolver).toHaveBeenCalled(); const resolverArg = resolver.mock.calls[0][0]; expect(resolverArg.Body).toContain("[Richbot]"); - expect(resolverArg.Body).not.toContain("[moltbot]"); + expect(resolverArg.Body).not.toContain("[openclaw]"); resetLoadConfigMock(); }); it("does not derive responsePrefix from identity.name when unset", async () => { diff --git a/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts b/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts index 6ace56327..d2b0de81a 100644 --- a/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts +++ b/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts @@ -14,8 +14,8 @@ vi.mock("../agents/pi-embedded.js", () => ({ resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { resetLogger, setLoggerOverride } from "../logging.js"; import { monitorWebChannel, SILENT_REPLY_TOKEN } from "./auto-reply.js"; import { resetBaileysMocks, resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; @@ -48,7 +48,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -63,7 +63,7 @@ afterEach(async () => { const makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -126,7 +126,7 @@ describe("web auto-reply", () => { setLoadConfigMock(() => ({ messages: { - groupChat: { mentionPatterns: ["@clawd"] }, + groupChat: { mentionPatterns: ["@openclaw"] }, }, session: { store: storePath }, })); @@ -209,7 +209,7 @@ describe("web auto-reply", () => { }, messages: { groupChat: { - mentionPatterns: ["\\bclawd\\b"], + mentionPatterns: ["\\bopenclaw\\b"], }, }, })); @@ -248,9 +248,9 @@ describe("web auto-reply", () => { expect(resolver).not.toHaveBeenCalled(); - // Text-based mentionPatterns still work (user can type "clawd" explicitly). + // Text-based mentionPatterns still work (user can type "openclaw" explicitly). await capturedOnMessage?.({ - body: "clawd ping", + body: "openclaw ping", from: "123@g.us", conversationId: "123@g.us", chatId: "123@g.us", @@ -272,7 +272,7 @@ describe("web auto-reply", () => { }); it("emits heartbeat logs with connection metadata", async () => { vi.useFakeTimers(); - const logPath = `/tmp/moltbot-heartbeat-${crypto.randomUUID()}.log`; + const logPath = `/tmp/openclaw-heartbeat-${crypto.randomUUID()}.log`; setLoggerOverride({ level: "trace", file: logPath }); const runtime = { @@ -313,7 +313,7 @@ describe("web auto-reply", () => { expect(content).toMatch(/messagesHandled/); }); it("logs outbound replies to file", async () => { - const logPath = `/tmp/moltbot-log-test-${crypto.randomUUID()}.log`; + const logPath = `/tmp/openclaw-log-test-${crypto.randomUUID()}.log`; setLoggerOverride({ level: "trace", file: logPath }); let capturedOnMessage: diff --git a/src/web/auto-reply.web-auto-reply.uses-per-agent-mention-patterns-group-gating.test.ts b/src/web/auto-reply.web-auto-reply.uses-per-agent-mention-patterns-group-gating.test.ts index 64f448ebc..80d63055e 100644 --- a/src/web/auto-reply.web-auto-reply.uses-per-agent-mention-patterns-group-gating.test.ts +++ b/src/web/auto-reply.web-auto-reply.uses-per-agent-mention-patterns-group-gating.test.ts @@ -46,7 +46,7 @@ const rmDirWithRetries = async (dir: string): Promise => { beforeEach(async () => { resetInboundDedupe(); previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-")); + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); process.env.HOME = tempHome; }); @@ -61,7 +61,7 @@ afterEach(async () => { const _makeSessionStore = async ( entries: Record = {}, ): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-session-")); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); const storePath = path.join(dir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify(entries)); const cleanup = async () => { @@ -200,7 +200,7 @@ describe("web auto-reply", () => { groups: { "*": { requireMention: false } }, }, }, - messages: { groupChat: { mentionPatterns: ["@clawd"] } }, + messages: { groupChat: { mentionPatterns: ["@openclaw"] } }, })); let capturedOnMessage: @@ -248,7 +248,7 @@ describe("web auto-reply", () => { groups: { "999@g.us": { requireMention: false } }, }, }, - messages: { groupChat: { mentionPatterns: ["@clawd"] } }, + messages: { groupChat: { mentionPatterns: ["@openclaw"] } }, })); let capturedOnMessage: @@ -265,7 +265,7 @@ describe("web auto-reply", () => { expect(capturedOnMessage).toBeDefined(); await capturedOnMessage?.({ - body: "@clawd hello", + body: "@openclaw hello", from: "123@g.us", conversationId: "123@g.us", chatId: "123@g.us", @@ -301,7 +301,7 @@ describe("web auto-reply", () => { }, }, }, - messages: { groupChat: { mentionPatterns: ["@clawd"] } }, + messages: { groupChat: { mentionPatterns: ["@openclaw"] } }, })); let capturedOnMessage: diff --git a/src/web/auto-reply/deliver-reply.ts b/src/web/auto-reply/deliver-reply.ts index 9b2bb8072..21ddc2d2a 100644 --- a/src/web/auto-reply/deliver-reply.ts +++ b/src/web/auto-reply/deliver-reply.ts @@ -1,13 +1,13 @@ -import { chunkMarkdownTextWithMode, type ChunkMode } from "../../auto-reply/chunk.js"; -import type { MarkdownTableMode } from "../../config/types.base.js"; -import { convertMarkdownTables } from "../../markdown/tables.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; +import type { MarkdownTableMode } from "../../config/types.base.js"; +import type { WebInboundMsg } from "./types.js"; +import { chunkMarkdownTextWithMode, type ChunkMode } from "../../auto-reply/chunk.js"; import { logVerbose, shouldLogVerbose } from "../../globals.js"; +import { convertMarkdownTables } from "../../markdown/tables.js"; import { loadWebMedia } from "../media.js"; import { newConnectionId } from "../reconnect.js"; import { formatError } from "../session.js"; import { whatsappOutboundLog } from "./loggers.js"; -import type { WebInboundMsg } from "./types.js"; import { elide } from "./util.js"; export async function deliverWebReply(params: { diff --git a/src/web/auto-reply/heartbeat-runner.ts b/src/web/auto-reply/heartbeat-runner.ts index 1a194c403..968e904fc 100644 --- a/src/web/auto-reply/heartbeat-runner.ts +++ b/src/web/auto-reply/heartbeat-runner.ts @@ -1,11 +1,11 @@ +import type { ReplyPayload } from "../../auto-reply/types.js"; import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, resolveHeartbeatPrompt, stripHeartbeatToken, } from "../../auto-reply/heartbeat.js"; -import { HEARTBEAT_TOKEN } from "../../auto-reply/tokens.js"; import { getReplyFromConfig } from "../../auto-reply/reply.js"; -import type { ReplyPayload } from "../../auto-reply/types.js"; +import { HEARTBEAT_TOKEN } from "../../auto-reply/tokens.js"; import { resolveWhatsAppHeartbeatRecipients } from "../../channels/plugins/whatsapp-heartbeat.js"; import { loadConfig } from "../../config/config.js"; import { @@ -28,11 +28,17 @@ import { elide } from "./util.js"; function resolveHeartbeatReplyPayload( replyResult: ReplyPayload | ReplyPayload[] | undefined, ): ReplyPayload | undefined { - if (!replyResult) return undefined; - if (!Array.isArray(replyResult)) return replyResult; + if (!replyResult) { + return undefined; + } + if (!Array.isArray(replyResult)) { + return replyResult; + } for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) { const payload = replyResult[idx]; - if (!payload) continue; + if (!payload) { + continue; + } if (payload.text || payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) { return payload; } @@ -221,7 +227,9 @@ export async function runWebHeartbeatOnce(opts: { store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt; await updateSessionStore(storePath, (nextStore) => { const nextEntry = nextStore[sessionSnapshot.key]; - if (!nextEntry) return; + if (!nextEntry) { + return; + } nextStore[sessionSnapshot.key] = { ...nextEntry, updatedAt: sessionSnapshot.entry.updatedAt, diff --git a/src/web/auto-reply/mentions.test.ts b/src/web/auto-reply/mentions.test.ts index 62d8613bc..f7ea598b4 100644 --- a/src/web/auto-reply/mentions.test.ts +++ b/src/web/auto-reply/mentions.test.ts @@ -19,11 +19,11 @@ const makeMsg = (overrides: Partial): WebInboundMsg => }) as WebInboundMsg; describe("isBotMentionedFromTargets", () => { - const mentionCfg = { mentionRegexes: [/\bclawd\b/i] }; + const mentionCfg = { mentionRegexes: [/\bopenclaw\b/i] }; it("ignores regex matches when other mentions are present", () => { const msg = makeMsg({ - body: "@Clawd please help", + body: "@OpenClaw please help", mentionedJids: ["19998887777@s.whatsapp.net"], selfE164: "+15551234567", selfJid: "15551234567@s.whatsapp.net", @@ -45,7 +45,7 @@ describe("isBotMentionedFromTargets", () => { it("falls back to regex when no mentions are present", () => { const msg = makeMsg({ - body: "clawd can you help?", + body: "openclaw can you help?", selfE164: "+15551234567", selfJid: "15551234567@s.whatsapp.net", }); diff --git a/src/web/auto-reply/mentions.ts b/src/web/auto-reply/mentions.ts index bf1352799..7d5dcc22f 100644 --- a/src/web/auto-reply/mentions.ts +++ b/src/web/auto-reply/mentions.ts @@ -1,7 +1,7 @@ -import { buildMentionRegexes, normalizeMentionText } from "../../auto-reply/reply/mentions.js"; import type { loadConfig } from "../../config/config.js"; -import { isSelfChatMode, jidToE164, normalizeE164 } from "../../utils.js"; import type { WebInboundMsg } from "./types.js"; +import { buildMentionRegexes, normalizeMentionText } from "../../auto-reply/reply/mentions.js"; +import { isSelfChatMode, jidToE164, normalizeE164 } from "../../utils.js"; export type MentionConfig = { mentionRegexes: RegExp[]; @@ -45,10 +45,14 @@ export function isBotMentionedFromTargets( const hasMentions = (msg.mentionedJids?.length ?? 0) > 0; if (hasMentions && !isSelfChat) { - if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) return true; + if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) { + return true; + } if (targets.selfJid) { // Some mentions use the bare JID; match on E.164 to be safe. - if (targets.normalizedMentions.includes(targets.selfJid)) return true; + if (targets.normalizedMentions.includes(targets.selfJid)) { + return true; + } } // If the message explicitly mentions someone else, do not fall back to regex matches. return false; @@ -56,17 +60,23 @@ export function isBotMentionedFromTargets( // Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot. } const bodyClean = clean(msg.body); - if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) return true; + if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) { + return true; + } // Fallback: detect body containing our own number (with or without +, spacing) if (targets.selfE164) { const selfDigits = targets.selfE164.replace(/\D/g, ""); if (selfDigits) { const bodyDigits = bodyClean.replace(/[^\d]/g, ""); - if (bodyDigits.includes(selfDigits)) return true; + if (bodyDigits.includes(selfDigits)) { + return true; + } const bodyNoSpace = msg.body.replace(/[\s-]/g, ""); const pattern = new RegExp(`\\+?${selfDigits}`, "i"); - if (pattern.test(bodyNoSpace)) return true; + if (pattern.test(bodyNoSpace)) { + return true; + } } } diff --git a/src/web/auto-reply/monitor.ts b/src/web/auto-reply/monitor.ts index 791b38967..5ca1dd864 100644 --- a/src/web/auto-reply/monitor.ts +++ b/src/web/auto-reply/monitor.ts @@ -1,7 +1,9 @@ -import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.js"; -import { getReplyFromConfig } from "../../auto-reply/reply.js"; +import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { resolveInboundDebounceMs } from "../../auto-reply/inbound-debounce.js"; +import { getReplyFromConfig } from "../../auto-reply/reply.js"; +import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { waitForever } from "../../cli/wait.js"; import { loadConfig } from "../../config/config.js"; import { logVerbose } from "../../globals.js"; @@ -11,7 +13,6 @@ import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejecti import { getChildLogger } from "../../logging.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; -import { formatCliCommand } from "../../cli/command-format.js"; import { resolveWhatsAppAccount } from "../accounts.js"; import { setActiveWebListener } from "../active-listener.js"; import { monitorWebInbox } from "../inbound.js"; @@ -28,7 +29,6 @@ import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js"; import { buildMentionConfig } from "./mentions.js"; import { createEchoTracker } from "./monitor/echo.js"; import { createWebOnMessageHandler } from "./monitor/on-message.js"; -import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js"; import { isLikelyWhatsAppCryptoError } from "./util.js"; export async function monitorWebChannel( @@ -141,7 +141,9 @@ export async function monitorWebChannel( let reconnectAttempts = 0; while (true) { - if (stopRequested()) break; + if (stopRequested()) { + break; + } const connectionId = newConnectionId(); const startedAt = Date.now(); @@ -175,9 +177,15 @@ export async function monitorWebChannel( const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "whatsapp" }); const shouldDebounce = (msg: WebInboundMsg) => { - if (msg.mediaPath || msg.mediaType) return false; - if (msg.location) return false; - if (msg.replyToId || msg.replyToBody) return false; + if (msg.mediaPath || msg.mediaType) { + return false; + } + if (msg.location) { + return false; + } + if (msg.replyToId || msg.replyToBody) { + return false; + } return !hasControlCommand(msg.body, cfg); }; @@ -219,7 +227,9 @@ export async function monitorWebChannel( setActiveWebListener(account.accountId, listener); unregisterUnhandled = registerUnhandledRejectionHandler((reason) => { - if (!isLikelyWhatsAppCryptoError(reason)) return false; + if (!isLikelyWhatsAppCryptoError(reason)) { + return false; + } const errorStr = formatError(reason); reconnectLogger.warn( { connectionId, error: errorStr }, @@ -239,8 +249,12 @@ export async function monitorWebChannel( unregisterUnhandled(); unregisterUnhandled = null; } - if (heartbeat) clearInterval(heartbeat); - if (watchdogTimer) clearInterval(watchdogTimer); + if (heartbeat) { + clearInterval(heartbeat); + } + if (watchdogTimer) { + clearInterval(watchdogTimer); + } if (backgroundTasks.size > 0) { await Promise.allSettled(backgroundTasks); backgroundTasks.clear(); @@ -279,9 +293,13 @@ export async function monitorWebChannel( }, heartbeatSeconds * 1000); watchdogTimer = setInterval(() => { - if (!lastMessageAt) return; + if (!lastMessageAt) { + return; + } const timeSinceLastMessage = Date.now() - lastMessageAt; - if (timeSinceLastMessage <= MESSAGE_TIMEOUT_MS) return; + if (timeSinceLastMessage <= MESSAGE_TIMEOUT_MS) { + return; + } const minutesSinceLastMessage = Math.floor(timeSinceLastMessage / 60000); heartbeatLogger.warn( { @@ -376,7 +394,7 @@ export async function monitorWebChannel( if (loggedOut) { runtime.error( - `WhatsApp session logged out. Run \`${formatCliCommand("moltbot channels login --channel web")}\` to relink.`, + `WhatsApp session logged out. Run \`${formatCliCommand("openclaw channels login --channel web")}\` to relink.`, ); await closeListener(); break; diff --git a/src/web/auto-reply/monitor/ack-reaction.ts b/src/web/auto-reply/monitor/ack-reaction.ts index 6a99da312..13a2b76e2 100644 --- a/src/web/auto-reply/monitor/ack-reaction.ts +++ b/src/web/auto-reply/monitor/ack-reaction.ts @@ -1,9 +1,9 @@ import type { loadConfig } from "../../../config/config.js"; -import { logVerbose } from "../../../globals.js"; +import type { WebInboundMsg } from "../types.js"; import { shouldAckReactionForWhatsApp } from "../../../channels/ack-reactions.js"; +import { logVerbose } from "../../../globals.js"; import { sendReactionWhatsApp } from "../../outbound.js"; import { formatError } from "../../session.js"; -import type { WebInboundMsg } from "../types.js"; import { resolveGroupActivationFor } from "./group-activation.js"; export function maybeSendAckReaction(params: { @@ -17,7 +17,9 @@ export function maybeSendAckReaction(params: { info: (obj: unknown, msg: string) => void; warn: (obj: unknown, msg: string) => void; }) { - if (!params.msg.id) return; + if (!params.msg.id) { + return; + } const ackConfig = params.cfg.channels?.whatsapp?.ackReaction; const emoji = (ackConfig?.emoji ?? "").trim(); @@ -45,7 +47,9 @@ export function maybeSendAckReaction(params: { groupActivated: activation === "always", }); - if (!shouldSendReaction()) return; + if (!shouldSendReaction()) { + return; + } params.info( { chatId: params.msg.chatId, messageId: params.msg.id, emoji }, diff --git a/src/web/auto-reply/monitor/broadcast.ts b/src/web/auto-reply/monitor/broadcast.ts index c8f84a048..f1ed5ac21 100644 --- a/src/web/auto-reply/monitor/broadcast.ts +++ b/src/web/auto-reply/monitor/broadcast.ts @@ -1,5 +1,7 @@ import type { loadConfig } from "../../../config/config.js"; import type { resolveAgentRoute } from "../../../routing/resolve-route.js"; +import type { WebInboundMsg } from "../types.js"; +import type { GroupHistoryEntry } from "./process-message.js"; import { buildAgentSessionKey } from "../../../routing/resolve-route.js"; import { buildAgentMainSessionKey, @@ -8,8 +10,6 @@ import { } from "../../../routing/session-key.js"; import { formatError } from "../../session.js"; import { whatsappInboundLog } from "../loggers.js"; -import type { WebInboundMsg } from "../types.js"; -import type { GroupHistoryEntry } from "./process-message.js"; export async function maybeBroadcastMessage(params: { cfg: ReturnType; @@ -29,8 +29,12 @@ export async function maybeBroadcastMessage(params: { ) => Promise; }) { const broadcastAgents = params.cfg.broadcast?.[params.peerId]; - if (!broadcastAgents || !Array.isArray(broadcastAgents)) return false; - if (broadcastAgents.length === 0) return false; + if (!broadcastAgents || !Array.isArray(broadcastAgents)) { + return false; + } + if (broadcastAgents.length === 0) { + return false; + } const strategy = params.cfg.broadcast?.strategy || "parallel"; whatsappInboundLog.info(`Broadcasting message to ${broadcastAgents.length} agents (${strategy})`); diff --git a/src/web/auto-reply/monitor/commands.ts b/src/web/auto-reply/monitor/commands.ts index 7c6d6423a..2947c6909 100644 --- a/src/web/auto-reply/monitor/commands.ts +++ b/src/web/auto-reply/monitor/commands.ts @@ -1,6 +1,8 @@ export function isStatusCommand(body: string) { const trimmed = body.trim().toLowerCase(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } return trimmed === "/status" || trimmed === "status" || trimmed.startsWith("/status "); } diff --git a/src/web/auto-reply/monitor/echo.ts b/src/web/auto-reply/monitor/echo.ts index 1b64b1146..ca13a98e9 100644 --- a/src/web/auto-reply/monitor/echo.ts +++ b/src/web/auto-reply/monitor/echo.ts @@ -24,14 +24,18 @@ export function createEchoTracker(params: { const trim = () => { while (recentlySent.size > maxItems) { - const firstKey = recentlySent.values().next().value as string | undefined; - if (!firstKey) break; + const firstKey = recentlySent.values().next().value; + if (!firstKey) { + break; + } recentlySent.delete(firstKey); } }; const rememberText: EchoTracker["rememberText"] = (text, opts) => { - if (!text) return; + if (!text) { + return; + } recentlySent.add(text); if (opts.combinedBody && opts.combinedBodySessionKey) { recentlySent.add( diff --git a/src/web/auto-reply/monitor/group-activation.ts b/src/web/auto-reply/monitor/group-activation.ts index 520671fec..944ddeb46 100644 --- a/src/web/auto-reply/monitor/group-activation.ts +++ b/src/web/auto-reply/monitor/group-activation.ts @@ -1,5 +1,5 @@ -import { normalizeGroupActivation } from "../../../auto-reply/group-activation.js"; import type { loadConfig } from "../../../config/config.js"; +import { normalizeGroupActivation } from "../../../auto-reply/group-activation.js"; import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention, @@ -51,6 +51,6 @@ export function resolveGroupActivationFor(params: { const store = loadSessionStore(storePath); const entry = store[params.sessionKey]; const requireMention = resolveGroupRequireMentionFor(params.cfg, params.conversationId); - const defaultActivation = requireMention === false ? "always" : "mention"; + const defaultActivation = !requireMention ? "always" : "mention"; return normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation; } diff --git a/src/web/auto-reply/monitor/group-gating.test.ts b/src/web/auto-reply/monitor/group-gating.test.ts index f707b592b..6eb1e2e2b 100644 --- a/src/web/auto-reply/monitor/group-gating.test.ts +++ b/src/web/auto-reply/monitor/group-gating.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { applyGroupGating } from "./group-gating.js"; const baseConfig = { @@ -9,7 +8,7 @@ const baseConfig = { groups: { "*": { requireMention: true } }, }, }, - session: { store: "/tmp/moltbot-sessions.json" }, + session: { store: "/tmp/openclaw-sessions.json" }, } as const; describe("applyGroupGating", () => { diff --git a/src/web/auto-reply/monitor/group-gating.ts b/src/web/auto-reply/monitor/group-gating.ts index 8d1a33645..d6fadf83b 100644 --- a/src/web/auto-reply/monitor/group-gating.ts +++ b/src/web/auto-reply/monitor/group-gating.ts @@ -1,12 +1,12 @@ +import type { loadConfig } from "../../../config/config.js"; +import type { MentionConfig } from "../mentions.js"; +import type { WebInboundMsg } from "../types.js"; import { hasControlCommand } from "../../../auto-reply/command-detection.js"; import { parseActivationCommand } from "../../../auto-reply/group-activation.js"; -import type { loadConfig } from "../../../config/config.js"; -import { normalizeE164 } from "../../../utils.js"; -import { resolveMentionGating } from "../../../channels/mention-gating.js"; -import type { MentionConfig } from "../mentions.js"; -import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js"; -import type { WebInboundMsg } from "../types.js"; import { recordPendingHistoryEntryIfEnabled } from "../../../auto-reply/reply/history.js"; +import { resolveMentionGating } from "../../../channels/mention-gating.js"; +import { normalizeE164 } from "../../../utils.js"; +import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js"; import { stripMentionsForCommand } from "./commands.js"; import { resolveGroupActivationFor, resolveGroupPolicyFor } from "./group-activation.js"; import { noteGroupMember } from "./group-members.js"; @@ -21,7 +21,9 @@ export type GroupHistoryEntry = { function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMsg) { const sender = normalizeE164(msg.senderE164 ?? ""); - if (!sender) return false; + if (!sender) { + return false; + } const owners = resolveOwnerList(baseMentionConfig, msg.selfE164 ?? undefined); return owners.includes(sender); } diff --git a/src/web/auto-reply/monitor/group-members.ts b/src/web/auto-reply/monitor/group-members.ts index 65e63c0a0..dc69935e9 100644 --- a/src/web/auto-reply/monitor/group-members.ts +++ b/src/web/auto-reply/monitor/group-members.ts @@ -6,10 +6,14 @@ export function noteGroupMember( e164?: string, name?: string, ) { - if (!e164 || !name) return; + if (!e164 || !name) { + return; + } const normalized = normalizeE164(e164); const key = normalized ?? e164; - if (!key) return; + if (!key) { + return; + } let roster = groupMemberNames.get(conversationId); if (!roster) { roster = new Map(); @@ -28,9 +32,13 @@ export function formatGroupMembers(params: { const ordered: string[] = []; if (participants?.length) { for (const entry of participants) { - if (!entry) continue; + if (!entry) { + continue; + } const normalized = normalizeE164(entry) ?? entry; - if (!normalized || seen.has(normalized)) continue; + if (!normalized || seen.has(normalized)) { + continue; + } seen.add(normalized); ordered.push(normalized); } @@ -38,16 +46,22 @@ export function formatGroupMembers(params: { if (roster) { for (const entry of roster.keys()) { const normalized = normalizeE164(entry) ?? entry; - if (!normalized || seen.has(normalized)) continue; + if (!normalized || seen.has(normalized)) { + continue; + } seen.add(normalized); ordered.push(normalized); } } if (ordered.length === 0 && fallbackE164) { const normalized = normalizeE164(fallbackE164) ?? fallbackE164; - if (normalized) ordered.push(normalized); + if (normalized) { + ordered.push(normalized); + } + } + if (ordered.length === 0) { + return undefined; } - if (ordered.length === 0) return undefined; return ordered .map((entry) => { const name = roster?.get(entry); diff --git a/src/web/auto-reply/monitor/last-route.ts b/src/web/auto-reply/monitor/last-route.ts index 5359dbbcd..2943537e1 100644 --- a/src/web/auto-reply/monitor/last-route.ts +++ b/src/web/auto-reply/monitor/last-route.ts @@ -51,7 +51,9 @@ export function updateLastRouteInBackground(params: { } export function awaitBackgroundTasks(backgroundTasks: Set>) { - if (backgroundTasks.size === 0) return Promise.resolve(); + if (backgroundTasks.size === 0) { + return Promise.resolve(); + } return Promise.allSettled(backgroundTasks).then(() => { backgroundTasks.clear(); }); diff --git a/src/web/auto-reply/monitor/message-line.test.ts b/src/web/auto-reply/monitor/message-line.test.ts index 9e0bf9ede..4bbdb8837 100644 --- a/src/web/auto-reply/monitor/message-line.test.ts +++ b/src/web/auto-reply/monitor/message-line.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it } from "vitest"; - import { buildInboundLine } from "./message-line.js"; describe("buildInboundLine", () => { it("prefixes group messages with sender", () => { const line = buildInboundLine({ cfg: { - agents: { defaults: { workspace: "/tmp/clawd" } }, + agents: { defaults: { workspace: "/tmp/openclaw" } }, channels: { whatsapp: { messagePrefix: "" } }, } as never, agentId: "main", diff --git a/src/web/auto-reply/monitor/message-line.ts b/src/web/auto-reply/monitor/message-line.ts index eddfbd039..a0c104a90 100644 --- a/src/web/auto-reply/monitor/message-line.ts +++ b/src/web/auto-reply/monitor/message-line.ts @@ -1,10 +1,12 @@ -import { resolveMessagePrefix } from "../../../agents/identity.js"; -import { formatInboundEnvelope, type EnvelopeFormatOptions } from "../../../auto-reply/envelope.js"; import type { loadConfig } from "../../../config/config.js"; import type { WebInboundMsg } from "../types.js"; +import { resolveMessagePrefix } from "../../../agents/identity.js"; +import { formatInboundEnvelope, type EnvelopeFormatOptions } from "../../../auto-reply/envelope.js"; export function formatReplyContext(msg: WebInboundMsg) { - if (!msg.replyToBody) return null; + if (!msg.replyToBody) { + return null; + } const sender = msg.replyToSender ?? "unknown sender"; const idPart = msg.replyToId ? ` id:${msg.replyToId}` : ""; return `[Replying to ${sender}${idPart}]\n${msg.replyToBody}\n[/Replying]`; diff --git a/src/web/auto-reply/monitor/on-message.ts b/src/web/auto-reply/monitor/on-message.ts index 7e260d49e..c34ded5d7 100644 --- a/src/web/auto-reply/monitor/on-message.ts +++ b/src/web/auto-reply/monitor/on-message.ts @@ -1,15 +1,15 @@ -import type { MsgContext } from "../../../auto-reply/templating.js"; import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; +import type { MsgContext } from "../../../auto-reply/templating.js"; import type { loadConfig } from "../../../config/config.js"; +import type { MentionConfig } from "../mentions.js"; +import type { WebInboundMsg } from "../types.js"; +import type { EchoTracker } from "./echo.js"; +import type { GroupHistoryEntry } from "./group-gating.js"; import { logVerbose } from "../../../globals.js"; import { resolveAgentRoute } from "../../../routing/resolve-route.js"; import { buildGroupHistoryKey } from "../../../routing/session-key.js"; import { normalizeE164 } from "../../../utils.js"; -import type { MentionConfig } from "../mentions.js"; -import type { WebInboundMsg } from "../types.js"; import { maybeBroadcastMessage } from "./broadcast.js"; -import type { EchoTracker } from "./echo.js"; -import type { GroupHistoryEntry } from "./group-gating.js"; import { applyGroupGating } from "./group-gating.js"; import { updateLastRouteInBackground } from "./last-route.js"; import { resolvePeerId } from "./peer.js"; @@ -138,7 +138,9 @@ export function createWebOnMessageHandler(params: { logVerbose, replyLogger: params.replyLogger, }); - if (!gating.shouldProcess) return; + if (!gating.shouldProcess) { + return; + } } else { // Ensure `peerId` for DMs is stable and stored as E.164 when possible. if (!msg.senderE164 && peerId && peerId.startsWith("+")) { diff --git a/src/web/auto-reply/monitor/peer.ts b/src/web/auto-reply/monitor/peer.ts index 0ebd8ac66..3bc91c539 100644 --- a/src/web/auto-reply/monitor/peer.ts +++ b/src/web/auto-reply/monitor/peer.ts @@ -1,9 +1,15 @@ -import { jidToE164, normalizeE164 } from "../../../utils.js"; import type { WebInboundMsg } from "../types.js"; +import { jidToE164, normalizeE164 } from "../../../utils.js"; export function resolvePeerId(msg: WebInboundMsg) { - if (msg.chatType === "group") return msg.conversationId ?? msg.from; - if (msg.senderE164) return normalizeE164(msg.senderE164) ?? msg.senderE164; - if (msg.from.includes("@")) return jidToE164(msg.from) ?? msg.from; + if (msg.chatType === "group") { + return msg.conversationId ?? msg.from; + } + if (msg.senderE164) { + return normalizeE164(msg.senderE164) ?? msg.senderE164; + } + if (msg.from.includes("@")) { + return jidToE164(msg.from) ?? msg.from; + } return normalizeE164(msg.from) ?? msg.from; } diff --git a/src/web/auto-reply/monitor/process-message.inbound-contract.test.ts b/src/web/auto-reply/monitor/process-message.inbound-contract.test.ts index 33417976b..af080eefc 100644 --- a/src/web/auto-reply/monitor/process-message.inbound-contract.test.ts +++ b/src/web/auto-reply/monitor/process-message.inbound-contract.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { expectInboundContextContract } from "../../../../test/helpers/inbound-contract.js"; let capturedCtx: unknown; diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 84f03cf0b..e8651529d 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -1,5 +1,12 @@ +import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; +import type { ReplyPayload } from "../../../auto-reply/types.js"; +import type { loadConfig } from "../../../config/config.js"; +import type { getChildLogger } from "../../../logging.js"; +import type { resolveAgentRoute } from "../../../routing/resolve-route.js"; +import type { WebInboundMsg } from "../types.js"; import { resolveIdentityNamePrefix } from "../../../agents/identity.js"; import { resolveChunkMode, resolveTextChunkLimit } from "../../../auto-reply/chunk.js"; +import { shouldComputeCommandAuthorized } from "../../../auto-reply/command-detection.js"; import { formatInboundEnvelope, resolveEnvelopeFormatOptions, @@ -8,30 +15,23 @@ import { buildHistoryContextFromEntries, type HistoryEntry, } from "../../../auto-reply/reply/history.js"; -import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js"; -import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; -import type { ReplyPayload } from "../../../auto-reply/types.js"; -import { shouldComputeCommandAuthorized } from "../../../auto-reply/command-detection.js"; import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js"; +import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js"; import { toLocationContext } from "../../../channels/location.js"; import { createReplyPrefixContext } from "../../../channels/reply-prefix.js"; -import type { loadConfig } from "../../../config/config.js"; +import { resolveMarkdownTableMode } from "../../../config/markdown-tables.js"; import { readSessionUpdatedAt, recordSessionMetaFromInbound, resolveStorePath, } from "../../../config/sessions.js"; -import { resolveMarkdownTableMode } from "../../../config/markdown-tables.js"; import { logVerbose, shouldLogVerbose } from "../../../globals.js"; -import type { getChildLogger } from "../../../logging.js"; import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js"; -import type { resolveAgentRoute } from "../../../routing/resolve-route.js"; import { jidToE164, normalizeE164 } from "../../../utils.js"; import { newConnectionId } from "../../reconnect.js"; import { formatError } from "../../session.js"; import { deliverWebReply } from "../deliver-reply.js"; import { whatsappInboundLog, whatsappOutboundLog } from "../loggers.js"; -import type { WebInboundMsg } from "../types.js"; import { elide } from "../util.js"; import { maybeSendAckReaction } from "./ack-reaction.js"; import { formatGroupMembers } from "./group-members.js"; @@ -60,13 +60,17 @@ async function resolveWhatsAppCommandAuthorized(params: { msg: WebInboundMsg; }): Promise { const useAccessGroups = params.cfg.commands?.useAccessGroups !== false; - if (!useAccessGroups) return true; + if (!useAccessGroups) { + return true; + } const isGroup = params.msg.chatType === "group"; const senderE164 = normalizeE164( isGroup ? (params.msg.senderE164 ?? "") : (params.msg.senderE164 ?? params.msg.from ?? ""), ); - if (!senderE164) return false; + if (!senderE164) { + return false; + } const configuredAllowFrom = params.cfg.channels?.whatsapp?.allowFrom ?? []; const configuredGroupAllowFrom = @@ -74,8 +78,12 @@ async function resolveWhatsAppCommandAuthorized(params: { (configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined); if (isGroup) { - if (!configuredGroupAllowFrom || configuredGroupAllowFrom.length === 0) return false; - if (configuredGroupAllowFrom.some((v) => String(v).trim() === "*")) return true; + if (!configuredGroupAllowFrom || configuredGroupAllowFrom.length === 0) { + return false; + } + if (configuredGroupAllowFrom.some((v) => String(v).trim() === "*")) { + return true; + } return normalizeAllowFromE164(configuredGroupAllowFrom).includes(senderE164); } @@ -89,7 +97,9 @@ async function resolveWhatsAppCommandAuthorized(params: { : params.msg.selfE164 ? [params.msg.selfE164] : []; - if (allowFrom.some((v) => String(v).trim() === "*")) return true; + if (allowFrom.some((v) => String(v).trim() === "*")) { + return true; + } return normalizeAllowFromE164(allowFrom).includes(senderE164); } @@ -221,9 +231,13 @@ export async function processMessage(params: { const dmRouteTarget = params.msg.chatType !== "group" ? (() => { - if (params.msg.senderE164) return normalizeE164(params.msg.senderE164); + if (params.msg.senderE164) { + return normalizeE164(params.msg.senderE164); + } // In direct chats, `msg.from` is already the canonical conversation id. - if (params.msg.from.includes("@")) return jidToE164(params.msg.from); + if (params.msg.from.includes("@")) { + return jidToE164(params.msg.from); + } return normalizeE164(params.msg.from); })() : undefined; @@ -252,7 +266,7 @@ export async function processMessage(params: { const responsePrefix = prefixContext.responsePrefix ?? (configuredResponsePrefix === undefined && isSelfChat - ? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[moltbot]") + ? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[openclaw]") : undefined); const ctxPayload = finalizeInboundContext({ diff --git a/src/web/auto-reply/session-snapshot.test.ts b/src/web/auto-reply/session-snapshot.test.ts index de79ba597..1f9d6dfc9 100644 --- a/src/web/auto-reply/session-snapshot.test.ts +++ b/src/web/auto-reply/session-snapshot.test.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it, vi } from "vitest"; - import { saveSessionStore } from "../../config/sessions.js"; import { getSessionSnapshot } from "./session-snapshot.js"; @@ -12,7 +10,7 @@ describe("getSessionSnapshot", () => { vi.useFakeTimers(); vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0)); try { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-snapshot-")); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-snapshot-")); const storePath = path.join(root, "sessions.json"); const sessionKey = "agent:main:whatsapp:dm:s1"; diff --git a/src/web/auto-reply/util.ts b/src/web/auto-reply/util.ts index e99492bb1..8a00c77bf 100644 --- a/src/web/auto-reply/util.ts +++ b/src/web/auto-reply/util.ts @@ -1,13 +1,21 @@ export function elide(text?: string, limit = 400) { - if (!text) return text; - if (text.length <= limit) return text; + if (!text) { + return text; + } + if (text.length <= limit) { + return text; + } return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`; } export function isLikelyWhatsAppCryptoError(reason: unknown) { const formatReason = (value: unknown): string => { - if (value == null) return ""; - if (typeof value === "string") return value; + if (value == null) { + return ""; + } + if (typeof value === "string") { + return value; + } if (value instanceof Error) { return `${value.message}\n${value.stack ?? ""}`; } @@ -18,11 +26,21 @@ export function isLikelyWhatsAppCryptoError(reason: unknown) { return Object.prototype.toString.call(value); } } - if (typeof value === "number") return String(value); - if (typeof value === "boolean") return String(value); - if (typeof value === "bigint") return String(value); - if (typeof value === "symbol") return value.description ?? value.toString(); - if (typeof value === "function") return value.name ? `[function ${value.name}]` : "[function]"; + if (typeof value === "number") { + return String(value); + } + if (typeof value === "boolean") { + return String(value); + } + if (typeof value === "bigint") { + return String(value); + } + if (typeof value === "symbol") { + return value.description ?? value.toString(); + } + if (typeof value === "function") { + return value.name ? `[function ${value.name}]` : "[function]"; + } return Object.prototype.toString.call(value); }; const raw = @@ -31,7 +49,9 @@ export function isLikelyWhatsAppCryptoError(reason: unknown) { const hasAuthError = haystack.includes("unsupported state or unable to authenticate data") || haystack.includes("bad mac"); - if (!hasAuthError) return false; + if (!hasAuthError) { + return false; + } return ( haystack.includes("@whiskeysockets/baileys") || haystack.includes("baileys") || diff --git a/src/web/inbound.media.test.ts b/src/web/inbound.media.test.ts index 49ba93a95..91e37a5b4 100644 --- a/src/web/inbound.media.test.ts +++ b/src/web/inbound.media.test.ts @@ -2,7 +2,6 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const readAllowFromStoreMock = vi.fn().mockResolvedValue([]); @@ -43,7 +42,7 @@ vi.mock("../media/store.js", async (importOriginal) => { }; }); -const HOME = path.join(os.tmpdir(), `moltbot-inbound-media-${crypto.randomUUID()}`); +const HOME = path.join(os.tmpdir(), `openclaw-inbound-media-${crypto.randomUUID()}`); process.env.HOME = HOME; vi.mock("@whiskeysockets/baileys", async () => { @@ -128,7 +127,9 @@ describe("web inbound media saves with extension", () => { // Allow a brief window for the async handler to fire on slower hosts. for (let i = 0; i < 50; i++) { - if (onMessage.mock.calls.length > 0) break; + if (onMessage.mock.calls.length > 0) { + break; + } await new Promise((resolve) => setTimeout(resolve, 10)); } @@ -179,7 +180,9 @@ describe("web inbound media saves with extension", () => { realSock.ev.emit("messages.upsert", upsert); for (let i = 0; i < 50; i++) { - if (onMessage.mock.calls.length > 0) break; + if (onMessage.mock.calls.length > 0) { + break; + } await new Promise((resolve) => setTimeout(resolve, 10)); } @@ -219,7 +222,9 @@ describe("web inbound media saves with extension", () => { realSock.ev.emit("messages.upsert", upsert); for (let i = 0; i < 50; i++) { - if (onMessage.mock.calls.length > 0) break; + if (onMessage.mock.calls.length > 0) { + break; + } await new Promise((resolve) => setTimeout(resolve, 10)); } diff --git a/src/web/inbound.test.ts b/src/web/inbound.test.ts index 97d17beab..70be8489d 100644 --- a/src/web/inbound.test.ts +++ b/src/web/inbound.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { extractLocationData, extractMediaPlaceholder, extractText } from "./inbound.js"; describe("web inbound helpers", () => { diff --git a/src/web/inbound/access-control.pairing-history.test.ts b/src/web/inbound/access-control.pairing-history.test.ts index 795839bf2..b5d5b721c 100644 --- a/src/web/inbound/access-control.pairing-history.test.ts +++ b/src/web/inbound/access-control.pairing-history.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - import { checkInboundAccessControl } from "./access-control.js"; const sendMessageMock = vi.fn(); diff --git a/src/web/inbound/extract.ts b/src/web/inbound/extract.ts index e48ac7be1..2cd9b8eb3 100644 --- a/src/web/inbound/extract.ts +++ b/src/web/inbound/extract.ts @@ -10,19 +10,23 @@ import { jidToE164 } from "../../utils.js"; import { parseVcard } from "../vcard.js"; function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | undefined { - const normalized = normalizeMessageContent(message as proto.IMessage | undefined); - return normalized as proto.IMessage | undefined; + const normalized = normalizeMessageContent(message); + return normalized; } function extractContextInfo(message: proto.IMessage | undefined): proto.IContextInfo | undefined { - if (!message) return undefined; + if (!message) { + return undefined; + } const contentType = getContentType(message); const candidate = contentType ? (message as Record)[contentType] : undefined; const contextInfo = candidate && typeof candidate === "object" && "contextInfo" in candidate ? (candidate as { contextInfo?: proto.IContextInfo }).contextInfo : undefined; - if (contextInfo) return contextInfo; + if (contextInfo) { + return contextInfo; + } const fallback = message.extendedTextMessage?.contextInfo ?? message.imageMessage?.contextInfo ?? @@ -36,19 +40,29 @@ function extractContextInfo(message: proto.IMessage | undefined): proto.IContext message.interactiveResponseMessage?.contextInfo ?? message.buttonsMessage?.contextInfo ?? message.listMessage?.contextInfo; - if (fallback) return fallback; + if (fallback) { + return fallback; + } for (const value of Object.values(message)) { - if (!value || typeof value !== "object") continue; - if (!("contextInfo" in value)) continue; + if (!value || typeof value !== "object") { + continue; + } + if (!("contextInfo" in value)) { + continue; + } const candidateContext = (value as { contextInfo?: proto.IContextInfo }).contextInfo; - if (candidateContext) return candidateContext; + if (candidateContext) { + return candidateContext; + } } return undefined; } export function extractMentionedJids(rawMessage: proto.IMessage | undefined): string[] | undefined { const message = unwrapMessage(rawMessage); - if (!message) return undefined; + if (!message) { + return undefined; + } const candidates: Array = [ message.extendedTextMessage?.contextInfo?.mentionedJid, @@ -64,34 +78,46 @@ export function extractMentionedJids(rawMessage: proto.IMessage | undefined): st ]; const flattened = candidates.flatMap((arr) => arr ?? []).filter(Boolean); - if (flattened.length === 0) return undefined; + if (flattened.length === 0) { + return undefined; + } return Array.from(new Set(flattened)); } export function extractText(rawMessage: proto.IMessage | undefined): string | undefined { const message = unwrapMessage(rawMessage); - if (!message) return undefined; + if (!message) { + return undefined; + } const extracted = extractMessageContent(message); const candidates = [message, extracted && extracted !== message ? extracted : undefined]; for (const candidate of candidates) { - if (!candidate) continue; + if (!candidate) { + continue; + } if (typeof candidate.conversation === "string" && candidate.conversation.trim()) { return candidate.conversation.trim(); } const extended = candidate.extendedTextMessage?.text; - if (extended?.trim()) return extended.trim(); + if (extended?.trim()) { + return extended.trim(); + } const caption = candidate.imageMessage?.caption ?? candidate.videoMessage?.caption ?? candidate.documentMessage?.caption; - if (caption?.trim()) return caption.trim(); + if (caption?.trim()) { + return caption.trim(); + } } const contactPlaceholder = extractContactPlaceholder(message) ?? (extracted && extracted !== message ? extractContactPlaceholder(extracted as proto.IMessage | undefined) : undefined); - if (contactPlaceholder) return contactPlaceholder; + if (contactPlaceholder) { + return contactPlaceholder; + } return undefined; } @@ -99,18 +125,32 @@ export function extractMediaPlaceholder( rawMessage: proto.IMessage | undefined, ): string | undefined { const message = unwrapMessage(rawMessage); - if (!message) return undefined; - if (message.imageMessage) return ""; - if (message.videoMessage) return ""; - if (message.audioMessage) return ""; - if (message.documentMessage) return ""; - if (message.stickerMessage) return ""; + if (!message) { + return undefined; + } + if (message.imageMessage) { + return ""; + } + if (message.videoMessage) { + return ""; + } + if (message.audioMessage) { + return ""; + } + if (message.documentMessage) { + return ""; + } + if (message.stickerMessage) { + return ""; + } return undefined; } function extractContactPlaceholder(rawMessage: proto.IMessage | undefined): string | undefined { const message = unwrapMessage(rawMessage); - if (!message) return undefined; + if (!message) { + return undefined; + } const contact = message.contactMessage ?? undefined; if (contact) { const { name, phones } = describeContact({ @@ -120,7 +160,9 @@ function extractContactPlaceholder(rawMessage: proto.IMessage | undefined): stri return formatContactPlaceholder(name, phones); } const contactsArray = message.contactsArrayMessage?.contacts ?? undefined; - if (!contactsArray || contactsArray.length === 0) return undefined; + if (!contactsArray || contactsArray.length === 0) { + return undefined; + } const labels = contactsArray .map((entry) => describeContact({ displayName: entry.displayName, vcard: entry.vcard })) .map((entry) => formatContactLabel(entry.name, entry.phones)) @@ -140,7 +182,9 @@ function describeContact(input: { displayName?: string | null; vcard?: string | function formatContactPlaceholder(name?: string, phones?: string[]): string { const label = formatContactLabel(name, phones); - if (!label) return ""; + if (!label) { + return ""; + } return ``; } @@ -158,17 +202,25 @@ function formatContactsPlaceholder(labels: string[], total: number): string { function formatContactLabel(name?: string, phones?: string[]): string | undefined { const phoneLabel = formatPhoneList(phones); const parts = [name, phoneLabel].filter((value): value is string => Boolean(value)); - if (parts.length === 0) return undefined; + if (parts.length === 0) { + return undefined; + } return parts.join(", "); } function formatPhoneList(phones?: string[]): string | undefined { const cleaned = phones?.map((phone) => phone.trim()).filter(Boolean) ?? []; - if (cleaned.length === 0) return undefined; + if (cleaned.length === 0) { + return undefined; + } const { shown, remaining } = summarizeList(cleaned, cleaned.length, 1); const [primary] = shown; - if (!primary) return undefined; - if (remaining === 0) return primary; + if (!primary) { + return undefined; + } + if (remaining === 0) { + return primary; + } return `${primary} (+${remaining} more)`; } @@ -186,7 +238,9 @@ export function extractLocationData( rawMessage: proto.IMessage | undefined, ): NormalizedLocation | null { const message = unwrapMessage(rawMessage); - if (!message) return null; + if (!message) { + return null; + } const live = message.liveLocationMessage ?? undefined; if (live) { @@ -242,17 +296,21 @@ export function describeReplyContext(rawMessage: proto.IMessage | undefined): { senderE164?: string; } | null { const message = unwrapMessage(rawMessage); - if (!message) return null; + if (!message) { + return null; + } const contextInfo = extractContextInfo(message); - const quoted = normalizeMessageContent( - contextInfo?.quotedMessage as proto.IMessage | undefined, - ) as proto.IMessage | undefined; - if (!quoted) return null; + const quoted = normalizeMessageContent(contextInfo?.quotedMessage as proto.IMessage | undefined); + if (!quoted) { + return null; + } const location = extractLocationData(quoted); const locationText = location ? formatLocationText(location) : undefined; const text = extractText(quoted); let body: string | undefined = [text, locationText].filter(Boolean).join("\n").trim(); - if (!body) body = extractMediaPlaceholder(quoted); + if (!body) { + body = extractMediaPlaceholder(quoted); + } if (!body) { const quotedType = quoted ? getContentType(quoted) : undefined; logVerbose( diff --git a/src/web/inbound/media.ts b/src/web/inbound/media.ts index 9e7d566f4..b99721ffb 100644 --- a/src/web/inbound/media.ts +++ b/src/web/inbound/media.ts @@ -1,11 +1,11 @@ import type { proto, WAMessage } from "@whiskeysockets/baileys"; import { downloadMediaMessage, normalizeMessageContent } from "@whiskeysockets/baileys"; -import { logVerbose } from "../../globals.js"; import type { createWaSocket } from "../session.js"; +import { logVerbose } from "../../globals.js"; function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | undefined { - const normalized = normalizeMessageContent(message as proto.IMessage | undefined); - return normalized as proto.IMessage | undefined; + const normalized = normalizeMessageContent(message); + return normalized; } export async function downloadInboundMedia( @@ -13,7 +13,9 @@ export async function downloadInboundMedia( sock: Awaited>, ): Promise<{ buffer: Buffer; mimetype?: string } | undefined> { const message = unwrapMessage(msg.message as proto.IMessage | undefined); - if (!message) return undefined; + if (!message) { + return undefined; + } const mimetype = message.imageMessage?.mimetype ?? message.videoMessage?.mimetype ?? @@ -31,7 +33,7 @@ export async function downloadInboundMedia( return undefined; } try { - const buffer = (await downloadMediaMessage( + const buffer = await downloadMediaMessage( msg as WAMessage, "buffer", {}, @@ -39,7 +41,7 @@ export async function downloadInboundMedia( reuploadRequest: sock.updateMediaMessage, logger: sock.logger, }, - )) as Buffer; + ); return { buffer, mimetype }; } catch (err) { logVerbose(`downloadMediaMessage failed: ${String(err)}`); diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index 3633cbce9..c7cfabeba 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -1,12 +1,13 @@ import type { AnyMessageContent, proto, WAMessage } from "@whiskeysockets/baileys"; import { DisconnectReason, isJidGroup } from "@whiskeysockets/baileys"; +import type { WebInboundMessage, WebListenerCloseReason } from "./types.js"; +import { createInboundDebouncer } from "../../auto-reply/inbound-debounce.js"; import { formatLocationText } from "../../channels/location.js"; import { logVerbose, shouldLogVerbose } from "../../globals.js"; import { recordChannelActivity } from "../../infra/channel-activity.js"; import { getChildLogger } from "../../logging/logger.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { saveMediaBuffer } from "../../media/store.js"; -import { createInboundDebouncer } from "../../auto-reply/inbound-debounce.js"; import { jidToE164, resolveJidToE164 } from "../../utils.js"; import { createWaSocket, getStatusCode, waitForWaConnection } from "../session.js"; import { checkInboundAccessControl } from "./access-control.js"; @@ -20,7 +21,6 @@ import { } from "./extract.js"; import { downloadInboundMedia } from "./media.js"; import { createWebSendApi } from "./send-api.js"; -import type { WebInboundMessage, WebListenerCloseReason } from "./types.js"; export async function monitorWebInbox(options: { verbose: boolean; @@ -48,7 +48,9 @@ export async function monitorWebInbox(options: { onCloseResolve = resolve; }); const resolveClose = (reason: WebListenerCloseReason) => { - if (!onCloseResolve) return; + if (!onCloseResolve) { + return; + } const resolver = onCloseResolve; onCloseResolve = null; resolver(reason); @@ -56,7 +58,9 @@ export async function monitorWebInbox(options: { try { await sock.sendPresenceUpdate("available"); - if (shouldLogVerbose()) logVerbose("Sent global 'available' presence on connect"); + if (shouldLogVerbose()) { + logVerbose("Sent global 'available' presence on connect"); + } } catch (err) { logVerbose(`Failed to send 'available' presence on connect: ${String(err)}`); } @@ -70,21 +74,27 @@ export async function monitorWebInbox(options: { msg.chatType === "group" ? (msg.senderJid ?? msg.senderE164 ?? msg.senderName ?? msg.from) : msg.from; - if (!senderKey) return null; + if (!senderKey) { + return null; + } const conversationKey = msg.chatType === "group" ? msg.chatId : msg.from; return `${msg.accountId}:${conversationKey}:${senderKey}`; }, shouldDebounce: options.shouldDebounce, onFlush: async (entries) => { const last = entries.at(-1); - if (!last) return; + if (!last) { + return; + } if (entries.length === 1) { await options.onMessage(last); return; } const mentioned = new Set(); for (const entry of entries) { - for (const jid of entry.mentionedJids ?? []) mentioned.add(jid); + for (const jid of entry.mentionedJids ?? []) { + mentioned.add(jid); + } } const combinedBody = entries .map((entry) => entry.body) @@ -114,7 +124,9 @@ export async function monitorWebInbox(options: { const getGroupMeta = async (jid: string) => { const cached = groupMetaCache.get(jid); - if (cached && cached.expires > Date.now()) return cached; + if (cached && cached.expires > Date.now()) { + return cached; + } try { const meta = await sock.groupMetadata(jid); const participants = @@ -140,7 +152,9 @@ export async function monitorWebInbox(options: { }; const handleMessagesUpsert = async (upsert: { type?: string; messages?: Array }) => { - if (upsert.type !== "notify" && upsert.type !== "append") return; + if (upsert.type !== "notify" && upsert.type !== "append") { + return; + } for (const msg of upsert.messages ?? []) { recordChannelActivity({ channel: "whatsapp", @@ -149,17 +163,25 @@ export async function monitorWebInbox(options: { }); const id = msg.key?.id ?? undefined; const remoteJid = msg.key?.remoteJid; - if (!remoteJid) continue; - if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) continue; + if (!remoteJid) { + continue; + } + if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) { + continue; + } const group = isJidGroup(remoteJid) === true; if (id) { const dedupeKey = `${options.accountId}:${remoteJid}:${id}`; - if (isRecentInboundMessage(dedupeKey)) continue; + if (isRecentInboundMessage(dedupeKey)) { + continue; + } } const participantJid = msg.key?.participant ?? undefined; const from = group ? remoteJid : await resolveInboundJid(remoteJid); - if (!from) continue; + if (!from) { + continue; + } const senderE164 = group ? participantJid ? await resolveInboundJid(participantJid) @@ -190,7 +212,9 @@ export async function monitorWebInbox(options: { sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) }, remoteJid, }); - if (!access.allowed) continue; + if (!access.allowed) { + continue; + } if (id && !access.isSelfChat && options.sendReadReceipts !== false) { const participant = msg.key?.participant; @@ -209,7 +233,9 @@ export async function monitorWebInbox(options: { } // If this is history/offline catch-up, mark read above but skip auto-reply. - if (upsert.type === "append") continue; + if (upsert.type === "append") { + continue; + } const location = extractLocationData(msg.message ?? undefined); const locationText = location ? formatLocationText(location) : undefined; @@ -219,7 +245,9 @@ export async function monitorWebInbox(options: { } if (!body) { body = extractMediaPlaceholder(msg.message ?? undefined); - if (!body) continue; + if (!body) { + continue; + } } const replyContext = describeReplyContext(msg.message as proto.IMessage | undefined); diff --git a/src/web/inbound/send-api.ts b/src/web/inbound/send-api.ts index 06860e896..7deb9540d 100644 --- a/src/web/inbound/send-api.ts +++ b/src/web/inbound/send-api.ts @@ -1,7 +1,7 @@ import type { AnyMessageContent, WAPresence } from "@whiskeysockets/baileys"; +import type { ActiveWebSendOptions } from "../active-listener.js"; import { recordChannelActivity } from "../../infra/channel-activity.js"; import { toWhatsappJid } from "../../utils.js"; -import type { ActiveWebSendOptions } from "../active-listener.js"; export function createWebSendApi(params: { sock: { diff --git a/src/web/login-qr.ts b/src/web/login-qr.ts index aa3334edb..323619be9 100644 --- a/src/web/login-qr.ts +++ b/src/web/login-qr.ts @@ -1,6 +1,5 @@ -import { randomUUID } from "node:crypto"; - import { DisconnectReason } from "@whiskeysockets/baileys"; +import { randomUUID } from "node:crypto"; import { loadConfig } from "../config/config.js"; import { danger, info, success } from "../globals.js"; import { logInfo } from "../logger.js"; @@ -66,18 +65,24 @@ function attachLoginWaiter(accountId: string, login: ActiveLogin) { login.waitPromise = waitForWaConnection(login.sock) .then(() => { const current = activeLogins.get(accountId); - if (current?.id === login.id) current.connected = true; + if (current?.id === login.id) { + current.connected = true; + } }) .catch((err) => { const current = activeLogins.get(accountId); - if (current?.id !== login.id) return; + if (current?.id !== login.id) { + return; + } current.error = formatError(err); current.errorStatus = getStatusCode(err); }); } async function restartLoginSocket(login: ActiveLogin, runtime: RuntimeEnv) { - if (login.restartAttempted) return false; + if (login.restartAttempted) { + return false; + } login.restartAttempted = true; runtime.log( info("WhatsApp asked for a restart after pairing (code 515); retrying connection once…"), @@ -151,10 +156,14 @@ export async function startWebLoginWithQr( sock = await createWaSocket(false, Boolean(opts.verbose), { authDir: account.authDir, onQr: (qr: string) => { - if (pendingQr) return; + if (pendingQr) { + return; + } pendingQr = qr; const current = activeLogins.get(account.accountId); - if (current && !current.qr) current.qr = qr; + if (current && !current.qr) { + current.qr = qr; + } clearTimeout(qrTimer); runtime.log(info("WhatsApp QR received.")); resolveQr?.(qr); @@ -180,7 +189,9 @@ export async function startWebLoginWithQr( verbose: Boolean(opts.verbose), }; activeLogins.set(account.accountId, login); - if (pendingQr && !login.qr) login.qr = pendingQr; + if (pendingQr && !login.qr) { + login.qr = pendingQr; + } attachLoginWaiter(account.accountId, login); let qr: string; diff --git a/src/web/login.coverage.test.ts b/src/web/login.coverage.test.ts index 120dcff62..7fc3c3990 100644 --- a/src/web/login.coverage.test.ts +++ b/src/web/login.coverage.test.ts @@ -1,8 +1,7 @@ +import { DisconnectReason } from "@whiskeysockets/baileys"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - -import { DisconnectReason } from "@whiskeysockets/baileys"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const rmMock = vi.spyOn(fs, "rm"); diff --git a/src/web/login.test.ts b/src/web/login.test.ts index 99c90c18a..c3ed32e52 100644 --- a/src/web/login.test.ts +++ b/src/web/login.test.ts @@ -1,7 +1,5 @@ import { EventEmitter } from "node:events"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { resetLogger, setLoggerOverride } from "../logging.js"; vi.mock("./session.js", () => { @@ -18,8 +16,8 @@ vi.mock("./session.js", () => { }; }); -import { loginWeb } from "./login.js"; import type { waitForWaConnection } from "./session.js"; +import { loginWeb } from "./login.js"; const { createWaSocket } = await import("./session.js"); diff --git a/src/web/login.ts b/src/web/login.ts index 845fa23b0..b336f8ebe 100644 --- a/src/web/login.ts +++ b/src/web/login.ts @@ -1,9 +1,9 @@ import { DisconnectReason } from "@whiskeysockets/baileys"; +import { formatCliCommand } from "../cli/command-format.js"; import { loadConfig } from "../config/config.js"; import { danger, info, success } from "../globals.js"; import { logInfo } from "../logger.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { formatCliCommand } from "../cli/command-format.js"; import { resolveWhatsAppAccount } from "./accounts.js"; import { createWaSocket, formatError, logoutWeb, waitForWaConnection } from "./session.js"; @@ -57,14 +57,14 @@ export async function loginWeb( }); console.error( danger( - `WhatsApp reported the session is logged out. Cleared cached web session; please rerun ${formatCliCommand("moltbot channels login")} and scan the QR again.`, + `WhatsApp reported the session is logged out. Cleared cached web session; please rerun ${formatCliCommand("openclaw channels login")} and scan the QR again.`, ), ); - throw new Error("Session logged out; cache cleared. Re-run login."); + throw new Error("Session logged out; cache cleared. Re-run login.", { cause: err }); } const formatted = formatError(err); console.error(danger(`WhatsApp Web connection ended before fully opening. ${formatted}`)); - throw new Error(formatted); + throw new Error(formatted, { cause: err }); } finally { // Let Baileys flush any final events before closing the socket. setTimeout(() => { diff --git a/src/web/logout.test.ts b/src/web/logout.test.ts index 54991d6b9..4f5cc64b1 100644 --- a/src/web/logout.test.ts +++ b/src/web/logout.test.ts @@ -1,8 +1,6 @@ import fs from "node:fs"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { isPathWithinBase } from "../../test/helpers/paths.js"; import { withTempHome } from "../../test/helpers/temp-home.js"; diff --git a/src/web/media.test.ts b/src/web/media.test.ts index bafac9bcd..21492db13 100644 --- a/src/web/media.test.ts +++ b/src/web/media.test.ts @@ -1,10 +1,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import sharp from "sharp"; import { afterEach, describe, expect, it, vi } from "vitest"; - import { optimizeImageToPng } from "../media/image-ops.js"; import { loadWebMedia, optimizeImageToJpeg } from "./media.js"; @@ -13,7 +11,7 @@ const tmpFiles: string[] = []; async function writeTempFile(buffer: Buffer, ext: string): Promise { const file = path.join( os.tmpdir(), - `moltbot-media-${Date.now()}-${Math.random().toString(16).slice(2)}${ext}`, + `openclaw-media-${Date.now()}-${Math.random().toString(16).slice(2)}${ext}`, ); tmpFiles.push(file); await fs.writeFile(file, buffer); @@ -118,7 +116,9 @@ describe("web media loading", () => { if (name === "content-disposition") { return 'attachment; filename="report.pdf"'; } - if (name === "content-type") return "application/pdf"; + if (name === "content-type") { + return "application/pdf"; + } return null; }, }, diff --git a/src/web/media.ts b/src/web/media.ts index 72f6d34de..52d2ca6bf 100644 --- a/src/web/media.ts +++ b/src/web/media.ts @@ -1,10 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; - import { logVerbose, shouldLogVerbose } from "../globals.js"; import { type MediaKind, maxBytesForKind, mediaKindFromMime } from "../media/constants.js"; -import { resolveUserPath } from "../utils.js"; import { fetchRemoteMedia } from "../media/fetch.js"; import { convertHeicToJpeg, @@ -13,6 +11,7 @@ import { resizeToJpeg, } from "../media/image-ops.js"; import { detectMime, extensionForMime } from "../media/mime.js"; +import { resolveUserPath } from "../utils.js"; export type WebMediaResult = { buffer: Buffer; @@ -43,15 +42,23 @@ function formatCapReduce(label: string, cap: number, size: number): string { } function isHeicSource(opts: { contentType?: string; fileName?: string }): boolean { - if (opts.contentType && HEIC_MIME_RE.test(opts.contentType.trim())) return true; - if (opts.fileName && HEIC_EXT_RE.test(opts.fileName.trim())) return true; + if (opts.contentType && HEIC_MIME_RE.test(opts.contentType.trim())) { + return true; + } + if (opts.fileName && HEIC_EXT_RE.test(opts.fileName.trim())) { + return true; + } return false; } function toJpegFileName(fileName?: string): string | undefined { - if (!fileName) return undefined; + if (!fileName) { + return undefined; + } const trimmed = fileName.trim(); - if (!trimmed) return fileName; + if (!trimmed) { + return fileName; + } const parsed = path.parse(trimmed); if (!parsed.ext || HEIC_EXT_RE.test(parsed.ext)) { return path.format({ dir: parsed.dir, name: parsed.name || trimmed, ext: ".jpg" }); @@ -69,8 +76,12 @@ type OptimizedImage = { }; function logOptimizedImage(params: { originalSize: number; optimized: OptimizedImage }): void { - if (!shouldLogVerbose()) return; - if (params.optimized.optimizedSize >= params.originalSize) return; + if (!shouldLogVerbose()) { + return; + } + if (params.optimized.optimizedSize >= params.originalSize) { + return; + } if (params.optimized.format === "png") { logVerbose( `Optimized PNG (preserving alpha) from ${formatMb(params.originalSize)}MB to ${formatMb(params.optimized.optimizedSize)}MB (side≤${params.optimized.resizeSide}px)`, @@ -207,7 +218,9 @@ async function loadWebMediaInternal( let fileName = path.basename(mediaUrl) || undefined; if (fileName && !path.extname(fileName) && mime) { const ext = extensionForMime(mime); - if (ext) fileName = `${fileName}${ext}`; + if (ext) { + fileName = `${fileName}${ext}`; + } } return await clampAndFinalize({ buffer: data, @@ -250,7 +263,7 @@ export async function optimizeImageToJpeg( try { source = await convertHeicToJpeg(buffer); } catch (err) { - throw new Error(`HEIC image conversion failed: ${String(err)}`); + throw new Error(`HEIC image conversion failed: ${String(err)}`, { cause: err }); } } const sides = [2048, 1536, 1280, 1024, 800]; diff --git a/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts b/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts index a9da1d6bb..3c21832da 100644 --- a/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts +++ b/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts @@ -69,9 +69,7 @@ const _getSock = () => (createWaSocket as unknown as () => Promise { created: true, }); resetWebInboundDedupe(); - authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-")); + authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-")); }); afterEach(() => { diff --git a/src/web/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts b/src/web/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts index 002f85f4a..8baf6ba19 100644 --- a/src/web/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts +++ b/src/web/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts @@ -69,9 +69,7 @@ const _getSock = () => (createWaSocket as unknown as () => Promise { created: true, }); resetWebInboundDedupe(); - authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-")); + authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-")); }); afterEach(() => { diff --git a/src/web/monitor-inbox.captures-media-path-image-messages.test.ts b/src/web/monitor-inbox.captures-media-path-image-messages.test.ts index a29e83556..e70e5def5 100644 --- a/src/web/monitor-inbox.captures-media-path-image-messages.test.ts +++ b/src/web/monitor-inbox.captures-media-path-image-messages.test.ts @@ -70,9 +70,7 @@ import crypto from "node:crypto"; import fsSync from "node:fs"; import os from "node:os"; import path from "node:path"; - import { afterEach, beforeEach, describe, expect, it } from "vitest"; - import { resetLogger, setLoggerOverride } from "../logging.js"; import { monitorWebInbox, resetWebInboundDedupe } from "./inbound.js"; @@ -88,7 +86,7 @@ describe("web monitor inbox", () => { created: true, }); resetWebInboundDedupe(); - authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-")); + authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-")); }); afterEach(() => { @@ -171,7 +169,7 @@ describe("web monitor inbox", () => { }); it("logs inbound bodies to file", async () => { - const logPath = path.join(os.tmpdir(), `moltbot-log-test-${crypto.randomUUID()}.log`); + const logPath = path.join(os.tmpdir(), `openclaw-log-test-${crypto.randomUUID()}.log`); setLoggerOverride({ level: "trace", file: logPath }); const onMessage = vi.fn(); diff --git a/src/web/monitor-inbox.streams-inbound-messages.test.ts b/src/web/monitor-inbox.streams-inbound-messages.test.ts index 1f1c61656..61e8dd383 100644 --- a/src/web/monitor-inbox.streams-inbound-messages.test.ts +++ b/src/web/monitor-inbox.streams-inbound-messages.test.ts @@ -69,9 +69,7 @@ const _getSock = () => (createWaSocket as unknown as () => Promise { created: true, }); resetWebInboundDedupe(); - authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-")); + authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-")); }); afterEach(() => { diff --git a/src/web/outbound.test.ts b/src/web/outbound.test.ts index ffb3e8538..1d9fef7d0 100644 --- a/src/web/outbound.test.ts +++ b/src/web/outbound.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import { resetLogger, setLoggerOverride } from "../logging.js"; import { setActiveWebListener } from "./active-listener.js"; diff --git a/src/web/outbound.ts b/src/web/outbound.ts index 0ca867961..1df957989 100644 --- a/src/web/outbound.ts +++ b/src/web/outbound.ts @@ -1,12 +1,11 @@ import { randomUUID } from "node:crypto"; - -import { getChildLogger } from "../logging/logger.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; -import { normalizePollInput, type PollInput } from "../polls.js"; -import { toWhatsappJid } from "../utils.js"; import { loadConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; +import { getChildLogger } from "../logging/logger.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; import { convertMarkdownTables } from "../markdown/tables.js"; +import { normalizePollInput, type PollInput } from "../polls.js"; +import { toWhatsappJid } from "../utils.js"; import { type ActiveWebSendOptions, requireActiveWebListener } from "./active-listener.js"; import { loadWebMedia } from "./media.js"; diff --git a/src/web/qr-image.test.ts b/src/web/qr-image.test.ts index fdd2ad779..6fdf25f43 100644 --- a/src/web/qr-image.test.ts +++ b/src/web/qr-image.test.ts @@ -1,13 +1,11 @@ import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; - import { describe, expect, it } from "vitest"; - import { renderQrPngBase64 } from "./qr-image.js"; describe("renderQrPngBase64", () => { it("renders a PNG data payload", async () => { - const b64 = await renderQrPngBase64("moltbot"); + const b64 = await renderQrPngBase64("openclaw"); const buf = Buffer.from(b64, "base64"); expect(buf.subarray(0, 8).toString("hex")).toBe("89504e470d0a1a0a"); }); diff --git a/src/web/qr-image.ts b/src/web/qr-image.ts index 3a495ea36..e60b0be67 100644 --- a/src/web/qr-image.ts +++ b/src/web/qr-image.ts @@ -12,8 +12,8 @@ type QRCodeConstructor = new ( isDark: (row: number, col: number) => boolean; }; -const QRCode = QRCodeModule as unknown as QRCodeConstructor; -const QRErrorCorrectLevel = QRErrorCorrectLevelModule as Record; +const QRCode = QRCodeModule as QRCodeConstructor; +const QRErrorCorrectLevel = QRErrorCorrectLevelModule; function createQrMatrix(input: string) { const qr = new QRCode(-1, QRErrorCorrectLevel.L); @@ -111,7 +111,9 @@ export async function renderQrPngBase64( const buf = Buffer.alloc(size * size * 4, 255); for (let row = 0; row < modules; row += 1) { for (let col = 0; col < modules; col += 1) { - if (!qr.isDark(row, col)) continue; + if (!qr.isDark(row, col)) { + continue; + } const startX = (col + marginModules) * scale; const startY = (row + marginModules) * scale; for (let y = 0; y < scale; y += 1) { diff --git a/src/web/reconnect.test.ts b/src/web/reconnect.test.ts index 63c5fba80..6166a509e 100644 --- a/src/web/reconnect.test.ts +++ b/src/web/reconnect.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { computeBackoff, DEFAULT_HEARTBEAT_SECONDS, @@ -11,7 +10,7 @@ import { } from "./reconnect.js"; describe("web reconnect helpers", () => { - const cfg: MoltbotConfig = {}; + const cfg: OpenClawConfig = {}; it("resolves sane reconnect defaults with clamps", () => { const policy = resolveReconnectPolicy(cfg, { diff --git a/src/web/reconnect.ts b/src/web/reconnect.ts index f727b7f9a..a00248106 100644 --- a/src/web/reconnect.ts +++ b/src/web/reconnect.ts @@ -1,6 +1,5 @@ import { randomUUID } from "node:crypto"; - -import type { MoltbotConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import type { BackoffPolicy } from "../infra/backoff.js"; import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; @@ -19,14 +18,16 @@ export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = { const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val)); -export function resolveHeartbeatSeconds(cfg: MoltbotConfig, overrideSeconds?: number): number { +export function resolveHeartbeatSeconds(cfg: OpenClawConfig, overrideSeconds?: number): number { const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds; - if (typeof candidate === "number" && candidate > 0) return candidate; + if (typeof candidate === "number" && candidate > 0) { + return candidate; + } return DEFAULT_HEARTBEAT_SECONDS; } export function resolveReconnectPolicy( - cfg: MoltbotConfig, + cfg: OpenClawConfig, overrides?: Partial, ): ReconnectPolicy { const reconnectOverrides = cfg.web?.reconnect ?? {}; diff --git a/src/web/session.test.ts b/src/web/session.test.ts index db38c5893..8092fbd2d 100644 --- a/src/web/session.test.ts +++ b/src/web/session.test.ts @@ -62,7 +62,9 @@ describe("web session", () => { it("logWebSelfId prints cached E.164 when creds exist", () => { const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => { - if (typeof p !== "string") return false; + if (typeof p !== "string") { + return false; + } return p.endsWith("creds.json"); }); const readSpy = vi.spyOn(fsSync, "readFileSync").mockImplementation((p) => { @@ -106,11 +108,13 @@ describe("web session", () => { }); it("does not clobber creds backup when creds.json is corrupted", async () => { - const credsSuffix = path.join(".clawdbot", "credentials", "whatsapp", "default", "creds.json"); + const credsSuffix = path.join(".openclaw", "credentials", "whatsapp", "default", "creds.json"); const copySpy = vi.spyOn(fsSync, "copyFileSync").mockImplementation(() => {}); const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => { - if (typeof p !== "string") return false; + if (typeof p !== "string") { + return false; + } return p.endsWith(credsSuffix); }); const statSpy = vi.spyOn(fsSync, "statSync").mockImplementation((p) => { @@ -182,9 +186,9 @@ describe("web session", () => { }); it("rotates creds backup when creds.json is valid JSON", async () => { - const credsSuffix = path.join(".clawdbot", "credentials", "whatsapp", "default", "creds.json"); + const credsSuffix = path.join(".openclaw", "credentials", "whatsapp", "default", "creds.json"); const backupSuffix = path.join( - ".clawdbot", + ".openclaw", "credentials", "whatsapp", "default", @@ -193,7 +197,9 @@ describe("web session", () => { const copySpy = vi.spyOn(fsSync, "copyFileSync").mockImplementation(() => {}); const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => { - if (typeof p !== "string") return false; + if (typeof p !== "string") { + return false; + } return p.endsWith(credsSuffix); }); const statSpy = vi.spyOn(fsSync, "statSync").mockImplementation((p) => { diff --git a/src/web/session.ts b/src/web/session.ts index dcf0e253f..c25d6d793 100644 --- a/src/web/session.ts +++ b/src/web/session.ts @@ -1,5 +1,3 @@ -import { randomUUID } from "node:crypto"; -import fsSync from "node:fs"; import { DisconnectReason, fetchLatestBaileysVersion, @@ -7,13 +5,14 @@ import { makeWASocket, useMultiFileAuthState, } from "@whiskeysockets/baileys"; +import { randomUUID } from "node:crypto"; +import fsSync from "node:fs"; import qrcode from "qrcode-terminal"; +import { formatCliCommand } from "../cli/command-format.js"; import { danger, success } from "../globals.js"; import { getChildLogger, toPinoLikeLogger } from "../logging.js"; import { ensureDir, resolveUserPath } from "../utils.js"; import { VERSION } from "../version.js"; -import { formatCliCommand } from "../cli/command-format.js"; - import { maybeRestoreCredsFromBackup, resolveDefaultWebAuthDir, @@ -46,9 +45,13 @@ function enqueueSaveCreds( function readCredsJsonRaw(filePath: string): string | null { try { - if (!fsSync.existsSync(filePath)) return null; + if (!fsSync.existsSync(filePath)) { + return null; + } const stats = fsSync.statSync(filePath); - if (!stats.isFile() || stats.size <= 1) return null; + if (!stats.isFile() || stats.size <= 1) { + return null; + } return fsSync.readFileSync(filePath, "utf-8"); } catch { return null; @@ -92,7 +95,7 @@ export async function createWaSocket( printQr: boolean, verbose: boolean, opts: { authDir?: string; onQr?: (qr: string) => void } = {}, -) { +): Promise> { const baseLogger = getChildLogger( { module: "baileys" }, { @@ -114,7 +117,7 @@ export async function createWaSocket( version, logger, printQRInTerminal: false, - browser: ["moltbot", "cli", VERSION], + browser: ["openclaw", "cli", VERSION], syncFullHistory: false, markOnlineOnConnect: false, }); @@ -137,7 +140,7 @@ export async function createWaSocket( if (status === DisconnectReason.loggedOut) { console.error( danger( - `WhatsApp session logged out. Run: ${formatCliCommand("moltbot channels login")}`, + `WhatsApp session logged out. Run: ${formatCliCommand("openclaw channels login")}`, ), ); } @@ -193,11 +196,13 @@ export function getStatusCode(err: unknown) { function safeStringify(value: unknown, limit = 800): string { try { - const seen = new WeakSet(); + const seen = new WeakSet(); const raw = JSON.stringify( value, (_key, v) => { - if (typeof v === "bigint") return v.toString(); + if (typeof v === "bigint") { + return v.toString(); + } if (typeof v === "function") { const maybeName = (v as { name?: unknown }).name; const name = @@ -205,14 +210,18 @@ function safeStringify(value: unknown, limit = 800): string { return `[Function ${name}]`; } if (typeof v === "object" && v) { - if (seen.has(v)) return "[Circular]"; + if (seen.has(v)) { + return "[Circular]"; + } seen.add(v); } return v; }, 2, ); - if (!raw) return String(value); + if (!raw) { + return String(value); + } return raw.length > limit ? `${raw.slice(0, limit)}…` : raw; } catch { return String(value); @@ -224,11 +233,15 @@ function extractBoomDetails(err: unknown): { error?: string; message?: string; } | null { - if (!err || typeof err !== "object") return null; + if (!err || typeof err !== "object") { + return null; + } const output = (err as { output?: unknown })?.output as | { statusCode?: unknown; payload?: unknown } | undefined; - if (!output || typeof output !== "object") return null; + if (!output || typeof output !== "object") { + return null; + } const payload = (output as { payload?: unknown }).payload as | { error?: unknown; message?: unknown; statusCode?: unknown } | undefined; @@ -236,18 +249,26 @@ function extractBoomDetails(err: unknown): { typeof (output as { statusCode?: unknown }).statusCode === "number" ? ((output as { statusCode?: unknown }).statusCode as number) : typeof payload?.statusCode === "number" - ? (payload.statusCode as number) + ? payload.statusCode : undefined; const error = typeof payload?.error === "string" ? payload.error : undefined; const message = typeof payload?.message === "string" ? payload.message : undefined; - if (!statusCode && !error && !message) return null; + if (!statusCode && !error && !message) { + return null; + } return { statusCode, error, message }; } export function formatError(err: unknown): string { - if (err instanceof Error) return err.message; - if (typeof err === "string") return err; - if (!err || typeof err !== "object") return String(err); + if (err instanceof Error) { + return err.message; + } + if (typeof err === "string") { + return err; + } + if (!err || typeof err !== "object") { + return String(err); + } // Baileys frequently wraps errors under `error` with a Boom-like shape. const boom = @@ -271,12 +292,22 @@ export function formatError(err: unknown): string { const message = messageCandidates[0]; const pieces: string[] = []; - if (typeof status === "number") pieces.push(`status=${status}`); - if (boom?.error) pieces.push(boom.error); - if (message) pieces.push(message); - if (codeText) pieces.push(`code=${codeText}`); + if (typeof status === "number") { + pieces.push(`status=${status}`); + } + if (boom?.error) { + pieces.push(boom.error); + } + if (message) { + pieces.push(message); + } + if (codeText) { + pieces.push(`code=${codeText}`); + } - if (pieces.length > 0) return pieces.join(" "); + if (pieces.length > 0) { + return pieces.join(" "); + } return safeStringify(err); } diff --git a/src/web/test-helpers.ts b/src/web/test-helpers.ts index 2bb58e8a6..78abd86a9 100644 --- a/src/web/test-helpers.ts +++ b/src/web/test-helpers.ts @@ -1,10 +1,9 @@ import { vi } from "vitest"; - import type { MockBaileysSocket } from "../../test/mocks/baileys.js"; import { createMockBaileys } from "../../test/mocks/baileys.js"; // Use globalThis to store the mock config so it survives vi.mock hoisting -const CONFIG_KEY = Symbol.for("moltbot:testConfigMock"); +const CONFIG_KEY = Symbol.for("openclaw:testConfigMock"); const DEFAULT_CONFIG = { channels: { whatsapp: { @@ -37,7 +36,9 @@ vi.mock("../config/config.js", async (importOriginal) => { ...actual, loadConfig: () => { const getter = (globalThis as Record)[CONFIG_KEY]; - if (typeof getter === "function") return getter(); + if (typeof getter === "function") { + return getter(); + } return DEFAULT_CONFIG; }, }; @@ -54,7 +55,7 @@ vi.mock("../media/store.js", () => ({ vi.mock("@whiskeysockets/baileys", () => { const created = createMockBaileys(); - (globalThis as Record)[Symbol.for("moltbot:lastSocket")] = + (globalThis as Record)[Symbol.for("openclaw:lastSocket")] = created.lastSocket; return created.mod; }); @@ -74,7 +75,7 @@ export const baileys = export function resetBaileysMocks() { const recreated = createMockBaileys(); - (globalThis as Record)[Symbol.for("moltbot:lastSocket")] = + (globalThis as Record)[Symbol.for("openclaw:lastSocket")] = recreated.lastSocket; baileys.makeWASocket.mockImplementation(recreated.mod.makeWASocket); baileys.useMultiFileAuthState.mockImplementation(recreated.mod.useMultiFileAuthState); @@ -83,8 +84,12 @@ export function resetBaileysMocks() { } export function getLastSocket(): MockBaileysSocket { - const getter = (globalThis as Record)[Symbol.for("moltbot:lastSocket")]; - if (typeof getter === "function") return (getter as () => MockBaileysSocket)(); - if (!getter) throw new Error("Baileys mock not initialized"); + const getter = (globalThis as Record)[Symbol.for("openclaw:lastSocket")]; + if (typeof getter === "function") { + return (getter as () => MockBaileysSocket)(); + } + if (!getter) { + throw new Error("Baileys mock not initialized"); + } throw new Error("Invalid Baileys socket getter"); } diff --git a/src/web/vcard.ts b/src/web/vcard.ts index 9b586a8fc..9f729f4d6 100644 --- a/src/web/vcard.ts +++ b/src/web/vcard.ts @@ -6,23 +6,35 @@ type ParsedVcard = { const ALLOWED_VCARD_KEYS = new Set(["FN", "N", "TEL"]); export function parseVcard(vcard?: string): ParsedVcard { - if (!vcard) return { phones: [] }; + if (!vcard) { + return { phones: [] }; + } const lines = vcard.split(/\r?\n/); let nameFromN: string | undefined; let nameFromFn: string | undefined; const phones: string[] = []; for (const rawLine of lines) { const line = rawLine.trim(); - if (!line) continue; + if (!line) { + continue; + } const colonIndex = line.indexOf(":"); - if (colonIndex === -1) continue; + if (colonIndex === -1) { + continue; + } const key = line.slice(0, colonIndex).toUpperCase(); const rawValue = line.slice(colonIndex + 1).trim(); - if (!rawValue) continue; + if (!rawValue) { + continue; + } const baseKey = normalizeVcardKey(key); - if (!baseKey || !ALLOWED_VCARD_KEYS.has(baseKey)) continue; + if (!baseKey || !ALLOWED_VCARD_KEYS.has(baseKey)) { + continue; + } const value = cleanVcardValue(rawValue); - if (!value) continue; + if (!value) { + continue; + } if (baseKey === "FN" && !nameFromFn) { nameFromFn = normalizeVcardName(value); continue; @@ -33,7 +45,9 @@ export function parseVcard(vcard?: string): ParsedVcard { } if (baseKey === "TEL") { const phone = normalizeVcardPhone(value); - if (phone) phones.push(phone); + if (phone) { + phones.push(phone); + } } } return { name: nameFromFn ?? nameFromN, phones }; @@ -41,7 +55,9 @@ export function parseVcard(vcard?: string): ParsedVcard { function normalizeVcardKey(key: string): string | undefined { const [primary] = key.split(";"); - if (!primary) return undefined; + if (!primary) { + return undefined; + } const segments = primary.split("."); return segments[segments.length - 1] || undefined; } @@ -56,7 +72,9 @@ function normalizeVcardName(value: string): string { function normalizeVcardPhone(value: string): string { const trimmed = value.trim(); - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } if (trimmed.toLowerCase().startsWith("tel:")) { return trimmed.slice(4).trim(); } diff --git a/src/whatsapp/normalize.test.ts b/src/whatsapp/normalize.test.ts index 19dd02381..330a10225 100644 --- a/src/whatsapp/normalize.test.ts +++ b/src/whatsapp/normalize.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { isWhatsAppGroupJid, isWhatsAppUserTarget, normalizeWhatsAppTarget } from "./normalize.js"; describe("normalizeWhatsAppTarget", () => { diff --git a/src/whatsapp/normalize.ts b/src/whatsapp/normalize.ts index e5ddb0952..1d661ddc7 100644 --- a/src/whatsapp/normalize.ts +++ b/src/whatsapp/normalize.ts @@ -8,16 +8,22 @@ function stripWhatsAppTargetPrefixes(value: string): string { for (;;) { const before = candidate; candidate = candidate.replace(/^whatsapp:/i, "").trim(); - if (candidate === before) return candidate; + if (candidate === before) { + return candidate; + } } } export function isWhatsAppGroupJid(value: string): boolean { const candidate = stripWhatsAppTargetPrefixes(value); const lower = candidate.toLowerCase(); - if (!lower.endsWith("@g.us")) return false; + if (!lower.endsWith("@g.us")) { + return false; + } const localPart = candidate.slice(0, candidate.length - "@g.us".length); - if (!localPart || localPart.includes("@")) return false; + if (!localPart || localPart.includes("@")) { + return false; + } return /^[0-9]+(-[0-9]+)*$/.test(localPart); } @@ -36,15 +42,21 @@ export function isWhatsAppUserTarget(value: string): boolean { */ function extractUserJidPhone(jid: string): string | null { const userMatch = jid.match(WHATSAPP_USER_JID_RE); - if (userMatch) return userMatch[1]; + if (userMatch) { + return userMatch[1]; + } const lidMatch = jid.match(WHATSAPP_LID_RE); - if (lidMatch) return lidMatch[1]; + if (lidMatch) { + return lidMatch[1]; + } return null; } export function normalizeWhatsAppTarget(value: string): string | null { const candidate = stripWhatsAppTargetPrefixes(value); - if (!candidate) return null; + if (!candidate) { + return null; + } if (isWhatsAppGroupJid(candidate)) { const localPart = candidate.slice(0, candidate.length - "@g.us".length); return `${localPart}@g.us`; @@ -52,13 +64,17 @@ export function normalizeWhatsAppTarget(value: string): string | null { // Handle user JIDs (e.g. "41796666864:0@s.whatsapp.net") if (isWhatsAppUserTarget(candidate)) { const phone = extractUserJidPhone(candidate); - if (!phone) return null; + if (!phone) { + return null; + } const normalized = normalizeE164(phone); return normalized.length > 1 ? normalized : null; } // If the caller passed a JID-ish string that we don't understand, fail fast. // Otherwise normalizeE164 would happily treat "group:120@g.us" as a phone number. - if (candidate.includes("@")) return null; + if (candidate.includes("@")) { + return null; + } const normalized = normalizeE164(candidate); return normalized.length > 1 ? normalized : null; } diff --git a/src/wizard/clack-prompter.ts b/src/wizard/clack-prompter.ts index 4e1581f92..d04abf5b7 100644 --- a/src/wizard/clack-prompter.ts +++ b/src/wizard/clack-prompter.ts @@ -10,11 +10,11 @@ import { spinner, text, } from "@clack/prompts"; +import type { WizardProgress, WizardPrompter } from "./prompts.js"; import { createCliProgress } from "../cli/progress.js"; import { note as emitNote } from "../terminal/note.js"; import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js"; import { theme } from "../terminal/theme.js"; -import type { WizardProgress, WizardPrompter } from "./prompts.js"; import { WizardCancelledError } from "./prompts.js"; function guardCancel(value: T | symbol): T { @@ -22,7 +22,7 @@ function guardCancel(value: T | symbol): T { cancel(stylePromptTitle("Setup cancelled.") ?? "Setup cancelled."); throw new WizardCancelledError(); } - return value as T; + return value; } export function createClackPrompter(): WizardPrompter { @@ -58,15 +58,17 @@ export function createClackPrompter(): WizardPrompter { initialValues: params.initialValues, }), ), - text: async (params) => - guardCancel( + text: async (params) => { + const validate = params.validate; + return guardCancel( await text({ message: stylePromptMessage(params.message), initialValue: params.initialValue, placeholder: params.placeholder, - validate: params.validate, + validate: validate ? (value) => validate(value ?? "") : undefined, }), - ), + ); + }, confirm: async (params) => guardCancel( await confirm({ diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index 96a4a4bf6..70b7f8430 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -1,14 +1,22 @@ import fs from "node:fs/promises"; import path from "node:path"; - +import type { OnboardOptions } from "../commands/onboard-types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { GatewayWizardSettings, WizardFlow } from "./onboarding.types.js"; +import type { WizardPrompter } from "./prompts.js"; import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js"; +import { formatCliCommand } from "../cli/command-format.js"; +import { + buildGatewayInstallPlan, + gatewayInstallErrorHint, +} from "../commands/daemon-install-helpers.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, GATEWAY_DAEMON_RUNTIME_OPTIONS, - type GatewayDaemonRuntime, } from "../commands/daemon-runtime.js"; -import { healthCommand } from "../commands/health.js"; import { formatHealthCheckFailure } from "../commands/health-format.js"; +import { healthCommand } from "../commands/health.js"; import { detectBrowserOpenSupport, formatControlUiSshHint, @@ -18,27 +26,17 @@ import { waitForGatewayReachable, resolveControlUiLinks, } from "../commands/onboard-helpers.js"; -import { formatCliCommand } from "../cli/command-format.js"; -import type { OnboardOptions } from "../commands/onboard-types.js"; -import type { MoltbotConfig } from "../config/config.js"; import { resolveGatewayService } from "../daemon/service.js"; import { isSystemdUserServiceAvailable } from "../daemon/systemd.js"; import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js"; -import type { RuntimeEnv } from "../runtime.js"; import { runTui } from "../tui/tui.js"; import { resolveUserPath } from "../utils.js"; -import { - buildGatewayInstallPlan, - gatewayInstallErrorHint, -} from "../commands/daemon-install-helpers.js"; -import type { GatewayWizardSettings, WizardFlow } from "./onboarding.types.js"; -import type { WizardPrompter } from "./prompts.js"; type FinalizeOnboardingOptions = { flow: WizardFlow; opts: OnboardOptions; - baseConfig: MoltbotConfig; - nextConfig: MoltbotConfig; + baseConfig: OpenClawConfig; + nextConfig: OpenClawConfig; workspaceDir: string; settings: GatewayWizardSettings; prompter: WizardPrompter; @@ -111,12 +109,12 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption if (installDaemon) { const daemonRuntime = flow === "quickstart" - ? (DEFAULT_GATEWAY_DAEMON_RUNTIME as GatewayDaemonRuntime) - : ((await prompter.select({ + ? DEFAULT_GATEWAY_DAEMON_RUNTIME + : await prompter.select({ message: "Gateway service runtime", options: GATEWAY_DAEMON_RUNTIME_OPTIONS, initialValue: opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME, - })) as GatewayDaemonRuntime); + }); if (flow === "quickstart") { await prompter.note( "QuickStart uses Node for the Gateway service (stable + supported).", @@ -126,14 +124,14 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption const service = resolveGatewayService(); const loaded = await service.isLoaded({ env: process.env }); if (loaded) { - const action = (await prompter.select({ + const action = await prompter.select({ message: "Gateway service already installed", options: [ { value: "restart", label: "Restart" }, { value: "reinstall", label: "Reinstall" }, { value: "skip", label: "Skip" }, ], - })) as "restart" | "reinstall" | "skip"; + }); if (action === "restart") { await withWizardProgress( "Gateway service", @@ -158,7 +156,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } } - if (!loaded || (loaded && (await service.isLoaded({ env: process.env })) === false)) { + if (!loaded || (loaded && !(await service.isLoaded({ env: process.env })))) { const progress = prompter.progress("Gateway service"); let installError: string | null = null; try { @@ -214,8 +212,8 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption await prompter.note( [ "Docs:", - "https://docs.molt.bot/gateway/health", - "https://docs.molt.bot/gateway/troubleshooting", + "https://docs.openclaw.ai/gateway/health", + "https://docs.openclaw.ai/gateway/troubleshooting", ].join("\n"), "Health check help", ); @@ -277,7 +275,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption tokenParam ? `Web UI (with token): ${authedUrl}` : undefined, `Gateway WS: ${links.wsUrl}`, gatewayStatusLine, - "Docs: https://docs.molt.bot/web/control-ui", + "Docs: https://docs.openclaw.ai/web/control-ui", ] .filter(Boolean) .join("\n"), @@ -305,14 +303,14 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption await prompter.note( [ "Gateway token: shared auth for the Gateway + Control UI.", - "Stored in: ~/.clawdbot/moltbot.json (gateway.auth.token) or CLAWDBOT_GATEWAY_TOKEN.", - "Web UI stores a copy in this browser's localStorage (moltbot.control.settings.v1).", - `Get the tokenized link anytime: ${formatCliCommand("moltbot dashboard --no-open")}`, + "Stored in: ~/.openclaw/openclaw.json (gateway.auth.token) or OPENCLAW_GATEWAY_TOKEN.", + "Web UI stores a copy in this browser's localStorage (openclaw.control.settings.v1).", + `Get the tokenized link anytime: ${formatCliCommand("openclaw dashboard --no-open")}`, ].join("\n"), "Token", ); - hatchChoice = (await prompter.select({ + hatchChoice = await prompter.select({ message: "How do you want to hatch your bot?", options: [ { value: "tui", label: "Hatch in TUI (recommended)" }, @@ -320,7 +318,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption { value: "later", label: "Do this later" }, ], initialValue: "tui", - })) as "tui" | "web" | "later"; + }); if (hatchChoice === "tui") { await runTui({ @@ -337,7 +335,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption if (seededInBackground) { await prompter.note( `Web UI seeded in the background. Open later with: ${formatCliCommand( - "moltbot dashboard --no-open", + "openclaw dashboard --no-open", )}`, "Web UI", ); @@ -364,8 +362,8 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption [ `Dashboard link (with token): ${authedUrl}`, controlUiOpened - ? "Opened in your browser. Keep that tab to control Moltbot." - : "Copy/paste this URL in a browser on this machine to control Moltbot.", + ? "Opened in your browser. Keep that tab to control OpenClaw." + : "Copy/paste this URL in a browser on this machine to control OpenClaw.", controlUiOpenHint, ] .filter(Boolean) @@ -374,7 +372,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption ); } else { await prompter.note( - `When you're ready: ${formatCliCommand("moltbot dashboard --no-open")}`, + `When you're ready: ${formatCliCommand("openclaw dashboard --no-open")}`, "Later", ); } @@ -383,14 +381,15 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } await prompter.note( - ["Back up your agent workspace.", "Docs: https://docs.molt.bot/concepts/agent-workspace"].join( - "\n", - ), + [ + "Back up your agent workspace.", + "Docs: https://docs.openclaw.ai/concepts/agent-workspace", + ].join("\n"), "Workspace backup", ); await prompter.note( - "Running agents on your computer is risky — harden your setup: https://docs.molt.bot/security", + "Running agents on your computer is risky — harden your setup: https://docs.openclaw.ai/security", "Security", ); @@ -422,8 +421,8 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption [ `Dashboard link (with token): ${authedUrl}`, controlUiOpened - ? "Opened in your browser. Keep that tab to control Moltbot." - : "Copy/paste this URL in a browser on this machine to control Moltbot.", + ? "Opened in your browser. Keep that tab to control OpenClaw." + : "Copy/paste this URL in a browser on this machine to control OpenClaw.", controlUiOpenHint, ] .filter(Boolean) @@ -443,33 +442,33 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption webSearchKey ? "API key: stored in config (tools.web.search.apiKey)." : "API key: provided via BRAVE_API_KEY env var (Gateway environment).", - "Docs: https://docs.molt.bot/tools/web", + "Docs: https://docs.openclaw.ai/tools/web", ].join("\n") : [ "If you want your agent to be able to search the web, you’ll need an API key.", "", - "Moltbot uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search won’t work.", + "OpenClaw uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search won’t work.", "", "Set it up interactively:", - `- Run: ${formatCliCommand("moltbot configure --section web")}`, + `- Run: ${formatCliCommand("openclaw configure --section web")}`, "- Enable web_search and paste your Brave Search API key", "", "Alternative: set BRAVE_API_KEY in the Gateway environment (no config changes).", - "Docs: https://docs.molt.bot/tools/web", + "Docs: https://docs.openclaw.ai/tools/web", ].join("\n"), "Web search (optional)", ); await prompter.note( - 'What now: https://molt.bot/showcase ("What People Are Building").', + 'What now: https://openclaw.ai/showcase ("What People Are Building").', "What now", ); await prompter.outro( controlUiOpened - ? "Onboarding complete. Dashboard opened with your token; keep that tab to control Moltbot." + ? "Onboarding complete. Dashboard opened with your token; keep that tab to control OpenClaw." : seededInBackground ? "Onboarding complete. Web UI seeded in the background; open it anytime with the tokenized link above." - : "Onboarding complete. Use the tokenized dashboard link above to control Moltbot.", + : "Onboarding complete. Use the tokenized dashboard link above to control OpenClaw.", ); } diff --git a/src/wizard/onboarding.gateway-config.test.ts b/src/wizard/onboarding.gateway-config.test.ts new file mode 100644 index 000000000..42a474f97 --- /dev/null +++ b/src/wizard/onboarding.gateway-config.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it, vi } from "vitest"; +import type { RuntimeEnv } from "../runtime.js"; +import type { WizardPrompter } from "./prompts.js"; + +const mocks = vi.hoisted(() => ({ + randomToken: vi.fn(), +})); + +vi.mock("../commands/onboard-helpers.js", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + randomToken: mocks.randomToken, + }; +}); + +vi.mock("../infra/tailscale.js", () => ({ + findTailscaleBinary: vi.fn(async () => undefined), +})); + +import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; + +describe("configureGatewayForOnboarding", () => { + it("generates a token when the prompt returns undefined", async () => { + mocks.randomToken.mockReturnValue("generated-token"); + + const selectQueue = ["loopback", "token", "off"]; + const textQueue = ["18789", undefined]; + const prompter: WizardPrompter = { + intro: vi.fn(async () => {}), + outro: vi.fn(async () => {}), + note: vi.fn(async () => {}), + select: vi.fn(async () => selectQueue.shift() as string), + multiselect: vi.fn(async () => []), + text: vi.fn(async () => textQueue.shift() as string), + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })), + }; + + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + const result = await configureGatewayForOnboarding({ + flow: "advanced", + baseConfig: {}, + nextConfig: {}, + localPort: 18789, + quickstartGateway: { + hasExisting: false, + port: 18789, + bind: "loopback", + authMode: "token", + tailscaleMode: "off", + token: undefined, + password: undefined, + customBindHost: undefined, + tailscaleResetOnExit: false, + }, + prompter, + runtime, + }); + + expect(result.settings.gatewayToken).toBe("generated-token"); + }); +}); diff --git a/src/wizard/onboarding.gateway-config.ts b/src/wizard/onboarding.gateway-config.ts index 1a097f42e..16f80135c 100644 --- a/src/wizard/onboarding.gateway-config.ts +++ b/src/wizard/onboarding.gateway-config.ts @@ -1,7 +1,5 @@ -import { randomToken } from "../commands/onboard-helpers.js"; import type { GatewayAuthChoice } from "../commands/onboard-types.js"; -import type { MoltbotConfig } from "../config/config.js"; -import { findTailscaleBinary } from "../infra/tailscale.js"; +import type { GatewayBindMode, GatewayTailscaleMode, OpenClawConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import type { GatewayWizardSettings, @@ -9,11 +7,13 @@ import type { WizardFlow, } from "./onboarding.types.js"; import type { WizardPrompter } from "./prompts.js"; +import { normalizeGatewayTokenInput, randomToken } from "../commands/onboard-helpers.js"; +import { findTailscaleBinary } from "../infra/tailscale.js"; type ConfigureGatewayOptions = { flow: WizardFlow; - baseConfig: MoltbotConfig; - nextConfig: MoltbotConfig; + baseConfig: OpenClawConfig; + nextConfig: OpenClawConfig; localPort: number; quickstartGateway: QuickstartGatewayDefaults; prompter: WizardPrompter; @@ -21,7 +21,7 @@ type ConfigureGatewayOptions = { }; type ConfigureGatewayResult = { - nextConfig: MoltbotConfig; + nextConfig: OpenClawConfig; settings: GatewayWizardSettings; }; @@ -45,10 +45,10 @@ export async function configureGatewayForOnboarding( 10, ); - let bind = ( + let bind: GatewayWizardSettings["bind"] = flow === "quickstart" ? quickstartGateway.bind - : ((await prompter.select({ + : await prompter.select({ message: "Gateway bind", options: [ { value: "loopback", label: "Loopback (127.0.0.1)" }, @@ -57,8 +57,7 @@ export async function configureGatewayForOnboarding( { value: "auto", label: "Auto (Loopback → LAN)" }, { value: "custom", label: "Custom IP" }, ], - })) as "loopback" | "lan" | "auto" | "custom" | "tailnet") - ) as "loopback" | "lan" | "auto" | "custom" | "tailnet"; + }); let customBindHost = quickstartGateway.customBindHost; if (bind === "custom") { @@ -69,17 +68,22 @@ export async function configureGatewayForOnboarding( placeholder: "192.168.1.100", initialValue: customBindHost ?? "", validate: (value) => { - if (!value) return "IP address is required for custom bind mode"; + if (!value) { + return "IP address is required for custom bind mode"; + } const trimmed = value.trim(); const parts = trimmed.split("."); - if (parts.length !== 4) return "Invalid IPv4 address (e.g., 192.168.1.100)"; + if (parts.length !== 4) { + return "Invalid IPv4 address (e.g., 192.168.1.100)"; + } if ( parts.every((part) => { const n = parseInt(part, 10); return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n); }) - ) + ) { return undefined; + } return "Invalid IPv4 address (each octet must be 0-255)"; }, }); @@ -87,7 +91,7 @@ export async function configureGatewayForOnboarding( } } - let authMode = ( + let authMode = flow === "quickstart" ? quickstartGateway.authMode : ((await prompter.select({ @@ -101,13 +105,12 @@ export async function configureGatewayForOnboarding( { value: "password", label: "Password" }, ], initialValue: "token", - })) as GatewayAuthChoice) - ) as GatewayAuthChoice; + })) as GatewayAuthChoice); - const tailscaleMode = ( + const tailscaleMode: GatewayWizardSettings["tailscaleMode"] = flow === "quickstart" ? quickstartGateway.tailscaleMode - : ((await prompter.select({ + : await prompter.select({ message: "Tailscale exposure", options: [ { value: "off", label: "Off", hint: "No Tailscale exposure" }, @@ -122,8 +125,7 @@ export async function configureGatewayForOnboarding( hint: "Public HTTPS via Tailscale Funnel (internet)", }, ], - })) as "off" | "serve" | "funnel") - ) as "off" | "serve" | "funnel"; + }); // Detect Tailscale binary before proceeding with serve/funnel setup. if (tailscaleMode !== "off") { @@ -145,7 +147,9 @@ export async function configureGatewayForOnboarding( let tailscaleResetOnExit = flow === "quickstart" ? quickstartGateway.tailscaleResetOnExit : false; if (tailscaleMode !== "off" && flow !== "quickstart") { await prompter.note( - ["Docs:", "https://docs.molt.bot/gateway/tailscale", "https://docs.molt.bot/web"].join("\n"), + ["Docs:", "https://docs.openclaw.ai/gateway/tailscale", "https://docs.openclaw.ai/web"].join( + "\n", + ), "Tailscale", ); tailscaleResetOnExit = Boolean( @@ -180,7 +184,7 @@ export async function configureGatewayForOnboarding( placeholder: "Needed for multi-machine or non-loopback access", initialValue: quickstartGateway.token ?? "", }); - gatewayToken = String(tokenInput).trim() || randomToken(); + gatewayToken = normalizeGatewayTokenInput(tokenInput) || randomToken(); } } @@ -222,11 +226,11 @@ export async function configureGatewayForOnboarding( gateway: { ...nextConfig.gateway, port, - bind, + bind: bind as GatewayBindMode, ...(bind === "custom" && customBindHost ? { customBindHost } : {}), tailscale: { ...nextConfig.gateway?.tailscale, - mode: tailscaleMode, + mode: tailscaleMode as GatewayTailscaleMode, resetOnExit: tailscaleResetOnExit, }, }, @@ -236,11 +240,11 @@ export async function configureGatewayForOnboarding( nextConfig, settings: { port, - bind, + bind: bind as GatewayBindMode, customBindHost: bind === "custom" ? customBindHost : undefined, authMode, gatewayToken, - tailscaleMode, + tailscaleMode: tailscaleMode as GatewayTailscaleMode, tailscaleResetOnExit, }, }; diff --git a/src/wizard/onboarding.test.ts b/src/wizard/onboarding.test.ts index df832391e..937f7b33c 100644 --- a/src/wizard/onboarding.test.ts +++ b/src/wizard/onboarding.test.ts @@ -2,11 +2,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; - -import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js"; import type { RuntimeEnv } from "../runtime.js"; -import { runOnboardingWizard } from "./onboarding.js"; import type { WizardPrompter } from "./prompts.js"; +import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js"; +import { runOnboardingWizard } from "./onboarding.js"; const setupChannels = vi.hoisted(() => vi.fn(async (cfg) => cfg)); const setupSkills = vi.hoisted(() => vi.fn(async (cfg) => cfg)); @@ -77,7 +76,7 @@ vi.mock("../tui/tui.js", () => ({ describe("runOnboardingWizard", () => { it("exits when config is invalid", async () => { readConfigFileSnapshot.mockResolvedValueOnce({ - path: "/tmp/.clawdbot/moltbot.json", + path: "/tmp/.openclaw/openclaw.json", exists: true, raw: "{}", parsed: {}, @@ -174,11 +173,13 @@ describe("runOnboardingWizard", () => { it("launches TUI without auto-delivery when hatching", async () => { runTui.mockClear(); - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-onboard-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-")); await fs.writeFile(path.join(workspaceDir, DEFAULT_BOOTSTRAP_FILENAME), "{}"); const select: WizardPrompter["select"] = vi.fn(async (opts) => { - if (opts.message === "How do you want to hatch your bot?") return "tui"; + if (opts.message === "How do you want to hatch your bot?") { + return "tui"; + } return "quickstart"; }); @@ -230,10 +231,12 @@ describe("runOnboardingWizard", () => { it("offers TUI hatch even without BOOTSTRAP.md", async () => { runTui.mockClear(); - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-onboard-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-")); const select: WizardPrompter["select"] = vi.fn(async (opts) => { - if (opts.message === "How do you want to hatch your bot?") return "tui"; + if (opts.message === "How do you want to hatch your bot?") { + return "tui"; + } return "quickstart"; }); diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 75543ca19..de1c6c36f 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -1,11 +1,22 @@ +import type { + GatewayAuthChoice, + OnboardMode, + OnboardOptions, + ResetScope, +} from "../commands/onboard-types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; import { ensureAuthProfileStore } from "../agents/auth-profiles.js"; import { listChannelPlugins } from "../channels/plugins/index.js"; +import { formatCliCommand } from "../cli/command-format.js"; +import { installCompletion } from "../cli/completion-cli.js"; +import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff, } from "../commands/auth-choice.js"; -import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.js"; import { applyPrimaryModel, promptDefaultModel } from "../commands/model-picker.js"; import { setupChannels } from "../commands/onboard-channels.js"; import { @@ -17,17 +28,9 @@ import { probeGatewayReachable, summarizeExistingConfig, } from "../commands/onboard-helpers.js"; +import { setupInternalHooks } from "../commands/onboard-hooks.js"; import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js"; import { setupSkills } from "../commands/onboard-skills.js"; -import { setupInternalHooks } from "../commands/onboard-hooks.js"; -import type { - GatewayAuthChoice, - OnboardMode, - OnboardOptions, - ResetScope, -} from "../commands/onboard-types.js"; -import { formatCliCommand } from "../cli/command-format.js"; -import type { MoltbotConfig } from "../config/config.js"; import { DEFAULT_GATEWAY_PORT, readConfigFileSnapshot, @@ -35,29 +38,29 @@ import { writeConfigFile, } from "../config/config.js"; import { logConfigUpdated } from "../config/logging.js"; -import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { resolveUserPath } from "../utils.js"; import { finalizeOnboardingWizard } from "./onboarding.finalize.js"; import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; -import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; import { WizardCancelledError, type WizardPrompter } from "./prompts.js"; async function requireRiskAcknowledgement(params: { opts: OnboardOptions; prompter: WizardPrompter; }) { - if (params.opts.acceptRisk === true) return; + if (params.opts.acceptRisk === true) { + return; + } await params.prompter.note( [ "Security warning — please read.", "", - "Moltbot is a hobby project and still in beta. Expect sharp edges.", + "OpenClaw is a hobby project and still in beta. Expect sharp edges.", "This bot can read files and run actions if tools are enabled.", "A bad prompt can trick it into doing unsafe things.", "", - "If you’re not comfortable with basic security and access control, don’t run Moltbot.", + "If you’re not comfortable with basic security and access control, don’t run OpenClaw.", "Ask someone experienced to help before enabling tools or exposing it to the internet.", "", "Recommended baseline:", @@ -67,10 +70,10 @@ async function requireRiskAcknowledgement(params: { "- Use the strongest available model for any bot with tools or untrusted inboxes.", "", "Run regularly:", - "moltbot security audit --deep", - "moltbot security audit --fix", + "openclaw security audit --deep", + "openclaw security audit --fix", "", - "Must read: https://docs.molt.bot/gateway/security", + "Must read: https://docs.openclaw.ai/gateway/security", ].join("\n"), "Security", ); @@ -90,11 +93,11 @@ export async function runOnboardingWizard( prompter: WizardPrompter, ) { printWizardHeader(runtime); - await prompter.intro("Moltbot onboarding"); + await prompter.intro("OpenClaw onboarding"); await requireRiskAcknowledgement({ opts, prompter }); const snapshot = await readConfigFileSnapshot(); - let baseConfig: MoltbotConfig = snapshot.valid ? snapshot.config : {}; + let baseConfig: OpenClawConfig = snapshot.valid ? snapshot.config : {}; if (snapshot.exists && !snapshot.valid) { await prompter.note(summarizeExistingConfig(baseConfig), "Invalid config"); @@ -103,19 +106,19 @@ export async function runOnboardingWizard( [ ...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`), "", - "Docs: https://docs.molt.bot/gateway/configuration", + "Docs: https://docs.openclaw.ai/gateway/configuration", ].join("\n"), "Config issues", ); } await prompter.outro( - `Config invalid. Run \`${formatCliCommand("moltbot doctor")}\` to repair it, then re-run onboarding.`, + `Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run onboarding.`, ); runtime.exit(1); return; } - const quickstartHint = `Configure details later via ${formatCliCommand("moltbot configure")}.`; + const quickstartHint = `Configure details later via ${formatCliCommand("openclaw configure")}.`; const manualHint = "Configure port, network, Tailscale, and auth options."; const explicitFlowRaw = opts.flow?.trim(); const normalizedExplicitFlow = explicitFlowRaw === "manual" ? "advanced" : explicitFlowRaw; @@ -134,14 +137,14 @@ export async function runOnboardingWizard( : undefined; let flow: WizardFlow = explicitFlow ?? - ((await prompter.select({ + (await prompter.select({ message: "Onboarding mode", options: [ { value: "quickstart", label: "QuickStart", hint: quickstartHint }, { value: "advanced", label: "Manual", hint: manualHint }, ], initialValue: "quickstart", - })) as "quickstart" | "advanced"); + })); if (opts.mode === "remote" && flow === "quickstart") { await prompter.note( @@ -154,14 +157,14 @@ export async function runOnboardingWizard( if (snapshot.exists) { await prompter.note(summarizeExistingConfig(baseConfig), "Existing config detected"); - const action = (await prompter.select({ + const action = await prompter.select({ message: "Config handling", options: [ { value: "keep", label: "Use existing values" }, { value: "modify", label: "Update values" }, { value: "reset", label: "Reset" }, ], - })) as "keep" | "modify" | "reset"; + }); if (action === "reset") { const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; @@ -237,19 +240,33 @@ export async function runOnboardingWizard( if (flow === "quickstart") { const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => { - if (value === "loopback") return "Loopback (127.0.0.1)"; - if (value === "lan") return "LAN"; - if (value === "custom") return "Custom IP"; - if (value === "tailnet") return "Tailnet (Tailscale IP)"; + if (value === "loopback") { + return "Loopback (127.0.0.1)"; + } + if (value === "lan") { + return "LAN"; + } + if (value === "custom") { + return "Custom IP"; + } + if (value === "tailnet") { + return "Tailnet (Tailscale IP)"; + } return "Auto"; }; const formatAuth = (value: GatewayAuthChoice) => { - if (value === "token") return "Token (default)"; + if (value === "token") { + return "Token (default)"; + } return "Password"; }; const formatTailscale = (value: "off" | "serve" | "funnel") => { - if (value === "off") return "Off"; - if (value === "serve") return "Serve"; + if (value === "off") { + return "Off"; + } + if (value === "serve") { + return "Serve"; + } return "Funnel"; }; const quickstartLines = quickstartGateway.hasExisting @@ -278,8 +295,8 @@ export async function runOnboardingWizard( const localUrl = `ws://127.0.0.1:${localPort}`; const localProbe = await probeGatewayReachable({ url: localUrl, - token: baseConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, - password: baseConfig.gateway?.auth?.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD, + token: baseConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN, + password: baseConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD, }); const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? ""; const remoteProbe = remoteUrl @@ -335,7 +352,7 @@ export async function runOnboardingWizard( const workspaceDir = resolveUserPath(workspaceInput.trim() || DEFAULT_WORKSPACE); - let nextConfig: MoltbotConfig = { + let nextConfig: OpenClawConfig = { ...baseConfig, agents: { ...baseConfig.agents, @@ -448,4 +465,16 @@ export async function runOnboardingWizard( prompter, runtime, }); + + const installShell = await prompter.confirm({ + message: "Install shell completion script?", + initialValue: true, + }); + + if (installShell) { + const shell = process.env.SHELL?.split("/").pop() || "zsh"; + // We pass 'yes=true' to skip any double-confirmation inside the helper, + // as the wizard prompt above serves as confirmation. + await installCompletion(shell, true); + } } diff --git a/src/wizard/session.test.ts b/src/wizard/session.test.ts index a2c92c732..bc187f1bf 100644 --- a/src/wizard/session.test.ts +++ b/src/wizard/session.test.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from "vitest"; - import { WizardSession } from "./session.js"; function noteRunner() { @@ -21,20 +20,26 @@ describe("WizardSession", () => { const secondPeek = await session.next(); expect(secondPeek.step?.id).toBe(first.step?.id); - if (!first.step) throw new Error("expected first step"); + if (!first.step) { + throw new Error("expected first step"); + } await session.answer(first.step.id, null); const second = await session.next(); expect(second.done).toBe(false); expect(second.step?.type).toBe("text"); - if (!second.step) throw new Error("expected second step"); + if (!second.step) { + throw new Error("expected second step"); + } await session.answer(second.step.id, "Peter"); const third = await session.next(); expect(third.step?.type).toBe("note"); - if (!third.step) throw new Error("expected third step"); + if (!third.step) { + throw new Error("expected third step"); + } await session.answer(third.step.id, null); const done = await session.next(); @@ -46,7 +51,9 @@ describe("WizardSession", () => { const session = noteRunner(); const first = await session.next(); await expect(session.answer("bad-id", null)).rejects.toThrow(/wizard: no pending step/i); - if (!first.step) throw new Error("expected first step"); + if (!first.step) { + throw new Error("expected first step"); + } await session.answer(first.step.id, null); }); diff --git a/src/wizard/session.ts b/src/wizard/session.ts index 358907668..5c4c76041 100644 --- a/src/wizard/session.ts +++ b/src/wizard/session.ts @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto"; - import { WizardCancelledError, type WizardProgress, type WizardPrompter } from "./prompts.js"; export type WizardStepOption = { @@ -201,7 +200,9 @@ export class WizardSession { } cancel() { - if (this.status !== "running") return; + if (this.status !== "running") { + return; + } this.status = "cancelled"; this.error = "cancelled"; this.currentStep = null; @@ -245,7 +246,9 @@ export class WizardSession { } private resolveStep(step: WizardStep | null) { - if (!this.stepDeferred) return; + if (!this.stepDeferred) { + return; + } const deferred = this.stepDeferred; this.stepDeferred = null; deferred.resolve(step); diff --git a/test/auto-reply.retry.test.ts b/test/auto-reply.retry.test.ts index 9a7a7b623..b3a773b28 100644 --- a/test/auto-reply.retry.test.ts +++ b/test/auto-reply.retry.test.ts @@ -9,9 +9,9 @@ vi.mock("../src/web/media.js", () => ({ })), })); +import type { WebInboundMessage } from "../src/web/inbound.js"; import { defaultRuntime } from "../src/runtime.js"; import { deliverWebReply } from "../src/web/auto-reply.js"; -import type { WebInboundMessage } from "../src/web/inbound.js"; const noopLogger = { info: vi.fn(), diff --git a/test/fixtures/child-process-bridge/child.js b/test/fixtures/child-process-bridge/child.js index 3c24f2172..9ef083e42 100644 --- a/test/fixtures/child-process-bridge/child.js +++ b/test/fixtures/child-process-bridge/child.js @@ -7,7 +7,9 @@ const server = http.createServer((_, res) => { server.listen(0, "127.0.0.1", () => { const addr = server.address(); - if (!addr || typeof addr === "string") throw new Error("unexpected address"); + if (!addr || typeof addr === "string") { + throw new Error("unexpected address"); + } process.stdout.write(`${addr.port}\n`); }); diff --git a/test/gateway.multi.e2e.test.ts b/test/gateway.multi.e2e.test.ts index 3885dfc02..7bbe7ecc3 100644 --- a/test/gateway.multi.e2e.test.ts +++ b/test/gateway.multi.e2e.test.ts @@ -6,8 +6,8 @@ import net from "node:net"; import os from "node:os"; import path from "node:path"; import { afterAll, describe, expect, it } from "vitest"; -import { loadOrCreateDeviceIdentity } from "../src/infra/device-identity.js"; import { GatewayClient } from "../src/gateway/client.js"; +import { loadOrCreateDeviceIdentity } from "../src/infra/device-identity.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js"; type GatewayInstance = { @@ -95,10 +95,10 @@ const spawnGatewayInstance = async (name: string): Promise => { const port = await getFreePort(); const hookToken = `token-${name}-${randomUUID()}`; const gatewayToken = `gateway-${name}-${randomUUID()}`; - const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), `moltbot-e2e-${name}-`)); - const configDir = path.join(homeDir, ".clawdbot"); + const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), `openclaw-e2e-${name}-`)); + const configDir = path.join(homeDir, ".openclaw"); await fs.mkdir(configDir, { recursive: true }); - const configPath = path.join(configDir, "moltbot.json"); + const configPath = path.join(configDir, "openclaw.json"); const stateDir = path.join(configDir, "state"); const config = { gateway: { port, auth: { mode: "token", token: gatewayToken } }, @@ -127,13 +127,13 @@ const spawnGatewayInstance = async (name: string): Promise => { env: { ...process.env, HOME: homeDir, - CLAWDBOT_CONFIG_PATH: configPath, - CLAWDBOT_STATE_DIR: stateDir, - CLAWDBOT_GATEWAY_TOKEN: "", - CLAWDBOT_GATEWAY_PASSWORD: "", - CLAWDBOT_SKIP_CHANNELS: "1", - CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1", - CLAWDBOT_SKIP_CANVAS_HOST: "1", + OPENCLAW_CONFIG_PATH: configPath, + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_GATEWAY_TOKEN: "", + OPENCLAW_GATEWAY_PASSWORD: "", + OPENCLAW_SKIP_CHANNELS: "1", + OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1", + OPENCLAW_SKIP_CANVAS_HOST: "1", }, stdio: ["ignore", "pipe", "pipe"], }, @@ -181,7 +181,9 @@ const stopGatewayInstance = async (inst: GatewayInstance) => { } const exited = await Promise.race([ new Promise((resolve) => { - if (inst.child.exitCode !== null) return resolve(true); + if (inst.child.exitCode !== null) { + return resolve(true); + } inst.child.once("exit", () => resolve(true)); }), sleep(5_000).then(() => false), @@ -225,6 +227,7 @@ const runCliJson = async (args: string[], env: NodeJS.ProcessEnv): Promise { - if (settled) return; + if (settled) { + return; + } settled = true; resolveReady?.(); }, onConnectError: (err) => { - if (settled) return; + if (settled) { + return; + } settled = true; rejectReady?.(err); }, onClose: (code, reason) => { - if (settled) return; + if (settled) { + return; + } settled = true; rejectReady?.(new Error(`gateway closed (${code}): ${reason}`)); }, @@ -335,12 +344,14 @@ const waitForNodeStatus = async (inst: GatewayInstance, nodeId: string, timeoutM const list = (await runCliJson( ["nodes", "status", "--json", "--url", `ws://127.0.0.1:${inst.port}`], { - CLAWDBOT_GATEWAY_TOKEN: inst.gatewayToken, - CLAWDBOT_GATEWAY_PASSWORD: "", + OPENCLAW_GATEWAY_TOKEN: inst.gatewayToken, + OPENCLAW_GATEWAY_PASSWORD: "", }, )) as NodeListPayload; const match = list.nodes?.find((n) => n.nodeId === nodeId); - if (match?.connected && match?.paired) return; + if (match?.connected && match?.paired) { + return; + } await sleep(50); } throw new Error(`timeout waiting for node status for ${nodeId}`); @@ -370,14 +381,14 @@ describe("gateway multi-instance e2e", () => { const [healthA, healthB] = (await Promise.all([ runCliJson(["health", "--json", "--timeout", "10000"], { - CLAWDBOT_GATEWAY_PORT: String(gwA.port), - CLAWDBOT_GATEWAY_TOKEN: gwA.gatewayToken, - CLAWDBOT_GATEWAY_PASSWORD: "", + OPENCLAW_GATEWAY_PORT: String(gwA.port), + OPENCLAW_GATEWAY_TOKEN: gwA.gatewayToken, + OPENCLAW_GATEWAY_PASSWORD: "", }), runCliJson(["health", "--json", "--timeout", "10000"], { - CLAWDBOT_GATEWAY_PORT: String(gwB.port), - CLAWDBOT_GATEWAY_TOKEN: gwB.gatewayToken, - CLAWDBOT_GATEWAY_PASSWORD: "", + OPENCLAW_GATEWAY_PORT: String(gwB.port), + OPENCLAW_GATEWAY_TOKEN: gwB.gatewayToken, + OPENCLAW_GATEWAY_PASSWORD: "", }), ])) as [HealthPayload, HealthPayload]; expect(healthA.ok).toBe(true); diff --git a/test/helpers/envelope-timestamp.ts b/test/helpers/envelope-timestamp.ts index 934608204..1bd7ad17c 100644 --- a/test/helpers/envelope-timestamp.ts +++ b/test/helpers/envelope-timestamp.ts @@ -28,7 +28,7 @@ function formatZonedTimestamp(date: Date, timeZone?: string): string { const hh = pick("hour"); const min = pick("minute"); const tz = [...parts] - .reverse() + .toReversed() .find((part) => part.type === "timeZoneName") ?.value?.trim(); @@ -41,8 +41,12 @@ function formatZonedTimestamp(date: Date, timeZone?: string): string { export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone = "utc"): string { const normalized = zone.trim().toLowerCase(); - if (normalized === "utc" || normalized === "gmt") return formatUtcTimestamp(date); - if (normalized === "local" || normalized === "host") return formatZonedTimestamp(date); + if (normalized === "utc" || normalized === "gmt") { + return formatUtcTimestamp(date); + } + if (normalized === "local" || normalized === "host") { + return formatZonedTimestamp(date); + } return formatZonedTimestamp(date, zone); } diff --git a/test/helpers/inbound-contract.ts b/test/helpers/inbound-contract.ts index 3e969057f..4ac4c2cc5 100644 --- a/test/helpers/inbound-contract.ts +++ b/test/helpers/inbound-contract.ts @@ -1,9 +1,8 @@ import { expect } from "vitest"; - +import type { MsgContext } from "../../src/auto-reply/templating.js"; import { normalizeChatType } from "../../src/channels/chat-type.js"; import { resolveConversationLabel } from "../../src/channels/conversation-label.js"; import { validateSenderIdentity } from "../../src/channels/sender-identity.js"; -import type { MsgContext } from "../../src/auto-reply/templating.js"; export function expectInboundContextContract(ctx: MsgContext) { expect(validateSenderIdentity(ctx)).toEqual([]); diff --git a/test/helpers/normalize-text.ts b/test/helpers/normalize-text.ts index 8f0d13a98..d81be0106 100644 --- a/test/helpers/normalize-text.ts +++ b/test/helpers/normalize-text.ts @@ -8,15 +8,21 @@ function stripAnsi(input: string): string { } const next = input[i + 1]; - if (next !== "[") continue; + if (next !== "[") { + continue; + } i += 1; while (i + 1 < input.length) { i += 1; const c = input[i]; - if (!c) break; + if (!c) { + break; + } const isLetter = (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c === "~"; - if (isLetter) break; + if (isLetter) { + break; + } } } return out; diff --git a/test/helpers/poll.ts b/test/helpers/poll.ts index 3aed881e8..0b1a212e9 100644 --- a/test/helpers/poll.ts +++ b/test/helpers/poll.ts @@ -17,7 +17,9 @@ export async function pollUntil( while (Date.now() - start < timeoutMs) { const value = await fn(); - if (value !== null && value !== undefined) return value; + if (value !== null && value !== undefined) { + return value; + } await sleep(intervalMs); } diff --git a/test/helpers/temp-home.ts b/test/helpers/temp-home.ts index b33716d63..30af94ca1 100644 --- a/test/helpers/temp-home.ts +++ b/test/helpers/temp-home.ts @@ -18,43 +18,55 @@ function snapshotEnv(): EnvSnapshot { userProfile: process.env.USERPROFILE, homeDrive: process.env.HOMEDRIVE, homePath: process.env.HOMEPATH, - stateDir: process.env.CLAWDBOT_STATE_DIR, + stateDir: process.env.OPENCLAW_STATE_DIR, }; } function restoreEnv(snapshot: EnvSnapshot) { const restoreKey = (key: string, value: string | undefined) => { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } }; restoreKey("HOME", snapshot.home); restoreKey("USERPROFILE", snapshot.userProfile); restoreKey("HOMEDRIVE", snapshot.homeDrive); restoreKey("HOMEPATH", snapshot.homePath); - restoreKey("CLAWDBOT_STATE_DIR", snapshot.stateDir); + restoreKey("OPENCLAW_STATE_DIR", snapshot.stateDir); } function snapshotExtraEnv(keys: string[]): Record { const snapshot: Record = {}; - for (const key of keys) snapshot[key] = process.env[key]; + for (const key of keys) { + snapshot[key] = process.env[key]; + } return snapshot; } function restoreExtraEnv(snapshot: Record) { for (const [key, value] of Object.entries(snapshot)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } } } function setTempHome(base: string) { process.env.HOME = base; process.env.USERPROFILE = base; - process.env.CLAWDBOT_STATE_DIR = path.join(base, ".clawdbot"); + process.env.OPENCLAW_STATE_DIR = path.join(base, ".openclaw"); - if (process.platform !== "win32") return; + if (process.platform !== "win32") { + return; + } const match = base.match(/^([A-Za-z]:)(.*)$/); - if (!match) return; + if (!match) { + return; + } process.env.HOMEDRIVE = match[1]; process.env.HOMEPATH = match[2] || "\\"; } @@ -63,7 +75,7 @@ export async function withTempHome( fn: (home: string) => Promise, opts: { env?: Record; prefix?: string } = {}, ): Promise { - const base = await fs.mkdtemp(path.join(os.tmpdir(), opts.prefix ?? "moltbot-test-home-")); + const base = await fs.mkdtemp(path.join(os.tmpdir(), opts.prefix ?? "openclaw-test-home-")); const snapshot = snapshotEnv(); const envKeys = Object.keys(opts.env ?? {}); for (const key of envKeys) { @@ -74,12 +86,15 @@ export async function withTempHome( const envSnapshot = snapshotExtraEnv(envKeys); setTempHome(base); - await fs.mkdir(path.join(base, ".clawdbot", "agents", "main", "sessions"), { recursive: true }); + await fs.mkdir(path.join(base, ".openclaw", "agents", "main", "sessions"), { recursive: true }); if (opts.env) { for (const [key, raw] of Object.entries(opts.env)) { const value = typeof raw === "function" ? raw(base) : raw; - if (value === undefined) delete process.env[key]; - else process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } } } diff --git a/test/inbound-contract.providers.test.ts b/test/inbound-contract.providers.test.ts index 81e6a0b48..1e0100e16 100644 --- a/test/inbound-contract.providers.test.ts +++ b/test/inbound-contract.providers.test.ts @@ -1,5 +1,4 @@ import { describe, it } from "vitest"; - import type { MsgContext } from "../src/auto-reply/templating.js"; import { finalizeInboundContext } from "../src/auto-reply/reply/inbound-context.js"; import { expectInboundContextContract } from "./helpers/inbound-contract.js"; diff --git a/test/media-understanding.auto.e2e.test.ts b/test/media-understanding.auto.e2e.test.ts index dafc6d42f..98e2c88c5 100644 --- a/test/media-understanding.auto.e2e.test.ts +++ b/test/media-understanding.auto.e2e.test.ts @@ -1,11 +1,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { afterEach, describe, expect, it, vi } from "vitest"; - -import type { MoltbotConfig } from "../src/config/config.js"; import type { MsgContext } from "../src/auto-reply/templating.js"; +import type { OpenClawConfig } from "../src/config/config.js"; const makeTempDir = async (prefix: string) => await fs.mkdtemp(path.join(os.tmpdir(), prefix)); @@ -16,7 +14,7 @@ const writeExecutable = async (dir: string, name: string, content: string) => { }; const makeTempMedia = async (ext: string) => { - const dir = await makeTempDir("moltbot-media-e2e-"); + const dir = await makeTempDir("openclaw-media-e2e-"); const filePath = path.join(dir, `sample${ext}`); await fs.writeFile(filePath, "audio"); return { dir, filePath }; @@ -52,8 +50,8 @@ describe("media understanding auto-detect (e2e)", () => { it("uses sherpa-onnx-offline when available", async () => { const snapshot = envSnapshot(); try { - const binDir = await makeTempDir("moltbot-bin-sherpa-"); - const modelDir = await makeTempDir("moltbot-sherpa-model-"); + const binDir = await makeTempDir("openclaw-bin-sherpa-"); + const modelDir = await makeTempDir("openclaw-sherpa-model-"); tempPaths.push(binDir, modelDir); await fs.writeFile(path.join(modelDir, "tokens.txt"), "a"); @@ -64,7 +62,7 @@ describe("media understanding auto-detect (e2e)", () => { await writeExecutable( binDir, "sherpa-onnx-offline", - "#!/usr/bin/env bash\n" + 'echo "{\\"text\\":\\"sherpa ok\\"}"\n', + `#!/usr/bin/env bash\necho "{\\"text\\":\\"sherpa ok\\"}"\n`, ); process.env.PATH = `${binDir}:/usr/bin:/bin`; @@ -79,7 +77,7 @@ describe("media understanding auto-detect (e2e)", () => { MediaPath: filePath, MediaType: "audio/wav", }; - const cfg: MoltbotConfig = { tools: { media: { audio: {} } } }; + const cfg: OpenClawConfig = { tools: { media: { audio: {} } } }; await applyMediaUnderstanding({ ctx, cfg }); @@ -92,8 +90,8 @@ describe("media understanding auto-detect (e2e)", () => { it("uses whisper-cli when sherpa is missing", async () => { const snapshot = envSnapshot(); try { - const binDir = await makeTempDir("moltbot-bin-whispercpp-"); - const modelDir = await makeTempDir("moltbot-whispercpp-model-"); + const binDir = await makeTempDir("openclaw-bin-whispercpp-"); + const modelDir = await makeTempDir("openclaw-whispercpp-model-"); tempPaths.push(binDir, modelDir); const modelPath = path.join(modelDir, "tiny.bin"); @@ -124,7 +122,7 @@ describe("media understanding auto-detect (e2e)", () => { MediaPath: filePath, MediaType: "audio/wav", }; - const cfg: MoltbotConfig = { tools: { media: { audio: {} } } }; + const cfg: OpenClawConfig = { tools: { media: { audio: {} } } }; await applyMediaUnderstanding({ ctx, cfg }); @@ -137,13 +135,13 @@ describe("media understanding auto-detect (e2e)", () => { it("uses gemini CLI for images when available", async () => { const snapshot = envSnapshot(); try { - const binDir = await makeTempDir("moltbot-bin-gemini-"); + const binDir = await makeTempDir("openclaw-bin-gemini-"); tempPaths.push(binDir); await writeExecutable( binDir, "gemini", - "#!/usr/bin/env bash\necho '{" + '\\"response\\":\\"gemini ok\\"' + "}'\n", + `#!/usr/bin/env bash\necho '{\\"response\\":\\"gemini ok\\"' + "}'\n`, ); process.env.PATH = `${binDir}:/usr/bin:/bin`; @@ -157,7 +155,7 @@ describe("media understanding auto-detect (e2e)", () => { MediaPath: filePath, MediaType: "image/png", }; - const cfg: MoltbotConfig = { tools: { media: { image: {} } } }; + const cfg: OpenClawConfig = { tools: { media: { image: {} } } }; await applyMediaUnderstanding({ ctx, cfg }); diff --git a/test/mocks/baileys.ts b/test/mocks/baileys.ts index e4aacbf1a..e04ef1a2d 100644 --- a/test/mocks/baileys.ts +++ b/test/mocks/baileys.ts @@ -1,5 +1,4 @@ import { EventEmitter } from "node:events"; - import { vi } from "vitest"; export type MockBaileysSocket = { diff --git a/test/provider-timeout.e2e.test.ts b/test/provider-timeout.e2e.test.ts index 458c2136a..82779cb49 100644 --- a/test/provider-timeout.e2e.test.ts +++ b/test/provider-timeout.e2e.test.ts @@ -2,9 +2,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; - import { describe, expect, it } from "vitest"; - import { GatewayClient } from "../src/gateway/client.js"; import { startGatewayServer } from "../src/gateway/server.js"; import { getDeterministicFreePortBlock } from "../src/test-utils/ports.js"; @@ -83,11 +81,16 @@ async function connectClient(params: { url: string; token: string }) { return await new Promise>((resolve, reject) => { let settled = false; const stop = (err?: Error, client?: InstanceType) => { - if (settled) return; + if (settled) { + return; + } settled = true; clearTimeout(timer); - if (err) reject(err); - else resolve(client as InstanceType); + if (err) { + reject(err); + } else { + resolve(client as InstanceType); + } }; const client = new GatewayClient({ url: params.url, @@ -118,12 +121,12 @@ describe("provider timeouts (e2e)", () => { async () => { const prev = { home: process.env.HOME, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + configPath: process.env.OPENCLAW_CONFIG_PATH, + token: process.env.OPENCLAW_GATEWAY_TOKEN, + skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, + skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, + skipCron: process.env.OPENCLAW_SKIP_CRON, + skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, }; const originalFetch = globalThis.fetch; @@ -146,24 +149,26 @@ describe("provider timeouts (e2e)", () => { return buildOpenAIResponsesSse("fallback-ok"); } - if (!originalFetch) throw new Error(`fetch is not available (url=${url})`); + if (!originalFetch) { + throw new Error(`fetch is not available (url=${url})`); + } return await originalFetch(input, init); }; (globalThis as unknown as { fetch: unknown }).fetch = fetchImpl; - const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-timeout-e2e-")); + const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-timeout-e2e-")); process.env.HOME = tempHome; - process.env.CLAWDBOT_SKIP_CHANNELS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CRON = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; const token = `test-${randomUUID()}`; - process.env.CLAWDBOT_GATEWAY_TOKEN = token; + process.env.OPENCLAW_GATEWAY_TOKEN = token; - const configDir = path.join(tempHome, ".clawdbot"); + const configDir = path.join(tempHome, ".openclaw"); await fs.mkdir(configDir, { recursive: true }); - const configPath = path.join(configDir, "moltbot.json"); + const configPath = path.join(configDir, "openclaw.json"); const cfg = { agents: { @@ -217,7 +222,7 @@ describe("provider timeouts (e2e)", () => { }; await fs.writeFile(configPath, `${JSON.stringify(cfg, null, 2)}\n`); - process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_CONFIG_PATH = configPath; const port = await getFreeGatewayPort(); const server = await startGatewayServer(port, { @@ -233,7 +238,7 @@ describe("provider timeouts (e2e)", () => { try { const sessionKey = "agent:dev:timeout-fallback"; - await client.request>("sessions.patch", { + await client.request("sessions.patch", { key: sessionKey, model: "primary/gpt-5.2", }); @@ -263,20 +268,41 @@ describe("provider timeouts (e2e)", () => { await server.close({ reason: "timeout fallback test complete" }); await fs.rm(tempHome, { recursive: true, force: true }); (globalThis as unknown as { fetch: unknown }).fetch = originalFetch; - if (prev.home === undefined) delete process.env.HOME; - else process.env.HOME = prev.home; - if (prev.configPath === undefined) delete process.env.CLAWDBOT_CONFIG_PATH; - else process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; - if (prev.token === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; - else process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; - if (prev.skipChannels === undefined) delete process.env.CLAWDBOT_SKIP_CHANNELS; - else process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; - if (prev.skipGmail === undefined) delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; - else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; - if (prev.skipCron === undefined) delete process.env.CLAWDBOT_SKIP_CRON; - else process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; - if (prev.skipCanvas === undefined) delete process.env.CLAWDBOT_SKIP_CANVAS_HOST; - else process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; + if (prev.home === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = prev.home; + } + if (prev.configPath === undefined) { + delete process.env.OPENCLAW_CONFIG_PATH; + } else { + process.env.OPENCLAW_CONFIG_PATH = prev.configPath; + } + if (prev.token === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = prev.token; + } + if (prev.skipChannels === undefined) { + delete process.env.OPENCLAW_SKIP_CHANNELS; + } else { + process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels; + } + if (prev.skipGmail === undefined) { + delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER; + } else { + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail; + } + if (prev.skipCron === undefined) { + delete process.env.OPENCLAW_SKIP_CRON; + } else { + process.env.OPENCLAW_SKIP_CRON = prev.skipCron; + } + if (prev.skipCanvas === undefined) { + delete process.env.OPENCLAW_SKIP_CANVAS_HOST; + } else { + process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas; + } } }, ); diff --git a/test/setup.ts b/test/setup.ts index 3d2e3ecff..215935b93 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -8,7 +8,7 @@ import type { ChannelOutboundAdapter, ChannelPlugin, } from "../src/channels/plugins/types.js"; -import type { MoltbotConfig } from "../src/config/config.js"; +import type { OpenClawConfig } from "../src/config/config.js"; import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js"; import { installProcessWarningFilter } from "../src/infra/warnings.js"; import { setActivePluginRegistry } from "../src/plugins/runtime.js"; @@ -80,23 +80,27 @@ const createStubPlugin = (params: { }, capabilities: { chatTypes: ["direct", "group"] }, config: { - listAccountIds: (cfg: MoltbotConfig) => { + listAccountIds: (cfg: OpenClawConfig) => { const channels = cfg.channels as Record | undefined; const entry = channels?.[params.id]; - if (!entry || typeof entry !== "object") return []; + if (!entry || typeof entry !== "object") { + return []; + } const accounts = (entry as { accounts?: Record }).accounts; const ids = accounts ? Object.keys(accounts).filter(Boolean) : []; return ids.length > 0 ? ids : ["default"]; }, - resolveAccount: (cfg: MoltbotConfig, accountId: string) => { + resolveAccount: (cfg: OpenClawConfig, accountId: string) => { const channels = cfg.channels as Record | undefined; const entry = channels?.[params.id]; - if (!entry || typeof entry !== "object") return {}; + if (!entry || typeof entry !== "object") { + return {}; + } const accounts = (entry as { accounts?: Record }).accounts; const match = accounts?.[accountId]; return (match && typeof match === "object") || typeof match === "string" ? match : entry; }, - isConfigured: async (_account, cfg: MoltbotConfig) => { + isConfigured: async (_account, cfg: OpenClawConfig) => { const channels = cfg.channels as Record | undefined; return Boolean(channels?.[params.id]); }, diff --git a/test/test-env.ts b/test/test-env.ts index 308170f35..a45068983 100644 --- a/test/test-env.ts +++ b/test/test-env.ts @@ -7,14 +7,19 @@ type RestoreEntry = { key: string; value: string | undefined }; function restoreEnv(entries: RestoreEntry[]): void { for (const { key, value } of entries) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } } } function loadProfileEnv(): void { const profilePath = path.join(os.homedir(), ".profile"); - if (!fs.existsSync(profilePath)) return; + if (!fs.existsSync(profilePath)) { + return; + } try { const output = execFileSync( "/bin/bash", @@ -24,11 +29,17 @@ function loadProfileEnv(): void { const entries = output.split("\0"); let applied = 0; for (const entry of entries) { - if (!entry) continue; + if (!entry) { + continue; + } const idx = entry.indexOf("="); - if (idx <= 0) continue; + if (idx <= 0) { + continue; + } const key = entry.slice(0, idx); - if (!key || (process.env[key] ?? "") !== "") continue; + if (!key || (process.env[key] ?? "") !== "") { + continue; + } process.env[key] = entry.slice(idx + 1); applied += 1; } @@ -43,8 +54,8 @@ function loadProfileEnv(): void { export function installTestEnv(): { cleanup: () => void; tempHome: string } { const live = process.env.LIVE === "1" || - process.env.CLAWDBOT_LIVE_TEST === "1" || - process.env.CLAWDBOT_LIVE_GATEWAY === "1"; + process.env.OPENCLAW_LIVE_TEST === "1" || + process.env.OPENCLAW_LIVE_GATEWAY === "1"; // Live tests must use the real user environment (keys, profiles, config). // The default test env isolates HOME to avoid touching real state. @@ -54,21 +65,21 @@ export function installTestEnv(): { cleanup: () => void; tempHome: string } { } const restore: RestoreEntry[] = [ - { key: "CLAWDBOT_TEST_FAST", value: process.env.CLAWDBOT_TEST_FAST }, + { key: "OPENCLAW_TEST_FAST", value: process.env.OPENCLAW_TEST_FAST }, { key: "HOME", value: process.env.HOME }, { key: "USERPROFILE", value: process.env.USERPROFILE }, { key: "XDG_CONFIG_HOME", value: process.env.XDG_CONFIG_HOME }, { key: "XDG_DATA_HOME", value: process.env.XDG_DATA_HOME }, { key: "XDG_STATE_HOME", value: process.env.XDG_STATE_HOME }, { key: "XDG_CACHE_HOME", value: process.env.XDG_CACHE_HOME }, - { key: "CLAWDBOT_STATE_DIR", value: process.env.CLAWDBOT_STATE_DIR }, - { key: "CLAWDBOT_CONFIG_PATH", value: process.env.CLAWDBOT_CONFIG_PATH }, - { key: "CLAWDBOT_GATEWAY_PORT", value: process.env.CLAWDBOT_GATEWAY_PORT }, - { key: "CLAWDBOT_BRIDGE_ENABLED", value: process.env.CLAWDBOT_BRIDGE_ENABLED }, - { key: "CLAWDBOT_BRIDGE_HOST", value: process.env.CLAWDBOT_BRIDGE_HOST }, - { key: "CLAWDBOT_BRIDGE_PORT", value: process.env.CLAWDBOT_BRIDGE_PORT }, - { key: "CLAWDBOT_CANVAS_HOST_PORT", value: process.env.CLAWDBOT_CANVAS_HOST_PORT }, - { key: "CLAWDBOT_TEST_HOME", value: process.env.CLAWDBOT_TEST_HOME }, + { key: "OPENCLAW_STATE_DIR", value: process.env.OPENCLAW_STATE_DIR }, + { key: "OPENCLAW_CONFIG_PATH", value: process.env.OPENCLAW_CONFIG_PATH }, + { key: "OPENCLAW_GATEWAY_PORT", value: process.env.OPENCLAW_GATEWAY_PORT }, + { key: "OPENCLAW_BRIDGE_ENABLED", value: process.env.OPENCLAW_BRIDGE_ENABLED }, + { key: "OPENCLAW_BRIDGE_HOST", value: process.env.OPENCLAW_BRIDGE_HOST }, + { key: "OPENCLAW_BRIDGE_PORT", value: process.env.OPENCLAW_BRIDGE_PORT }, + { key: "OPENCLAW_CANVAS_HOST_PORT", value: process.env.OPENCLAW_CANVAS_HOST_PORT }, + { key: "OPENCLAW_TEST_HOME", value: process.env.OPENCLAW_TEST_HOME }, { key: "TELEGRAM_BOT_TOKEN", value: process.env.TELEGRAM_BOT_TOKEN }, { key: "DISCORD_BOT_TOKEN", value: process.env.DISCORD_BOT_TOKEN }, { key: "SLACK_BOT_TOKEN", value: process.env.SLACK_BOT_TOKEN }, @@ -80,23 +91,23 @@ export function installTestEnv(): { cleanup: () => void; tempHome: string } { { key: "NODE_OPTIONS", value: process.env.NODE_OPTIONS }, ]; - const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "moltbot-test-home-")); + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-home-")); process.env.HOME = tempHome; process.env.USERPROFILE = tempHome; - process.env.CLAWDBOT_TEST_HOME = tempHome; - process.env.CLAWDBOT_TEST_FAST = "1"; + process.env.OPENCLAW_TEST_HOME = tempHome; + process.env.OPENCLAW_TEST_FAST = "1"; // Ensure test runs never touch the developer's real config/state, even if they have overrides set. - delete process.env.CLAWDBOT_CONFIG_PATH; + delete process.env.OPENCLAW_CONFIG_PATH; // Prefer deriving state dir from HOME so nested tests that change HOME also isolate correctly. - delete process.env.CLAWDBOT_STATE_DIR; + delete process.env.OPENCLAW_STATE_DIR; // Prefer test-controlled ports over developer overrides (avoid port collisions across tests/workers). - delete process.env.CLAWDBOT_GATEWAY_PORT; - delete process.env.CLAWDBOT_BRIDGE_ENABLED; - delete process.env.CLAWDBOT_BRIDGE_HOST; - delete process.env.CLAWDBOT_BRIDGE_PORT; - delete process.env.CLAWDBOT_CANVAS_HOST_PORT; + delete process.env.OPENCLAW_GATEWAY_PORT; + delete process.env.OPENCLAW_BRIDGE_ENABLED; + delete process.env.OPENCLAW_BRIDGE_HOST; + delete process.env.OPENCLAW_BRIDGE_PORT; + delete process.env.OPENCLAW_CANVAS_HOST_PORT; // Avoid leaking real GitHub/Copilot tokens into non-live test runs. delete process.env.TELEGRAM_BOT_TOKEN; delete process.env.DISCORD_BOT_TOKEN; @@ -109,9 +120,9 @@ export function installTestEnv(): { cleanup: () => void; tempHome: string } { // Avoid leaking local dev tooling flags into tests (e.g. --inspect). delete process.env.NODE_OPTIONS; - // Windows: prefer the legacy default state dir so auth/profile tests match real paths. + // Windows: prefer the default state dir so auth/profile tests match real paths. if (process.platform === "win32") { - process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot"); + process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw"); } process.env.XDG_CONFIG_HOME = path.join(tempHome, ".config"); diff --git a/tsconfig.json b/tsconfig.json index 8f82c611d..1c537b405 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,20 @@ { "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "dist", - "rootDir": "src", - "strict": true, + "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "resolveJsonModule": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["DOM", "DOM.Iterable", "ES2023", "ScriptHost"], + "noEmit": true, + "declaration": true, "noEmitOnError": true, - "allowSyntheticDefaultImports": true + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": "src", + "skipLibCheck": true, + "strict": true, + "target": "es2023" }, "include": ["src/**/*"], "exclude": [ diff --git a/ui/index.html b/ui/index.html index 3100a1973..dc03f4911 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,12 +3,14 @@ - Moltbot Control + OpenClaw Control - + + + - + diff --git a/ui/package.json b/ui/package.json index 3376e1029..dbf223ffc 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,10 +1,10 @@ { - "name": "moltbot-control-ui", + "name": "openclaw-control-ui", "private": true, "type": "module", "scripts": { - "dev": "vite", "build": "vite build", + "dev": "vite", "preview": "vite preview", "test": "vitest run --config vitest.config.ts" }, @@ -17,8 +17,7 @@ }, "devDependencies": { "@vitest/browser-playwright": "4.0.18", - "playwright": "^1.58.0", - "typescript": "^5.9.3", + "playwright": "^1.58.1", "vitest": "4.0.18" } } diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png new file mode 100644 index 000000000..71781843f Binary files /dev/null and b/ui/public/apple-touch-icon.png differ diff --git a/ui/public/favicon-32.png b/ui/public/favicon-32.png new file mode 100644 index 000000000..563c79b0e Binary files /dev/null and b/ui/public/favicon-32.png differ diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 000000000..bcbc1e10c --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index f77cff9ed..b83afd32c 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -81,9 +81,11 @@ --theme-switch-y: 50%; /* Typography - Space Grotesk for personality */ - --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; + --mono: + "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; --font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - --font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-display: + "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; /* Shadows - Richer with subtle color */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); @@ -239,7 +241,7 @@ html.theme-transition::view-transition-new(theme) { } } -moltbot-app { +openclaw-app { display: block; position: relative; z-index: 1; @@ -340,7 +342,8 @@ select { } @keyframes pulse-subtle { - 0%, 100% { + 0%, + 100% { opacity: 1; } 50% { @@ -349,7 +352,8 @@ select { } @keyframes glow-pulse { - 0%, 100% { + 0%, + 100% { box-shadow: 0 0 0 rgba(255, 92, 92, 0); } 50% { @@ -358,12 +362,24 @@ select { } /* Stagger animation delays for grouped elements */ -.stagger-1 { animation-delay: 0ms; } -.stagger-2 { animation-delay: 50ms; } -.stagger-3 { animation-delay: 100ms; } -.stagger-4 { animation-delay: 150ms; } -.stagger-5 { animation-delay: 200ms; } -.stagger-6 { animation-delay: 250ms; } +.stagger-1 { + animation-delay: 0ms; +} +.stagger-2 { + animation-delay: 50ms; +} +.stagger-3 { + animation-delay: 100ms; +} +.stagger-4 { + animation-delay: 150ms; +} +.stagger-5 { + animation-delay: 200ms; +} +.stagger-6 { + animation-delay: 250ms; +} /* Focus visible styles */ :focus-visible { diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index 39b641826..e91989dfc 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -105,7 +105,9 @@ img.chat-avatar { border-radius: var(--radius-lg); padding: 10px 14px; box-shadow: none; - transition: background 150ms ease-out, border-color 150ms ease-out; + transition: + background 150ms ease-out, + border-color 150ms ease-out; max-width: 100%; word-wrap: break-word; } @@ -128,7 +130,9 @@ img.chat-avatar { cursor: pointer; opacity: 0; pointer-events: none; - transition: opacity 120ms ease-out, background 120ms ease-out; + transition: + opacity 120ms ease-out, + background 120ms ease-out; } .chat-copy-btn__icon { @@ -243,7 +247,8 @@ img.chat-avatar { } @keyframes pulsing-border { - 0%, 100% { + 0%, + 100% { border-color: var(--border); } 50% { diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 589b0b62d..faee10f5e 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -77,7 +77,10 @@ display: flex; align-items: center; justify-content: center; - transition: background 150ms ease-out, color 150ms ease-out, border-color 150ms ease-out; + transition: + background 150ms ease-out, + color 150ms ease-out, + border-color 150ms ease-out; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } diff --git a/ui/src/styles/chat/tool-cards.css b/ui/src/styles/chat/tool-cards.css index 052e63dbb..6384db115 100644 --- a/ui/src/styles/chat/tool-cards.css +++ b/ui/src/styles/chat/tool-cards.css @@ -6,7 +6,9 @@ margin-top: 8px; background: var(--card); box-shadow: inset 0 1px 0 var(--card-highlight); - transition: border-color 150ms ease-out, background 150ms ease-out; + transition: + border-color 150ms ease-out, + background 150ms ease-out; /* Fixed max-height to ensure cards don't expand too much */ max-height: 120px; overflow: hidden; @@ -187,7 +189,9 @@ } @keyframes reading-pulse { - 0%, 60%, 100% { + 0%, + 60%, + 100% { opacity: 0.3; transform: scale(0.8); } diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 27dfe62d1..848ffc365 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -1,4 +1,4 @@ -@import './chat.css'; +@import "./chat.css"; /* =========================================== Cards - Refined with depth @@ -14,12 +14,16 @@ border-color var(--duration-normal) var(--ease-out), box-shadow var(--duration-normal) var(--ease-out), transform var(--duration-normal) var(--ease-out); - box-shadow: var(--shadow-sm), inset 0 1px 0 var(--card-highlight); + box-shadow: + var(--shadow-sm), + inset 0 1px 0 var(--card-highlight); } .card:hover { border-color: var(--border-strong); - box-shadow: var(--shadow-md), inset 0 1px 0 var(--card-highlight); + box-shadow: + var(--shadow-md), + inset 0 1px 0 var(--card-highlight); } .card-title { @@ -53,7 +57,9 @@ .stat:hover { border-color: var(--border-strong); - box-shadow: var(--shadow-sm), inset 0 1px 0 var(--card-highlight); + box-shadow: + var(--shadow-sm), + inset 0 1px 0 var(--card-highlight); } .stat-label { @@ -351,7 +357,9 @@ .btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); - box-shadow: var(--shadow-md), 0 0 20px var(--accent-glow); + box-shadow: + var(--shadow-md), + 0 0 20px var(--accent-glow); } /* Keyboard shortcut badge (shadcn style) */ @@ -571,7 +579,8 @@ } @keyframes compaction-pulse { - 0%, 100% { + 0%, + 100% { opacity: 0.7; } 50% { @@ -1050,7 +1059,8 @@ } @keyframes chatStreamPulse { - 0%, 100% { + 0%, + 100% { border-color: var(--border); } 50% { @@ -1103,7 +1113,9 @@ } @keyframes chatReadingDot { - 0%, 80%, 100% { + 0%, + 80%, + 100% { opacity: 0.4; transform: translateY(0); } diff --git a/ui/src/ui/app-channels.ts b/ui/src/ui/app-channels.ts index 91ff734ed..3f139ea5a 100644 --- a/ui/src/ui/app-channels.ts +++ b/ui/src/ui/app-channels.ts @@ -1,3 +1,5 @@ +import type { OpenClawApp } from "./app"; +import type { NostrProfile } from "./types"; import { loadChannels, logoutWhatsApp, @@ -5,32 +7,30 @@ import { waitWhatsAppLogin, } from "./controllers/channels"; import { loadConfig, saveConfig } from "./controllers/config"; -import type { MoltbotApp } from "./app"; -import type { NostrProfile } from "./types"; import { createNostrProfileFormState } from "./views/channels.nostr-profile-form"; -export async function handleWhatsAppStart(host: MoltbotApp, force: boolean) { +export async function handleWhatsAppStart(host: OpenClawApp, force: boolean) { await startWhatsAppLogin(host, force); await loadChannels(host, true); } -export async function handleWhatsAppWait(host: MoltbotApp) { +export async function handleWhatsAppWait(host: OpenClawApp) { await waitWhatsAppLogin(host); await loadChannels(host, true); } -export async function handleWhatsAppLogout(host: MoltbotApp) { +export async function handleWhatsAppLogout(host: OpenClawApp) { await logoutWhatsApp(host); await loadChannels(host, true); } -export async function handleChannelConfigSave(host: MoltbotApp) { +export async function handleChannelConfigSave(host: OpenClawApp) { await saveConfig(host); await loadConfig(host); await loadChannels(host, true); } -export async function handleChannelConfigReload(host: MoltbotApp) { +export async function handleChannelConfigReload(host: OpenClawApp) { await loadConfig(host); await loadChannels(host, true); } @@ -49,7 +49,7 @@ function parseValidationErrors(details: unknown): Record { return errors; } -function resolveNostrAccountId(host: MoltbotApp): string { +function resolveNostrAccountId(host: OpenClawApp): string { const accounts = host.channelsSnapshot?.channelAccounts?.nostr ?? []; return accounts[0]?.accountId ?? host.nostrProfileAccountId ?? "default"; } @@ -59,7 +59,7 @@ function buildNostrProfileUrl(accountId: string, suffix = ""): string { } export function handleNostrProfileEdit( - host: MoltbotApp, + host: OpenClawApp, accountId: string, profile: NostrProfile | null, ) { @@ -67,13 +67,13 @@ export function handleNostrProfileEdit( host.nostrProfileFormState = createNostrProfileFormState(profile ?? undefined); } -export function handleNostrProfileCancel(host: MoltbotApp) { +export function handleNostrProfileCancel(host: OpenClawApp) { host.nostrProfileFormState = null; host.nostrProfileAccountId = null; } export function handleNostrProfileFieldChange( - host: MoltbotApp, + host: OpenClawApp, field: keyof NostrProfile, value: string, ) { @@ -92,7 +92,7 @@ export function handleNostrProfileFieldChange( }; } -export function handleNostrProfileToggleAdvanced(host: MoltbotApp) { +export function handleNostrProfileToggleAdvanced(host: OpenClawApp) { const state = host.nostrProfileFormState; if (!state) return; host.nostrProfileFormState = { @@ -101,7 +101,7 @@ export function handleNostrProfileToggleAdvanced(host: MoltbotApp) { }; } -export async function handleNostrProfileSave(host: MoltbotApp) { +export async function handleNostrProfileSave(host: OpenClawApp) { const state = host.nostrProfileFormState; if (!state || state.saving) return; const accountId = resolveNostrAccountId(host); @@ -122,9 +122,12 @@ export async function handleNostrProfileSave(host: MoltbotApp) { }, body: JSON.stringify(state.values), }); - const data = (await response.json().catch(() => null)) as - | { ok?: boolean; error?: string; details?: unknown; persisted?: boolean } - | null; + const data = (await response.json().catch(() => null)) as { + ok?: boolean; + error?: string; + details?: unknown; + persisted?: boolean; + } | null; if (!response.ok || data?.ok === false || !data) { const errorMessage = data?.error ?? `Profile update failed (${response.status})`; @@ -167,7 +170,7 @@ export async function handleNostrProfileSave(host: MoltbotApp) { } } -export async function handleNostrProfileImport(host: MoltbotApp) { +export async function handleNostrProfileImport(host: OpenClawApp) { const state = host.nostrProfileFormState; if (!state || state.importing) return; const accountId = resolveNostrAccountId(host); @@ -187,9 +190,13 @@ export async function handleNostrProfileImport(host: MoltbotApp) { }, body: JSON.stringify({ autoMerge: true }), }); - const data = (await response.json().catch(() => null)) as - | { ok?: boolean; error?: string; imported?: NostrProfile; merged?: NostrProfile; saved?: boolean } - | null; + const data = (await response.json().catch(() => null)) as { + ok?: boolean; + error?: string; + imported?: NostrProfile; + merged?: NostrProfile; + saved?: boolean; + } | null; if (!response.ok || data?.ok === false || !data) { const errorMessage = data?.error ?? `Profile import failed (${response.status})`; diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts index 77149f9ad..cd2c8e8e0 100644 --- a/ui/src/ui/app-chat.ts +++ b/ui/src/ui/app-chat.ts @@ -1,14 +1,14 @@ -import { abortChatRun, loadChatHistory, sendChatMessage } from "./controllers/chat"; -import { loadSessions } from "./controllers/sessions"; -import { generateUUID } from "./uuid"; -import { resetToolStream } from "./app-tool-stream"; +import type { OpenClawApp } from "./app"; +import type { GatewayHelloOk } from "./gateway"; +import type { ChatAttachment, ChatQueueItem } from "./ui-types"; +import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js"; import { scheduleChatScroll } from "./app-scroll"; import { setLastActiveSessionKey } from "./app-settings"; +import { resetToolStream } from "./app-tool-stream"; +import { abortChatRun, loadChatHistory, sendChatMessage } from "./controllers/chat"; +import { loadSessions } from "./controllers/sessions"; import { normalizeBasePath } from "./navigation"; -import type { GatewayHelloOk } from "./gateway"; -import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js"; -import type { MoltbotApp } from "./app"; -import type { ChatAttachment, ChatQueueItem } from "./ui-types"; +import { generateUUID } from "./uuid"; type ChatHost = { connected: boolean; @@ -21,8 +21,11 @@ type ChatHost = { basePath: string; hello: GatewayHelloOk | null; chatAvatarUrl: string | null; + refreshSessionsAfterChat: Set; }; +export const CHAT_SESSIONS_ACTIVE_MINUTES = 10; + export function isChatBusy(host: ChatHost) { return host.chatSending || Boolean(host.chatRunId); } @@ -41,13 +44,26 @@ export function isChatStopCommand(text: string) { ); } +function isChatResetCommand(text: string) { + const trimmed = text.trim(); + if (!trimmed) return false; + const normalized = trimmed.toLowerCase(); + if (normalized === "/new" || normalized === "/reset") return true; + return normalized.startsWith("/new ") || normalized.startsWith("/reset "); +} + export async function handleAbortChat(host: ChatHost) { if (!host.connected) return; host.chatMessage = ""; - await abortChatRun(host as unknown as MoltbotApp); + await abortChatRun(host as unknown as OpenClawApp); } -function enqueueChatMessage(host: ChatHost, text: string, attachments?: ChatAttachment[]) { +function enqueueChatMessage( + host: ChatHost, + text: string, + attachments?: ChatAttachment[], + refreshSessions?: boolean, +) { const trimmed = text.trim(); const hasAttachments = Boolean(attachments && attachments.length > 0); if (!trimmed && !hasAttachments) return; @@ -58,6 +74,7 @@ function enqueueChatMessage(host: ChatHost, text: string, attachments?: ChatAtta text: trimmed, createdAt: Date.now(), attachments: hasAttachments ? attachments?.map((att) => ({ ...att })) : undefined, + refreshSessions, }, ]; } @@ -71,10 +88,12 @@ async function sendChatMessageNow( attachments?: ChatAttachment[]; previousAttachments?: ChatAttachment[]; restoreAttachments?: boolean; + refreshSessions?: boolean; }, ) { resetToolStream(host as unknown as Parameters[0]); - const ok = await sendChatMessage(host as unknown as MoltbotApp, message, opts?.attachments); + const runId = await sendChatMessage(host as unknown as OpenClawApp, message, opts?.attachments); + const ok = Boolean(runId); if (!ok && opts?.previousDraft != null) { host.chatMessage = opts.previousDraft; } @@ -82,7 +101,10 @@ async function sendChatMessageNow( host.chatAttachments = opts.previousAttachments; } if (ok) { - setLastActiveSessionKey(host as unknown as Parameters[0], host.sessionKey); + setLastActiveSessionKey( + host as unknown as Parameters[0], + host.sessionKey, + ); } if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) { host.chatMessage = opts.previousDraft; @@ -94,6 +116,9 @@ async function sendChatMessageNow( if (ok && !host.chatRunId) { void flushChatQueue(host); } + if (ok && opts?.refreshSessions && runId) { + host.refreshSessionsAfterChat.add(runId); + } return ok; } @@ -102,7 +127,10 @@ async function flushChatQueue(host: ChatHost) { const [next, ...rest] = host.chatQueue; if (!next) return; host.chatQueue = rest; - const ok = await sendChatMessageNow(host, next.text, { attachments: next.attachments }); + const ok = await sendChatMessageNow(host, next.text, { + attachments: next.attachments, + refreshSessions: next.refreshSessions, + }); if (!ok) { host.chatQueue = [next, ...host.chatQueue]; } @@ -132,6 +160,7 @@ export async function handleSendChat( return; } + const refreshSessions = isChatResetCommand(message); if (messageOverride == null) { host.chatMessage = ""; // Clear attachments when sending @@ -139,7 +168,7 @@ export async function handleSendChat( } if (isChatBusy(host)) { - enqueueChatMessage(host, message, attachmentsToSend); + enqueueChatMessage(host, message, attachmentsToSend, refreshSessions); return; } @@ -149,13 +178,16 @@ export async function handleSendChat( attachments: hasAttachments ? attachmentsToSend : undefined, previousAttachments: messageOverride == null ? attachments : undefined, restoreAttachments: Boolean(messageOverride && opts?.restoreDraft), + refreshSessions, }); } export async function refreshChat(host: ChatHost) { await Promise.all([ - loadChatHistory(host as unknown as MoltbotApp), - loadSessions(host as unknown as MoltbotApp), + loadChatHistory(host as unknown as OpenClawApp), + loadSessions(host as unknown as OpenClawApp, { + activeMinutes: CHAT_SESSIONS_ACTIVE_MINUTES, + }), refreshChatAvatar(host), ]); scheduleChatScroll(host as unknown as Parameters[0], true); @@ -170,7 +202,9 @@ type SessionDefaultsSnapshot = { function resolveAgentIdForSession(host: ChatHost): string | null { const parsed = parseAgentSessionKey(host.sessionKey); if (parsed?.agentId) return parsed.agentId; - const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined; + const snapshot = host.hello?.snapshot as + | { sessionDefaults?: SessionDefaultsSnapshot } + | undefined; const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim(); return fallback || "main"; } diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index b2355709c..fab712475 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -1,31 +1,27 @@ -import { loadChatHistory } from "./controllers/chat"; -import { loadDevices } from "./controllers/devices"; -import { loadNodes } from "./controllers/nodes"; -import { loadAgents } from "./controllers/agents"; -import type { GatewayEventFrame, GatewayHelloOk } from "./gateway"; -import { GatewayBrowserClient } from "./gateway"; +import type { OpenClawApp } from "./app"; import type { EventLogEntry } from "./app-events"; -import type { AgentsListResult, PresenceEntry, HealthSnapshot, StatusSummary } from "./types"; +import type { ExecApprovalRequest } from "./controllers/exec-approval"; +import type { GatewayEventFrame, GatewayHelloOk } from "./gateway"; import type { Tab } from "./navigation"; import type { UiSettings } from "./storage"; +import type { AgentsListResult, PresenceEntry, HealthSnapshot, StatusSummary } from "./types"; +import { CHAT_SESSIONS_ACTIVE_MINUTES, flushChatQueueForEvent } from "./app-chat"; +import { applySettings, loadCron, refreshActiveTab, setLastActiveSessionKey } from "./app-settings"; import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream"; -import { flushChatQueueForEvent } from "./app-chat"; -import { - applySettings, - loadCron, - refreshActiveTab, - setLastActiveSessionKey, -} from "./app-settings"; +import { loadAgents } from "./controllers/agents"; +import { loadAssistantIdentity } from "./controllers/assistant-identity"; +import { loadChatHistory } from "./controllers/chat"; import { handleChatEvent, type ChatEventPayload } from "./controllers/chat"; +import { loadDevices } from "./controllers/devices"; import { addExecApproval, parseExecApprovalRequested, parseExecApprovalResolved, removeExecApproval, } from "./controllers/exec-approval"; -import type { MoltbotApp } from "./app"; -import type { ExecApprovalRequest } from "./controllers/exec-approval"; -import { loadAssistantIdentity } from "./controllers/assistant-identity"; +import { loadNodes } from "./controllers/nodes"; +import { loadSessions } from "./controllers/sessions"; +import { GatewayBrowserClient } from "./gateway"; type GatewayHost = { settings: UiSettings; @@ -50,6 +46,7 @@ type GatewayHost = { assistantAgentId: string | null; sessionKey: string; chatRunId: string | null; + refreshSessionsAfterChat: Set; execApprovalQueue: ExecApprovalRequest[]; execApprovalError: string | null; }; @@ -75,8 +72,7 @@ function normalizeSessionKeyForDefaults( raw === "main" || raw === mainKey || (defaultAgentId && - (raw === `agent:${defaultAgentId}:main` || - raw === `agent:${defaultAgentId}:${mainKey}`)); + (raw === `agent:${defaultAgentId}:main` || raw === `agent:${defaultAgentId}:${mainKey}`)); return isAlias ? mainSessionKey : raw; } @@ -120,7 +116,7 @@ export function connectGateway(host: GatewayHost) { url: host.settings.gatewayUrl, token: host.settings.token.trim() ? host.settings.token : undefined, password: host.password.trim() ? host.password : undefined, - clientName: "moltbot-control-ui", + clientName: "openclaw-control-ui", mode: "webchat", onHello: (hello) => { host.connected = true; @@ -133,10 +129,10 @@ export function connectGateway(host: GatewayHost) { (host as unknown as { chatStream: string | null }).chatStream = null; (host as unknown as { chatStreamStartedAt: number | null }).chatStreamStartedAt = null; resetToolStream(host as unknown as Parameters[0]); - void loadAssistantIdentity(host as unknown as MoltbotApp); - void loadAgents(host as unknown as MoltbotApp); - void loadNodes(host as unknown as MoltbotApp, { quiet: true }); - void loadDevices(host as unknown as MoltbotApp, { quiet: true }); + void loadAssistantIdentity(host as unknown as OpenClawApp); + void loadAgents(host as unknown as OpenClawApp); + void loadNodes(host as unknown as OpenClawApp, { quiet: true }); + void loadDevices(host as unknown as OpenClawApp, { quiet: true }); void refreshActiveTab(host as unknown as Parameters[0]); }, onClose: ({ code, reason }) => { @@ -188,14 +184,21 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { payload.sessionKey, ); } - const state = handleChatEvent(host as unknown as MoltbotApp, payload); + const state = handleChatEvent(host as unknown as OpenClawApp, payload); if (state === "final" || state === "error" || state === "aborted") { resetToolStream(host as unknown as Parameters[0]); - void flushChatQueueForEvent( - host as unknown as Parameters[0], - ); + void flushChatQueueForEvent(host as unknown as Parameters[0]); + const runId = payload?.runId; + if (runId && host.refreshSessionsAfterChat.has(runId)) { + host.refreshSessionsAfterChat.delete(runId); + if (state === "final") { + void loadSessions(host as unknown as OpenClawApp, { + activeMinutes: CHAT_SESSIONS_ACTIVE_MINUTES, + }); + } + } } - if (state === "final") void loadChatHistory(host as unknown as MoltbotApp); + if (state === "final") void loadChatHistory(host as unknown as OpenClawApp); return; } @@ -214,7 +217,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { } if (evt.event === "device.pair.requested" || evt.event === "device.pair.resolved") { - void loadDevices(host as unknown as MoltbotApp, { quiet: true }); + void loadDevices(host as unknown as OpenClawApp, { quiet: true }); } if (evt.event === "exec.approval.requested") { diff --git a/ui/src/ui/app-lifecycle.ts b/ui/src/ui/app-lifecycle.ts index 71af9d202..de0253399 100644 --- a/ui/src/ui/app-lifecycle.ts +++ b/ui/src/ui/app-lifecycle.ts @@ -1,14 +1,5 @@ import type { Tab } from "./navigation"; import { connectGateway } from "./app-gateway"; -import { - applySettingsFromUrl, - attachThemeListener, - detachThemeListener, - inferBasePath, - syncTabWithLocation, - syncThemeWithSettings, -} from "./app-settings"; -import { observeTopbar, scheduleChatScroll, scheduleLogsScroll } from "./app-scroll"; import { startLogsPolling, startNodesPolling, @@ -17,6 +8,15 @@ import { startDebugPolling, stopDebugPolling, } from "./app-polling"; +import { observeTopbar, scheduleChatScroll, scheduleLogsScroll } from "./app-scroll"; +import { + applySettingsFromUrl, + attachThemeListener, + detachThemeListener, + inferBasePath, + syncTabWithLocation, + syncThemeWithSettings, +} from "./app-settings"; type LifecycleHost = { basePath: string; @@ -35,20 +35,11 @@ type LifecycleHost = { export function handleConnected(host: LifecycleHost) { host.basePath = inferBasePath(); - syncTabWithLocation( - host as unknown as Parameters[0], - true, - ); - syncThemeWithSettings( - host as unknown as Parameters[0], - ); - attachThemeListener( - host as unknown as Parameters[0], - ); + applySettingsFromUrl(host as unknown as Parameters[0]); + syncTabWithLocation(host as unknown as Parameters[0], true); + syncThemeWithSettings(host as unknown as Parameters[0]); + attachThemeListener(host as unknown as Parameters[0]); window.addEventListener("popstate", host.popStateHandler); - applySettingsFromUrl( - host as unknown as Parameters[0], - ); connectGateway(host as unknown as Parameters[0]); startNodesPolling(host as unknown as Parameters[0]); if (host.tab === "logs") { @@ -68,17 +59,12 @@ export function handleDisconnected(host: LifecycleHost) { stopNodesPolling(host as unknown as Parameters[0]); stopLogsPolling(host as unknown as Parameters[0]); stopDebugPolling(host as unknown as Parameters[0]); - detachThemeListener( - host as unknown as Parameters[0], - ); + detachThemeListener(host as unknown as Parameters[0]); host.topbarObserver?.disconnect(); host.topbarObserver = null; } -export function handleUpdated( - host: LifecycleHost, - changed: Map, -) { +export function handleUpdated(host: LifecycleHost, changed: Map) { if ( host.tab === "chat" && (changed.has("chatMessages") || diff --git a/ui/src/ui/app-polling.ts b/ui/src/ui/app-polling.ts index 3255bdaeb..c0aa7c9d1 100644 --- a/ui/src/ui/app-polling.ts +++ b/ui/src/ui/app-polling.ts @@ -1,7 +1,7 @@ +import type { OpenClawApp } from "./app"; +import { loadDebug } from "./controllers/debug"; import { loadLogs } from "./controllers/logs"; import { loadNodes } from "./controllers/nodes"; -import { loadDebug } from "./controllers/debug"; -import type { MoltbotApp } from "./app"; type PollingHost = { nodesPollInterval: number | null; @@ -13,7 +13,7 @@ type PollingHost = { export function startNodesPolling(host: PollingHost) { if (host.nodesPollInterval != null) return; host.nodesPollInterval = window.setInterval( - () => void loadNodes(host as unknown as MoltbotApp, { quiet: true }), + () => void loadNodes(host as unknown as OpenClawApp, { quiet: true }), 5000, ); } @@ -28,7 +28,7 @@ export function startLogsPolling(host: PollingHost) { if (host.logsPollInterval != null) return; host.logsPollInterval = window.setInterval(() => { if (host.tab !== "logs") return; - void loadLogs(host as unknown as MoltbotApp, { quiet: true }); + void loadLogs(host as unknown as OpenClawApp, { quiet: true }); }, 2000); } @@ -42,7 +42,7 @@ export function startDebugPolling(host: PollingHost) { if (host.debugPollInterval != null) return; host.debugPollInterval = window.setInterval(() => { if (host.tab !== "debug") return; - void loadDebug(host as unknown as MoltbotApp); + void loadDebug(host as unknown as OpenClawApp); }, 3000); } diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 22f8d90db..f2ff17961 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -1,14 +1,14 @@ import { html } from "lit"; import { repeat } from "lit/directives/repeat.js"; - import type { AppViewState } from "./app-view-state"; -import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation"; -import { icons } from "./icons"; -import { loadChatHistory } from "./controllers/chat"; -import { syncUrlWithSessionKey } from "./app-settings"; -import type { SessionsListResult } from "./types"; import type { ThemeMode } from "./theme"; import type { ThemeTransitionContext } from "./theme-transition"; +import type { SessionsListResult } from "./types"; +import { refreshChat } from "./app-chat"; +import { syncUrlWithSessionKey } from "./app-settings"; +import { loadChatHistory } from "./controllers/chat"; +import { icons } from "./icons"; +import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation"; export function renderTab(state: AppViewState, tab: Tab) { const href = pathForTab(tab, state.basePath); @@ -39,14 +39,50 @@ export function renderTab(state: AppViewState, tab: Tab) { } export function renderChatControls(state: AppViewState) { - const sessionOptions = resolveSessionOptions(state.sessionKey, state.sessionsResult); + const mainSessionKey = resolveMainSessionKey(state.hello, state.sessionsResult); + const sessionOptions = resolveSessionOptions( + state.sessionKey, + state.sessionsResult, + mainSessionKey, + ); const disableThinkingToggle = state.onboarding; const disableFocusToggle = state.onboarding; const showThinking = state.onboarding ? false : state.settings.chatShowThinking; const focusActive = state.onboarding ? true : state.settings.chatFocusMode; // Refresh icon - const refreshIcon = html``; - const focusIcon = html``; + const refreshIcon = html` + + + + + `; + const focusIcon = html` + + + + + + + + `; return html`
- ${state.lastError - ? html`
${state.lastError}
` - : nothing} + ${state.lastError ? html`
${state.lastError}
` : nothing} ${isChat ? renderChatControls(state) : nothing}
- ${state.tab === "overview" - ? renderOverview({ - connected: state.connected, - hello: state.hello, - settings: state.settings, - password: state.password, - lastError: state.lastError, - presenceCount, - sessionsCount, - cronEnabled: state.cronStatus?.enabled ?? null, - cronNext, - lastChannelsRefresh: state.channelsLastSuccess, - onSettingsChange: (next) => state.applySettings(next), - onPasswordChange: (next) => (state.password = next), - onSessionKeyChange: (next) => { - state.sessionKey = next; - state.chatMessage = ""; - state.resetToolStream(); - state.applySettings({ - ...state.settings, - sessionKey: next, - lastActiveSessionKey: next, - }); - void state.loadAssistantIdentity(); - }, - onConnect: () => state.connect(), - onRefresh: () => state.loadOverview(), - }) - : nothing} + ${ + state.tab === "overview" + ? renderOverview({ + connected: state.connected, + hello: state.hello, + settings: state.settings, + password: state.password, + lastError: state.lastError, + presenceCount, + sessionsCount, + cronEnabled: state.cronStatus?.enabled ?? null, + cronNext, + lastChannelsRefresh: state.channelsLastSuccess, + onSettingsChange: (next) => state.applySettings(next), + onPasswordChange: (next) => (state.password = next), + onSessionKeyChange: (next) => { + state.sessionKey = next; + state.chatMessage = ""; + state.resetToolStream(); + state.applySettings({ + ...state.settings, + sessionKey: next, + lastActiveSessionKey: next, + }); + void state.loadAssistantIdentity(); + }, + onConnect: () => state.connect(), + onRefresh: () => state.loadOverview(), + }) + : nothing + } - ${state.tab === "channels" - ? renderChannels({ - connected: state.connected, - loading: state.channelsLoading, - snapshot: state.channelsSnapshot, - lastError: state.channelsError, - lastSuccessAt: state.channelsLastSuccess, - whatsappMessage: state.whatsappLoginMessage, - whatsappQrDataUrl: state.whatsappLoginQrDataUrl, - whatsappConnected: state.whatsappLoginConnected, - whatsappBusy: state.whatsappBusy, - configSchema: state.configSchema, - configSchemaLoading: state.configSchemaLoading, - configForm: state.configForm, - configUiHints: state.configUiHints, - configSaving: state.configSaving, - configFormDirty: state.configFormDirty, - nostrProfileFormState: state.nostrProfileFormState, - nostrProfileAccountId: state.nostrProfileAccountId, - onRefresh: (probe) => loadChannels(state, probe), - onWhatsAppStart: (force) => state.handleWhatsAppStart(force), - onWhatsAppWait: () => state.handleWhatsAppWait(), - onWhatsAppLogout: () => state.handleWhatsAppLogout(), - onConfigPatch: (path, value) => updateConfigFormValue(state, path, value), - onConfigSave: () => state.handleChannelConfigSave(), - onConfigReload: () => state.handleChannelConfigReload(), - onNostrProfileEdit: (accountId, profile) => - state.handleNostrProfileEdit(accountId, profile), - onNostrProfileCancel: () => state.handleNostrProfileCancel(), - onNostrProfileFieldChange: (field, value) => - state.handleNostrProfileFieldChange(field, value), - onNostrProfileSave: () => state.handleNostrProfileSave(), - onNostrProfileImport: () => state.handleNostrProfileImport(), - onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(), - }) - : nothing} + ${ + state.tab === "channels" + ? renderChannels({ + connected: state.connected, + loading: state.channelsLoading, + snapshot: state.channelsSnapshot, + lastError: state.channelsError, + lastSuccessAt: state.channelsLastSuccess, + whatsappMessage: state.whatsappLoginMessage, + whatsappQrDataUrl: state.whatsappLoginQrDataUrl, + whatsappConnected: state.whatsappLoginConnected, + whatsappBusy: state.whatsappBusy, + configSchema: state.configSchema, + configSchemaLoading: state.configSchemaLoading, + configForm: state.configForm, + configUiHints: state.configUiHints, + configSaving: state.configSaving, + configFormDirty: state.configFormDirty, + nostrProfileFormState: state.nostrProfileFormState, + nostrProfileAccountId: state.nostrProfileAccountId, + onRefresh: (probe) => loadChannels(state, probe), + onWhatsAppStart: (force) => state.handleWhatsAppStart(force), + onWhatsAppWait: () => state.handleWhatsAppWait(), + onWhatsAppLogout: () => state.handleWhatsAppLogout(), + onConfigPatch: (path, value) => updateConfigFormValue(state, path, value), + onConfigSave: () => state.handleChannelConfigSave(), + onConfigReload: () => state.handleChannelConfigReload(), + onNostrProfileEdit: (accountId, profile) => + state.handleNostrProfileEdit(accountId, profile), + onNostrProfileCancel: () => state.handleNostrProfileCancel(), + onNostrProfileFieldChange: (field, value) => + state.handleNostrProfileFieldChange(field, value), + onNostrProfileSave: () => state.handleNostrProfileSave(), + onNostrProfileImport: () => state.handleNostrProfileImport(), + onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(), + }) + : nothing + } - ${state.tab === "instances" - ? renderInstances({ - loading: state.presenceLoading, - entries: state.presenceEntries, - lastError: state.presenceError, - statusMessage: state.presenceStatus, - onRefresh: () => loadPresence(state), - }) - : nothing} + ${ + state.tab === "instances" + ? renderInstances({ + loading: state.presenceLoading, + entries: state.presenceEntries, + lastError: state.presenceError, + statusMessage: state.presenceStatus, + onRefresh: () => loadPresence(state), + }) + : nothing + } - ${state.tab === "sessions" - ? renderSessions({ - loading: state.sessionsLoading, - result: state.sessionsResult, - error: state.sessionsError, - activeMinutes: state.sessionsFilterActive, - limit: state.sessionsFilterLimit, - includeGlobal: state.sessionsIncludeGlobal, - includeUnknown: state.sessionsIncludeUnknown, - basePath: state.basePath, - onFiltersChange: (next) => { - state.sessionsFilterActive = next.activeMinutes; - state.sessionsFilterLimit = next.limit; - state.sessionsIncludeGlobal = next.includeGlobal; - state.sessionsIncludeUnknown = next.includeUnknown; - }, - onRefresh: () => loadSessions(state), - onPatch: (key, patch) => patchSession(state, key, patch), - onDelete: (key) => deleteSession(state, key), - }) - : nothing} + ${ + state.tab === "sessions" + ? renderSessions({ + loading: state.sessionsLoading, + result: state.sessionsResult, + error: state.sessionsError, + activeMinutes: state.sessionsFilterActive, + limit: state.sessionsFilterLimit, + includeGlobal: state.sessionsIncludeGlobal, + includeUnknown: state.sessionsIncludeUnknown, + basePath: state.basePath, + onFiltersChange: (next) => { + state.sessionsFilterActive = next.activeMinutes; + state.sessionsFilterLimit = next.limit; + state.sessionsIncludeGlobal = next.includeGlobal; + state.sessionsIncludeUnknown = next.includeUnknown; + }, + onRefresh: () => loadSessions(state), + onPatch: (key, patch) => patchSession(state, key, patch), + onDelete: (key) => deleteSession(state, key), + }) + : nothing + } - ${state.tab === "cron" - ? renderCron({ - loading: state.cronLoading, - status: state.cronStatus, - jobs: state.cronJobs, - error: state.cronError, - busy: state.cronBusy, - form: state.cronForm, - channels: state.channelsSnapshot?.channelMeta?.length - ? state.channelsSnapshot.channelMeta.map((entry) => entry.id) - : state.channelsSnapshot?.channelOrder ?? [], - channelLabels: state.channelsSnapshot?.channelLabels ?? {}, - channelMeta: state.channelsSnapshot?.channelMeta ?? [], - runsJobId: state.cronRunsJobId, - runs: state.cronRuns, - onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }), - onRefresh: () => state.loadCron(), - onAdd: () => addCronJob(state), - onToggle: (job, enabled) => toggleCronJob(state, job, enabled), - onRun: (job) => runCronJob(state, job), - onRemove: (job) => removeCronJob(state, job), - onLoadRuns: (jobId) => loadCronRuns(state, jobId), - }) - : nothing} + ${ + state.tab === "cron" + ? renderCron({ + loading: state.cronLoading, + status: state.cronStatus, + jobs: state.cronJobs, + error: state.cronError, + busy: state.cronBusy, + form: state.cronForm, + channels: state.channelsSnapshot?.channelMeta?.length + ? state.channelsSnapshot.channelMeta.map((entry) => entry.id) + : (state.channelsSnapshot?.channelOrder ?? []), + channelLabels: state.channelsSnapshot?.channelLabels ?? {}, + channelMeta: state.channelsSnapshot?.channelMeta ?? [], + runsJobId: state.cronRunsJobId, + runs: state.cronRuns, + onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }), + onRefresh: () => state.loadCron(), + onAdd: () => addCronJob(state), + onToggle: (job, enabled) => toggleCronJob(state, job, enabled), + onRun: (job) => runCronJob(state, job), + onRemove: (job) => removeCronJob(state, job), + onLoadRuns: (jobId) => loadCronRuns(state, jobId), + }) + : nothing + } - ${state.tab === "skills" - ? renderSkills({ - loading: state.skillsLoading, - report: state.skillsReport, - error: state.skillsError, - filter: state.skillsFilter, - edits: state.skillEdits, - messages: state.skillMessages, - busyKey: state.skillsBusyKey, - onFilterChange: (next) => (state.skillsFilter = next), - onRefresh: () => loadSkills(state, { clearMessages: true }), - onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled), - onEdit: (key, value) => updateSkillEdit(state, key, value), - onSaveKey: (key) => saveSkillApiKey(state, key), - onInstall: (skillKey, name, installId) => - installSkill(state, skillKey, name, installId), - }) - : nothing} + ${ + state.tab === "skills" + ? renderSkills({ + loading: state.skillsLoading, + report: state.skillsReport, + error: state.skillsError, + filter: state.skillsFilter, + edits: state.skillEdits, + messages: state.skillMessages, + busyKey: state.skillsBusyKey, + onFilterChange: (next) => (state.skillsFilter = next), + onRefresh: () => loadSkills(state, { clearMessages: true }), + onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled), + onEdit: (key, value) => updateSkillEdit(state, key, value), + onSaveKey: (key) => saveSkillApiKey(state, key), + onInstall: (skillKey, name, installId) => + installSkill(state, skillKey, name, installId), + }) + : nothing + } - ${state.tab === "nodes" - ? renderNodes({ - loading: state.nodesLoading, - nodes: state.nodes, - devicesLoading: state.devicesLoading, - devicesError: state.devicesError, - devicesList: state.devicesList, - configForm: state.configForm ?? (state.configSnapshot?.config as Record | null), - configLoading: state.configLoading, - configSaving: state.configSaving, - configDirty: state.configFormDirty, - configFormMode: state.configFormMode, - execApprovalsLoading: state.execApprovalsLoading, - execApprovalsSaving: state.execApprovalsSaving, - execApprovalsDirty: state.execApprovalsDirty, - execApprovalsSnapshot: state.execApprovalsSnapshot, - execApprovalsForm: state.execApprovalsForm, - execApprovalsSelectedAgent: state.execApprovalsSelectedAgent, - execApprovalsTarget: state.execApprovalsTarget, - execApprovalsTargetNodeId: state.execApprovalsTargetNodeId, - onRefresh: () => loadNodes(state), - onDevicesRefresh: () => loadDevices(state), - onDeviceApprove: (requestId) => approveDevicePairing(state, requestId), - onDeviceReject: (requestId) => rejectDevicePairing(state, requestId), - onDeviceRotate: (deviceId, role, scopes) => - rotateDeviceToken(state, { deviceId, role, scopes }), - onDeviceRevoke: (deviceId, role) => - revokeDeviceToken(state, { deviceId, role }), - onLoadConfig: () => loadConfig(state), - onLoadExecApprovals: () => { - const target = - state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId - ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } - : { kind: "gateway" as const }; - return loadExecApprovals(state, target); - }, - onBindDefault: (nodeId) => { - if (nodeId) { - updateConfigFormValue(state, ["tools", "exec", "node"], nodeId); - } else { - removeConfigFormValue(state, ["tools", "exec", "node"]); - } - }, - onBindAgent: (agentIndex, nodeId) => { - const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"]; - if (nodeId) { - updateConfigFormValue(state, basePath, nodeId); - } else { - removeConfigFormValue(state, basePath); - } - }, - onSaveBindings: () => saveConfig(state), - onExecApprovalsTargetChange: (kind, nodeId) => { - state.execApprovalsTarget = kind; - state.execApprovalsTargetNodeId = nodeId; - state.execApprovalsSnapshot = null; - state.execApprovalsForm = null; - state.execApprovalsDirty = false; - state.execApprovalsSelectedAgent = null; - }, - onExecApprovalsSelectAgent: (agentId) => { - state.execApprovalsSelectedAgent = agentId; - }, - onExecApprovalsPatch: (path, value) => - updateExecApprovalsFormValue(state, path, value), - onExecApprovalsRemove: (path) => - removeExecApprovalsFormValue(state, path), - onSaveExecApprovals: () => { - const target = - state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId - ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } - : { kind: "gateway" as const }; - return saveExecApprovals(state, target); - }, - }) - : nothing} + ${ + state.tab === "nodes" + ? renderNodes({ + loading: state.nodesLoading, + nodes: state.nodes, + devicesLoading: state.devicesLoading, + devicesError: state.devicesError, + devicesList: state.devicesList, + configForm: + state.configForm ?? + (state.configSnapshot?.config as Record | null), + configLoading: state.configLoading, + configSaving: state.configSaving, + configDirty: state.configFormDirty, + configFormMode: state.configFormMode, + execApprovalsLoading: state.execApprovalsLoading, + execApprovalsSaving: state.execApprovalsSaving, + execApprovalsDirty: state.execApprovalsDirty, + execApprovalsSnapshot: state.execApprovalsSnapshot, + execApprovalsForm: state.execApprovalsForm, + execApprovalsSelectedAgent: state.execApprovalsSelectedAgent, + execApprovalsTarget: state.execApprovalsTarget, + execApprovalsTargetNodeId: state.execApprovalsTargetNodeId, + onRefresh: () => loadNodes(state), + onDevicesRefresh: () => loadDevices(state), + onDeviceApprove: (requestId) => approveDevicePairing(state, requestId), + onDeviceReject: (requestId) => rejectDevicePairing(state, requestId), + onDeviceRotate: (deviceId, role, scopes) => + rotateDeviceToken(state, { deviceId, role, scopes }), + onDeviceRevoke: (deviceId, role) => revokeDeviceToken(state, { deviceId, role }), + onLoadConfig: () => loadConfig(state), + onLoadExecApprovals: () => { + const target = + state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId + ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } + : { kind: "gateway" as const }; + return loadExecApprovals(state, target); + }, + onBindDefault: (nodeId) => { + if (nodeId) { + updateConfigFormValue(state, ["tools", "exec", "node"], nodeId); + } else { + removeConfigFormValue(state, ["tools", "exec", "node"]); + } + }, + onBindAgent: (agentIndex, nodeId) => { + const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"]; + if (nodeId) { + updateConfigFormValue(state, basePath, nodeId); + } else { + removeConfigFormValue(state, basePath); + } + }, + onSaveBindings: () => saveConfig(state), + onExecApprovalsTargetChange: (kind, nodeId) => { + state.execApprovalsTarget = kind; + state.execApprovalsTargetNodeId = nodeId; + state.execApprovalsSnapshot = null; + state.execApprovalsForm = null; + state.execApprovalsDirty = false; + state.execApprovalsSelectedAgent = null; + }, + onExecApprovalsSelectAgent: (agentId) => { + state.execApprovalsSelectedAgent = agentId; + }, + onExecApprovalsPatch: (path, value) => + updateExecApprovalsFormValue(state, path, value), + onExecApprovalsRemove: (path) => removeExecApprovalsFormValue(state, path), + onSaveExecApprovals: () => { + const target = + state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId + ? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId } + : { kind: "gateway" as const }; + return saveExecApprovals(state, target); + }, + }) + : nothing + } - ${state.tab === "chat" - ? renderChat({ - sessionKey: state.sessionKey, - onSessionKeyChange: (next) => { - state.sessionKey = next; - state.chatMessage = ""; - state.chatAttachments = []; - state.chatStream = null; - state.chatStreamStartedAt = null; - state.chatRunId = null; - state.chatQueue = []; - state.resetToolStream(); - state.resetChatScroll(); - state.applySettings({ - ...state.settings, - sessionKey: next, - lastActiveSessionKey: next, - }); - void state.loadAssistantIdentity(); - void loadChatHistory(state); - void refreshChatAvatar(state); - }, - thinkingLevel: state.chatThinkingLevel, - showThinking, - loading: state.chatLoading, - sending: state.chatSending, - compactionStatus: state.compactionStatus, - assistantAvatarUrl: chatAvatarUrl, - messages: state.chatMessages, - toolMessages: state.chatToolMessages, - stream: state.chatStream, - streamStartedAt: state.chatStreamStartedAt, - draft: state.chatMessage, - queue: state.chatQueue, - connected: state.connected, - canSend: state.connected, - disabledReason: chatDisabledReason, - error: state.lastError, - sessions: state.sessionsResult, - focusMode: chatFocus, - onRefresh: () => { - state.resetToolStream(); - return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]); - }, - onToggleFocusMode: () => { - if (state.onboarding) return; - state.applySettings({ - ...state.settings, - chatFocusMode: !state.settings.chatFocusMode, - }); - }, - onChatScroll: (event) => state.handleChatScroll(event), - onDraftChange: (next) => (state.chatMessage = next), - attachments: state.chatAttachments, - onAttachmentsChange: (next) => (state.chatAttachments = next), - onSend: () => state.handleSendChat(), - canAbort: Boolean(state.chatRunId), - onAbort: () => void state.handleAbortChat(), - onQueueRemove: (id) => state.removeQueuedMessage(id), - onNewSession: () => - state.handleSendChat("/new", { restoreDraft: true }), - // Sidebar props for tool output viewing - sidebarOpen: state.sidebarOpen, - sidebarContent: state.sidebarContent, - sidebarError: state.sidebarError, - splitRatio: state.splitRatio, - onOpenSidebar: (content: string) => state.handleOpenSidebar(content), - onCloseSidebar: () => state.handleCloseSidebar(), - onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio), - assistantName: state.assistantName, - assistantAvatar: state.assistantAvatar, - }) - : nothing} + ${ + state.tab === "chat" + ? renderChat({ + sessionKey: state.sessionKey, + onSessionKeyChange: (next) => { + state.sessionKey = next; + state.chatMessage = ""; + state.chatAttachments = []; + state.chatStream = null; + state.chatStreamStartedAt = null; + state.chatRunId = null; + state.chatQueue = []; + state.resetToolStream(); + state.resetChatScroll(); + state.applySettings({ + ...state.settings, + sessionKey: next, + lastActiveSessionKey: next, + }); + void state.loadAssistantIdentity(); + void loadChatHistory(state); + void refreshChatAvatar(state); + }, + thinkingLevel: state.chatThinkingLevel, + showThinking, + loading: state.chatLoading, + sending: state.chatSending, + compactionStatus: state.compactionStatus, + assistantAvatarUrl: chatAvatarUrl, + messages: state.chatMessages, + toolMessages: state.chatToolMessages, + stream: state.chatStream, + streamStartedAt: state.chatStreamStartedAt, + draft: state.chatMessage, + queue: state.chatQueue, + connected: state.connected, + canSend: state.connected, + disabledReason: chatDisabledReason, + error: state.lastError, + sessions: state.sessionsResult, + focusMode: chatFocus, + onRefresh: () => { + state.resetToolStream(); + return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]); + }, + onToggleFocusMode: () => { + if (state.onboarding) return; + state.applySettings({ + ...state.settings, + chatFocusMode: !state.settings.chatFocusMode, + }); + }, + onChatScroll: (event) => state.handleChatScroll(event), + onDraftChange: (next) => (state.chatMessage = next), + attachments: state.chatAttachments, + onAttachmentsChange: (next) => (state.chatAttachments = next), + onSend: () => state.handleSendChat(), + canAbort: Boolean(state.chatRunId), + onAbort: () => void state.handleAbortChat(), + onQueueRemove: (id) => state.removeQueuedMessage(id), + onNewSession: () => state.handleSendChat("/new", { restoreDraft: true }), + // Sidebar props for tool output viewing + sidebarOpen: state.sidebarOpen, + sidebarContent: state.sidebarContent, + sidebarError: state.sidebarError, + splitRatio: state.splitRatio, + onOpenSidebar: (content: string) => state.handleOpenSidebar(content), + onCloseSidebar: () => state.handleCloseSidebar(), + onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio), + assistantName: state.assistantName, + assistantAvatar: state.assistantAvatar, + }) + : nothing + } - ${state.tab === "config" - ? renderConfig({ - raw: state.configRaw, - originalRaw: state.configRawOriginal, - valid: state.configValid, - issues: state.configIssues, - loading: state.configLoading, - saving: state.configSaving, - applying: state.configApplying, - updating: state.updateRunning, - connected: state.connected, - schema: state.configSchema, - schemaLoading: state.configSchemaLoading, - uiHints: state.configUiHints, - formMode: state.configFormMode, - formValue: state.configForm, - originalValue: state.configFormOriginal, - searchQuery: state.configSearchQuery, - activeSection: state.configActiveSection, - activeSubsection: state.configActiveSubsection, - onRawChange: (next) => { - state.configRaw = next; - }, - onFormModeChange: (mode) => (state.configFormMode = mode), - onFormPatch: (path, value) => updateConfigFormValue(state, path, value), - onSearchChange: (query) => (state.configSearchQuery = query), - onSectionChange: (section) => { - state.configActiveSection = section; - state.configActiveSubsection = null; - }, - onSubsectionChange: (section) => (state.configActiveSubsection = section), - onReload: () => loadConfig(state), - onSave: () => saveConfig(state), - onApply: () => applyConfig(state), - onUpdate: () => runUpdate(state), - }) - : nothing} + ${ + state.tab === "config" + ? renderConfig({ + raw: state.configRaw, + originalRaw: state.configRawOriginal, + valid: state.configValid, + issues: state.configIssues, + loading: state.configLoading, + saving: state.configSaving, + applying: state.configApplying, + updating: state.updateRunning, + connected: state.connected, + schema: state.configSchema, + schemaLoading: state.configSchemaLoading, + uiHints: state.configUiHints, + formMode: state.configFormMode, + formValue: state.configForm, + originalValue: state.configFormOriginal, + searchQuery: state.configSearchQuery, + activeSection: state.configActiveSection, + activeSubsection: state.configActiveSubsection, + onRawChange: (next) => { + state.configRaw = next; + }, + onFormModeChange: (mode) => (state.configFormMode = mode), + onFormPatch: (path, value) => updateConfigFormValue(state, path, value), + onSearchChange: (query) => (state.configSearchQuery = query), + onSectionChange: (section) => { + state.configActiveSection = section; + state.configActiveSubsection = null; + }, + onSubsectionChange: (section) => (state.configActiveSubsection = section), + onReload: () => loadConfig(state), + onSave: () => saveConfig(state), + onApply: () => applyConfig(state), + onUpdate: () => runUpdate(state), + }) + : nothing + } - ${state.tab === "debug" - ? renderDebug({ - loading: state.debugLoading, - status: state.debugStatus, - health: state.debugHealth, - models: state.debugModels, - heartbeat: state.debugHeartbeat, - eventLog: state.eventLog, - callMethod: state.debugCallMethod, - callParams: state.debugCallParams, - callResult: state.debugCallResult, - callError: state.debugCallError, - onCallMethodChange: (next) => (state.debugCallMethod = next), - onCallParamsChange: (next) => (state.debugCallParams = next), - onRefresh: () => loadDebug(state), - onCall: () => callDebugMethod(state), - }) - : nothing} + ${ + state.tab === "debug" + ? renderDebug({ + loading: state.debugLoading, + status: state.debugStatus, + health: state.debugHealth, + models: state.debugModels, + heartbeat: state.debugHeartbeat, + eventLog: state.eventLog, + callMethod: state.debugCallMethod, + callParams: state.debugCallParams, + callResult: state.debugCallResult, + callError: state.debugCallError, + onCallMethodChange: (next) => (state.debugCallMethod = next), + onCallParamsChange: (next) => (state.debugCallParams = next), + onRefresh: () => loadDebug(state), + onCall: () => callDebugMethod(state), + }) + : nothing + } - ${state.tab === "logs" - ? renderLogs({ - loading: state.logsLoading, - error: state.logsError, - file: state.logsFile, - entries: state.logsEntries, - filterText: state.logsFilterText, - levelFilters: state.logsLevelFilters, - autoFollow: state.logsAutoFollow, - truncated: state.logsTruncated, - onFilterTextChange: (next) => (state.logsFilterText = next), - onLevelToggle: (level, enabled) => { - state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled }; - }, - onToggleAutoFollow: (next) => (state.logsAutoFollow = next), - onRefresh: () => loadLogs(state, { reset: true }), - onExport: (lines, label) => state.exportLogs(lines, label), - onScroll: (event) => state.handleLogsScroll(event), - }) - : nothing} + ${ + state.tab === "logs" + ? renderLogs({ + loading: state.logsLoading, + error: state.logsError, + file: state.logsFile, + entries: state.logsEntries, + filterText: state.logsFilterText, + levelFilters: state.logsLevelFilters, + autoFollow: state.logsAutoFollow, + truncated: state.logsTruncated, + onFilterTextChange: (next) => (state.logsFilterText = next), + onLevelToggle: (level, enabled) => { + state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled }; + }, + onToggleAutoFollow: (next) => (state.logsAutoFollow = next), + onRefresh: () => loadLogs(state, { reset: true }), + onExport: (lines, label) => state.exportLogs(lines, label), + onScroll: (event) => state.handleLogsScroll(event), + }) + : nothing + } ${renderExecApprovalPrompt(state)} ${renderGatewayUrlConfirmation(state)} diff --git a/ui/src/ui/app-scroll.ts b/ui/src/ui/app-scroll.ts index c3c29f479..36977047d 100644 --- a/ui/src/ui/app-scroll.ts +++ b/ui/src/ui/app-scroll.ts @@ -35,8 +35,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) { host.chatScrollFrame = null; const target = pickScrollTarget(); if (!target) return; - const distanceFromBottom = - target.scrollHeight - target.scrollTop - target.clientHeight; + const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight; const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200; if (!shouldStick) return; if (force) host.chatHasAutoScrolled = true; @@ -49,8 +48,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) { if (!latest) return; const latestDistanceFromBottom = latest.scrollHeight - latest.scrollTop - latest.clientHeight; - const shouldStickRetry = - force || host.chatUserNearBottom || latestDistanceFromBottom < 200; + const shouldStickRetry = force || host.chatUserNearBottom || latestDistanceFromBottom < 200; if (!shouldStickRetry) return; latest.scrollTop = latest.scrollHeight; host.chatUserNearBottom = true; @@ -78,16 +76,14 @@ export function scheduleLogsScroll(host: ScrollHost, force = false) { export function handleChatScroll(host: ScrollHost, event: Event) { const container = event.currentTarget as HTMLElement | null; if (!container) return; - const distanceFromBottom = - container.scrollHeight - container.scrollTop - container.clientHeight; + const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; host.chatUserNearBottom = distanceFromBottom < 200; } export function handleLogsScroll(host: ScrollHost, event: Event) { const container = event.currentTarget as HTMLElement | null; if (!container) return; - const distanceFromBottom = - container.scrollHeight - container.scrollTop - container.clientHeight; + const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; host.logsAtBottom = distanceFromBottom < 80; } @@ -103,7 +99,7 @@ export function exportLogs(lines: string[], label: string) { const anchor = document.createElement("a"); const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-"); anchor.href = url; - anchor.download = `moltbot-logs-${label}-${stamp}.log`; + anchor.download = `openclaw-logs-${label}-${stamp}.log`; anchor.click(); URL.revokeObjectURL(url); } diff --git a/ui/src/ui/app-settings.test.ts b/ui/src/ui/app-settings.test.ts index aae48df6f..33c87cf37 100644 --- a/ui/src/ui/app-settings.test.ts +++ b/ui/src/ui/app-settings.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - import type { Tab } from "./navigation"; import { setTabFromRoute } from "./app-settings"; diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 7e3ab29cf..e821c6bfe 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -1,22 +1,34 @@ +import type { OpenClawApp } from "./app"; +import { refreshChat } from "./app-chat"; +import { + startLogsPolling, + stopLogsPolling, + startDebugPolling, + stopDebugPolling, +} from "./app-polling"; +import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll"; +import { loadChannels } from "./controllers/channels"; import { loadConfig, loadConfigSchema } from "./controllers/config"; import { loadCronJobs, loadCronStatus } from "./controllers/cron"; -import { loadChannels } from "./controllers/channels"; import { loadDebug } from "./controllers/debug"; -import { loadLogs } from "./controllers/logs"; import { loadDevices } from "./controllers/devices"; -import { loadNodes } from "./controllers/nodes"; import { loadExecApprovals } from "./controllers/exec-approvals"; +import { loadLogs } from "./controllers/logs"; +import { loadNodes } from "./controllers/nodes"; import { loadPresence } from "./controllers/presence"; import { loadSessions } from "./controllers/sessions"; import { loadSkills } from "./controllers/skills"; -import { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation"; +import { + inferBasePathFromPathname, + normalizeBasePath, + normalizePath, + pathForTab, + tabFromPath, + type Tab, +} from "./navigation"; import { saveSettings, type UiSettings } from "./storage"; import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme"; import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition"; -import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll"; -import { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from "./app-polling"; -import { refreshChat } from "./app-chat"; -import type { MoltbotApp } from "./app"; type SettingsHost = { settings: UiSettings; @@ -114,8 +126,7 @@ export function applySettingsFromUrl(host: SettingsHost) { export function setTab(host: SettingsHost, next: Tab) { if (host.tab !== next) host.tab = next; if (next === "chat") host.chatHasAutoScrolled = false; - if (next === "logs") - startLogsPolling(host as unknown as Parameters[0]); + if (next === "logs") startLogsPolling(host as unknown as Parameters[0]); else stopLogsPolling(host as unknown as Parameters[0]); if (next === "debug") startDebugPolling(host as unknown as Parameters[0]); @@ -124,11 +135,7 @@ export function setTab(host: SettingsHost, next: Tab) { syncUrlWithTab(host, next, false); } -export function setTheme( - host: SettingsHost, - next: ThemeMode, - context?: ThemeTransitionContext, -) { +export function setTheme(host: SettingsHost, next: ThemeMode, context?: ThemeTransitionContext) { const applyTheme = () => { host.theme = next; applySettings(host, { ...host.settings, theme: next }); @@ -145,15 +152,15 @@ export function setTheme( export async function refreshActiveTab(host: SettingsHost) { if (host.tab === "overview") await loadOverview(host); if (host.tab === "channels") await loadChannelsTab(host); - if (host.tab === "instances") await loadPresence(host as unknown as MoltbotApp); - if (host.tab === "sessions") await loadSessions(host as unknown as MoltbotApp); + if (host.tab === "instances") await loadPresence(host as unknown as OpenClawApp); + if (host.tab === "sessions") await loadSessions(host as unknown as OpenClawApp); if (host.tab === "cron") await loadCron(host); - if (host.tab === "skills") await loadSkills(host as unknown as MoltbotApp); + if (host.tab === "skills") await loadSkills(host as unknown as OpenClawApp); if (host.tab === "nodes") { - await loadNodes(host as unknown as MoltbotApp); - await loadDevices(host as unknown as MoltbotApp); - await loadConfig(host as unknown as MoltbotApp); - await loadExecApprovals(host as unknown as MoltbotApp); + await loadNodes(host as unknown as OpenClawApp); + await loadDevices(host as unknown as OpenClawApp); + await loadConfig(host as unknown as OpenClawApp); + await loadExecApprovals(host as unknown as OpenClawApp); } if (host.tab === "chat") { await refreshChat(host as unknown as Parameters[0]); @@ -163,26 +170,23 @@ export async function refreshActiveTab(host: SettingsHost) { ); } if (host.tab === "config") { - await loadConfigSchema(host as unknown as MoltbotApp); - await loadConfig(host as unknown as MoltbotApp); + await loadConfigSchema(host as unknown as OpenClawApp); + await loadConfig(host as unknown as OpenClawApp); } if (host.tab === "debug") { - await loadDebug(host as unknown as MoltbotApp); + await loadDebug(host as unknown as OpenClawApp); host.eventLog = host.eventLogBuffer; } if (host.tab === "logs") { host.logsAtBottom = true; - await loadLogs(host as unknown as MoltbotApp, { reset: true }); - scheduleLogsScroll( - host as unknown as Parameters[0], - true, - ); + await loadLogs(host as unknown as OpenClawApp, { reset: true }); + scheduleLogsScroll(host as unknown as Parameters[0], true); } } export function inferBasePath() { if (typeof window === "undefined") return ""; - const configured = window.__CLAWDBOT_CONTROL_UI_BASE_PATH__; + const configured = window.__OPENCLAW_CONTROL_UI_BASE_PATH__; if (typeof configured === "string" && configured.trim()) { return normalizeBasePath(configured); } @@ -262,8 +266,7 @@ export function onPopState(host: SettingsHost) { export function setTabFromRoute(host: SettingsHost, next: Tab) { if (host.tab !== next) host.tab = next; if (next === "chat") host.chatHasAutoScrolled = false; - if (next === "logs") - startLogsPolling(host as unknown as Parameters[0]); + if (next === "logs") startLogsPolling(host as unknown as Parameters[0]); else stopLogsPolling(host as unknown as Parameters[0]); if (next === "debug") startDebugPolling(host as unknown as Parameters[0]); @@ -294,11 +297,7 @@ export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) { } } -export function syncUrlWithSessionKey( - host: SettingsHost, - sessionKey: string, - replace: boolean, -) { +export function syncUrlWithSessionKey(host: SettingsHost, sessionKey: string, replace: boolean) { if (typeof window === "undefined") return; const url = new URL(window.location.href); url.searchParams.set("session", sessionKey); @@ -308,26 +307,26 @@ export function syncUrlWithSessionKey( export async function loadOverview(host: SettingsHost) { await Promise.all([ - loadChannels(host as unknown as MoltbotApp, false), - loadPresence(host as unknown as MoltbotApp), - loadSessions(host as unknown as MoltbotApp), - loadCronStatus(host as unknown as MoltbotApp), - loadDebug(host as unknown as MoltbotApp), + loadChannels(host as unknown as OpenClawApp, false), + loadPresence(host as unknown as OpenClawApp), + loadSessions(host as unknown as OpenClawApp), + loadCronStatus(host as unknown as OpenClawApp), + loadDebug(host as unknown as OpenClawApp), ]); } export async function loadChannelsTab(host: SettingsHost) { await Promise.all([ - loadChannels(host as unknown as MoltbotApp, true), - loadConfigSchema(host as unknown as MoltbotApp), - loadConfig(host as unknown as MoltbotApp), + loadChannels(host as unknown as OpenClawApp, true), + loadConfigSchema(host as unknown as OpenClawApp), + loadConfig(host as unknown as OpenClawApp), ]); } export async function loadCron(host: SettingsHost) { await Promise.all([ - loadChannels(host as unknown as MoltbotApp, false), - loadCronStatus(host as unknown as MoltbotApp), - loadCronJobs(host as unknown as MoltbotApp), + loadChannels(host as unknown as OpenClawApp, false), + loadCronStatus(host as unknown as OpenClawApp), + loadCronJobs(host as unknown as OpenClawApp), ]); } diff --git a/ui/src/ui/app-tool-stream.ts b/ui/src/ui/app-tool-stream.ts index 2fbe12b7a..f43814998 100644 --- a/ui/src/ui/app-tool-stream.ts +++ b/ui/src/ui/app-tool-stream.ts @@ -191,8 +191,7 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo } if (payload.stream !== "tool") return; - const sessionKey = - typeof payload.sessionKey === "string" ? payload.sessionKey : undefined; + const sessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey : undefined; if (sessionKey && sessionKey !== host.sessionKey) return; // Fallback: only accept session-less events for the active run. if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return; diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index f58656bfb..7ccbf59d4 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -1,3 +1,8 @@ +import type { EventLogEntry } from "./app-events"; +import type { DevicePairingList } from "./controllers/devices"; +import type { ExecApprovalRequest } from "./controllers/exec-approval"; +import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals"; +import type { SkillMessage } from "./controllers/skills"; import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway"; import type { Tab } from "./navigation"; import type { UiSettings } from "./storage"; @@ -20,14 +25,6 @@ import type { StatusSummary, } from "./types"; import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types"; -import type { EventLogEntry } from "./app-events"; -import type { SkillMessage } from "./controllers/skills"; -import type { - ExecApprovalsFile, - ExecApprovalsSnapshot, -} from "./controllers/exec-approvals"; -import type { DevicePairingList } from "./controllers/devices"; -import type { ExecApprovalRequest } from "./controllers/exec-approval"; import type { NostrProfileFormState } from "./views/channels.nostr-profile-form"; export type AppViewState = { diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 26f4a5836..54ba72498 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -1,10 +1,10 @@ import { LitElement, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; - +import type { EventLogEntry } from "./app-events"; +import type { DevicePairingList } from "./controllers/devices"; +import type { ExecApprovalRequest } from "./controllers/exec-approval"; +import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals"; import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway"; -import { resolveInjectedAssistantIdentity } from "./assistant-identity"; -import { loadSettings, type UiSettings } from "./storage"; -import { renderApp } from "./app-render"; import type { Tab } from "./navigation"; import type { ResolvedTheme, ThemeMode } from "./theme"; import type { @@ -24,45 +24,7 @@ import type { StatusSummary, NostrProfile, } from "./types"; -import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types"; -import type { EventLogEntry } from "./app-events"; -import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults"; -import type { - ExecApprovalsFile, - ExecApprovalsSnapshot, -} from "./controllers/exec-approvals"; -import type { DevicePairingList } from "./controllers/devices"; -import type { ExecApprovalRequest } from "./controllers/exec-approval"; -import { - resetToolStream as resetToolStreamInternal, - type ToolStreamEntry, -} from "./app-tool-stream"; -import { - exportLogs as exportLogsInternal, - handleChatScroll as handleChatScrollInternal, - handleLogsScroll as handleLogsScrollInternal, - resetChatScroll as resetChatScrollInternal, -} from "./app-scroll"; -import { connectGateway as connectGatewayInternal } from "./app-gateway"; -import { - handleConnected, - handleDisconnected, - handleFirstUpdated, - handleUpdated, -} from "./app-lifecycle"; -import { - applySettings as applySettingsInternal, - loadCron as loadCronInternal, - loadOverview as loadOverviewInternal, - setTab as setTabInternal, - setTheme as setThemeInternal, - onPopState as onPopStateInternal, -} from "./app-settings"; -import { - handleAbortChat as handleAbortChatInternal, - handleSendChat as handleSendChatInternal, - removeQueuedMessage as removeQueuedMessageInternal, -} from "./app-chat"; +import type { NostrProfileFormState } from "./views/channels.nostr-profile-form"; import { handleChannelConfigReload as handleChannelConfigReloadInternal, handleChannelConfigSave as handleChannelConfigSaveInternal, @@ -76,12 +38,46 @@ import { handleWhatsAppStart as handleWhatsAppStartInternal, handleWhatsAppWait as handleWhatsAppWaitInternal, } from "./app-channels"; -import type { NostrProfileFormState } from "./views/channels.nostr-profile-form"; +import { + handleAbortChat as handleAbortChatInternal, + handleSendChat as handleSendChatInternal, + removeQueuedMessage as removeQueuedMessageInternal, +} from "./app-chat"; +import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults"; +import { connectGateway as connectGatewayInternal } from "./app-gateway"; +import { + handleConnected, + handleDisconnected, + handleFirstUpdated, + handleUpdated, +} from "./app-lifecycle"; +import { renderApp } from "./app-render"; +import { + exportLogs as exportLogsInternal, + handleChatScroll as handleChatScrollInternal, + handleLogsScroll as handleLogsScrollInternal, + resetChatScroll as resetChatScrollInternal, +} from "./app-scroll"; +import { + applySettings as applySettingsInternal, + loadCron as loadCronInternal, + loadOverview as loadOverviewInternal, + setTab as setTabInternal, + setTheme as setThemeInternal, + onPopState as onPopStateInternal, +} from "./app-settings"; +import { + resetToolStream as resetToolStreamInternal, + type ToolStreamEntry, +} from "./app-tool-stream"; +import { resolveInjectedAssistantIdentity } from "./assistant-identity"; import { loadAssistantIdentity as loadAssistantIdentityInternal } from "./controllers/assistant-identity"; +import { loadSettings, type UiSettings } from "./storage"; +import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types"; declare global { interface Window { - __CLAWDBOT_CONTROL_UI_BASE_PATH__?: string; + __OPENCLAW_CONTROL_UI_BASE_PATH__?: string; } } @@ -96,8 +92,8 @@ function resolveOnboardingMode(): boolean { return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; } -@customElement("moltbot-app") -export class MoltbotApp extends LitElement { +@customElement("openclaw-app") +export class OpenClawApp extends LitElement { @state() settings: UiSettings = loadSettings(); @state() password = ""; @state() tab: Tab = "chat"; @@ -258,11 +254,10 @@ export class MoltbotApp extends LitElement { private logsScrollFrame: number | null = null; private toolStreamById = new Map(); private toolStreamOrder: string[] = []; + refreshSessionsAfterChat = new Set(); basePath = ""; private popStateHandler = () => - onPopStateInternal( - this as unknown as Parameters[0], - ); + onPopStateInternal(this as unknown as Parameters[0]); private themeMedia: MediaQueryList | null = null; private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null; private topbarObserver: ResizeObserver | null = null; @@ -286,16 +281,11 @@ export class MoltbotApp extends LitElement { } protected updated(changed: Map) { - handleUpdated( - this as unknown as Parameters[0], - changed, - ); + handleUpdated(this as unknown as Parameters[0], changed); } connect() { - connectGatewayInternal( - this as unknown as Parameters[0], - ); + connectGatewayInternal(this as unknown as Parameters[0]); } handleChatScroll(event: Event) { @@ -317,15 +307,11 @@ export class MoltbotApp extends LitElement { } resetToolStream() { - resetToolStreamInternal( - this as unknown as Parameters[0], - ); + resetToolStreamInternal(this as unknown as Parameters[0]); } resetChatScroll() { - resetChatScrollInternal( - this as unknown as Parameters[0], - ); + resetChatScrollInternal(this as unknown as Parameters[0]); } async loadAssistantIdentity() { @@ -333,10 +319,7 @@ export class MoltbotApp extends LitElement { } applySettings(next: UiSettings) { - applySettingsInternal( - this as unknown as Parameters[0], - next, - ); + applySettingsInternal(this as unknown as Parameters[0], next); } setTab(next: Tab) { @@ -344,29 +327,19 @@ export class MoltbotApp extends LitElement { } setTheme(next: ThemeMode, context?: Parameters[2]) { - setThemeInternal( - this as unknown as Parameters[0], - next, - context, - ); + setThemeInternal(this as unknown as Parameters[0], next, context); } async loadOverview() { - await loadOverviewInternal( - this as unknown as Parameters[0], - ); + await loadOverviewInternal(this as unknown as Parameters[0]); } async loadCron() { - await loadCronInternal( - this as unknown as Parameters[0], - ); + await loadCronInternal(this as unknown as Parameters[0]); } async handleAbortChat() { - await handleAbortChatInternal( - this as unknown as Parameters[0], - ); + await handleAbortChatInternal(this as unknown as Parameters[0]); } removeQueuedMessage(id: string) { @@ -453,10 +426,10 @@ export class MoltbotApp extends LitElement { const nextGatewayUrl = this.pendingGatewayUrl; if (!nextGatewayUrl) return; this.pendingGatewayUrl = null; - applySettingsInternal( - this as unknown as Parameters[0], - { ...this.settings, gatewayUrl: nextGatewayUrl }, - ); + applySettingsInternal(this as unknown as Parameters[0], { + ...this.settings, + gatewayUrl: nextGatewayUrl, + }); this.connect(); } diff --git a/ui/src/ui/assistant-identity.ts b/ui/src/ui/assistant-identity.ts index c21ac6de6..6159cc36e 100644 --- a/ui/src/ui/assistant-identity.ts +++ b/ui/src/ui/assistant-identity.ts @@ -12,8 +12,8 @@ export type AssistantIdentity = { declare global { interface Window { - __CLAWDBOT_ASSISTANT_NAME__?: string; - __CLAWDBOT_ASSISTANT_AVATAR__?: string; + __OPENCLAW_ASSISTANT_NAME__?: string; + __OPENCLAW_ASSISTANT_AVATAR__?: string; } } @@ -28,13 +28,10 @@ function coerceIdentityValue(value: string | undefined, maxLength: number): stri export function normalizeAssistantIdentity( input?: Partial | null, ): AssistantIdentity { - const name = - coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME; + const name = coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME; const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null; const agentId = - typeof input?.agentId === "string" && input.agentId.trim() - ? input.agentId.trim() - : null; + typeof input?.agentId === "string" && input.agentId.trim() ? input.agentId.trim() : null; return { agentId, name, avatar }; } @@ -43,7 +40,7 @@ export function resolveInjectedAssistantIdentity(): AssistantIdentity { return normalizeAssistantIdentity({}); } return normalizeAssistantIdentity({ - name: window.__CLAWDBOT_ASSISTANT_NAME__, - avatar: window.__CLAWDBOT_ASSISTANT_AVATAR__, + name: window.__OPENCLAW_ASSISTANT_NAME__, + avatar: window.__OPENCLAW_ASSISTANT_AVATAR__, }); } diff --git a/ui/src/ui/chat-markdown.browser.test.ts b/ui/src/ui/chat-markdown.browser.test.ts index cb2011e6c..86057f354 100644 --- a/ui/src/ui/chat-markdown.browser.test.ts +++ b/ui/src/ui/chat-markdown.browser.test.ts @@ -1,28 +1,27 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { OpenClawApp } from "./app"; -import { MoltbotApp } from "./app"; - -const originalConnect = MoltbotApp.prototype.connect; +const originalConnect = OpenClawApp.prototype.connect; function mountApp(pathname: string) { window.history.replaceState({}, "", pathname); - const app = document.createElement("moltbot-app") as MoltbotApp; + const app = document.createElement("openclaw-app") as OpenClawApp; document.body.append(app); return app; } beforeEach(() => { - MoltbotApp.prototype.connect = () => { + OpenClawApp.prototype.connect = () => { // no-op: avoid real gateway WS connections in browser tests }; - window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined; + window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined; localStorage.clear(); document.body.innerHTML = ""; }); afterEach(() => { - MoltbotApp.prototype.connect = originalConnect; - window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined; + OpenClawApp.prototype.connect = originalConnect; + window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined; localStorage.clear(); document.body.innerHTML = ""; }); @@ -46,9 +45,7 @@ describe("chat markdown rendering", () => { await app.updateComplete; - const toolCards = Array.from( - app.querySelectorAll(".chat-tool-card"), - ); + const toolCards = Array.from(app.querySelectorAll(".chat-tool-card")); const toolCard = toolCards.find((card) => card.querySelector(".chat-tool-card__preview, .chat-tool-card__inline"), ); diff --git a/ui/src/ui/chat/copy-as-markdown.ts b/ui/src/ui/chat/copy-as-markdown.ts index 1309f08b8..3d11eb32e 100644 --- a/ui/src/ui/chat/copy-as-markdown.ts +++ b/ui/src/ui/chat/copy-as-markdown.ts @@ -38,9 +38,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult { aria-label=${idleLabel} @click=${async (e: Event) => { const btn = e.currentTarget as HTMLButtonElement | null; - const iconContainer = btn?.querySelector( - ".chat-copy-btn__icon", - ) as HTMLElement | null; + const iconContainer = btn?.querySelector(".chat-copy-btn__icon") as HTMLElement | null; if (!btn || btn.dataset.copying === "1") return; diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 4a9ccec14..97ad421f6 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -1,16 +1,15 @@ import { html, nothing } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; - import type { AssistantIdentity } from "../assistant-identity"; -import { toSanitizedMarkdownHtml } from "../markdown"; import type { MessageGroup } from "../types/chat-types"; +import { toSanitizedMarkdownHtml } from "../markdown"; import { renderCopyAsMarkdownButton } from "./copy-as-markdown"; -import { isToolResultMessage, normalizeRoleForGrouping } from "./message-normalizer"; import { extractTextCached, extractThinkingCached, formatReasoningMarkdown, } from "./message-extract"; +import { isToolResultMessage, normalizeRoleForGrouping } from "./message-normalizer"; import { extractToolCards, renderToolCardSidebar } from "./tool-cards"; type ImageBlock = { @@ -35,9 +34,7 @@ function extractImages(message: unknown): ImageBlock[] { const data = source.data as string; const mediaType = (source.media_type as string) || "image/png"; // If data is already a data URL, use it directly - const url = data.startsWith("data:") - ? data - : `data:${mediaType};base64,${data}`; + const url = data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`; images.push({ url }); } else if (typeof b.url === "string") { images.push({ url: b.url }); @@ -122,11 +119,7 @@ export function renderMessageGroup( ? assistantName : normalizedRole; const roleClass = - normalizedRole === "user" - ? "user" - : normalizedRole === "assistant" - ? "assistant" - : "other"; + normalizedRole === "user" ? "user" : normalizedRole === "assistant" ? "assistant" : "other"; const timestamp = new Date(group.timestamp).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", @@ -143,8 +136,7 @@ export function renderMessageGroup( renderGroupedMessage( item.message, { - isStreaming: - group.isStreaming && index === group.messages.length - 1, + isStreaming: group.isStreaming && index === group.messages.length - 1, showReasoning: opts.showReasoning, }, opts.onOpenSidebar, @@ -159,10 +151,7 @@ export function renderMessageGroup( `; } -function renderAvatar( - role: string, - assistant?: Pick, -) { +function renderAvatar(role: string, assistant?: Pick) { const normalized = normalizeRoleForGrouping(role); const assistantName = assistant?.name?.trim() || "Assistant"; const assistantAvatar = assistant?.avatar?.trim() || ""; @@ -179,7 +168,7 @@ function renderAvatar( ? "user" : normalized === "assistant" ? "assistant" - : normalized === "tool" + : normalized === "tool" ? "tool" : "other"; @@ -199,9 +188,7 @@ function renderAvatar( function isAvatarUrl(value: string): boolean { return ( - /^https?:\/\//i.test(value) || - /^data:image\//i.test(value) || - /^\//.test(value) // Relative paths from avatar endpoint + /^https?:\/\//i.test(value) || /^data:image\//i.test(value) || /^\//.test(value) // Relative paths from avatar endpoint ); } @@ -245,13 +232,9 @@ function renderGroupedMessage( const extractedText = extractTextCached(message); const extractedThinking = - opts.showReasoning && role === "assistant" - ? extractThinkingCached(message) - : null; + opts.showReasoning && role === "assistant" ? extractThinkingCached(message) : null; const markdownBase = extractedText?.trim() ? extractedText : null; - const reasoningMarkdown = extractedThinking - ? formatReasoningMarkdown(extractedThinking) - : null; + const reasoningMarkdown = extractedThinking ? formatReasoningMarkdown(extractedThinking) : null; const markdown = markdownBase; const canCopyMarkdown = role === "assistant" && Boolean(markdown?.trim()); @@ -265,9 +248,7 @@ function renderGroupedMessage( .join(" "); if (!markdown && hasToolCards && isToolResult) { - return html`${toolCards.map((card) => - renderToolCardSidebar(card, onOpenSidebar), - )}`; + return html`${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}`; } if (!markdown && !hasToolCards && !hasImages) return nothing; @@ -276,14 +257,18 @@ function renderGroupedMessage(
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing} ${renderMessageImages(images)} - ${reasoningMarkdown - ? html`
${unsafeHTML( - toSanitizedMarkdownHtml(reasoningMarkdown), - )}
` - : nothing} - ${markdown - ? html`
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
` - : nothing} + ${ + reasoningMarkdown + ? html`
${unsafeHTML( + toSanitizedMarkdownHtml(reasoningMarkdown), + )}
` + : nothing + } + ${ + markdown + ? html`
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
` + : nothing + } ${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}
`; diff --git a/ui/src/ui/chat/message-extract.test.ts b/ui/src/ui/chat/message-extract.test.ts index 5dc0e5d35..6b557daa1 100644 --- a/ui/src/ui/chat/message-extract.test.ts +++ b/ui/src/ui/chat/message-extract.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { extractText, extractTextCached, diff --git a/ui/src/ui/chat/message-extract.ts b/ui/src/ui/chat/message-extract.ts index 76dcfa591..6a63a073f 100644 --- a/ui/src/ui/chat/message-extract.ts +++ b/ui/src/ui/chat/message-extract.ts @@ -90,13 +90,9 @@ export function extractThinking(message: unknown): string | null { const rawText = extractRawText(message); if (!rawText) return null; const matches = [ - ...rawText.matchAll( - /<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi, - ), + ...rawText.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi), ]; - const extracted = matches - .map((m) => (m[1] ?? "").trim()) - .filter(Boolean); + const extracted = matches.map((m) => (m[1] ?? "").trim()).filter(Boolean); return extracted.length > 0 ? extracted.join("\n") : null; } diff --git a/ui/src/ui/chat/message-normalizer.test.ts b/ui/src/ui/chat/message-normalizer.test.ts index 132a4be17..c9c241b07 100644 --- a/ui/src/ui/chat/message-normalizer.test.ts +++ b/ui/src/ui/chat/message-normalizer.test.ts @@ -44,8 +44,18 @@ describe("message-normalizer", () => { expect(result.role).toBe("assistant"); expect(result.content).toHaveLength(2); - expect(result.content[0]).toEqual({ type: "text", text: "Here is the result", name: undefined, args: undefined }); - expect(result.content[1]).toEqual({ type: "tool_use", text: undefined, name: "bash", args: { command: "ls" } }); + expect(result.content[0]).toEqual({ + type: "text", + text: "Here is the result", + name: undefined, + args: undefined, + }); + expect(result.content[1]).toEqual({ + type: "tool_use", + text: undefined, + name: "bash", + args: { command: "ls" }, + }); }); it("normalizes message with text field (alternative format)", () => { diff --git a/ui/src/ui/chat/message-normalizer.ts b/ui/src/ui/chat/message-normalizer.ts index e4cbab81d..388939b9f 100644 --- a/ui/src/ui/chat/message-normalizer.ts +++ b/ui/src/ui/chat/message-normalizer.ts @@ -2,10 +2,7 @@ * Message normalization utilities for chat rendering. */ -import type { - NormalizedMessage, - MessageContentItem, -} from "../types/chat-types"; +import type { NormalizedMessage, MessageContentItem } from "../types/chat-types"; /** * Normalize a raw message object into a consistent structure. @@ -16,8 +13,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage { // Detect tool messages by common gateway shapes. // Some tool events come through as assistant role with tool_* items in the content array. - const hasToolId = - typeof m.toolCallId === "string" || typeof m.tool_call_id === "string"; + const hasToolId = typeof m.toolCallId === "string" || typeof m.tool_call_id === "string"; const contentRaw = m.content; const contentItems = Array.isArray(contentRaw) ? contentRaw : null; diff --git a/ui/src/ui/chat/tool-cards.ts b/ui/src/ui/chat/tool-cards.ts index bf82fa49a..19e8cf82e 100644 --- a/ui/src/ui/chat/tool-cards.ts +++ b/ui/src/ui/chat/tool-cards.ts @@ -1,15 +1,11 @@ import { html, nothing } from "lit"; - -import { formatToolDetail, resolveToolDisplay } from "../tool-display"; -import { icons } from "../icons"; import type { ToolCard } from "../types/chat-types"; +import { icons } from "../icons"; +import { formatToolDetail, resolveToolDisplay } from "../tool-display"; import { TOOL_INLINE_THRESHOLD } from "./constants"; -import { - formatToolOutputForSidebar, - getTruncatedPreview, -} from "./tool-helpers"; -import { isToolResultMessage } from "./message-normalizer"; import { extractTextCached } from "./message-extract"; +import { isToolResultMessage } from "./message-normalizer"; +import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers"; export function extractToolCards(message: unknown): ToolCard[] { const m = message as Record; @@ -38,10 +34,7 @@ export function extractToolCards(message: unknown): ToolCard[] { cards.push({ kind: "result", name, text }); } - if ( - isToolResultMessage(message) && - !cards.some((card) => card.kind === "result") - ) { + if (isToolResultMessage(message) && !cards.some((card) => card.kind === "result")) { const name = (typeof m.toolName === "string" && m.toolName) || (typeof m.tool_name === "string" && m.tool_name) || @@ -53,10 +46,7 @@ export function extractToolCards(message: unknown): ToolCard[] { return cards; } -export function renderToolCardSidebar( - card: ToolCard, - onOpenSidebar?: (content: string) => void, -) { +export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content: string) => void) { const display = resolveToolDisplay({ name: card.name, args: card.args }); const detail = formatToolDetail(display); const hasText = Boolean(card.text?.trim()); @@ -86,36 +76,42 @@ export function renderToolCardSidebar( @click=${handleClick} role=${canClick ? "button" : nothing} tabindex=${canClick ? "0" : nothing} - @keydown=${canClick - ? (e: KeyboardEvent) => { - if (e.key !== "Enter" && e.key !== " ") return; - e.preventDefault(); - handleClick?.(); - } - : nothing} + @keydown=${ + canClick + ? (e: KeyboardEvent) => { + if (e.key !== "Enter" && e.key !== " ") return; + e.preventDefault(); + handleClick?.(); + } + : nothing + } >
${icons[display.icon]} ${display.label}
- ${canClick - ? html`${hasText ? "View" : ""} ${icons.check}` - : nothing} + ${ + canClick + ? html`${hasText ? "View" : ""} ${icons.check}` + : nothing + } ${isEmpty && !canClick ? html`${icons.check}` : nothing}
- ${detail - ? html`
${detail}
` - : nothing} - ${isEmpty - ? html`
Completed
` - : nothing} - ${showCollapsed - ? html`
${getTruncatedPreview(card.text!)}
` - : nothing} - ${showInline - ? html`
${card.text}
` - : nothing} + ${detail ? html`
${detail}
` : nothing} + ${ + isEmpty + ? html` +
Completed
+ ` + : nothing + } + ${ + showCollapsed + ? html`
${getTruncatedPreview(card.text!)}
` + : nothing + } + ${showInline ? html`
${card.text}
` : nothing} `; } diff --git a/ui/src/ui/chat/tool-helpers.test.ts b/ui/src/ui/chat/tool-helpers.test.ts index 432bd4f7a..d1e166233 100644 --- a/ui/src/ui/chat/tool-helpers.test.ts +++ b/ui/src/ui/chat/tool-helpers.test.ts @@ -16,7 +16,7 @@ describe("tool-helpers", () => { }); it("formats valid JSON array as code block", () => { - const input = '[1, 2, 3]'; + const input = "[1, 2, 3]"; const result = formatToolOutputForSidebar(input); expect(result).toBe(`\`\`\`json diff --git a/ui/src/ui/components/resizable-divider.ts b/ui/src/ui/components/resizable-divider.ts index a14cca35a..98aba4bc6 100644 --- a/ui/src/ui/components/resizable-divider.ts +++ b/ui/src/ui/components/resizable-divider.ts @@ -24,7 +24,7 @@ export class ResizableDivider extends LitElement { flex-shrink: 0; position: relative; } - + :host::before { content: ""; position: absolute; @@ -33,18 +33,20 @@ export class ResizableDivider extends LitElement { right: -4px; bottom: 0; } - + :host(:hover) { background: var(--accent, #007bff); } - + :host(.dragging) { background: var(--accent, #007bff); } `; render() { - return html``; + return html` + + `; } connectedCallback() { @@ -89,7 +91,7 @@ export class ResizableDivider extends LitElement { detail: { splitRatio: newRatio }, bubbles: true, composed: true, - }) + }), ); }; diff --git a/ui/src/ui/config-form.browser.test.ts b/ui/src/ui/config-form.browser.test.ts index 92d1982d6..5f64ee7f2 100644 --- a/ui/src/ui/config-form.browser.test.ts +++ b/ui/src/ui/config-form.browser.test.ts @@ -1,6 +1,5 @@ import { render } from "lit"; import { describe, expect, it, vi } from "vitest"; - import { analyzeConfigSchema, renderConfigForm } from "./views/config-form"; const rootSchema = { @@ -29,12 +28,7 @@ const rootSchema = { type: "boolean", }, bind: { - anyOf: [ - { const: "auto" }, - { const: "lan" }, - { const: "tailnet" }, - { const: "loopback" }, - ], + anyOf: [{ const: "auto" }, { const: "lan" }, { const: "tailnet" }, { const: "loopback" }], }, }, }; @@ -57,17 +51,12 @@ describe("config form renderer", () => { container, ); - const tokenInput = container.querySelector( - "input[type='password']", - ) as HTMLInputElement | null; + const tokenInput = container.querySelector("input[type='password']") as HTMLInputElement | null; expect(tokenInput).not.toBeNull(); if (!tokenInput) return; tokenInput.value = "abc123"; tokenInput.dispatchEvent(new Event("input", { bubbles: true })); - expect(onPatch).toHaveBeenCalledWith( - ["gateway", "auth", "token"], - "abc123", - ); + expect(onPatch).toHaveBeenCalledWith(["gateway", "auth", "token"], "abc123"); const tokenButton = Array.from( container.querySelectorAll(".cfg-segmented__btn"), @@ -76,9 +65,7 @@ describe("config form renderer", () => { tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(onPatch).toHaveBeenCalledWith(["mode"], "token"); - const checkbox = container.querySelector( - "input[type='checkbox']", - ) as HTMLInputElement | null; + const checkbox = container.querySelector("input[type='checkbox']") as HTMLInputElement | null; expect(checkbox).not.toBeNull(); if (!checkbox) return; checkbox.checked = true; @@ -101,9 +88,7 @@ describe("config form renderer", () => { container, ); - const addButton = container.querySelector( - ".cfg-array__add", - ) as HTMLButtonElement | null; + const addButton = container.querySelector(".cfg-array__add") as HTMLButtonElement | null; expect(addButton).not.toBeUndefined(); addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]); diff --git a/ui/src/ui/controllers/assistant-identity.ts b/ui/src/ui/controllers/assistant-identity.ts index dd720efd3..98eb09087 100644 --- a/ui/src/ui/controllers/assistant-identity.ts +++ b/ui/src/ui/controllers/assistant-identity.ts @@ -1,8 +1,5 @@ import type { GatewayBrowserClient } from "../gateway"; -import { - normalizeAssistantIdentity, - type AssistantIdentity, -} from "../assistant-identity"; +import { normalizeAssistantIdentity, type AssistantIdentity } from "../assistant-identity"; export type AssistantIdentityState = { client: GatewayBrowserClient | null; diff --git a/ui/src/ui/controllers/chat.test.ts b/ui/src/ui/controllers/chat.test.ts index c75ceefc4..3bd3aeb7f 100644 --- a/ui/src/ui/controllers/chat.test.ts +++ b/ui/src/ui/controllers/chat.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from "vitest"; - -import { - handleChatEvent, - type ChatEventPayload, - type ChatState, -} from "./chat"; +import { handleChatEvent, type ChatEventPayload, type ChatState } from "./chat"; function createState(overrides: Partial = {}): ChatState { return { diff --git a/ui/src/ui/controllers/chat.ts b/ui/src/ui/controllers/chat.ts index 6a3e68175..582105114 100644 --- a/ui/src/ui/controllers/chat.ts +++ b/ui/src/ui/controllers/chat.ts @@ -1,7 +1,7 @@ -import { extractText } from "../chat/message-extract"; import type { GatewayBrowserClient } from "../gateway"; -import { generateUUID } from "../uuid"; import type { ChatAttachment } from "../ui-types"; +import { extractText } from "../chat/message-extract"; +import { generateUUID } from "../uuid"; export type ChatState = { client: GatewayBrowserClient | null; @@ -55,11 +55,11 @@ export async function sendChatMessage( state: ChatState, message: string, attachments?: ChatAttachment[], -): Promise { - if (!state.client || !state.connected) return false; +): Promise { + if (!state.client || !state.connected) return null; const msg = message.trim(); const hasAttachments = attachments && attachments.length > 0; - if (!msg && !hasAttachments) return false; + if (!msg && !hasAttachments) return null; const now = Date.now(); @@ -117,7 +117,7 @@ export async function sendChatMessage( idempotencyKey: runId, attachments: apiAttachments, }); - return true; + return runId; } catch (err) { const error = String(err); state.chatRunId = null; @@ -132,7 +132,7 @@ export async function sendChatMessage( timestamp: Date.now(), }, ]; - return false; + return null; } finally { state.chatSending = false; } @@ -144,9 +144,7 @@ export async function abortChatRun(state: ChatState): Promise { try { await state.client.request( "chat.abort", - runId - ? { sessionKey: state.sessionKey, runId } - : { sessionKey: state.sessionKey }, + runId ? { sessionKey: state.sessionKey, runId } : { sessionKey: state.sessionKey }, ); return true; } catch (err) { @@ -155,20 +153,13 @@ export async function abortChatRun(state: ChatState): Promise { } } -export function handleChatEvent( - state: ChatState, - payload?: ChatEventPayload, -) { +export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) { if (!payload) return null; if (payload.sessionKey !== state.sessionKey) return null; // Final from another run (e.g. sub-agent announce): refresh history to show new message. - // See https://github.com/moltbot/moltbot/issues/1909 - if ( - payload.runId && - state.chatRunId && - payload.runId !== state.chatRunId - ) { + // See https://github.com/openclaw/openclaw/issues/1909 + if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId) { if (payload.state === "final") return "final"; return null; } diff --git a/ui/src/ui/controllers/config.test.ts b/ui/src/ui/controllers/config.test.ts index e1bbb1c92..d3b120f61 100644 --- a/ui/src/ui/controllers/config.test.ts +++ b/ui/src/ui/controllers/config.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { applyConfigSnapshot, applyConfig, @@ -46,11 +45,11 @@ describe("applyConfigSnapshot", () => { config: { gateway: { mode: "remote", port: 9999 } }, valid: true, issues: [], - raw: "{\n \"gateway\": { \"mode\": \"remote\", \"port\": 9999 }\n}\n", + raw: '{\n "gateway": { "mode": "remote", "port": 9999 }\n}\n', }); expect(state.configRaw).toBe( - "{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n", + '{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n', ); }); @@ -129,7 +128,7 @@ describe("updateConfigFormValue", () => { updateConfigFormValue(state, ["gateway", "port"], 18789); expect(state.configRaw).toBe( - "{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n", + '{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n', ); }); }); @@ -142,7 +141,7 @@ describe("applyConfig", () => { state.client = { request } as unknown as ConfigState["client"]; state.applySessionKey = "agent:main:whatsapp:dm:+15555550123"; state.configFormMode = "raw"; - state.configRaw = "{\n agent: { workspace: \"~/clawd\" }\n}\n"; + state.configRaw = '{\n agent: { workspace: "~/openclaw" }\n}\n'; state.configSnapshot = { hash: "hash-123", }; @@ -150,7 +149,7 @@ describe("applyConfig", () => { await applyConfig(state); expect(request).toHaveBeenCalledWith("config.apply", { - raw: "{\n agent: { workspace: \"~/clawd\" }\n}\n", + raw: '{\n agent: { workspace: "~/openclaw" }\n}\n', baseHash: "hash-123", sessionKey: "agent:main:whatsapp:dm:+15555550123", }); diff --git a/ui/src/ui/controllers/config.ts b/ui/src/ui/controllers/config.ts index c66876eba..84b9ae515 100644 --- a/ui/src/ui/controllers/config.ts +++ b/ui/src/ui/controllers/config.ts @@ -1,9 +1,5 @@ import type { GatewayBrowserClient } from "../gateway"; -import type { - ConfigSchemaResponse, - ConfigSnapshot, - ConfigUiHints, -} from "../types"; +import type { ConfigSchemaResponse, ConfigSnapshot, ConfigUiHints } from "../types"; import { cloneConfigObject, removePathValue, @@ -57,10 +53,7 @@ export async function loadConfigSchema(state: ConfigState) { if (state.configSchemaLoading) return; state.configSchemaLoading = true; try { - const res = (await state.client.request( - "config.schema", - {}, - )) as ConfigSchemaResponse; + const res = (await state.client.request("config.schema", {})) as ConfigSchemaResponse; applyConfigSchema(state, res); } catch (err) { state.lastError = String(err); @@ -69,10 +62,7 @@ export async function loadConfigSchema(state: ConfigState) { } } -export function applyConfigSchema( - state: ConfigState, - res: ConfigSchemaResponse, -) { +export function applyConfigSchema(state: ConfigState, res: ConfigSchemaResponse) { state.configSchema = res.schema ?? null; state.configUiHints = res.uiHints ?? {}; state.configSchemaVersion = res.version ?? null; @@ -175,9 +165,7 @@ export function updateConfigFormValue( path: Array, value: unknown, ) { - const base = cloneConfigObject( - state.configForm ?? state.configSnapshot?.config ?? {}, - ); + const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {}); setPathValue(base, path, value); state.configForm = base; state.configFormDirty = true; @@ -186,13 +174,8 @@ export function updateConfigFormValue( } } -export function removeConfigFormValue( - state: ConfigState, - path: Array, -) { - const base = cloneConfigObject( - state.configForm ?? state.configSnapshot?.config ?? {}, - ); +export function removeConfigFormValue(state: ConfigState, path: Array) { + const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {}); removePathValue(base, path); state.configForm = base; state.configFormDirty = true; diff --git a/ui/src/ui/controllers/config/form-utils.ts b/ui/src/ui/controllers/config/form-utils.ts index fd40bb5ac..1edd97b9c 100644 --- a/ui/src/ui/controllers/config/form-utils.ts +++ b/ui/src/ui/controllers/config/form-utils.ts @@ -22,16 +22,14 @@ export function setPathValue( if (typeof key === "number") { if (!Array.isArray(current)) return; if (current[key] == null) { - current[key] = - typeof nextKey === "number" ? [] : ({} as Record); + current[key] = typeof nextKey === "number" ? [] : ({} as Record); } current = current[key] as Record | unknown[]; } else { if (typeof current !== "object" || current == null) return; const record = current as Record; if (record[key] == null) { - record[key] = - typeof nextKey === "number" ? [] : ({} as Record); + record[key] = typeof nextKey === "number" ? [] : ({} as Record); } current = record[key] as Record | unknown[]; } @@ -59,9 +57,7 @@ export function removePathValue( current = current[key] as Record | unknown[]; } else { if (typeof current !== "object" || current == null) return; - current = (current as Record)[key] as - | Record - | unknown[]; + current = (current as Record)[key] as Record | unknown[]; } if (current == null) return; } diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index d24e65936..ac128cab8 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -1,7 +1,7 @@ -import { toNumber } from "../format"; import type { GatewayBrowserClient } from "../gateway"; import type { CronJob, CronRunLogEntry, CronStatus } from "../types"; import type { CronFormState } from "../ui-types"; +import { toNumber } from "../format"; export type CronState = { client: GatewayBrowserClient | null; @@ -103,8 +103,7 @@ export async function addCronJob(state: CronState) { wakeMode: state.cronForm.wakeMode, payload, isolation: - state.cronForm.postToMainPrefix.trim() && - state.cronForm.sessionTarget === "isolated" + state.cronForm.postToMainPrefix.trim() && state.cronForm.sessionTarget === "isolated" ? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() } : undefined, }; @@ -125,11 +124,7 @@ export async function addCronJob(state: CronState) { } } -export async function toggleCronJob( - state: CronState, - job: CronJob, - enabled: boolean, -) { +export async function toggleCronJob(state: CronState, job: CronJob, enabled: boolean) { if (!state.client || !state.connected || state.cronBusy) return; state.cronBusy = true; state.cronError = null; diff --git a/ui/src/ui/controllers/debug.ts b/ui/src/ui/controllers/debug.ts index 5aa1eec43..2f189af88 100644 --- a/ui/src/ui/controllers/debug.ts +++ b/ui/src/ui/controllers/debug.ts @@ -29,9 +29,7 @@ export async function loadDebug(state: DebugState) { state.debugStatus = status as StatusSummary; state.debugHealth = health as HealthSnapshot; const modelPayload = models as { models?: unknown[] } | undefined; - state.debugModels = Array.isArray(modelPayload?.models) - ? modelPayload?.models - : []; + state.debugModels = Array.isArray(modelPayload?.models) ? modelPayload?.models : []; state.debugHeartbeat = heartbeat as unknown; } catch (err) { state.debugCallError = String(err); diff --git a/ui/src/ui/controllers/devices.ts b/ui/src/ui/controllers/devices.ts index f08d7afb6..e63547ba7 100644 --- a/ui/src/ui/controllers/devices.ts +++ b/ui/src/ui/controllers/devices.ts @@ -1,6 +1,6 @@ import type { GatewayBrowserClient } from "../gateway"; -import { loadOrCreateDeviceIdentity } from "../device-identity"; import { clearDeviceAuthToken, storeDeviceAuthToken } from "../device-auth"; +import { loadOrCreateDeviceIdentity } from "../device-identity"; export type DeviceTokenSummary = { role: string; @@ -118,9 +118,7 @@ export async function revokeDeviceToken( params: { deviceId: string; role: string }, ) { if (!state.client || !state.connected) return; - const confirmed = window.confirm( - `Revoke token for ${params.deviceId} (${params.role})?`, - ); + const confirmed = window.confirm(`Revoke token for ${params.deviceId} (${params.role})?`); if (!confirmed) return; try { await state.client.request("device.token.revoke", params); diff --git a/ui/src/ui/controllers/exec-approval.ts b/ui/src/ui/controllers/exec-approval.ts index 60e0fc7a0..968b14efc 100644 --- a/ui/src/ui/controllers/exec-approval.ts +++ b/ui/src/ui/controllers/exec-approval.ts @@ -80,6 +80,9 @@ export function addExecApproval( return next; } -export function removeExecApproval(queue: ExecApprovalRequest[], id: string): ExecApprovalRequest[] { +export function removeExecApproval( + queue: ExecApprovalRequest[], + id: string, +): ExecApprovalRequest[] { return pruneExecApprovalQueue(queue).filter((entry) => entry.id !== id); } diff --git a/ui/src/ui/controllers/exec-approvals.ts b/ui/src/ui/controllers/exec-approvals.ts index ba938b9f3..87804642f 100644 --- a/ui/src/ui/controllers/exec-approvals.ts +++ b/ui/src/ui/controllers/exec-approvals.ts @@ -34,9 +34,7 @@ export type ExecApprovalsSnapshot = { file: ExecApprovalsFile; }; -export type ExecApprovalsTarget = - | { kind: "gateway" } - | { kind: "node"; nodeId: string }; +export type ExecApprovalsTarget = { kind: "gateway" } | { kind: "node"; nodeId: string }; export type ExecApprovalsState = { client: GatewayBrowserClient | null; @@ -120,10 +118,7 @@ export async function saveExecApprovals( state.lastError = "Exec approvals hash missing; reload and retry."; return; } - const file = - state.execApprovalsForm ?? - state.execApprovalsSnapshot?.file ?? - {}; + const file = state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {}; const rpc = resolveExecApprovalsSaveRpc(target, { file, baseHash }); if (!rpc) { state.lastError = "Select a node before saving exec approvals."; diff --git a/ui/src/ui/controllers/logs.ts b/ui/src/ui/controllers/logs.ts index 48f78dd23..662b5d7cb 100644 --- a/ui/src/ui/controllers/logs.ts +++ b/ui/src/ui/controllers/logs.ts @@ -16,14 +16,7 @@ export type LogsState = { }; const LOG_BUFFER_LIMIT = 2000; -const LEVELS = new Set([ - "trace", - "debug", - "info", - "warn", - "error", - "fatal", -]); +const LEVELS = new Set(["trace", "debug", "info", "warn", "error", "fatal"]); function parseMaybeJsonString(value: unknown) { if (typeof value !== "string") return null; @@ -53,11 +46,7 @@ export function parseLogLine(line: string): LogEntry { ? (obj._meta as Record) : null; const time = - typeof obj.time === "string" - ? obj.time - : typeof meta?.date === "string" - ? meta?.date - : null; + typeof obj.time === "string" ? obj.time : typeof meta?.date === "string" ? meta?.date : null; const level = normalizeLevel(meta?.logLevelName ?? meta?.level); const contextCandidate = @@ -94,17 +83,14 @@ export function parseLogLine(line: string): LogEntry { } } -export async function loadLogs( - state: LogsState, - opts?: { reset?: boolean; quiet?: boolean }, -) { +export async function loadLogs(state: LogsState, opts?: { reset?: boolean; quiet?: boolean }) { if (!state.client || !state.connected) return; if (state.logsLoading && !opts?.quiet) return; if (!opts?.quiet) state.logsLoading = true; state.logsError = null; try { const res = await state.client.request("logs.tail", { - cursor: opts?.reset ? undefined : state.logsCursor ?? undefined, + cursor: opts?.reset ? undefined : (state.logsCursor ?? undefined), limit: state.logsLimit, maxBytes: state.logsMaxBytes, }); diff --git a/ui/src/ui/controllers/nodes.ts b/ui/src/ui/controllers/nodes.ts index 2a6a8219d..b9255aaea 100644 --- a/ui/src/ui/controllers/nodes.ts +++ b/ui/src/ui/controllers/nodes.ts @@ -8,10 +8,7 @@ export type NodesState = { lastError: string | null; }; -export async function loadNodes( - state: NodesState, - opts?: { quiet?: boolean }, -) { +export async function loadNodes(state: NodesState, opts?: { quiet?: boolean }) { if (!state.client || !state.connected) return; if (state.nodesLoading) return; state.nodesLoading = true; diff --git a/ui/src/ui/controllers/presence.ts b/ui/src/ui/controllers/presence.ts index 67ac2761d..3dbec9061 100644 --- a/ui/src/ui/controllers/presence.ts +++ b/ui/src/ui/controllers/presence.ts @@ -17,9 +17,7 @@ export async function loadPresence(state: PresenceState) { state.presenceError = null; state.presenceStatus = null; try { - const res = (await state.client.request("system-presence", {})) as - | PresenceEntry[] - | undefined; + const res = (await state.client.request("system-presence", {})) as PresenceEntry[] | undefined; if (Array.isArray(res)) { state.presenceEntries = res; state.presenceStatus = res.length === 0 ? "No instances yet." : null; diff --git a/ui/src/ui/controllers/sessions.ts b/ui/src/ui/controllers/sessions.ts index 5c5077037..82e8a8db1 100644 --- a/ui/src/ui/controllers/sessions.ts +++ b/ui/src/ui/controllers/sessions.ts @@ -1,6 +1,6 @@ import type { GatewayBrowserClient } from "../gateway"; -import { toNumber } from "../format"; import type { SessionsListResult } from "../types"; +import { toNumber } from "../format"; export type SessionsState = { client: GatewayBrowserClient | null; @@ -14,18 +14,28 @@ export type SessionsState = { sessionsIncludeUnknown: boolean; }; -export async function loadSessions(state: SessionsState) { +export async function loadSessions( + state: SessionsState, + overrides?: { + activeMinutes?: number; + limit?: number; + includeGlobal?: boolean; + includeUnknown?: boolean; + }, +) { if (!state.client || !state.connected) return; if (state.sessionsLoading) return; state.sessionsLoading = true; state.sessionsError = null; try { + const includeGlobal = overrides?.includeGlobal ?? state.sessionsIncludeGlobal; + const includeUnknown = overrides?.includeUnknown ?? state.sessionsIncludeUnknown; + const activeMinutes = overrides?.activeMinutes ?? toNumber(state.sessionsFilterActive, 0); + const limit = overrides?.limit ?? toNumber(state.sessionsFilterLimit, 0); const params: Record = { - includeGlobal: state.sessionsIncludeGlobal, - includeUnknown: state.sessionsIncludeUnknown, + includeGlobal, + includeUnknown, }; - const activeMinutes = toNumber(state.sessionsFilterActive, 0); - const limit = toNumber(state.sessionsFilterLimit, 0); if (activeMinutes > 0) params.activeMinutes = activeMinutes; if (limit > 0) params.limit = limit; const res = (await state.client.request("sessions.list", params)) as diff --git a/ui/src/ui/controllers/skills.ts b/ui/src/ui/controllers/skills.ts index 6e26a98ae..5708b12ef 100644 --- a/ui/src/ui/controllers/skills.ts +++ b/ui/src/ui/controllers/skills.ts @@ -45,9 +45,7 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions state.skillsLoading = true; state.skillsError = null; try { - const res = (await state.client.request("skills.status", {})) as - | SkillStatusReport - | undefined; + const res = (await state.client.request("skills.status", {})) as SkillStatusReport | undefined; if (res) state.skillsReport = res; } catch (err) { state.skillsError = getErrorMessage(err); @@ -56,19 +54,11 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions } } -export function updateSkillEdit( - state: SkillsState, - skillKey: string, - value: string, -) { +export function updateSkillEdit(state: SkillsState, skillKey: string, value: string) { state.skillEdits = { ...state.skillEdits, [skillKey]: value }; } -export async function updateSkillEnabled( - state: SkillsState, - skillKey: string, - enabled: boolean, -) { +export async function updateSkillEnabled(state: SkillsState, skillKey: string, enabled: boolean) { if (!state.client || !state.connected) return; state.skillsBusyKey = skillKey; state.skillsError = null; diff --git a/ui/src/ui/device-auth.ts b/ui/src/ui/device-auth.ts index 588c574a8..e06d50611 100644 --- a/ui/src/ui/device-auth.ts +++ b/ui/src/ui/device-auth.ts @@ -11,7 +11,7 @@ type DeviceAuthStore = { tokens: Record; }; -const STORAGE_KEY = "moltbot.device.auth.v1"; +const STORAGE_KEY = "openclaw.device.auth.v1"; function normalizeRole(role: string): string { return role.trim(); diff --git a/ui/src/ui/device-identity.ts b/ui/src/ui/device-identity.ts index dd2eccb8b..2070fbdc1 100644 --- a/ui/src/ui/device-identity.ts +++ b/ui/src/ui/device-identity.ts @@ -14,7 +14,7 @@ export type DeviceIdentity = { privateKey: string; }; -const STORAGE_KEY = "moltbot-device-identity-v1"; +const STORAGE_KEY = "openclaw-device-identity-v1"; function base64UrlEncode(bytes: Uint8Array): string { let binary = ""; diff --git a/ui/src/ui/focus-mode.browser.test.ts b/ui/src/ui/focus-mode.browser.test.ts index 157f8a0ab..1e8164d85 100644 --- a/ui/src/ui/focus-mode.browser.test.ts +++ b/ui/src/ui/focus-mode.browser.test.ts @@ -1,28 +1,27 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { OpenClawApp } from "./app"; -import { MoltbotApp } from "./app"; - -const originalConnect = MoltbotApp.prototype.connect; +const originalConnect = OpenClawApp.prototype.connect; function mountApp(pathname: string) { window.history.replaceState({}, "", pathname); - const app = document.createElement("moltbot-app") as MoltbotApp; + const app = document.createElement("openclaw-app") as OpenClawApp; document.body.append(app); return app; } beforeEach(() => { - MoltbotApp.prototype.connect = () => { + OpenClawApp.prototype.connect = () => { // no-op: avoid real gateway WS connections in browser tests }; - window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined; + window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined; localStorage.clear(); document.body.innerHTML = ""; }); afterEach(() => { - MoltbotApp.prototype.connect = originalConnect; - window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined; + OpenClawApp.prototype.connect = originalConnect; + window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined; localStorage.clear(); document.body.innerHTML = ""; }); @@ -36,9 +35,7 @@ describe("chat focus mode", () => { expect(shell).not.toBeNull(); expect(shell?.classList.contains("shell--chat-focus")).toBe(false); - const toggle = app.querySelector( - 'button[title^="Toggle focus mode"]', - ); + const toggle = app.querySelector('button[title^="Toggle focus mode"]'); expect(toggle).not.toBeNull(); toggle?.click(); @@ -47,9 +44,7 @@ describe("chat focus mode", () => { const link = app.querySelector('a.nav-item[href="/channels"]'); expect(link).not.toBeNull(); - link?.dispatchEvent( - new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }), - ); + link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 })); await app.updateComplete; expect(app.tab).toBe("channels"); diff --git a/ui/src/ui/format.test.ts b/ui/src/ui/format.test.ts index f8b1e8e56..b52dc0e4f 100644 --- a/ui/src/ui/format.test.ts +++ b/ui/src/ui/format.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { stripThinkingTags } from "./format"; describe("stripThinkingTags", () => { diff --git a/ui/src/ui/format.ts b/ui/src/ui/format.ts index 461195a7a..cdefd2f56 100644 --- a/ui/src/ui/format.ts +++ b/ui/src/ui/format.ts @@ -42,7 +42,10 @@ export function clampText(value: string, max = 120): string { return `${value.slice(0, Math.max(0, max - 1))}…`; } -export function truncateText(value: string, max: number): { +export function truncateText( + value: string, + max: number, +): { text: string; truncated: boolean; total: number; diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index fc8dde08a..3336e09b5 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -1,13 +1,13 @@ -import { generateUUID } from "./uuid"; +import { buildDeviceAuthPayload } from "../../../src/gateway/device-auth.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, type GatewayClientMode, type GatewayClientName, } from "../../../src/gateway/protocol/client-info.js"; -import { buildDeviceAuthPayload } from "../../../src/gateway/device-auth.js"; -import { loadOrCreateDeviceIdentity, signDevicePayload } from "./device-identity"; import { clearDeviceAuthToken, loadDeviceAuthToken, storeDeviceAuthToken } from "./device-auth"; +import { loadOrCreateDeviceIdentity, signDevicePayload } from "./device-identity"; +import { generateUUID } from "./uuid"; export type GatewayEventFrame = { type: "event"; diff --git a/ui/src/ui/icons.ts b/ui/src/ui/icons.ts index eaf8f0e27..b6e9c6b85 100644 --- a/ui/src/ui/icons.ts +++ b/ui/src/ui/icons.ts @@ -5,40 +5,223 @@ import { html, type TemplateResult } from "lit"; export const icons = { // Navigation icons - messageSquare: html``, - barChart: html``, - link: html``, - radio: html``, - fileText: html``, - zap: html``, - monitor: html``, - settings: html``, - bug: html``, - scrollText: html``, - folder: html``, + messageSquare: html` + + + + `, + barChart: html` + + + + + + `, + link: html` + + + + + `, + radio: html` + + + + + `, + fileText: html` + + + + + + + + `, + zap: html` + + `, + monitor: html` + + + + + + `, + settings: html` + + + + + `, + bug: html` + + + + + + + + + + + + + + `, + scrollText: html` + + + + + + + `, + folder: html` + + + + `, // UI icons - menu: html``, - x: html``, - check: html``, - copy: html``, - search: html``, - brain: html``, - book: html``, - loader: html``, + menu: html` + + + + + + `, + x: html` + + + + + `, + check: html` + + `, + copy: html` + + + + + `, + search: html` + + + + + `, + brain: html` + + + + + + + + + + + + `, + book: html` + + + + `, + loader: html` + + + + + + + + + + + `, // Tool icons - wrench: html``, - fileCode: html``, - edit: html``, - penLine: html``, - paperclip: html``, - globe: html``, - image: html``, - smartphone: html``, - plug: html``, - circle: html``, - puzzle: html``, + wrench: html` + + + + `, + fileCode: html` + + + + + + + `, + edit: html` + + + + + `, + penLine: html` + + + + + `, + paperclip: html` + + + + `, + globe: html` + + + + + + `, + image: html` + + + + + + `, + smartphone: html` + + + + + `, + plug: html` + + + + + + + `, + circle: html` + + `, + puzzle: html` + + + + `, } as const; export type IconName = keyof typeof icons; @@ -52,7 +235,10 @@ export function renderIcon(name: IconName, className = "nav-item__icon"): Templa } // Legacy function for compatibility -export function renderEmojiIcon(iconContent: string | TemplateResult, className: string): TemplateResult { +export function renderEmojiIcon( + iconContent: string | TemplateResult, + className: string, +): TemplateResult { return html``; } diff --git a/ui/src/ui/markdown.test.ts b/ui/src/ui/markdown.test.ts index 396ff0fa5..278485fe7 100644 --- a/ui/src/ui/markdown.test.ts +++ b/ui/src/ui/markdown.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { toSanitizedMarkdownHtml } from "./markdown"; describe("toSanitizedMarkdownHtml", () => { diff --git a/ui/src/ui/navigation.browser.test.ts b/ui/src/ui/navigation.browser.test.ts index 7a021e64f..2e4246840 100644 --- a/ui/src/ui/navigation.browser.test.ts +++ b/ui/src/ui/navigation.browser.test.ts @@ -1,13 +1,12 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { MoltbotApp } from "./app"; +import { OpenClawApp } from "./app"; import "../styles.css"; -const originalConnect = MoltbotApp.prototype.connect; +const originalConnect = OpenClawApp.prototype.connect; function mountApp(pathname: string) { window.history.replaceState({}, "", pathname); - const app = document.createElement("moltbot-app") as MoltbotApp; + const app = document.createElement("openclaw-app") as OpenClawApp; document.body.append(app); return app; } @@ -19,17 +18,17 @@ function nextFrame() { } beforeEach(() => { - MoltbotApp.prototype.connect = () => { + OpenClawApp.prototype.connect = () => { // no-op: avoid real gateway WS connections in browser tests }; - window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined; + window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined; localStorage.clear(); document.body.innerHTML = ""; }); afterEach(() => { - MoltbotApp.prototype.connect = originalConnect; - window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined; + OpenClawApp.prototype.connect = originalConnect; + window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined; localStorage.clear(); document.body.innerHTML = ""; }); @@ -53,35 +52,31 @@ describe("control UI routing", () => { }); it("infers nested base paths", async () => { - const app = mountApp("/apps/moltbot/cron"); + const app = mountApp("/apps/openclaw/cron"); await app.updateComplete; - expect(app.basePath).toBe("/apps/moltbot"); + expect(app.basePath).toBe("/apps/openclaw"); expect(app.tab).toBe("cron"); - expect(window.location.pathname).toBe("/apps/moltbot/cron"); + expect(window.location.pathname).toBe("/apps/openclaw/cron"); }); it("honors explicit base path overrides", async () => { - window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = "/moltbot"; - const app = mountApp("/moltbot/sessions"); + window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = "/openclaw"; + const app = mountApp("/openclaw/sessions"); await app.updateComplete; - expect(app.basePath).toBe("/moltbot"); + expect(app.basePath).toBe("/openclaw"); expect(app.tab).toBe("sessions"); - expect(window.location.pathname).toBe("/moltbot/sessions"); + expect(window.location.pathname).toBe("/openclaw/sessions"); }); it("updates the URL when clicking nav items", async () => { const app = mountApp("/chat"); await app.updateComplete; - const link = app.querySelector( - 'a.nav-item[href="/channels"]', - ); + const link = app.querySelector('a.nav-item[href="/channels"]'); expect(link).not.toBeNull(); - link?.dispatchEvent( - new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }), - ); + link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 })); await app.updateComplete; expect(app.tab).toBe("channels"); @@ -169,7 +164,7 @@ describe("control UI routing", () => { it("hydrates token from URL params even when settings already set", async () => { localStorage.setItem( - "moltbot.control.settings.v1", + "openclaw.control.settings.v1", JSON.stringify({ token: "existing-token" }), ); const app = mountApp("/ui/overview?token=abc123"); diff --git a/ui/src/ui/navigation.test.ts b/ui/src/ui/navigation.test.ts index 7b15deb4a..3348ad462 100644 --- a/ui/src/ui/navigation.test.ts +++ b/ui/src/ui/navigation.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; - import { TAB_GROUPS, iconForTab, @@ -73,7 +72,7 @@ describe("subtitleForTab", () => { it("returns descriptive subtitles", () => { expect(subtitleForTab("chat")).toContain("chat session"); - expect(subtitleForTab("config")).toContain("moltbot.json"); + expect(subtitleForTab("config")).toContain("openclaw.json"); }); }); @@ -95,7 +94,7 @@ describe("normalizeBasePath", () => { }); it("handles nested paths", () => { - expect(normalizeBasePath("/apps/moltbot")).toBe("/apps/moltbot"); + expect(normalizeBasePath("/apps/openclaw")).toBe("/apps/openclaw"); }); }); @@ -122,7 +121,7 @@ describe("pathForTab", () => { it("prepends base path", () => { expect(pathForTab("chat", "/ui")).toBe("/ui/chat"); - expect(pathForTab("sessions", "/apps/moltbot")).toBe("/apps/moltbot/sessions"); + expect(pathForTab("sessions", "/apps/openclaw")).toBe("/apps/openclaw/sessions"); }); }); @@ -139,7 +138,7 @@ describe("tabFromPath", () => { it("handles base paths", () => { expect(tabFromPath("/ui/chat", "/ui")).toBe("chat"); - expect(tabFromPath("/apps/moltbot/sessions", "/apps/moltbot")).toBe("sessions"); + expect(tabFromPath("/apps/openclaw/sessions", "/apps/openclaw")).toBe("sessions"); }); it("returns null for unknown path", () => { @@ -164,7 +163,7 @@ describe("inferBasePathFromPathname", () => { it("infers base path from nested paths", () => { expect(inferBasePathFromPathname("/ui/chat")).toBe("/ui"); - expect(inferBasePathFromPathname("/apps/moltbot/sessions")).toBe("/apps/moltbot"); + expect(inferBasePathFromPathname("/apps/openclaw/sessions")).toBe("/apps/openclaw"); }); it("handles index.html suffix", () => { diff --git a/ui/src/ui/navigation.ts b/ui/src/ui/navigation.ts index 966abec96..8557a21f4 100644 --- a/ui/src/ui/navigation.ts +++ b/ui/src/ui/navigation.ts @@ -37,9 +37,7 @@ const TAB_PATHS: Record = { logs: "/logs", }; -const PATH_TO_TAB = new Map( - Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]), -); +const PATH_TO_TAB = new Map(Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab])); export function normalizeBasePath(basePath: string): string { if (!basePath) return ""; @@ -177,7 +175,7 @@ export function subtitleForTab(tab: Tab) { case "chat": return "Direct gateway chat session for quick interventions."; case "config": - return "Edit ~/.clawdbot/moltbot.json safely."; + return "Edit ~/.openclaw/openclaw.json safely."; case "debug": return "Gateway snapshots, events, and manual RPC calls."; case "logs": diff --git a/ui/src/ui/presenter.ts b/ui/src/ui/presenter.ts index ddb99d9c5..0fd153321 100644 --- a/ui/src/ui/presenter.ts +++ b/ui/src/ui/presenter.ts @@ -1,5 +1,5 @@ -import { formatAgo, formatDurationMs, formatMs } from "./format"; import type { CronJob, GatewaySessionRow, PresenceEntry } from "./types"; +import { formatAgo, formatDurationMs, formatMs } from "./format"; export function formatPresenceSummary(entry: PresenceEntry): string { const host = entry.host ?? "unknown"; diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index 4b1836bfb..3e1214f11 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -1,4 +1,4 @@ -const KEY = "moltbot.control.settings.v1"; +const KEY = "openclaw.control.settings.v1"; import type { ThemeMode } from "./theme"; @@ -49,22 +49,16 @@ export function loadSettings(): UiSettings { ? parsed.sessionKey.trim() : defaults.sessionKey, lastActiveSessionKey: - typeof parsed.lastActiveSessionKey === "string" && - parsed.lastActiveSessionKey.trim() + typeof parsed.lastActiveSessionKey === "string" && parsed.lastActiveSessionKey.trim() ? parsed.lastActiveSessionKey.trim() - : (typeof parsed.sessionKey === "string" && - parsed.sessionKey.trim()) || + : (typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()) || defaults.lastActiveSessionKey, theme: - parsed.theme === "light" || - parsed.theme === "dark" || - parsed.theme === "system" + parsed.theme === "light" || parsed.theme === "dark" || parsed.theme === "system" ? parsed.theme : defaults.theme, chatFocusMode: - typeof parsed.chatFocusMode === "boolean" - ? parsed.chatFocusMode - : defaults.chatFocusMode, + typeof parsed.chatFocusMode === "boolean" ? parsed.chatFocusMode : defaults.chatFocusMode, chatShowThinking: typeof parsed.chatShowThinking === "boolean" ? parsed.chatShowThinking @@ -76,12 +70,9 @@ export function loadSettings(): UiSettings { ? parsed.splitRatio : defaults.splitRatio, navCollapsed: - typeof parsed.navCollapsed === "boolean" - ? parsed.navCollapsed - : defaults.navCollapsed, + typeof parsed.navCollapsed === "boolean" ? parsed.navCollapsed : defaults.navCollapsed, navGroupsCollapsed: - typeof parsed.navGroupsCollapsed === "object" && - parsed.navGroupsCollapsed !== null + typeof parsed.navGroupsCollapsed === "object" && parsed.navGroupsCollapsed !== null ? parsed.navGroupsCollapsed : defaults.navGroupsCollapsed, }; diff --git a/ui/src/ui/theme-transition.ts b/ui/src/ui/theme-transition.ts index b039c667b..10c3942c9 100644 --- a/ui/src/ui/theme-transition.ts +++ b/ui/src/ui/theme-transition.ts @@ -55,8 +55,7 @@ export const startThemeTransition = ({ const document_ = documentReference as DocumentWithViewTransition; const prefersReducedMotion = hasReducedMotionPreference(); - const canUseViewTransition = - Boolean(document_.startViewTransition) && !prefersReducedMotion; + const canUseViewTransition = Boolean(document_.startViewTransition) && !prefersReducedMotion; if (canUseViewTransition) { let xPercent = 0.5; @@ -71,11 +70,7 @@ export const startThemeTransition = ({ yPercent = clamp01(context.pointerClientY / window.innerHeight); } else if (context?.element) { const rect = context.element.getBoundingClientRect(); - if ( - rect.width > 0 && - rect.height > 0 && - typeof window !== "undefined" - ) { + if (rect.width > 0 && rect.height > 0 && typeof window !== "undefined") { xPercent = clamp01((rect.left + rect.width / 2) / window.innerWidth); yPercent = clamp01((rect.top + rect.height / 2) / window.innerHeight); } diff --git a/ui/src/ui/theme.ts b/ui/src/ui/theme.ts index 47bd5670b..4d2db4f27 100644 --- a/ui/src/ui/theme.ts +++ b/ui/src/ui/theme.ts @@ -5,9 +5,7 @@ export function getSystemTheme(): ResolvedTheme { if (typeof window === "undefined" || typeof window.matchMedia !== "function") { return "dark"; } - return window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } export function resolveTheme(mode: ThemeMode): ResolvedTheme { diff --git a/ui/src/ui/tool-display.json b/ui/src/ui/tool-display.json index 4a6bd0524..e4cea776e 100644 --- a/ui/src/ui/tool-display.json +++ b/ui/src/ui/tool-display.json @@ -90,7 +90,13 @@ }, "act": { "label": "act", - "detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"] + "detailKeys": [ + "request.kind", + "request.ref", + "request.selector", + "request.text", + "request.value" + ] } } }, @@ -117,9 +123,15 @@ "approve": { "label": "approve", "detailKeys": ["requestId"] }, "reject": { "label": "reject", "detailKeys": ["requestId"] }, "notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] }, - "camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] }, + "camera_snap": { + "label": "camera snap", + "detailKeys": ["node", "nodeId", "facing", "deviceId"] + }, "camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] }, - "camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] }, + "camera_clip": { + "label": "camera clip", + "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] + }, "screen_record": { "label": "screen record", "detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"] diff --git a/ui/src/ui/tool-display.ts b/ui/src/ui/tool-display.ts index 4b2de6ecb..4acbe0b47 100644 --- a/ui/src/ui/tool-display.ts +++ b/ui/src/ui/tool-display.ts @@ -1,5 +1,5 @@ -import rawConfig from "./tool-display.json"; import type { IconName } from "./icons"; +import rawConfig from "./tool-display.json"; type ToolDisplayActionSpec = { label?: string; @@ -153,8 +153,7 @@ export function resolveToolDisplay(params: { detail = resolveWriteDetail(params.args); } - const detailKeys = - actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? []; + const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? []; if (!detail && detailKeys.length > 0) { detail = resolveDetailFromKeys(params.args, detailKeys); } @@ -192,7 +191,5 @@ export function formatToolSummary(display: ToolDisplay): string { function shortenHomeInString(input: string): string { if (!input) return input; - return input - .replace(/\/Users\/[^/]+/g, "~") - .replace(/\/home\/[^/]+/g, "~"); + return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~"); } diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 1a5ec0731..b2ce740c5 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -514,13 +514,7 @@ export type StatusSummary = Record; export type HealthSnapshot = Record; -export type LogLevel = - | "trace" - | "debug" - | "info" - | "warn" - | "error" - | "fatal"; +export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; export type LogEntry = { raw: string; diff --git a/ui/src/ui/ui-types.ts b/ui/src/ui/ui-types.ts index 196d6d114..afb80c179 100644 --- a/ui/src/ui/ui-types.ts +++ b/ui/src/ui/ui-types.ts @@ -9,6 +9,7 @@ export type ChatQueueItem = { text: string; createdAt: number; attachments?: ChatAttachment[]; + refreshSessions?: boolean; }; export const CRON_CHANNEL_LAST = "last"; diff --git a/ui/src/ui/uuid.test.ts b/ui/src/ui/uuid.test.ts index 2d5421bdd..946a1866d 100644 --- a/ui/src/ui/uuid.test.ts +++ b/ui/src/ui/uuid.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; - import { generateUUID } from "./uuid"; describe("generateUUID", () => { diff --git a/ui/src/ui/views/channels.config.ts b/ui/src/ui/views/channels.config.ts index 3c4d2c7df..5c2eb6209 100644 --- a/ui/src/ui/views/channels.config.ts +++ b/ui/src/ui/views/channels.config.ts @@ -1,13 +1,7 @@ import { html } from "lit"; - import type { ConfigUiHints } from "../types"; import type { ChannelsProps } from "./channels.types"; -import { - analyzeConfigSchema, - renderNode, - schemaType, - type JsonSchema, -} from "./config-form"; +import { analyzeConfigSchema, renderNode, schemaType, type JsonSchema } from "./config-form"; type ChannelConfigFormProps = { channelId: string; @@ -61,9 +55,7 @@ function resolveChannelValue( (fromChannels && typeof fromChannels === "object" ? (fromChannels as Record) : null) ?? - (fallback && typeof fallback === "object" - ? (fallback as Record) - : null); + (fallback && typeof fallback === "object" ? (fallback as Record) : null); return resolved ?? {}; } @@ -71,11 +63,15 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) { const analysis = analyzeConfigSchema(props.schema); const normalized = analysis.schema; if (!normalized) { - return html`
Schema unavailable. Use Raw.
`; + return html` +
Schema unavailable. Use Raw.
+ `; } const node = resolveSchemaNode(normalized, ["channels", props.channelId]); if (!node) { - return html`
Channel config schema unavailable.
`; + return html` +
Channel config schema unavailable.
+ `; } const configValue = props.configValue ?? {}; const value = resolveChannelValue(configValue, props.channelId); @@ -95,24 +91,25 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) { `; } -export function renderChannelConfigSection(params: { - channelId: string; - props: ChannelsProps; -}) { +export function renderChannelConfigSection(params: { channelId: string; props: ChannelsProps }) { const { channelId, props } = params; const disabled = props.configSaving || props.configSchemaLoading; return html`
- ${props.configSchemaLoading - ? html`
Loading config schema…
` - : renderChannelConfigForm({ - channelId, - configValue: props.configForm, - schema: props.configSchema, - uiHints: props.configUiHints, - disabled, - onPatch: props.onConfigPatch, - })} + ${ + props.configSchemaLoading + ? html` +
Loading config schema…
+ ` + : renderChannelConfigForm({ + channelId, + configValue: props.configForm, + schema: props.configSchema, + uiHints: props.configUiHints, + disabled, + onPatch: props.onConfigPatch, + }) + }
- ${discord?.lastError - ? html`
+ ${ + discord?.lastError + ? html`
${discord.lastError}
` - : nothing} + : nothing + } - ${discord?.probe - ? html`
+ ${ + discord?.probe + ? html`
Probe ${discord.probe.ok ? "ok" : "failed"} · ${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "discord", props })} diff --git a/ui/src/ui/views/channels.googlechat.ts b/ui/src/ui/views/channels.googlechat.ts index a014ac89e..fcfeffd22 100644 --- a/ui/src/ui/views/channels.googlechat.ts +++ b/ui/src/ui/views/channels.googlechat.ts @@ -1,9 +1,8 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { GoogleChatStatus } from "../types"; -import { renderChannelConfigSection } from "./channels.config"; import type { ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; +import { renderChannelConfigSection } from "./channels.config"; export function renderGoogleChatCard(params: { props: ChannelsProps; @@ -34,9 +33,11 @@ export function renderGoogleChatCard(params: {
Audience - ${googleChat?.audienceType - ? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}` - : "n/a"} + ${ + googleChat?.audienceType + ? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}` + : "n/a" + }
@@ -49,18 +50,22 @@ export function renderGoogleChatCard(params: {
- ${googleChat?.lastError - ? html`
+ ${ + googleChat?.lastError + ? html`
${googleChat.lastError}
` - : nothing} + : nothing + } - ${googleChat?.probe - ? html`
+ ${ + googleChat?.probe + ? html`
Probe ${googleChat.probe.ok ? "ok" : "failed"} · ${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "googlechat", props })} diff --git a/ui/src/ui/views/channels.imessage.ts b/ui/src/ui/views/channels.imessage.ts index 85fd90d03..6a67b148c 100644 --- a/ui/src/ui/views/channels.imessage.ts +++ b/ui/src/ui/views/channels.imessage.ts @@ -1,8 +1,7 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { IMessageStatus } from "../types"; import type { ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; import { renderChannelConfigSection } from "./channels.config"; export function renderIMessageCard(params: { @@ -37,18 +36,22 @@ export function renderIMessageCard(params: {
- ${imessage?.lastError - ? html`
+ ${ + imessage?.lastError + ? html`
${imessage.lastError}
` - : nothing} + : nothing + } - ${imessage?.probe - ? html`
+ ${ + imessage?.probe + ? html`
Probe ${imessage.probe.ok ? "ok" : "failed"} · ${imessage.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "imessage", props })} diff --git a/ui/src/ui/views/channels.nostr-profile-form.ts b/ui/src/ui/views/channels.nostr-profile-form.ts index 8565d8ef9..a18d1c981 100644 --- a/ui/src/ui/views/channels.nostr-profile-form.ts +++ b/ui/src/ui/views/channels.nostr-profile-form.ts @@ -5,7 +5,6 @@ */ import { html, nothing, type TemplateResult } from "lit"; - import type { NostrProfile as NostrProfileType } from "../types"; // ============================================================================ @@ -82,7 +81,7 @@ export function renderNostrProfileForm(params: { placeholder?: string; maxLength?: number; help?: string; - } = {} + } = {}, ) => { const { type = "text", placeholder, maxLength, help } = opts; const value = state.values[field] ?? ""; @@ -169,13 +168,17 @@ export function renderNostrProfileForm(params: {
Account: ${accountId}
- ${state.error - ? html`
${state.error}
` - : nothing} + ${ + state.error + ? html`
${state.error}
` + : nothing + } - ${state.success - ? html`
${state.success}
` - : nothing} + ${ + state.success + ? html`
${state.success}
` + : nothing + } ${renderPicturePreview()} @@ -204,8 +207,9 @@ export function renderNostrProfileForm(params: { help: "HTTPS URL to your profile picture", })} - ${state.showAdvanced - ? html` + ${ + state.showAdvanced + ? html`
Advanced
@@ -232,7 +236,8 @@ export function renderNostrProfileForm(params: { })}
` - : nothing} + : nothing + }
- ${isDirty - ? html`
- You have unsaved changes -
` - : nothing} + ${ + isDirty + ? html` +
+ You have unsaved changes +
+ ` + : nothing + }
`; } @@ -284,7 +293,7 @@ export function renderNostrProfileForm(params: { * Create initial form state from existing profile */ export function createNostrProfileFormState( - profile: NostrProfileType | undefined + profile: NostrProfileType | undefined, ): NostrProfileFormState { const values: NostrProfileType = { name: profile?.name ?? "", @@ -305,8 +314,6 @@ export function createNostrProfileFormState( error: null, success: null, fieldErrors: {}, - showAdvanced: Boolean( - profile?.banner || profile?.website || profile?.nip05 || profile?.lud16 - ), + showAdvanced: Boolean(profile?.banner || profile?.website || profile?.nip05 || profile?.lud16), }; } diff --git a/ui/src/ui/views/channels.nostr.ts b/ui/src/ui/views/channels.nostr.ts index 05152d80b..0792f8046 100644 --- a/ui/src/ui/views/channels.nostr.ts +++ b/ui/src/ui/views/channels.nostr.ts @@ -1,8 +1,7 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { ChannelAccountSnapshot, NostrStatus } from "../types"; import type { ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; import { renderChannelConfigSection } from "./channels.config"; import { renderNostrProfileForm, @@ -44,8 +43,7 @@ export function renderNostrCard(params: { const summaryConfigured = nostr?.configured ?? primaryAccount?.configured ?? false; const summaryRunning = nostr?.running ?? primaryAccount?.running ?? false; const summaryPublicKey = - nostr?.publicKey ?? - (primaryAccount as { publicKey?: string } | undefined)?.publicKey; + nostr?.publicKey ?? (primaryAccount as { publicKey?: string } | undefined)?.publicKey; const summaryLastStartAt = nostr?.lastStartAt ?? primaryAccount?.lastStartAt ?? null; const summaryLastError = nostr?.lastError ?? primaryAccount?.lastError ?? null; const hasMultipleAccounts = nostrAccounts.length > 1; @@ -79,11 +77,13 @@ export function renderNostrCard(params: { Last inbound ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}
- ${account.lastError - ? html` + ${ + account.lastError + ? html` ` - : nothing} + : nothing + }
`; @@ -100,17 +100,19 @@ export function renderNostrCard(params: { } const profile = - (primaryAccount as - | { - profile?: { - name?: string; - displayName?: string; - about?: string; - picture?: string; - nip05?: string; - }; - } - | undefined)?.profile ?? nostr?.profile; + ( + primaryAccount as + | { + profile?: { + name?: string; + displayName?: string; + about?: string; + picture?: string; + nip05?: string; + }; + } + | undefined + )?.profile ?? nostr?.profile; const { name, displayName, about, picture, nip05 } = profile ?? {}; const hasAnyProfileData = name || displayName || about || picture || nip05; @@ -118,8 +120,9 @@ export function renderNostrCard(params: {
Profile
- ${summaryConfigured - ? html` + ${ + summaryConfigured + ? html`
- ${hasAnyProfileData - ? html` + ${ + hasAnyProfileData + ? html`
- ${picture - ? html` + ${ + picture + ? html`
` - : nothing} + : nothing + } ${name ? html`
Name${name}
` : nothing} - ${displayName - ? html`
Display Name${displayName}
` - : nothing} - ${about - ? html`
About${about}
` - : nothing} + ${ + displayName + ? html`
Display Name${displayName}
` + : nothing + } + ${ + about + ? html`
About${about}
` + : nothing + } ${nip05 ? html`
NIP-05${nip05}
` : nothing}
` - : html` -
- No profile set. Click "Edit Profile" to add your name, bio, and avatar. -
- `} + : html` +
+ No profile set. Click "Edit Profile" to add your name, bio, and avatar. +
+ ` + }
`; }; @@ -172,13 +184,14 @@ export function renderNostrCard(params: {
Decentralized DMs via Nostr relays (NIP-04).
${accountCountLabel} - ${hasMultipleAccounts - ? html` + ${ + hasMultipleAccounts + ? html` ` - : html` + : html`
Configured @@ -199,11 +212,14 @@ export function renderNostrCard(params: { ${summaryLastStartAt ? formatAgo(summaryLastStartAt) : "n/a"}
- `} + ` + } - ${summaryLastError - ? html`
${summaryLastError}
` - : nothing} + ${ + summaryLastError + ? html`
${summaryLastError}
` + : nothing + } ${renderProfileSection()} diff --git a/ui/src/ui/views/channels.shared.ts b/ui/src/ui/views/channels.shared.ts index 9af0c2ea1..7da38a713 100644 --- a/ui/src/ui/views/channels.shared.ts +++ b/ui/src/ui/views/channels.shared.ts @@ -1,5 +1,4 @@ import { html, nothing } from "lit"; - import type { ChannelAccountSnapshot } from "../types"; import type { ChannelKey, ChannelsProps } from "./channels.types"; diff --git a/ui/src/ui/views/channels.signal.ts b/ui/src/ui/views/channels.signal.ts index 9d4f6c147..050b14bb8 100644 --- a/ui/src/ui/views/channels.signal.ts +++ b/ui/src/ui/views/channels.signal.ts @@ -1,8 +1,7 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { SignalStatus } from "../types"; import type { ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; import { renderChannelConfigSection } from "./channels.config"; export function renderSignalCard(params: { @@ -41,18 +40,22 @@ export function renderSignalCard(params: { - ${signal?.lastError - ? html`
+ ${ + signal?.lastError + ? html`
${signal.lastError}
` - : nothing} + : nothing + } - ${signal?.probe - ? html`
+ ${ + signal?.probe + ? html`
Probe ${signal.probe.ok ? "ok" : "failed"} · ${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "signal", props })} diff --git a/ui/src/ui/views/channels.slack.ts b/ui/src/ui/views/channels.slack.ts index eb93ac4c3..d018f40ef 100644 --- a/ui/src/ui/views/channels.slack.ts +++ b/ui/src/ui/views/channels.slack.ts @@ -1,8 +1,7 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { SlackStatus } from "../types"; import type { ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; import { renderChannelConfigSection } from "./channels.config"; export function renderSlackCard(params: { @@ -37,18 +36,22 @@ export function renderSlackCard(params: {
- ${slack?.lastError - ? html`
+ ${ + slack?.lastError + ? html`
${slack.lastError}
` - : nothing} + : nothing + } - ${slack?.probe - ? html`
+ ${ + slack?.probe + ? html`
Probe ${slack.probe.ok ? "ok" : "failed"} · ${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "slack", props })} diff --git a/ui/src/ui/views/channels.telegram.ts b/ui/src/ui/views/channels.telegram.ts index 498d98f87..d2347c0f8 100644 --- a/ui/src/ui/views/channels.telegram.ts +++ b/ui/src/ui/views/channels.telegram.ts @@ -1,8 +1,7 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { ChannelAccountSnapshot, TelegramStatus } from "../types"; import type { ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; import { renderChannelConfigSection } from "./channels.config"; export function renderTelegramCard(params: { @@ -39,13 +38,15 @@ export function renderTelegramCard(params: { Last inbound ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}
- ${account.lastError - ? html` + ${ + account.lastError + ? html` ` - : nothing} + : nothing + }
`; @@ -57,13 +58,14 @@ export function renderTelegramCard(params: {
Bot status and channel configuration.
${accountCountLabel} - ${hasMultipleAccounts - ? html` + ${ + hasMultipleAccounts + ? html` ` - : html` + : html`
Configured @@ -86,20 +88,25 @@ export function renderTelegramCard(params: { ${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : "n/a"}
- `} + ` + } - ${telegram?.lastError - ? html`
+ ${ + telegram?.lastError + ? html`
${telegram.lastError}
` - : nothing} + : nothing + } - ${telegram?.probe - ? html`
+ ${ + telegram?.probe + ? html`
Probe ${telegram.probe.ok ? "ok" : "failed"} · ${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: "telegram", props })} diff --git a/ui/src/ui/views/channels.ts b/ui/src/ui/views/channels.ts index a0fce8f40..444b22e59 100644 --- a/ui/src/ui/views/channels.ts +++ b/ui/src/ui/views/channels.ts @@ -1,6 +1,4 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { ChannelAccountSnapshot, ChannelUiMetaEntry, @@ -15,17 +13,14 @@ import type { TelegramStatus, WhatsAppStatus, } from "../types"; -import type { - ChannelKey, - ChannelsChannelData, - ChannelsProps, -} from "./channels.types"; -import { channelEnabled, renderChannelAccountCount } from "./channels.shared"; +import type { ChannelKey, ChannelsChannelData, ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; import { renderChannelConfigSection } from "./channels.config"; import { renderDiscordCard } from "./channels.discord"; import { renderGoogleChatCard } from "./channels.googlechat"; import { renderIMessageCard } from "./channels.imessage"; import { renderNostrCard } from "./channels.nostr"; +import { channelEnabled, renderChannelAccountCount } from "./channels.shared"; import { renderSignalCard } from "./channels.signal"; import { renderSlackCard } from "./channels.slack"; import { renderTelegramCard } from "./channels.telegram"; @@ -33,12 +28,8 @@ import { renderWhatsAppCard } from "./channels.whatsapp"; export function renderChannels(props: ChannelsProps) { const channels = props.snapshot?.channels as Record | null; - const whatsapp = (channels?.whatsapp ?? undefined) as - | WhatsAppStatus - | undefined; - const telegram = (channels?.telegram ?? undefined) as - | TelegramStatus - | undefined; + const whatsapp = (channels?.whatsapp ?? undefined) as WhatsAppStatus | undefined; + const telegram = (channels?.telegram ?? undefined) as TelegramStatus | undefined; const discord = (channels?.discord ?? null) as DiscordStatus | null; const googlechat = (channels?.googlechat ?? null) as GoogleChatStatus | null; const slack = (channels?.slack ?? null) as SlackStatus | null; @@ -82,11 +73,13 @@ export function renderChannels(props: ChannelsProps) {
${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}
- ${props.lastError - ? html`
+ ${ + props.lastError + ? html`
${props.lastError}
` - : nothing} + : nothing + }
 ${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
       
@@ -101,27 +94,11 @@ function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKe if (snapshot?.channelOrder?.length) { return snapshot.channelOrder; } - return [ - "whatsapp", - "telegram", - "discord", - "googlechat", - "slack", - "signal", - "imessage", - "nostr", - ]; + return ["whatsapp", "telegram", "discord", "googlechat", "slack", "signal", "imessage", "nostr"]; } -function renderChannel( - key: ChannelKey, - props: ChannelsProps, - data: ChannelsChannelData, -) { - const accountCountLabel = renderChannelAccountCount( - key, - data.channelAccounts, - ); +function renderChannel(key: ChannelKey, props: ChannelsProps, data: ChannelsChannelData) { + const accountCountLabel = renderChannelAccountCount(key, data.channelAccounts); switch (key) { case "whatsapp": return renderWhatsAppCard({ @@ -218,13 +195,14 @@ function renderGenericChannelCard(
Channel status and configuration.
${accountCountLabel} - ${accounts.length > 0 - ? html` + ${ + accounts.length > 0 + ? html` ` - : html` + : html`
Configured @@ -239,13 +217,16 @@ function renderGenericChannelCard( ${connected == null ? "n/a" : connected ? "Yes" : "No"}
- `} + ` + } - ${lastError - ? html`
+ ${ + lastError + ? html`
${lastError}
` - : nothing} + : nothing + } ${renderChannelConfigSection({ channelId: key, props })}
@@ -259,10 +240,7 @@ function resolveChannelMetaMap( return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry])); } -function resolveChannelLabel( - snapshot: ChannelsStatusSnapshot | null, - key: string, -): string { +function resolveChannelLabel(snapshot: ChannelsStatusSnapshot | null, key: string): string { const meta = resolveChannelMetaMap(snapshot)[key]; return meta?.label ?? snapshot?.channelLabels?.[key] ?? key; } @@ -316,13 +294,15 @@ function renderGenericAccount(account: ChannelAccountSnapshot) { Last inbound ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}
- ${account.lastError - ? html` + ${ + account.lastError + ? html` ` - : nothing} + : nothing + } `; diff --git a/ui/src/ui/views/channels.types.ts b/ui/src/ui/views/channels.types.ts index b69507bf3..fa1a9094b 100644 --- a/ui/src/ui/views/channels.types.ts +++ b/ui/src/ui/views/channels.types.ts @@ -1,16 +1,16 @@ import type { - ChannelAccountSnapshot, - ChannelsStatusSnapshot, - ConfigUiHints, - DiscordStatus, - GoogleChatStatus, - IMessageStatus, - NostrProfile, - NostrStatus, - SignalStatus, - SlackStatus, - TelegramStatus, - WhatsAppStatus, + ChannelAccountSnapshot, + ChannelsStatusSnapshot, + ConfigUiHints, + DiscordStatus, + GoogleChatStatus, + IMessageStatus, + NostrProfile, + NostrStatus, + SignalStatus, + SlackStatus, + TelegramStatus, + WhatsAppStatus, } from "../types"; import type { NostrProfileFormState } from "./channels.nostr-profile-form"; diff --git a/ui/src/ui/views/channels.whatsapp.ts b/ui/src/ui/views/channels.whatsapp.ts index eae3be695..ad1fcf7a9 100644 --- a/ui/src/ui/views/channels.whatsapp.ts +++ b/ui/src/ui/views/channels.whatsapp.ts @@ -1,8 +1,7 @@ import { html, nothing } from "lit"; - -import { formatAgo } from "../format"; import type { WhatsAppStatus } from "../types"; import type { ChannelsProps } from "./channels.types"; +import { formatAgo } from "../format"; import { renderChannelConfigSection } from "./channels.config"; import { formatDuration } from "./channels.shared"; @@ -39,9 +38,7 @@ export function renderWhatsAppCard(params: {
Last connect - ${whatsapp?.lastConnectedAt - ? formatAgo(whatsapp.lastConnectedAt) - : "n/a"} + ${whatsapp?.lastConnectedAt ? formatAgo(whatsapp.lastConnectedAt) : "n/a"}
@@ -53,30 +50,34 @@ export function renderWhatsAppCard(params: {
Auth age - ${whatsapp?.authAgeMs != null - ? formatDuration(whatsapp.authAgeMs) - : "n/a"} + ${whatsapp?.authAgeMs != null ? formatDuration(whatsapp.authAgeMs) : "n/a"}
- ${whatsapp?.lastError - ? html`
+ ${ + whatsapp?.lastError + ? html`
${whatsapp.lastError}
` - : nothing} + : nothing + } - ${props.whatsappMessage - ? html`
+ ${ + props.whatsappMessage + ? html`
${props.whatsappMessage}
` - : nothing} + : nothing + } - ${props.whatsappQrDataUrl - ? html`
+ ${ + props.whatsappQrDataUrl + ? html`
WhatsApp QR
` - : nothing} + : nothing + }
`; return html`
- ${props.disabledReason - ? html`
${props.disabledReason}
` - : nothing} + ${props.disabledReason ? html`
${props.disabledReason}
` : nothing} - ${props.error - ? html`
${props.error}
` - : nothing} + ${props.error ? html`
${props.error}
` : nothing} ${renderCompactionIndicator(props.compactionStatus)} - ${props.focusMode - ? html` + ${ + props.focusMode + ? html`
` - : nothing} + : nothing + }
${renderAttachmentPreview(props)} diff --git a/ui/src/ui/views/config-form.analyze.ts b/ui/src/ui/views/config-form.analyze.ts index 5e72ffff1..a6451806c 100644 --- a/ui/src/ui/views/config-form.analyze.ts +++ b/ui/src/ui/views/config-form.analyze.ts @@ -47,8 +47,7 @@ function normalizeSchemaNode( const nullable = Array.isArray(schema.type) && schema.type.includes("null"); const type = - schemaType(schema) ?? - (schema.properties || schema.additionalProperties ? "object" : undefined); + schemaType(schema) ?? (schema.properties || schema.additionalProperties ? "object" : undefined); normalized.type = type ?? schema.type; normalized.nullable = nullable || schema.nullable; @@ -73,24 +72,15 @@ function normalizeSchemaNode( unsupported.add(pathLabel); } else if (schema.additionalProperties === false) { normalized.additionalProperties = false; - } else if ( - schema.additionalProperties && - typeof schema.additionalProperties === "object" - ) { + } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") { if (!isAnySchema(schema.additionalProperties as JsonSchema)) { - const res = normalizeSchemaNode( - schema.additionalProperties as JsonSchema, - [...path, "*"], - ); - normalized.additionalProperties = - res.schema ?? (schema.additionalProperties as JsonSchema); + const res = normalizeSchemaNode(schema.additionalProperties as JsonSchema, [...path, "*"]); + normalized.additionalProperties = res.schema ?? (schema.additionalProperties as JsonSchema); if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel); } } } else if (type === "array") { - const itemsSchema = Array.isArray(schema.items) - ? schema.items[0] - : schema.items; + const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items; if (!itemsSchema) { unsupported.add(pathLabel); } else { diff --git a/ui/src/ui/views/config-form.node.ts b/ui/src/ui/views/config-form.node.ts index 17a182281..768db4508 100644 --- a/ui/src/ui/views/config-form.node.ts +++ b/ui/src/ui/views/config-form.node.ts @@ -28,11 +28,69 @@ function jsonValue(value: unknown): string { // SVG Icons as template literals const icons = { - chevronDown: html``, - plus: html``, - minus: html``, - trash: html``, - edit: html``, + chevronDown: html` + + + + `, + plus: html` + + + + + `, + minus: html` + + + + `, + trash: html` + + + + + `, + edit: html` + + + + + `, }; export function renderNode(params: { @@ -64,7 +122,7 @@ export function renderNode(params: { if (schema.anyOf || schema.oneOf) { const variants = schema.anyOf ?? schema.oneOf ?? []; const nonNull = variants.filter( - (v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null"))) + (v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null"))), ); if (nonNull.length === 1) { @@ -88,16 +146,18 @@ export function renderNode(params: { ${showLabel ? html`` : nothing} ${help ? html`
${help}
` : nothing}
- ${literals.map((lit, idx) => html` + ${literals.map( + (lit, idx) => html` - `)} + `, + )}
`; @@ -109,11 +169,9 @@ export function renderNode(params: { } // Handle mixed primitive types - const primitiveTypes = new Set( - nonNull.map((variant) => schemaType(variant)).filter(Boolean) - ); + const primitiveTypes = new Set(nonNull.map((variant) => schemaType(variant)).filter(Boolean)); const normalizedTypes = new Set( - [...primitiveTypes].map((v) => (v === "integer" ? "number" : v)) + [...primitiveTypes].map((v) => (v === "integer" ? "number" : v)), ); if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) { @@ -147,16 +205,18 @@ export function renderNode(params: { ${showLabel ? html`` : nothing} ${help ? html`
${help}
` : nothing}
- ${options.map((opt) => html` + ${options.map( + (opt) => html` - `)} + `, + )}
`; @@ -176,9 +236,14 @@ export function renderNode(params: { // Boolean - toggle row if (type === "boolean") { - const displayValue = typeof value === "boolean" ? value : typeof schema.default === "boolean" ? schema.default : false; + const displayValue = + typeof value === "boolean" + ? value + : typeof schema.default === "boolean" + ? schema.default + : false; return html` -
`; @@ -365,9 +434,11 @@ function renderSelect(params: { }} > - ${options.map((opt, idx) => html` + ${options.map( + (opt, idx) => html` - `)} + `, + )} `; @@ -390,9 +461,10 @@ function renderObject(params: { const help = hint?.help ?? schema.description; const fallback = value ?? schema.default; - const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback) - ? (fallback as Record) - : {}; + const obj = + fallback && typeof fallback === "object" && !Array.isArray(fallback) + ? (fallback as Record) + : {}; const props = schema.properties ?? {}; const entries = Object.entries(props); @@ -421,18 +493,22 @@ function renderObject(params: { unsupported, disabled, onPatch, - }) + }), )} - ${allowExtra ? renderMapField({ - schema: additional as JsonSchema, - value: obj, - path, - hints, - unsupported, - disabled, - reservedKeys: reserved, - onPatch, - }) : nothing} + ${ + allowExtra + ? renderMapField({ + schema: additional as JsonSchema, + value: obj, + path, + hints, + unsupported, + disabled, + reservedKeys: reserved, + onPatch, + }) + : nothing + } `; } @@ -455,18 +531,22 @@ function renderObject(params: { unsupported, disabled, onPatch, - }) + }), )} - ${allowExtra ? renderMapField({ - schema: additional as JsonSchema, - value: obj, - path, - hints, - unsupported, - disabled, - reservedKeys: reserved, - onPatch, - }) : nothing} + ${ + allowExtra + ? renderMapField({ + schema: additional as JsonSchema, + value: obj, + path, + hints, + unsupported, + disabled, + reservedKeys: reserved, + onPatch, + }) + : nothing + } `; @@ -504,7 +584,7 @@ function renderArray(params: {
${showLabel ? html`${label}` : nothing} - ${arr.length} item${arr.length !== 1 ? 's' : ''} + ${arr.length} item${arr.length !== 1 ? "s" : ""}
`; } @@ -603,9 +687,12 @@ function renderMapField(params: {
- ${entries.length === 0 ? html` -
No custom entries.
- ` : html` + ${ + entries.length === 0 + ? html` +
No custom entries.
+ ` + : html`
${entries.map(([key, entryValue]) => { const valuePath = [...path, key]; @@ -631,8 +718,9 @@ function renderMapField(params: { />
- ${anySchema - ? html` + ${ + anySchema + ? html` ` - : renderNode({ - schema, - value: entryValue, - path: valuePath, - hints, - unsupported, - disabled, - showLabel: false, - onPatch, - })} + : renderNode({ + schema, + value: entryValue, + path: valuePath, + hints, + unsupported, + disabled, + showLabel: false, + onPatch, + }) + }
- ` : nothing} + ` + : nothing + } @@ -289,7 +477,8 @@ export function renderConfig(props: ConfigProps) { ${sidebarIcons.all} All Settings - ${allSections.map(section => html` + ${allSections.map( + (section) => html` - `)} + `, + )} @@ -325,11 +515,15 @@ export function renderConfig(props: ConfigProps) {
- ${hasChanges ? html` + ${ + hasChanges + ? html` ${props.formMode === "raw" ? "Unsaved changes" : `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`} - ` : html` - No changes - `} + ` + : html` + No changes + ` + }
- ${hasChanges && props.formMode === "form" ? html` + ${ + hasChanges && props.formMode === "form" + ? html`
View ${diff.length} pending change${diff.length !== 1 ? "s" : ""} @@ -369,7 +565,8 @@ export function renderConfig(props: ConfigProps) {
- ${diff.map(change => html` + ${diff.map( + (change) => html`
${change.path}
@@ -378,27 +575,35 @@ export function renderConfig(props: ConfigProps) { ${truncateValue(change.to)}
- `)} + `, + )}
- ` : nothing} + ` + : nothing + } - ${activeSectionMeta && props.formMode === "form" - ? html` + ${ + activeSectionMeta && props.formMode === "form" + ? html`
${getSectionIcon(props.activeSection ?? "")}
${activeSectionMeta.label}
- ${activeSectionMeta.description - ? html`
${activeSectionMeta.description}
` - : nothing} + ${ + activeSectionMeta.description + ? html`
${activeSectionMeta.description}
` + : nothing + }
` - : nothing} + : nothing + } - ${allowSubnav - ? html` + ${ + allowSubnav + ? html`
- ${props.callError - ? html`
+ ${ + props.callError + ? html`
${props.callError}
` - : nothing} - ${props.callResult - ? html`
${props.callResult}
` - : nothing} + : nothing + } + ${ + props.callResult + ? html`
${props.callResult}
` + : nothing + }
@@ -121,9 +121,12 @@ export function renderDebug(props: DebugProps) {
Event Log
Latest gateway events.
- ${props.eventLog.length === 0 - ? html`
No events yet.
` - : html` + ${ + props.eventLog.length === 0 + ? html` +
No events yet.
+ ` + : html`
${props.eventLog.map( (evt) => html` @@ -139,7 +142,8 @@ export function renderDebug(props: DebugProps) { `, )}
- `} + ` + }
`; } diff --git a/ui/src/ui/views/exec-approval.ts b/ui/src/ui/views/exec-approval.ts index 548d56683..33efc947f 100644 --- a/ui/src/ui/views/exec-approval.ts +++ b/ui/src/ui/views/exec-approval.ts @@ -1,5 +1,4 @@ import { html, nothing } from "lit"; - import type { AppViewState } from "../app-view-state"; function formatRemaining(ms: number): string { @@ -32,9 +31,11 @@ export function renderExecApprovalPrompt(state: AppViewState) {
Exec approval needed
${remaining}
- ${queueCount > 1 - ? html`
${queueCount} pending
` - : nothing} + ${ + queueCount > 1 + ? html`
${queueCount} pending
` + : nothing + }
${request.command}
@@ -46,9 +47,11 @@ export function renderExecApprovalPrompt(state: AppViewState) { ${renderMetaRow("Security", request.security)} ${renderMetaRow("Ask", request.ask)}
- ${state.execApprovalError - ? html`
${state.execApprovalError}
` - : nothing} + ${ + state.execApprovalError + ? html`
${state.execApprovalError}
` + : nothing + }
- ${props.lastError - ? html`
+ ${ + props.lastError + ? html`
${props.lastError}
` - : nothing} - ${props.statusMessage - ? html`
+ : nothing + } + ${ + props.statusMessage + ? html`
${props.statusMessage}
` - : nothing} + : nothing + }
- ${props.entries.length === 0 - ? html`
No instances reported yet.
` - : props.entries.map((entry) => renderEntry(entry))} + ${ + props.entries.length === 0 + ? html` +
No instances reported yet.
+ ` + : props.entries.map((entry) => renderEntry(entry)) + }
`; } function renderEntry(entry: PresenceEntry) { - const lastInput = - entry.lastInputSeconds != null - ? `${entry.lastInputSeconds}s ago` - : "n/a"; + const lastInput = entry.lastInputSeconds != null ? `${entry.lastInputSeconds}s ago` : "n/a"; const mode = entry.mode ?? "unknown"; const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : []; const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : []; @@ -66,12 +70,12 @@ function renderEntry(entry: PresenceEntry) { ${roles.map((role) => html`${role}`)} ${scopesLabel ? html`${scopesLabel}` : nothing} ${entry.platform ? html`${entry.platform}` : nothing} - ${entry.deviceFamily - ? html`${entry.deviceFamily}` - : nothing} - ${entry.modelIdentifier - ? html`${entry.modelIdentifier}` - : nothing} + ${entry.deviceFamily ? html`${entry.deviceFamily}` : nothing} + ${ + entry.modelIdentifier + ? html`${entry.modelIdentifier}` + : nothing + } ${entry.version ? html`${entry.version}` : nothing}
diff --git a/ui/src/ui/views/logs.ts b/ui/src/ui/views/logs.ts index 6da434dbe..7962c0a10 100644 --- a/ui/src/ui/views/logs.ts +++ b/ui/src/ui/views/logs.ts @@ -1,5 +1,4 @@ import { html, nothing } from "lit"; - import type { LogEntry, LogLevel } from "../types"; const LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"]; @@ -60,7 +59,11 @@ export function renderLogs(props: LogsProps) { @@ -72,8 +75,7 @@ export function renderLogs(props: LogsProps) { Filter - props.onFilterTextChange((e.target as HTMLInputElement).value)} + @input=${(e: Event) => props.onFilterTextChange((e.target as HTMLInputElement).value)} placeholder="Search logs" /> @@ -104,23 +106,32 @@ export function renderLogs(props: LogsProps) { )} - ${props.file - ? html`
File: ${props.file}
` - : nothing} - ${props.truncated - ? html`
- Log output truncated; showing latest chunk. -
` - : nothing} - ${props.error - ? html`
${props.error}
` - : nothing} + ${ + props.file + ? html`
File: ${props.file}
` + : nothing + } + ${ + props.truncated + ? html` +
Log output truncated; showing latest chunk.
+ ` + : nothing + } + ${ + props.error + ? html`
${props.error}
` + : nothing + }
- ${filtered.length === 0 - ? html`
No log entries.
` - : filtered.map( - (entry) => html` + ${ + filtered.length === 0 + ? html` +
No log entries.
+ ` + : filtered.map( + (entry) => html`
${formatTime(entry.time)}
${entry.level ?? ""}
@@ -128,7 +139,8 @@ export function renderLogs(props: LogsProps) {
${entry.message ?? entry.raw}
`, - )} + ) + }
`; diff --git a/ui/src/ui/views/markdown-sidebar.ts b/ui/src/ui/views/markdown-sidebar.ts index 828c524a3..285e2bf11 100644 --- a/ui/src/ui/views/markdown-sidebar.ts +++ b/ui/src/ui/views/markdown-sidebar.ts @@ -1,6 +1,5 @@ import { html, nothing } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; - import { icons } from "../icons"; import { toSanitizedMarkdownHtml } from "../markdown"; @@ -21,16 +20,20 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) { `; diff --git a/ui/src/ui/views/nodes.ts b/ui/src/ui/views/nodes.ts index 31beca988..592281036 100644 --- a/ui/src/ui/views/nodes.ts +++ b/ui/src/ui/views/nodes.ts @@ -1,17 +1,16 @@ import { html, nothing } from "lit"; - -import { clampText, formatAgo, formatList } from "../format"; -import type { - ExecApprovalsAllowlistEntry, - ExecApprovalsFile, - ExecApprovalsSnapshot, -} from "../controllers/exec-approvals"; import type { DevicePairingList, DeviceTokenSummary, PairedDevice, PendingDevice, } from "../controllers/devices"; +import type { + ExecApprovalsAllowlistEntry, + ExecApprovalsFile, + ExecApprovalsSnapshot, +} from "../controllers/exec-approvals"; +import { clampText, formatAgo, formatList } from "../format"; export type NodesProps = { loading: boolean; @@ -68,9 +67,13 @@ export function renderNodes(props: NodesProps) {
- ${props.nodes.length === 0 - ? html`
No nodes found.
` - : props.nodes.map((n) => renderNode(n))} + ${ + props.nodes.length === 0 + ? html` +
No nodes found.
+ ` + : props.nodes.map((n) => renderNode(n)) + }
`; @@ -91,25 +94,35 @@ function renderDevices(props: NodesProps) { ${props.devicesLoading ? "Loading…" : "Refresh"} - ${props.devicesError - ? html`
${props.devicesError}
` - : nothing} + ${ + props.devicesError + ? html`
${props.devicesError}
` + : nothing + }
- ${pending.length > 0 - ? html` + ${ + pending.length > 0 + ? html`
Pending
${pending.map((req) => renderPendingDevice(req, props))} ` - : nothing} - ${paired.length > 0 - ? html` + : nothing + } + ${ + paired.length > 0 + ? html`
Paired
${paired.map((device) => renderPairedDevice(device, props))} ` - : nothing} - ${pending.length === 0 && paired.length === 0 - ? html`
No paired devices.
` - : nothing} + : nothing + } + ${ + pending.length === 0 && paired.length === 0 + ? html` +
No paired devices.
+ ` + : nothing + }
`; @@ -156,14 +169,18 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
${name}
${device.deviceId}${ip}
${roles} · ${scopes}
- ${tokens.length === 0 - ? html`
Tokens: none
` - : html` + ${ + tokens.length === 0 + ? html` +
Tokens: none
+ ` + : html`
Tokens
${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}
- `} + ` + } `; @@ -183,16 +200,18 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node > Rotate - ${token.revokedAtMs - ? nothing - : html` + ${ + token.revokedAtMs + ? nothing + : html` - `} + ` + } `; @@ -389,21 +408,17 @@ function resolveExecApprovalsState(props: NodesProps): ExecApprovalsState { const targetNodes = resolveExecApprovalsNodes(props.nodes); const target = props.execApprovalsTarget; let targetNodeId = - target === "node" && props.execApprovalsTargetNodeId - ? props.execApprovalsTargetNodeId - : null; + target === "node" && props.execApprovalsTargetNodeId ? props.execApprovalsTargetNodeId : null; if (target === "node" && targetNodeId && !targetNodes.some((node) => node.id === targetNodeId)) { targetNodeId = null; } const selectedScope = resolveExecApprovalsScope(props.execApprovalsSelectedAgent, agents); const selectedAgent = selectedScope !== EXEC_APPROVALS_DEFAULT_SCOPE - ? ((form?.agents ?? {})[selectedScope] as Record | undefined) ?? - null + ? (((form?.agents ?? {})[selectedScope] as Record | undefined) ?? null) : null; const allowlist = Array.isArray((selectedAgent as { allowlist?: unknown })?.allowlist) - ? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ?? - []) + ? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ?? []) : []; return { ready, @@ -450,20 +465,25 @@ function renderBindings(state: BindingState) { - ${state.formMode === "raw" - ? html`
- Switch the Config tab to Form mode to edit bindings here. -
` - : nothing} + ${ + state.formMode === "raw" + ? html` +
+ Switch the Config tab to Form mode to edit bindings here. +
+ ` + : nothing + } - ${!state.ready - ? html`
+ ${ + !state.ready + ? html`
Load config to edit bindings.
` - : html` + : html`
@@ -493,19 +513,26 @@ function renderBindings(state: BindingState) { )} - ${!supportsBinding - ? html`
No nodes with system.run available.
` - : nothing} + ${ + !supportsBinding + ? html` +
No nodes with system.run available.
+ ` + : nothing + }
- ${state.agents.length === 0 - ? html`
No agents found.
` - : state.agents.map((agent) => - renderAgentBinding(agent, state), - )} + ${ + state.agents.length === 0 + ? html` +
No agents found.
+ ` + : state.agents.map((agent) => renderAgentBinding(agent, state)) + }
- `} + ` + } `; } @@ -533,20 +560,24 @@ function renderExecApprovals(state: ExecApprovalsState) { ${renderExecApprovalsTarget(state)} - ${!ready - ? html`
+ ${ + !ready + ? html`
Load exec approvals to edit allowlists.
` - : html` + : html` ${renderExecApprovalsTabs(state)} ${renderExecApprovalsPolicy(state)} - ${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE - ? nothing - : renderExecApprovalsAllowlist(state)} - `} + ${ + state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE + ? nothing + : renderExecApprovalsAllowlist(state) + } + ` + } `; } @@ -583,8 +614,9 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) { - ${state.target === "node" - ? html` + ${ + state.target === "node" + ? html`
@@ -249,8 +252,8 @@ function renderRow( onPatch(row.key, { reasoningLevel: value || null }); }} > - ${REASONING_LEVELS.map((level) => - html``, + ${REASONING_LEVELS.map( + (level) => html``, )}
diff --git a/ui/src/ui/views/skills.ts b/ui/src/ui/views/skills.ts index cfc024cb1..b799518a7 100644 --- a/ui/src/ui/views/skills.ts +++ b/ui/src/ui/views/skills.ts @@ -1,8 +1,7 @@ import { html, nothing } from "lit"; - -import { clampText } from "../format"; -import type { SkillStatusEntry, SkillStatusReport } from "../types"; import type { SkillMessageMap } from "../controllers/skills"; +import type { SkillStatusEntry, SkillStatusReport } from "../types"; +import { clampText } from "../format"; export type SkillsProps = { loading: boolean; @@ -25,10 +24,7 @@ export function renderSkills(props: SkillsProps) { const filter = props.filter.trim().toLowerCase(); const filtered = filter ? skills.filter((skill) => - [skill.name, skill.description, skill.source] - .join(" ") - .toLowerCase() - .includes(filter), + [skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter), ) : skills; @@ -49,25 +45,30 @@ export function renderSkills(props: SkillsProps) { Filter - props.onFilterChange((e.target as HTMLInputElement).value)} + @input=${(e: Event) => props.onFilterChange((e.target as HTMLInputElement).value)} placeholder="Search skills" />
${filtered.length} shown
- ${props.error - ? html`
${props.error}
` - : nothing} + ${ + props.error + ? html`
${props.error}
` + : nothing + } - ${filtered.length === 0 - ? html`
No skills found.
` - : html` + ${ + filtered.length === 0 + ? html` +
No skills found.
+ ` + : html`
${filtered.map((skill) => renderSkill(skill, props))}
- `} + ` + } `; } @@ -76,8 +77,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { const busy = props.busyKey === skill.skillKey; const apiKey = props.edits[skill.skillKey] ?? ""; const message = props.messages[skill.skillKey] ?? null; - const canInstall = - skill.install.length > 0 && skill.missing.bins.length > 0; + const canInstall = skill.install.length > 0 && skill.missing.bins.length > 0; const missing = [ ...skill.missing.bins.map((b) => `bin:${b}`), ...skill.missing.env.map((e) => `env:${e}`), @@ -99,22 +99,32 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { ${skill.eligible ? "eligible" : "blocked"} - ${skill.disabled ? html`disabled` : nothing} + ${ + skill.disabled + ? html` + disabled + ` + : nothing + } - ${missing.length > 0 - ? html` + ${ + missing.length > 0 + ? html`
Missing: ${missing.join(", ")}
` - : nothing} - ${reasons.length > 0 - ? html` + : nothing + } + ${ + reasons.length > 0 + ? html`
Reason: ${reasons.join(", ")}
` - : nothing} + : nothing + }
@@ -125,19 +135,21 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { > ${skill.disabled ? "Enable" : "Disable"} - ${canInstall - ? html`` - : nothing} + : nothing + }
- ${message - ? html`
${message.message}
` - : nothing} - ${skill.primaryEnv - ? html` + : nothing + } + ${ + skill.primaryEnv + ? html`
API key ` - : nothing} + : nothing + }
`; diff --git a/ui/vite.config.ts b/ui/vite.config.ts index c347c2b0e..39ffdd111 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -13,7 +13,7 @@ function normalizeBase(input: string): string { } export default defineConfig(({ command }) => { - const envBase = process.env.CLAWDBOT_CONTROL_UI_BASE_PATH?.trim(); + const envBase = process.env.OPENCLAW_CONTROL_UI_BASE_PATH?.trim(); const base = envBase ? normalizeBase(envBase) : "./"; return { base, diff --git a/vitest.config.ts b/vitest.config.ts index 92c962a1f..f5f35faaa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,7 +12,7 @@ const ciWorkers = isWindows ? 2 : 3; export default defineConfig({ resolve: { alias: { - "clawdbot/plugin-sdk": path.join(repoRoot, "src", "plugin-sdk", "index.ts"), + "openclaw/plugin-sdk": path.join(repoRoot, "src", "plugin-sdk", "index.ts"), }, }, test: { @@ -20,11 +20,7 @@ export default defineConfig({ hookTimeout: isWindows ? 180_000 : 120_000, pool: "forks", maxWorkers: isCI ? ciWorkers : localWorkers, - include: [ - "src/**/*.test.ts", - "extensions/**/*.test.ts", - "test/format-error.test.ts", - ], + include: ["src/**/*.test.ts", "extensions/**/*.test.ts", "test/format-error.test.ts"], setupFiles: ["test/setup.ts"], exclude: [ "dist/**", @@ -32,7 +28,7 @@ export default defineConfig({ "apps/macos/.build/**", "**/node_modules/**", "**/vendor/**", - "dist/Moltbot.app/**", + "dist/OpenClaw.app/**", "**/*.live.test.ts", "**/*.e2e.test.ts", ], diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 6f1d97139..0bb824878 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ "apps/macos/**", "apps/macos/.build/**", "**/vendor/**", - "dist/Moltbot.app/**", + "dist/OpenClaw.app/**", ], }, }); diff --git a/vitest.live.config.ts b/vitest.live.config.ts index 1d2c3165e..8c5b826fa 100644 --- a/vitest.live.config.ts +++ b/vitest.live.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ "apps/macos/**", "apps/macos/.build/**", "**/vendor/**", - "dist/Moltbot.app/**", + "dist/OpenClaw.app/**", ], }, });