diff --git a/.github/workflows/docs-external-links.yml b/.github/workflows/docs-external-links.yml index 203e85950fbe..59f808a4f96b 100644 --- a/.github/workflows/docs-external-links.yml +++ b/.github/workflows/docs-external-links.yml @@ -29,7 +29,7 @@ jobs: --no-progress --base-url https://docs.openclaw.ai --exclude-all-private - --accept '100..=399,401,403,429' + --accept '100..=399,401,403,429,500..=599' --accept-timeouts --max-retries 2 --retry-wait-time 2 diff --git a/README.md b/README.md index 7cfdb4baec22..20c543a23bc3 100644 --- a/README.md +++ b/README.md @@ -330,23 +330,23 @@ Thanks to all clawtributors: BradGroux christianklotz odysseus0 hclsys byungsker pashpashpash stakeswky github-actions[bot] xinhuagu MonkeyLeeT 100yenadmin mcinteerj samzong chilu18 darkamenosa widingmarcus-cyber cgdusek Lukavyi davidrudduck VACInc MoerAI velvet-shark HenryLoenwind omarshahine bohdanpodvirnyi Verite Igiraneza akramcodez Kaneki-x aether-ai-agent joaohlisboa -MaudeBot davidguttman justinhuangcode lml2468 wirjo iHildy mudrii advaitpaliwal czekaj dlauer +MaudeBot davidguttman justinhuangcode lml2468 wirjo iHildy mudrii advaitpaliwal czekaj dlauer Solvely-Colin feiskyer brandonwise conroywhitney mneves75 jaydenfyi davemorin joeykrug kevinWangSheng pejmanjohn Lanfei liuy lc0rp teconomix omair445 dorukardahan mmaps Tobias Bischoff adhitShet pandego bradleypriest bjesuiter grp06 shadril238 kesku YuriNachos vrknetha smartprogrammer93 nachx639 jnMetaCode Phineas1500 dingn42 geekhuashan Nanako0129 AytuncYildizli BruceMacD jjjojoj mvanhorn bugkill3r rahthakor GodsBoy SARAMALI15792 Radek Paclt Elarwei001 ingyukoh SnowSky1 lewiswigmore Hiroshi Tanaka aldoeliacim Jakub Rusz -Tony Dehnke roshanasingh4 zssggle-rgb adam91holt graysurf xadenryan sfo2001 Jamieson O'Reilly hsrvc tomsun28 +Tony Dehnke roshanasingh4 zssggle-rgb adam91holt graysurf xadenryan sfo2001 Jamieson O'Reilly hsrvc tomsun28 BillChirico carrotRakko ranausmanai arkyu2077 hoyyeva luoyanglang sibbl gregmousseau sahilsatralkar akoscz rrenamed YuzuruS Hongwei Ma mitchmcalister juanpablodlc shtse8 thebenignhacker nimbleenigma Linux2010 shichangs efe-arv Hsiao A nabbilkhan ayanesakura lupuletic polooooo xaeon2026 shrey150 taw0002 dinakars777 giulio-leone nyanjou meaningfool kunalk16 ide-rea Jonathan Jing yelog markmusson kiranvk-2011 Sathvik Veerapaneni rogerdigital artwalker azade-c chinar-amrutkar maxsumrall Minidoracat unisone ly85206559 Sam Padilla AnonO6 -afurm 황재원 Leszek Szpunar Mrseenz Yida-Dev kesor mazhe-nerd Harald Buerbaumer magimetal Hiren Patel +afurm 황재원 Leszek Szpunar Mrseenz Yida-Dev kesor mazhe-nerd Harald Buerbaumer magimetal Hiren Patel BinHPdev RyanLee-Dev cathrynlavery al3mart JustYannicc abhisekbasu1 dbhurley Kris Wu tmimmanuel JustasM Simantak Dabhade NicholasSpisak natefikru dunamismax Simone Macario ENCHIGO xingsy97 emonty jadilson12 Yi-Cheng Wang Mathias Nagler Sean McLellan gumclaw RichardCao MKV21 petter-b CodeForgeNet Johnson Shi durenzidu dougvk -Whoaa512 zimeg Tseka Luk Ryan Haines ufhy Daan van der Plas bittoby XuHao Lucenx9 HeMuling +Whoaa512 zimeg Tseka Luk Ryan Haines ufhy Daan van der Plas bittoby XuHao Lucenx9 HeMuling AaronLuo00 YUJIE2002 DhruvBhatia0 Divanoli Mydeen Pitchai Bronko rubyrunsstuff rabsef-bicrym IVY-AI-gif pvtclawn stephenschoettler Dale Babiy LeftX David Gelberg Engr. Arif Ahmed Joy Masataka Shinohara 2233admin ameno- battman21 bcherny bobashopcashier dguido druide67 guirguispierre jzakirov loganprit martinfrancois neo1027144-creator RealKai42 schumilin shuofengzhang @@ -357,7 +357,7 @@ Thanks to all clawtributors: fal3 mkbehr mteam88 gupsammy Shailesh Garnet Liu Thorfinn Protocol-zero-0 Paul van Oorschot Patrick Yingxi Pan Ptah.ai 정우용 artuskg Anandesh-Sharma zidongdesign innocent-children El-Fitz arthurbr11 jackheuberger Sergiusz Xu Gu hyojin jeann2013 jogelin rmorse scz2011 Andyliu benithors xiwuqi Alvin -AARON AGENT Derek YU Marvin Andrew Jeon stain lu OpenCils Stefan Galescu SP Michael Flanagan Gracie Gould +AARON AGENT Derek YU Marvin Andrew Jeon stain lu OpenCils Stefan Galescu SP Michael Flanagan Gracie Gould cash-echo-bot visionik WalterSumbon huangcj krizpoon rodbland2021 Thomas M sar618 fagemx daymade Tyson Cung Igor Markelov Eng. Juan Combetto connorshea bonald Keenan nachoiacovino zhumengzhu Amine Harch el korane zhoulc777 Alex Navarro Tanwa Arpornthip TIHU Aftabbs Alex-Alaniz jarvis-medmatic Tom Ron day253 Jaaneek Justin Song @@ -373,19 +373,19 @@ Thanks to all clawtributors: Andy Rajat Joshi cyb1278588254 Zoher Ghadyali Manik Vahsith tarouca MrBrain Daniel Zou Lilo Jason SUMUKH Bakhtier Sizhaev Ganghyun Kim AkashKobal Brian wu-tian807 Vasanth Rao Naik Sabavat Kinfey Artemii VibhorGautam John Rood velamints2 Benji Peng JINNYEONG KIM Rahul kumar Pal Rockcent Limitless 24601 awkoy dawondyifraw -google-labs-jules[bot] henrino3 Kansodata kaonash p6l-richard pi0 skainguyen1412 Starhappysh xdanger Penchan +google-labs-jules[bot] henrino3 Kansodata kaonash p6l-richard pi0 skainguyen1412 Starhappysh xdanger Penchan scald Serhii a Doğu Abaris ysqander andranik-sahakyan Wangnov Austin lisitan Rishi Vhavle Frank Harris Kenny Lee Alice Losasso edincampara Felix Hellström Varun Chopra wangai-studio sleontenko Yassine Amjad Anton Eicher Drake Thomsen Hinata Kaga (samon) andreabadesso chenxin-yan cordx56 dvrshil MarvinCui Yeom-JinHo Jeremy Mumford Charlie Niño Sharoon Sharif Oren MattQ Parker Todd Brooks Yufeng He Milofax Steve (OpenClaw) zhoulf1006 Jonatan Sebastian B Otaegui -Matthew ABFS Tech alexstyl Ethan Palm Qkal cygaar Umut CAN Jakob antons austinm911 +Matthew ABFS Tech alexstyl Ethan Palm Qkal cygaar Umut CAN Jakob antons austinm911 mahmoudashraf93 philipp-spiess pkrmf joshrad-dev factnest365-ops yingchunbai AJ (@techfren) Marchel Fahrezi futhgar Zhang Rémi Dan Ballance Eric Su Kimitaka Watanabe Justin Ling Raymond Berger lutr0 claude AngryBird Fabian Williams 0x4C33 8BlT atalovesyou erikpr1994 jonasjancarik longmaba mitschabaude-bot thesash Max easternbloc chrisrodz gabriel-trigo manmal neist wes-davis manuelhettich sktbrd larlyssa pcty-nextgen-service-account Syhids tmchow Marc Gratch xtao JackyWay Josh Phillips T5-AndyML huohua-dev imfing Randy Torres Marco Di Dionisio iamadig humanwritten Rob Axelsen Pratham Dubey 0oAstro aaronn Arturo Asleep123 dantelex fcatuhe -gtsifrikas hrdwdmrbl hugobarauna jayhickey jiulingyun Jonathan D. Rhyne (DJ-D) jverdi kitze loukotal minghinmatthewlam +gtsifrikas hrdwdmrbl hugobarauna jayhickey jiulingyun Jonathan D. Rhyne (DJ-D) jverdi kitze loukotal minghinmatthewlam MSch odrobnik oswalpalash ratulsarna reeltimeapps snopoke sreekaransrinath timkrase diff --git a/docs/providers/deepgram.md b/docs/providers/deepgram.md index dd53a71eb180..3d050e1a5470 100644 --- a/docs/providers/deepgram.md +++ b/docs/providers/deepgram.md @@ -19,7 +19,6 @@ returns them. | Detail | Value | | ------------- | ---------------------------------------------------------- | -| Website | [deepgram.com](https://deepgram.com) | | Docs | [developers.deepgram.com](https://developers.deepgram.com) | | Auth | `DEEPGRAM_API_KEY` | | Default model | `nova-3` | diff --git a/docs/providers/ollama-cloud.md b/docs/providers/ollama-cloud.md index 99bbb828b75c..014190fe0861 100644 --- a/docs/providers/ollama-cloud.md +++ b/docs/providers/ollama-cloud.md @@ -18,7 +18,7 @@ embeddings, and custom host details, see [Ollama](/providers/ollama). ## Setup -Create an Ollama Cloud API key at [ollama.com/settings/keys](https://ollama.com/settings/keys), then run: +Follow [Ollama's API key instructions](https://docs.ollama.com/api/authentication#api-keys), then run: ```bash openclaw onboard --auth-choice ollama-cloud diff --git a/docs/start/showcase.md b/docs/start/showcase.md index e5ad60c81cd8..c7f2fa82d6ad 100644 --- a/docs/start/showcase.md +++ b/docs/start/showcase.md @@ -119,7 +119,7 @@ Desktop app to manage skills and commands across Agents, Claude, Codex, and Open Agents UI app - + **Community** • `voice` `tts` `telegram` Wraps papla.media TTS and sends results as Telegram voice notes (no annoying autoplay). diff --git a/docs/tools/acp-agents-setup.md b/docs/tools/acp-agents-setup.md index 9385aec00302..80d91dd11bc0 100644 --- a/docs/tools/acp-agents-setup.md +++ b/docs/tools/acp-agents-setup.md @@ -32,7 +32,7 @@ Built-in acpx harness aliases (from the pinned `acpx` dependency): | Alias | Wraps | | ------------ | ------------------------------------------------------------------------------------------------------ | | `claude` | [Claude Code](https://claude.ai/code) | -| `codex` | [Codex CLI](https://codex.openai.com) | +| `codex` | [Codex CLI](https://developers.openai.com/codex/cli) | | `copilot` | [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli) | | `cursor` | [Cursor CLI](https://cursor.com/docs/cli/acp) (`cursor-agent acp`) | | `droid` | [Factory Droid](https://www.factory.ai) | diff --git a/scripts/clawtributors-map.json b/scripts/clawtributors-map.json index f04fd4b61a54..9b7cc85a7daa 100644 --- a/scripts/clawtributors-map.json +++ b/scripts/clawtributors-map.json @@ -16,7 +16,7 @@ "atalovesyou", "0xJonHoldsCrypto", "hougangdev", - "jiulingyun" + "mf-yang" ], "seedCommit": "d6863f87", "displayName": { diff --git a/scripts/update-clawtributors.ts b/scripts/update-clawtributors.ts index 90e51f515b31..29a43fd57cbd 100644 --- a/scripts/update-clawtributors.ts +++ b/scripts/update-clawtributors.ts @@ -48,6 +48,7 @@ for (const item of contributors) { contributionsByLogin.set(item.login.toLowerCase(), item.contributions); } apiByLogin.set(item.login.toLowerCase(), { + id: item.id, login: item.login, html_url: item.html_url, avatar_url: normalizeAvatar(item.avatar_url), @@ -183,16 +184,36 @@ const entriesByKey = new Map(); for (const seed of seedEntries) { const login = - loginFromUrl(seed.html_url) ?? + (seed.html_url ? loginFromUrl(seed.html_url) : null) ?? resolveLogin(seed.display, null, apiByLogin, nameToLogin, emailToLogin); - if (!login) { + const accountId = accountIdFromAvatarUrl(seed.avatar_url); + if (!login && !accountId) { continue; } - const key = login.toLowerCase(); - const user = apiByLogin.get(key) ?? fetchUser(login); + const loginKey = login?.toLowerCase(); + const userByLogin = loginKey ? apiByLogin.get(loginKey) : undefined; + // Avatar account IDs survive renames and cannot be claimed by a handle squatter. + const user = accountId + ? userByLogin?.id === accountId + ? userByLogin + : fetchUserByAccountId(accountId) + : (userByLogin ?? (login ? fetchUser(login) : null)); if (!user) { + const key = accountId ? `github-id:${accountId}` : expectDefined(loginKey, "seed login key"); + entriesByKey.set(key, { + key, + display: seed.display, + html_url: null, + avatar_url: normalizeAvatar(seed.avatar_url), + lines: 0, + commits: 0, + prs: 0, + score: 0, + firstCommitDate: loginKey ? (firstCommitByLogin.get(loginKey) ?? "") : "", + }); continue; } + const key = user.login.toLowerCase(); apiByLogin.set(key, user); const existing = entriesByKey.get(key); if (!existing) { @@ -318,7 +339,8 @@ for (let i = 0; i < visibleEntries.length; i += PER_LINE) { // (default identicons come back 420px) and never upscales tiny source // avatars, so markdown images render off-grid without explicit sizing. const alt = escapeHtmlAttribute(entry.display); - return `${alt}`; + const image = `${alt}`; + return entry.html_url ? `${image}` : image; }); markdownLines.push(parts.join(" ")); } @@ -446,27 +468,70 @@ function normalizeAvatar(url: string): string { } } +function accountIdFromAvatarUrl(url: string): number | null { + try { + const parsed = new URL(url); + if (parsed.hostname !== "avatars.githubusercontent.com") { + return null; + } + const match = /^\/u\/(\d+)\/?$/u.exec(parsed.pathname); + const accountId = match?.[1] ? Number(match[1]) : Number.NaN; + return Number.isSafeInteger(accountId) && accountId > 0 ? accountId : null; + } catch { + return null; + } +} + +function parseUser(responseText: string): User | null { + const parsed = JSON.parse(responseText); + if (!parsed?.login || !parsed?.html_url || !parsed?.avatar_url) { + return null; + } + return { + id: typeof parsed.id === "number" ? parsed.id : undefined, + login: parsed.login, + html_url: parsed.html_url, + avatar_url: normalizeAvatar(parsed.avatar_url), + }; +} + function fetchUser(login: string): User | null { const normalized = normalizeLogin(login); if (!normalized) { return null; } try { - const data = runGh(["api", `users/${normalized}`]); - const parsed = JSON.parse(data); - if (!parsed?.login || !parsed?.html_url || !parsed?.avatar_url) { + return parseUser(runGh(["api", `users/${normalized}`])); + } catch (error) { + if (isGitHubMissing(error)) { return null; } - return { - login: parsed.login, - html_url: parsed.html_url, - avatar_url: normalizeAvatar(parsed.avatar_url), - }; - } catch { - return null; + throw error; } } +function fetchUserByAccountId(accountId: number): User | null { + try { + return parseUser(runGh(["api", `user/${accountId}`])); + } catch (error) { + if (isGitHubMissing(error)) { + return null; + } + throw error; + } +} + +function isGitHubMissing(error: unknown): boolean { + const message = error instanceof Error ? error.message : ""; + const stderr = (error as { stderr?: unknown } | null)?.stderr; + const stderrText = Buffer.isBuffer(stderr) + ? stderr.toString("utf8") + : typeof stderr === "string" + ? stderr + : ""; + return /\bHTTP (?:404|410)\b/u.test(`${message}\n${stderrText}`); +} + function isDefaultGitHubAvatar(login: string): Promise { const normalized = normalizeLogin(login)?.toLowerCase(); if (!normalized) { @@ -850,13 +915,17 @@ function escapeHtmlAttribute(value: string): string { function parseReadmeEntries( content: string, -): Array<{ display: string; html_url: string; avatar_url: string }> { +): Array<{ display: string; html_url: string | null; avatar_url: string }> { const rangeValue = findClawtributorsRange(content); if (!rangeValue) { return []; } const blockValue = content.slice(rangeValue.start, rangeValue.end); - const entriesValue: Array<{ display: string; html_url: string; avatar_url: string }> = []; + const entriesValue: Array<{ + display: string; + html_url: string | null; + avatar_url: string; + }> = []; const markdown = /\[!\[([^\]]+)\]\(([^)]+)\)\]\(([^)]+)\)/g; for (const match of blockValue.matchAll(markdown)) { const [, alt, src, href] = match; @@ -886,7 +955,7 @@ function parseReadmeEntries( if (entriesValue.some((entry) => entry.display === alt && entry.avatar_url === src)) { continue; } - entriesValue.push({ html_url: fallbackHref(alt), avatar_url: src, display: alt }); + entriesValue.push({ html_url: null, avatar_url: src, display: alt }); } return entriesValue; } @@ -967,11 +1036,6 @@ function loginFromUrl(url: string): string | null { return login; } -function fallbackHref(value: string): string { - const encoded = encodeURIComponent(value.trim()); - return encoded ? `https://github.com/search?q=${encoded}` : "https://github.com"; -} - function pickDisplay( baseName: string | null | undefined, login: string, diff --git a/scripts/update-clawtributors.types.ts b/scripts/update-clawtributors.types.ts index f2f1d6138b12..948c2275e699 100644 --- a/scripts/update-clawtributors.types.ts +++ b/scripts/update-clawtributors.types.ts @@ -8,6 +8,7 @@ export type MapConfig = { }; export type ApiContributor = { + id?: number; login?: string; html_url?: string; avatar_url?: string; @@ -17,6 +18,7 @@ export type ApiContributor = { }; export type User = { + id?: number; login: string; html_url: string; avatar_url: string; @@ -26,7 +28,7 @@ export type Entry = { key: string; login?: string; display: string; - html_url: string; + html_url: string | null; avatar_url: string; lines: number; commits: number; diff --git a/test/scripts/update-clawtributors.test.ts b/test/scripts/update-clawtributors.test.ts index a09c8efdd7d8..f20e6dde07c8 100644 --- a/test/scripts/update-clawtributors.test.ts +++ b/test/scripts/update-clawtributors.test.ts @@ -20,9 +20,24 @@ afterEach(() => { function mockClawtributorsFixture({ ensureLogins = [], + seedEntry, + accountLookup, + accountLookupError, + accountLookupStatus = 404, + loginLookupError, runGh, }: { ensureLogins?: string[]; + seedEntry?: string; + accountLookup?: { + id: number; + login: string; + html_url: string; + avatar_url: string; + } | null; + accountLookupError?: Error; + accountLookupStatus?: 404 | 410; + loginLookupError?: Error; runGh?: GhRunner; } = {}) { const readme = [ @@ -38,7 +53,7 @@ function mockClawtributorsFixture({ vi.doMock("node:fs", () => ({ readFileSync: vi.fn((path: string) => { if (path.endsWith("scripts/clawtributors-map.json")) { - return `${JSON.stringify({ ensureLogins })}\n`; + return `${JSON.stringify({ ensureLogins, seedCommit: seedEntry ? "seed-sha" : undefined })}\n`; } if (path.endsWith("README.md")) { return readme; @@ -54,6 +69,7 @@ function mockClawtributorsFixture({ }), })); const contributor = { + id: 1, login: "octo", name: "Octo", html_url: "https://github.com/octo", @@ -64,6 +80,12 @@ function mockClawtributorsFixture({ if (cmd === "git log --reverse --format=%aN%x1f%aE%x1f%aI --numstat") { return ""; } + if (cmd === "git show seed-sha:README.md" && seedEntry) { + return readme.replace( + "\n", + `\n${seedEntry}\n`, + ); + } if (cmd === "git rev-list --max-parents=0 HEAD") { return "root-sha\n"; } @@ -103,13 +125,28 @@ function mockClawtributorsFixture({ return ""; } if (args[0] === "api" && args[1]?.startsWith("users/")) { + if (loginLookupError) { + throw loginLookupError; + } const login = args[1].slice("users/".length); return JSON.stringify({ + id: 2, login, html_url: `https://github.com/${login}`, avatar_url: "https://avatars.githubusercontent.com/u/2?v=4", }); } + if (args[0] === "api" && args[1]?.startsWith("user/")) { + if (accountLookupError) { + throw accountLookupError; + } + if (!accountLookup) { + throw Object.assign(new Error("gh api failed"), { + stderr: Buffer.from(`gh: account missing (HTTP ${accountLookupStatus})\n`), + }); + } + return JSON.stringify(accountLookup); + } throw new Error(`unexpected gh arguments: ${args.join(" ")}`); }, ); @@ -190,6 +227,87 @@ describe("update-clawtributors", () => { expect(fixture.execPlainGh).toHaveBeenCalledTimes(1); }); + it("aborts on transient login lookup failures", async () => { + const fixture = mockClawtributorsFixture({ + ensureLogins: ["extra"], + loginLookupError: new Error("GitHub login lookup unavailable"), + }); + + await expect(importUpdateClawtributors()).rejects.toThrow("GitHub login lookup unavailable"); + + expect(fixture.readWrittenReadme()).toBe(""); + }); + + it("resolves renamed seed profiles by stable account id", async () => { + const fixture = mockClawtributorsFixture({ + seedEntry: + 'Original credit', + accountLookup: { + id: 42, + login: "new-handle", + html_url: "https://github.com/new-handle", + avatar_url: "https://avatars.githubusercontent.com/u/42?v=4", + }, + }); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(null, { status: 404 })), + ); + vi.spyOn(console, "log").mockImplementation(() => undefined); + + await importUpdateClawtributors(); + + expect(fixture.execPlainGh).toHaveBeenCalledWith( + ["api", "user/42"], + expect.objectContaining({ timeout: 120_000, killSignal: "SIGKILL" }), + ); + expect(fixture.readWrittenReadme()).toContain( + 'Original credit', + ); + expect(fixture.readWrittenReadme()).not.toContain('href="https://github.com/old-handle"'); + }); + + it.each([404, 410] as const)( + "keeps deleted seed profiles as unlinked credit on HTTP %s", + async (accountLookupStatus) => { + const fixture = mockClawtributorsFixture({ + seedEntry: + 'Gone contributor', + accountLookup: null, + accountLookupStatus, + }); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(null, { status: 404 })), + ); + vi.spyOn(console, "log").mockImplementation(() => undefined); + + await importUpdateClawtributors(); + + expect(fixture.readWrittenReadme()).toContain( + 'Gone contributor', + ); + expect(fixture.readWrittenReadme()).not.toContain('href="https://github.com/gone-handle"'); + }, + ); + + it("aborts instead of unlinking credit on transient account lookup failures", async () => { + const fixture = mockClawtributorsFixture({ + seedEntry: + 'Current contributor', + accountLookupError: new Error("GitHub API unavailable"), + }); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(null, { status: 404 })), + ); + vi.spyOn(console, "log").mockImplementation(() => undefined); + + await expect(importUpdateClawtributors()).rejects.toThrow("GitHub API unavailable"); + + expect(fixture.readWrittenReadme()).toBe(""); + }); + it("rejects unsafe avatar probe content lengths before reading the body", async () => { const fixture = mockClawtributorsFixture(); const arrayBuffer = vi.fn(async () => new ArrayBuffer(0));