diff --git a/README.md b/README.md index d0db5214685..6ce277c2cec 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,19 @@
**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, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat). 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. +It answers you on the channels you already use. 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. +Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat. + [Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [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/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd) +New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started) + Preferred setup: run `openclaw onboard` in your terminal. OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup 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.openclaw.ai/start/getting-started) ## Sponsors @@ -91,11 +94,6 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin Model note: while many providers and models are supported, prefer a current flagship model from the provider you trust and already use. See [Onboarding](https://docs.openclaw.ai/start/onboarding). -## Models (selection + auth) - -- 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 24 (recommended) or Node 22.16+**. @@ -129,34 +127,7 @@ openclaw agent --message "Ship checklist" --thinking high Upgrading? [Updating guide](https://docs.openclaw.ai/install/updating) (and run `openclaw doctor`). -## Development channels - -- **stable**: tagged releases (`vYYYY.M.D` or `vYYYY.M.D-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
'); -const end = readme.indexOf("
", start); +const block = `${CLAWTRIBUTORS_START}\n${markdownLines.join("\n\n")}\n${CLAWTRIBUTORS_END}`; +const hiddenBlock = buildHiddenReadmeBlock(entries, visibleEntries); +const hiddenRange = findHiddenReadmeRange(currentReadme); +const readmeWithoutMeta = hiddenRange + ? `${currentReadme.slice(0, hiddenRange.start)}${currentReadme.slice(hiddenRange.end)}` + : currentReadme; +const range = findClawtributorsRange(readmeWithoutMeta); -if (start === -1 || end === -1) { +if (!range) { throw new Error("README.md missing clawtributors block"); } -const next = `${readme.slice(0, start)}\n${block}${readme.slice(end)}`;
+const next = `${readmeWithoutMeta.slice(0, range.start)}${block}\n${hiddenBlock}${readmeWithoutMeta.slice(range.end)}`;
writeFileSync(readmePath, next);
-console.log(`Updated README clawtributors: ${entries.length} entries`);
+console.log(
+ `Updated README clawtributors: ${visibleEntries.length} visible (${entries.length - visibleEntries.length} default-avatar entries hidden)`,
+);
console.log(`\nTop 25 by composite score: (commits*2 + PRs*10 + sqrt(LOC)) * tenure`);
console.log(` tenure = 1.0 + (days_since_first_commit / repo_age)^2 * 0.5`);
console.log(
`${"#".padStart(3)} ${"login".padEnd(24)} ${"score".padStart(8)} ${"tenure".padStart(7)} ${"commits".padStart(8)} ${"PRs".padStart(6)} ${"LOC".padStart(10)} first commit`,
);
console.log("-".repeat(85));
-for (const entry of entries.slice(0, 25)) {
+for (const [index, entry] of visibleEntries.slice(0, 25).entries()) {
const login = (entry.login ?? entry.key).slice(0, 24);
const fd = entry.firstCommitDate || "?";
const daysIn =
@@ -316,7 +331,7 @@ for (const entry of entries.slice(0, 25)) {
const tr = Math.min(1, daysIn / repoAgeDays);
const tenure = 1.0 + tr * tr * 0.5;
console.log(
- `${entries.indexOf(entry) + 1}`.padStart(3) +
+ `${index + 1}`.padStart(3) +
` ${login.padEnd(24)} ${entry.score.toFixed(0).padStart(8)} ${tenure.toFixed(2).padStart(6)}x ${String(entry.commits).padStart(8)} ${String(entry.prs).padStart(6)} ${String(entry.lines).padStart(10)} ${fd}`,
);
}
@@ -386,12 +401,15 @@ function normalizeAvatar(url: string): string {
if (!/^https?:/i.test(url)) {
return url;
}
- const lower = url.toLowerCase();
- if (lower.includes("s=") || lower.includes("size=")) {
+ try {
+ const parsed = new URL(url);
+ parsed.searchParams.delete("s");
+ parsed.searchParams.delete("size");
+ parsed.searchParams.set("s", String(AVATAR_SIZE));
+ return parsed.toString();
+ } catch {
return url;
}
- const sep = url.includes("?") ? "&" : "?";
- return `${url}${sep}s=48`;
}
function fetchUser(login: string): User | null {
@@ -418,6 +436,159 @@ function fetchUser(login: string): User | null {
}
}
+function isDefaultGitHubAvatar(login: string): Promise ');
- const end = content.indexOf(" ');
+ const legacyEnd = content.indexOf("]*alt="([^"]+)"[^>]*>/g;
for (const match of block.matchAll(linked)) {
const [, href, src, alt] = match;
@@ -535,6 +717,70 @@ function parseReadmeEntries(
return entries;
}
+function parseHiddenReadmeLogins(content: string): string[] {
+ const range = findHiddenReadmeRange(content);
+ if (!range) {
+ return [];
+ }
+ const block = content.slice(range.start, range.end);
+ return block
+ .split("\n")
+ .map((line) => normalizeLogin(line.trim())?.toLowerCase() ?? null)
+ .filter((login): login is string => Boolean(login));
+}
+
+function buildHiddenReadmeBlock(entries: Entry[], visibleEntries: Entry[]): string {
+ const visibleLogins = new Set(
+ visibleEntries
+ .map((entry) => normalizeLogin(entry.login ?? entry.key)?.toLowerCase() ?? null)
+ .filter((login): login is string => Boolean(login)),
+ );
+ const hiddenLogins = entries
+ .map((entry) => normalizeLogin(entry.login ?? entry.key)?.toLowerCase() ?? null)
+ .filter((login): login is string => Boolean(login))
+ .filter((login) => !visibleLogins.has(login))
+ .toSorted((a, b) => a.localeCompare(b));
+ const notice =
+ "default-avatar-cache: hidden from the rendered wall because these users still use GitHub's default avatar";
+ if (hiddenLogins.length === 0) {
+ return `${CLAWTRIBUTORS_HIDDEN_START}\n${notice}\n${CLAWTRIBUTORS_HIDDEN_END}\n`;
+ }
+ return `${CLAWTRIBUTORS_HIDDEN_START}\n${notice}\n${hiddenLogins.join("\n")}\n${CLAWTRIBUTORS_HIDDEN_END}\n`;
+}
+
+function findClawtributorsRange(content: string): { start: number; end: number } | null {
+ const markerStart = content.indexOf(CLAWTRIBUTORS_START);
+ const markerEnd = content.indexOf(CLAWTRIBUTORS_END, markerStart);
+ if (markerStart !== -1 && markerEnd !== -1) {
+ return {
+ start: markerStart,
+ end: markerEnd + CLAWTRIBUTORS_END.length,
+ };
+ }
+
+ const legacyStart = content.indexOf('