GitHub AgentScan

Show AgentScan info for GitHub users

Per 28-08-2026. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         GitHub AgentScan
// @version      0.1.5
// @description  Show AgentScan info for GitHub users
// @license      MIT
// @author       Bjorn Lu
// @homepageURL  https://github.com/bluwy/github-agentscan-userscript
// @supportURL   https://github.com/bluwy/github-agentscan-userscript/issues
// @namespace    https://github.com/bluwy
// @match        https://github.com/**
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant        GM.xmlHttpRequest
// @connect      github-agentscan-userscript.bjornlu.workers.dev
// @inject-into  content
// ==/UserScript==

(() => {
  // node_modules/.pnpm/[email protected]/node_modules/flru/dist/flru.mjs
  function flru_default(max) {
    var num, curr, prev;
    var limit = max || 1;
    function keep(key, value) {
      if (++num > limit) {
        prev = curr;
        reset(1);
        ++num;
      }
      curr[key] = value;
    }
    function reset(isPartial) {
      num = 0;
      curr = /* @__PURE__ */ Object.create(null);
      isPartial || (prev = /* @__PURE__ */ Object.create(null));
    }
    reset();
    return {
      clear: reset,
      has: function(key) {
        return curr[key] !== void 0 || prev[key] !== void 0;
      },
      get: function(key) {
        var val = curr[key];
        if (val !== void 0) return val;
        if ((val = prev[key]) !== void 0) {
          keep(key, val);
          return val;
        }
      },
      set: function(key, value) {
        if (curr[key] !== void 0) {
          curr[key] = value;
        } else {
          keep(key, value);
        }
      }
    };
  }

  // src/utils-fetch.ts
  function fetchJson(input, init) {
    return new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        ...getSharedOptions(input, init, reject),
        responseType: "json",
        onload: (response) => {
          resolve(response.response);
        }
      });
    });
  }
  function getSharedOptions(input, init, reject) {
    return {
      ...init,
      url: input instanceof Request ? input.url : input.toString(),
      // @ts-expect-error
      method: init?.method || "GET",
      headers: init?.headers ? init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : Array.isArray(init.headers) ? Object.fromEntries(init.headers) : init.headers : void 0,
      data: init?.body?.toString() ?? void 0,
      onerror: (err) => reject(err),
      ontimeout: () => reject(new Error("Request timed out")),
      onabort: () => reject(new Error("Request aborted"))
    };
  }

  // src/identify-result.ts
  var cache = flru_default(30);
  async function getIdentifyResult(username) {
    const cached = cache.get(username);
    if (cached !== void 0) return cached;
    const promise = fetchJson(`${"https://github-agentscan-userscript.bjornlu.workers.dev"}/identify/${username}`);
    cache.set(username, promise);
    promise.then((result) => cache.set(username, result));
    return promise;
  }

  // src/utils-username.ts
  function getUsername(authorEl) {
    const fromText = authorEl.textContent?.trim();
    if (fromText === "") return void 0;
    const fromUrl = new URL(authorEl.href);
    const fromUrlPathnameParts = fromUrl.pathname.split("/");
    if (fromUrl.search === "" && fromUrlPathnameParts.length === 2) return fromUrlPathnameParts[1];
    if (fromText && /^[a-zA-Z0-9-]+$/.test(fromText)) return fromText;
  }

  // src/utils-debounce.ts
  function debounce(cb, delay) {
    let t;
    return () => {
      if (t != null) clearTimeout(t);
      t = setTimeout(cb, delay);
    };
  }

  // src/utils-wait.ts
  var inViewResolvers = /* @__PURE__ */ new Map();
  var inViewIntersectionObserver = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      const element = entry.target;
      inViewIntersectionObserver.unobserve(element);
      inViewResolvers.get(element)?.resolve();
      inViewResolvers.delete(element);
    }
  });
  var inViewRemovalObserver = new MutationObserver(
    debounce(() => {
      for (const [element, resolvers] of inViewResolvers) {
        if (element.isConnected) continue;
        inViewIntersectionObserver.unobserve(element);
        inViewResolvers.delete(element);
        resolvers.reject(new Error("Element was removed from the DOM"));
      }
    }, 200)
  );
  inViewRemovalObserver.observe(document, { childList: true, subtree: true });
  function waitUntilInView(element) {
    const p = promiseWithResolvers();
    if (!element.isConnected) {
      p.reject(new Error("Element is not connected to the DOM"));
      return p.promise;
    }
    inViewResolvers.set(element, p);
    inViewIntersectionObserver.observe(element);
    return p.promise;
  }
  function promiseWithResolvers() {
    let resolve = () => {
    };
    let reject = () => {
    };
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  }

  // src/features/identify.ts
  var analyzedUserElements = /* @__PURE__ */ new WeakSet();
  function identify() {
    const authorEls = document.querySelectorAll(
      [
        // PR
        "a.author",
        // Issue
        'a[class*="IssueBodyHeaderAuthor"]',
        'a[class*="ActivityHeader-module__AuthorName"]',
        // PR list
        ".opened-by > a",
        // Issue list
        'a[class*="IssueItem-module__authorCreatedLink"]',
        // Home page
        'a.Link[data-hovercard-type="user"][data-octo-dimensions="link_type:self"]',
        // Commits page
        'a[class*="AuthorAvatar-module__authorHoverableLink"]',
        'a[class*="AuthorLink-module__authorNameLink"]'
      ].join(", ")
    );
    for (const authorEl of authorEls) {
      if (analyzedUserElements.has(authorEl)) continue;
      analyzedUserElements.add(authorEl);
      const parent = authorEl.parentElement;
      if (!parent) continue;
      const alreadyLabeled = parent.querySelector("[data-github-agentscan-userscript]");
      if (alreadyLabeled) continue;
      const isAlreadyBot = Array.from(
        parent.querySelectorAll(
          [
            // PR
            "span.Label",
            // Issue
            'span[data-component="Label"]',
            // PR list
            "span.tooltipped > span.Label"
          ].join(", ")
        )
      ).find((label) => {
        const text = label.textContent?.trim().toLowerCase();
        return text === "ai" || text === "bot";
      });
      if (isAlreadyBot) continue;
      const username = getUsername(authorEl);
      if (!username) continue;
      if (username.endsWith("[bot]")) continue;
      identifyUsername(username, authorEl).catch((err) => {
        console.error("Error fetching identify result for user", username, err);
      });
    }
  }
  async function identifyUsername(username, authorEl) {
    try {
      await waitUntilInView(authorEl);
    } catch {
      return;
    }
    const identifyResult = await getIdentifyResult(username);
    if (!identifyResult) return;
    if (!authorEl.isConnected) return;
    const agentscanLink = `https://agentscan.tools/user/${username}`;
    let label = null;
    if (identifyResult.isCommunityFlagged) {
      label = createLabel(
        "AI",
        agentscanLink,
        "Label--danger",
        "This user has been flagged as AI by the community"
      );
    } else if (identifyResult.classification === "automation") {
      label = createLabel(
        "AI",
        agentscanLink,
        "Label--severe",
        "This user has been flagged as automation by AgentScan"
      );
    } else if (identifyResult.classification === "mixed") {
      label = createLabel(
        "AI",
        agentscanLink,
        "Label--warning",
        "This user has been flagged as mixed by AgentScan"
      );
    } else if (false) {
      label = createLabel("Human", agentscanLink, "Label--secondary", "Living and breathing");
    }
    if (label && authorEl.parentElement) {
      const parentStyle = getComputedStyle(authorEl.parentElement);
      const alreadyHasMargin = parentStyle.display === "flex" && parentStyle.gap.endsWith("px");
      if (!alreadyHasMargin) {
        ;
        label.childNodes[0].classList.add("ml-1");
      }
    }
    if (label) {
      authorEl.insertAdjacentElement("afterend", label);
    }
  }
  function createLabel(text, link, labelClass, description) {
    const label = document.createElement("span");
    label.className = "tooltipped tooltipped-n";
    label.ariaLabel = description;
    label.dataset.viewComponent = "true";
    label.dataset.githubAgentscanUserscript = "";
    const child = document.createElement("a");
    child.className = ["Label", labelClass].filter(Boolean).join(" ");
    child.textContent = text;
    child.href = link;
    child.target = "_blank";
    label.appendChild(child);
    return label;
  }

  // src/features/report.ts
  var analyzedEls = /* @__PURE__ */ new WeakSet();
  function report() {
    handlePR();
  }
  function handlePR() {
    const reportButtons = document.querySelectorAll(
      [
        // Normal comment
        '.timeline-comment-header a[aria-label="Report abusive content"]',
        // Review comment
        '.timeline-comment-group a[aria-label="Report content"]'
      ].join(", ")
    );
    for (const button of reportButtons) {
      if (analyzedEls.has(button)) continue;
      analyzedEls.add(button);
      const header = button.closest(".timeline-comment-header, .timeline-comment-group");
      if (!header) continue;
      const authorEl = header.querySelector("a.author");
      if (!authorEl) continue;
      const username = getUsername(authorEl);
      if (!username) continue;
      const userId = header.querySelector('img[src^="https://avatars.githubusercontent.com/u/"]')?.getAttribute("src")?.match(/\/u\/(\d+)\?/)?.[1];
      if (!userId) continue;
      const reportButton = button.cloneNode();
      reportButton.href = buildSimpleReportIssueUrl(username, userId);
      reportButton.target = "_blank";
      reportButton.textContent = "Report to AgentScan";
      reportButton.setAttribute("aria-label", "Report to AgentScan");
      delete reportButton.dataset.gaClick;
      delete reportButton.dataset.testSelector;
      button.insertAdjacentElement("afterend", reportButton);
      buildFullReportIssueUrl(username, userId).then((url) => {
        if (!url) return;
        if (!reportButton.isConnected) return;
        reportButton.href = url;
      }).catch((err) => {
        console.error("Error building report issue URL for user", username, err);
      });
    }
  }
  function buildSimpleReportIssueUrl(username, userId) {
    const url = new URL("https://github.com/matteogabriele/agentscan/issues/new");
    url.searchParams.set("template", "report-automated-account.yml");
    url.searchParams.set("title", `[AUTOMATION] ${username}`);
    url.searchParams.set("username", username);
    url.searchParams.set("user-id", userId);
    url.searchParams.set("evidence", `- Flagged in: ${withoutBacklink(location.href)}`);
    return url.toString();
  }
  async function buildFullReportIssueUrl(username, userId) {
    const result = await getIdentifyResult(username);
    if (!result) return null;
    const url = new URL("https://github.com/matteogabriele/agentscan/issues/new");
    url.searchParams.set("template", "report-automated-account.yml");
    url.searchParams.set("title", `[AUTOMATION] ${username}`);
    url.searchParams.set("username", username);
    url.searchParams.set("user-id", userId);
    url.searchParams.set(
      "reason",
      `AgentScan classified this account as possible "${result.classification}" (score ${result.score}/100).`
    );
    url.searchParams.set("evidence", `- Flagged in: ${withoutBacklink(location.href)}`);
    return url.toString();
  }
  function withoutBacklink(url) {
    try {
      const parsed = new URL(url);
      if (parsed.hostname === "github.com") {
        parsed.hostname = "redirect.github.com";
      }
      return parsed.toString();
    } catch {
      return url;
    }
  }

  // src/index.ts
  var run = debounce(() => {
    identify();
    report();
  }, 200);
  document.addEventListener("pjax:end", () => run());
  document.addEventListener("turbo:render", () => run());
  var observer = new MutationObserver(() => run());
  observer.observe(document.body, { childList: true, subtree: true });
})();