Compare commits
4 Commits
develop
...
v2026.2.6-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaddbdae52 | ||
|
|
8d0e7997c8 | ||
|
|
31a7e4f937 | ||
|
|
c5194d8148 |
@@ -110,7 +110,6 @@ git commit -m "Add Clawd workspace"
|
||||
- **OpenHue CLI** — Philips Hue lighting control for scenes and automations.
|
||||
- **OpenAI Whisper** — Local speech-to-text for quick dictation and voicemail transcripts.
|
||||
- **Gemini CLI** — Google Gemini models from the terminal for fast Q&A.
|
||||
- **bird** — X/Twitter CLI to tweet, reply, read threads, and search without a browser.
|
||||
- **agent-tools** — Utility toolkit for automations and helper scripts.
|
||||
|
||||
## Usage Notes
|
||||
|
||||
@@ -35,6 +35,7 @@ For a high-level overview, see [Onboarding Wizard](/start/wizard).
|
||||
- **OpenAI Code (Codex) subscription (OAuth)**: browser flow; paste the `code#state`.
|
||||
- Sets `agents.defaults.model` to `openai-codex/gpt-5.2` when model is unset or `openai/*`.
|
||||
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then saves it to `~/.openclaw/.env` so launchd can read it.
|
||||
- **xAI (Grok) API key**: prompts for `XAI_API_KEY` and configures xAI as a model provider.
|
||||
- **OpenCode Zen (multi-model proxy)**: prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`, get it at https://opencode.ai/auth).
|
||||
- **API key**: stores the key for you.
|
||||
- **Vercel AI Gateway (multi-model proxy)**: prompts for `AI_GATEWAY_API_KEY`.
|
||||
|
||||
@@ -145,6 +145,9 @@ What you set:
|
||||
Sets `agents.defaults.model` to `openai/gpt-5.1-codex` when model is unset, `openai/*`, or `openai-codex/*`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="xAI (Grok) API key">
|
||||
Prompts for `XAI_API_KEY` and configures xAI as a model provider.
|
||||
</Accordion>
|
||||
<Accordion title="OpenCode Zen">
|
||||
Prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`).
|
||||
Setup URL: [opencode.ai/auth](https://opencode.ai/auth).
|
||||
|
||||
@@ -34,8 +34,7 @@ If you have multiple profiles, pass `--browser-profile <name>` (the default is `
|
||||
|
||||
## X/Twitter: recommended flow
|
||||
|
||||
- **Read/search/threads:** use the **bird** CLI skill (no browser, stable).
|
||||
- Repo: [https://github.com/steipete/bird](https://github.com/steipete/bird)
|
||||
- **Read/search/threads:** use the **host** browser (manual login).
|
||||
- **Post updates:** use the **host** browser (manual login).
|
||||
|
||||
## Sandboxing + host browser access
|
||||
|
||||
@@ -102,7 +102,7 @@ Legacy `agents.default` entries are migrated to `agents.main` on load.
|
||||
|
||||
Examples:
|
||||
|
||||
- `~/Projects/**/bin/bird`
|
||||
- `~/Projects/**/bin/peekaboo`
|
||||
- `~/.local/bin/*`
|
||||
- `/opt/homebrew/bin/rg`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.2.6",
|
||||
"version": "2026.2.6-1",
|
||||
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
---
|
||||
name: bird
|
||||
description: X/Twitter CLI for reading, searching, posting, and engagement via cookies.
|
||||
homepage: https://bird.fast
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "🐦",
|
||||
"requires": { "bins": ["bird"] },
|
||||
"install":
|
||||
[
|
||||
{
|
||||
"id": "brew",
|
||||
"kind": "brew",
|
||||
"formula": "steipete/tap/bird",
|
||||
"bins": ["bird"],
|
||||
"label": "Install bird (brew)",
|
||||
"os": ["darwin"],
|
||||
},
|
||||
{
|
||||
"id": "npm",
|
||||
"kind": "node",
|
||||
"package": "@steipete/bird",
|
||||
"bins": ["bird"],
|
||||
"label": "Install bird (npm)",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# bird 🐦
|
||||
|
||||
Fast X/Twitter CLI using GraphQL + cookie auth.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# npm/pnpm/bun
|
||||
npm install -g @steipete/bird
|
||||
|
||||
# Homebrew (macOS, prebuilt binary)
|
||||
brew install steipete/tap/bird
|
||||
|
||||
# One-shot (no install)
|
||||
bunx @steipete/bird whoami
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
`bird` uses cookie-based auth.
|
||||
|
||||
Use `--auth-token` / `--ct0` to pass cookies directly, or `--cookie-source` for browser cookies.
|
||||
|
||||
Run `bird check` to see which source is active. For Arc/Brave, use `--chrome-profile-dir <path>`.
|
||||
|
||||
## Commands
|
||||
|
||||
### Account & Auth
|
||||
|
||||
```bash
|
||||
bird whoami # Show logged-in account
|
||||
bird check # Show credential sources
|
||||
bird query-ids --fresh # Refresh GraphQL query ID cache
|
||||
```
|
||||
|
||||
### Reading Tweets
|
||||
|
||||
```bash
|
||||
bird read <url-or-id> # Read a single tweet
|
||||
bird <url-or-id> # Shorthand for read
|
||||
bird thread <url-or-id> # Full conversation thread
|
||||
bird replies <url-or-id> # List replies to a tweet
|
||||
```
|
||||
|
||||
### Timelines
|
||||
|
||||
```bash
|
||||
bird home # Home timeline (For You)
|
||||
bird home --following # Following timeline
|
||||
bird user-tweets @handle -n 20 # User's profile timeline
|
||||
bird mentions # Tweets mentioning you
|
||||
bird mentions --user @handle # Mentions of another user
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
```bash
|
||||
bird search "query" -n 10
|
||||
bird search "from:steipete" --all --max-pages 3
|
||||
```
|
||||
|
||||
### News & Trending
|
||||
|
||||
```bash
|
||||
bird news -n 10 # AI-curated from Explore tabs
|
||||
bird news --ai-only # Filter to AI-curated only
|
||||
bird news --sports # Sports tab
|
||||
bird news --with-tweets # Include related tweets
|
||||
bird trending # Alias for news
|
||||
```
|
||||
|
||||
### Lists
|
||||
|
||||
```bash
|
||||
bird lists # Your lists
|
||||
bird lists --member-of # Lists you're a member of
|
||||
bird list-timeline <id> -n 20 # Tweets from a list
|
||||
```
|
||||
|
||||
### Bookmarks & Likes
|
||||
|
||||
```bash
|
||||
bird bookmarks -n 10
|
||||
bird bookmarks --folder-id <id> # Specific folder
|
||||
bird bookmarks --include-parent # Include parent tweet
|
||||
bird bookmarks --author-chain # Author's self-reply chain
|
||||
bird bookmarks --full-chain-only # Full reply chain
|
||||
bird unbookmark <url-or-id>
|
||||
bird likes -n 10
|
||||
```
|
||||
|
||||
### Social Graph
|
||||
|
||||
```bash
|
||||
bird following -n 20 # Users you follow
|
||||
bird followers -n 20 # Users following you
|
||||
bird following --user <id> # Another user's following
|
||||
bird about @handle # Account origin/location info
|
||||
```
|
||||
|
||||
### Engagement Actions
|
||||
|
||||
```bash
|
||||
bird follow @handle # Follow a user
|
||||
bird unfollow @handle # Unfollow a user
|
||||
```
|
||||
|
||||
### Posting
|
||||
|
||||
```bash
|
||||
bird tweet "hello world"
|
||||
bird reply <url-or-id> "nice thread!"
|
||||
bird tweet "check this out" --media image.png --alt "description"
|
||||
```
|
||||
|
||||
**⚠️ Posting risks**: Posting is more likely to be rate limited; if blocked, use the browser tool instead.
|
||||
|
||||
## Media Uploads
|
||||
|
||||
```bash
|
||||
bird tweet "hi" --media img.png --alt "description"
|
||||
bird tweet "pics" --media a.jpg --media b.jpg # Up to 4 images
|
||||
bird tweet "video" --media clip.mp4 # Or 1 video
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
Commands supporting pagination: `replies`, `thread`, `search`, `bookmarks`, `likes`, `list-timeline`, `following`, `followers`, `user-tweets`
|
||||
|
||||
```bash
|
||||
bird bookmarks --all # Fetch all pages
|
||||
bird bookmarks --max-pages 3 # Limit pages
|
||||
bird bookmarks --cursor <cursor> # Resume from cursor
|
||||
bird replies <id> --all --delay 1000 # Delay between pages (ms)
|
||||
```
|
||||
|
||||
## Output Options
|
||||
|
||||
```bash
|
||||
--json # JSON output
|
||||
--json-full # JSON with raw API response
|
||||
--plain # No emoji, no color (script-friendly)
|
||||
--no-emoji # Disable emoji
|
||||
--no-color # Disable ANSI colors (or set NO_COLOR=1)
|
||||
--quote-depth n # Max quoted tweet depth in JSON (default: 1)
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
```bash
|
||||
--auth-token <token> # Set auth_token cookie
|
||||
--ct0 <token> # Set ct0 cookie
|
||||
--cookie-source <source> # Cookie source for browser cookies (repeatable)
|
||||
--chrome-profile <name> # Chrome profile name
|
||||
--chrome-profile-dir <path> # Chrome/Chromium profile dir or cookie DB path
|
||||
--firefox-profile <name> # Firefox profile
|
||||
--timeout <ms> # Request timeout
|
||||
--cookie-timeout <ms> # Cookie extraction timeout
|
||||
```
|
||||
|
||||
## Config File
|
||||
|
||||
`~/.config/bird/config.json5` (global) or `./.birdrc.json5` (project):
|
||||
|
||||
```json5
|
||||
{
|
||||
cookieSource: ["chrome"],
|
||||
chromeProfileDir: "/path/to/Arc/Profile",
|
||||
timeoutMs: 20000,
|
||||
quoteDepth: 1,
|
||||
}
|
||||
```
|
||||
|
||||
Environment variables: `BIRD_TIMEOUT_MS`, `BIRD_COOKIE_TIMEOUT_MS`, `BIRD_QUOTE_DEPTH`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Query IDs stale (404 errors)
|
||||
|
||||
```bash
|
||||
bird query-ids --fresh
|
||||
```
|
||||
|
||||
### Cookie extraction fails
|
||||
|
||||
- Check browser is logged into X
|
||||
- Try different `--cookie-source`
|
||||
- For Arc/Brave: use `--chrome-profile-dir`
|
||||
|
||||
---
|
||||
|
||||
**TL;DR**: Read/search/engage with CLI. Post carefully or use browser. 🐦
|
||||
@@ -58,7 +58,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||
.option(
|
||||
"--auth-choice <choice>",
|
||||
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|xai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|xai-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||
)
|
||||
.option(
|
||||
"--token-provider <id>",
|
||||
|
||||
@@ -155,8 +155,8 @@ export function buildAuthChoiceOptions(params: {
|
||||
});
|
||||
options.push({ value: "chutes", label: "Chutes (OAuth)" });
|
||||
options.push({ value: "openai-api-key", label: "OpenAI API key" });
|
||||
options.push({ value: "openrouter-api-key", label: "OpenRouter API key" });
|
||||
options.push({ value: "xai-api-key", label: "xAI (Grok) API key" });
|
||||
options.push({ value: "openrouter-api-key", label: "OpenRouter API key" });
|
||||
options.push({
|
||||
value: "ai-gateway-api-key",
|
||||
label: "Vercel AI Gateway API key",
|
||||
|
||||
@@ -83,8 +83,8 @@ describe("dashboardCommand", () => {
|
||||
customBindHost: undefined,
|
||||
basePath: undefined,
|
||||
});
|
||||
expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/");
|
||||
expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/");
|
||||
expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
|
||||
expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
"Opened in your browser. Keep that tab to control OpenClaw.",
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ export async function dashboardCommand(
|
||||
const bind = cfg.gateway?.bind ?? "loopback";
|
||||
const basePath = cfg.gateway?.controlUi?.basePath;
|
||||
const customBindHost = cfg.gateway?.customBindHost;
|
||||
const token = cfg.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? "";
|
||||
|
||||
const links = resolveControlUiLinks({
|
||||
port,
|
||||
@@ -30,7 +31,10 @@ export async function dashboardCommand(
|
||||
customBindHost,
|
||||
basePath,
|
||||
});
|
||||
const dashboardUrl = links.httpUrl;
|
||||
// Prefer URL fragment to avoid leaking auth tokens via query params.
|
||||
const dashboardUrl = token
|
||||
? `${links.httpUrl}#token=${encodeURIComponent(token)}`
|
||||
: links.httpUrl;
|
||||
|
||||
runtime.log(`Dashboard URL: ${dashboardUrl}`);
|
||||
|
||||
@@ -48,6 +52,7 @@ export async function dashboardCommand(
|
||||
hint = formatControlUiSshHint({
|
||||
port,
|
||||
basePath,
|
||||
token: token || undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -179,16 +179,24 @@ export async function detectBrowserOpenSupport(): Promise<BrowserOpenSupport> {
|
||||
return { ok: true, command: resolved.command };
|
||||
}
|
||||
|
||||
export function formatControlUiSshHint(params: { port: number; basePath?: string }): string {
|
||||
export function formatControlUiSshHint(params: {
|
||||
port: number;
|
||||
basePath?: string;
|
||||
token?: string;
|
||||
}): string {
|
||||
const basePath = normalizeControlUiBasePath(params.basePath);
|
||||
const uiPath = basePath ? `${basePath}/` : "/";
|
||||
const localUrl = `http://localhost:${params.port}${uiPath}`;
|
||||
const authedUrl = params.token
|
||||
? `${localUrl}#token=${encodeURIComponent(params.token)}`
|
||||
: undefined;
|
||||
const sshTarget = resolveSshTargetHint();
|
||||
return [
|
||||
"No GUI detected. Open from your computer:",
|
||||
`ssh -N -L ${params.port}:127.0.0.1:${params.port} ${sshTarget}`,
|
||||
"Then open:",
|
||||
localUrl,
|
||||
authedUrl,
|
||||
"Docs:",
|
||||
"https://docs.openclaw.ai/gateway/remote",
|
||||
"https://docs.openclaw.ai/web/control-ui",
|
||||
|
||||
@@ -255,7 +255,10 @@ export async function finalizeOnboardingWizard(
|
||||
customBindHost: settings.customBindHost,
|
||||
basePath: controlUiBasePath,
|
||||
});
|
||||
const dashboardUrl = links.httpUrl;
|
||||
const authedUrl =
|
||||
settings.authMode === "token" && settings.gatewayToken
|
||||
? `${links.httpUrl}#token=${encodeURIComponent(settings.gatewayToken)}`
|
||||
: links.httpUrl;
|
||||
const gatewayProbe = await probeGatewayReachable({
|
||||
url: links.wsUrl,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
@@ -275,7 +278,10 @@ export async function finalizeOnboardingWizard(
|
||||
|
||||
await prompter.note(
|
||||
[
|
||||
`Web UI: ${dashboardUrl}`,
|
||||
`Web UI: ${links.httpUrl}`,
|
||||
settings.authMode === "token" && settings.gatewayToken
|
||||
? `Web UI (with token): ${authedUrl}`
|
||||
: undefined,
|
||||
`Gateway WS: ${links.wsUrl}`,
|
||||
gatewayStatusLine,
|
||||
"Docs: https://docs.openclaw.ai/web/control-ui",
|
||||
@@ -312,7 +318,7 @@ export async function finalizeOnboardingWizard(
|
||||
`Generate token: ${formatCliCommand("openclaw doctor --generate-gateway-token")}`,
|
||||
"Web UI stores a copy in this browser's localStorage (openclaw.control.settings.v1).",
|
||||
`Open the dashboard anytime: ${formatCliCommand("openclaw dashboard --no-open")}`,
|
||||
"Paste the token into Control UI settings if prompted.",
|
||||
"If prompted: paste the token into Control UI settings (or use the tokenized dashboard URL).",
|
||||
].join("\n"),
|
||||
"Token",
|
||||
);
|
||||
@@ -341,22 +347,24 @@ export async function finalizeOnboardingWizard(
|
||||
} else if (hatchChoice === "web") {
|
||||
const browserSupport = await detectBrowserOpenSupport();
|
||||
if (browserSupport.ok) {
|
||||
controlUiOpened = await openUrl(dashboardUrl);
|
||||
controlUiOpened = await openUrl(authedUrl);
|
||||
if (!controlUiOpened) {
|
||||
controlUiOpenHint = formatControlUiSshHint({
|
||||
port: settings.port,
|
||||
basePath: controlUiBasePath,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
controlUiOpenHint = formatControlUiSshHint({
|
||||
port: settings.port,
|
||||
basePath: controlUiBasePath,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
});
|
||||
}
|
||||
await prompter.note(
|
||||
[
|
||||
`Dashboard link: ${dashboardUrl}`,
|
||||
`Dashboard link (with token): ${authedUrl}`,
|
||||
controlUiOpened
|
||||
? "Opened in your browser. Keep that tab to control OpenClaw."
|
||||
: "Copy/paste this URL in a browser on this machine to control OpenClaw.",
|
||||
@@ -442,23 +450,25 @@ export async function finalizeOnboardingWizard(
|
||||
if (shouldOpenControlUi) {
|
||||
const browserSupport = await detectBrowserOpenSupport();
|
||||
if (browserSupport.ok) {
|
||||
controlUiOpened = await openUrl(dashboardUrl);
|
||||
controlUiOpened = await openUrl(authedUrl);
|
||||
if (!controlUiOpened) {
|
||||
controlUiOpenHint = formatControlUiSshHint({
|
||||
port: settings.port,
|
||||
basePath: controlUiBasePath,
|
||||
token: settings.gatewayToken,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
controlUiOpenHint = formatControlUiSshHint({
|
||||
port: settings.port,
|
||||
basePath: controlUiBasePath,
|
||||
token: settings.gatewayToken,
|
||||
});
|
||||
}
|
||||
|
||||
await prompter.note(
|
||||
[
|
||||
`Dashboard link: ${dashboardUrl}`,
|
||||
`Dashboard link (with token): ${authedUrl}`,
|
||||
controlUiOpened
|
||||
? "Opened in your browser. Keep that tab to control OpenClaw."
|
||||
: "Copy/paste this URL in a browser on this machine to control OpenClaw.",
|
||||
|
||||
@@ -82,18 +82,26 @@ export function setLastActiveSessionKey(host: SettingsHost, next: string) {
|
||||
}
|
||||
|
||||
export function applySettingsFromUrl(host: SettingsHost) {
|
||||
if (!window.location.search) {
|
||||
if (!window.location.search && !window.location.hash) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const tokenRaw = params.get("token");
|
||||
const passwordRaw = params.get("password");
|
||||
const sessionRaw = params.get("session");
|
||||
const gatewayUrlRaw = params.get("gatewayUrl");
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
const hashParams = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
|
||||
|
||||
const tokenRaw = params.get("token") ?? hashParams.get("token");
|
||||
const passwordRaw = params.get("password") ?? hashParams.get("password");
|
||||
const sessionRaw = params.get("session") ?? hashParams.get("session");
|
||||
const gatewayUrlRaw = params.get("gatewayUrl") ?? hashParams.get("gatewayUrl");
|
||||
let shouldCleanUrl = false;
|
||||
|
||||
if (tokenRaw != null) {
|
||||
const token = tokenRaw.trim();
|
||||
if (token && token !== host.settings.token) {
|
||||
applySettings(host, { ...host.settings, token });
|
||||
}
|
||||
params.delete("token");
|
||||
hashParams.delete("token");
|
||||
shouldCleanUrl = true;
|
||||
}
|
||||
|
||||
@@ -103,6 +111,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||
(host as { password: string }).password = password;
|
||||
}
|
||||
params.delete("password");
|
||||
hashParams.delete("password");
|
||||
shouldCleanUrl = true;
|
||||
}
|
||||
|
||||
@@ -124,14 +133,16 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||
host.pendingGatewayUrl = gatewayUrl;
|
||||
}
|
||||
params.delete("gatewayUrl");
|
||||
hashParams.delete("gatewayUrl");
|
||||
shouldCleanUrl = true;
|
||||
}
|
||||
|
||||
if (!shouldCleanUrl) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(window.location.href);
|
||||
url.search = params.toString();
|
||||
const nextHash = hashParams.toString();
|
||||
url.hash = nextHash ? `#${nextHash}` : "";
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
|
||||
|
||||
@@ -151,11 +151,11 @@ describe("control UI routing", () => {
|
||||
expect(container.scrollTop).toBe(maxScroll);
|
||||
});
|
||||
|
||||
it("strips token URL params without importing them", async () => {
|
||||
it("hydrates token from URL params and strips it", async () => {
|
||||
const app = mountApp("/ui/overview?token=abc123");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("");
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(window.location.pathname).toBe("/ui/overview");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
@@ -169,7 +169,7 @@ describe("control UI routing", () => {
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
|
||||
it("does not override stored settings from URL token params", async () => {
|
||||
it("hydrates token from URL params even when settings already set", async () => {
|
||||
localStorage.setItem(
|
||||
"openclaw.control.settings.v1",
|
||||
JSON.stringify({ token: "existing-token" }),
|
||||
@@ -177,8 +177,17 @@ describe("control UI routing", () => {
|
||||
const app = mountApp("/ui/overview?token=abc123");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("existing-token");
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(window.location.pathname).toBe("/ui/overview");
|
||||
expect(window.location.search).toBe("");
|
||||
});
|
||||
|
||||
it("hydrates token from URL hash and strips it", async () => {
|
||||
const app = mountApp("/ui/overview#token=abc123");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.settings.token).toBe("abc123");
|
||||
expect(window.location.pathname).toBe("/ui/overview");
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user