GitHub AgentScan

Show AgentScan info for GitHub users

Stan na 22-08-2026. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         GitHub AgentScan
// @version      0.1.0
// @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) 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-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"]'
      ].join(", ")
    );
    for (const authorEl of authorEls) {
      if (analyzedUserElements.has(authorEl)) continue;
      analyzedUserElements.add(authorEl);
      const parent = authorEl.parentElement;
      if (!parent) 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 = authorEl.textContent?.trim();
      if (!username) 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);
    let label = null;
    if (identifyResult.isCommunityFlagged) {
      label = createLabel("AI", "Label--danger", "This user has been flagged as AI by the community");
    } else if (identifyResult.classification === "automation") {
      label = createLabel(
        "AI",
        "Label--severe",
        "This user has been flagged as automation by AgentScan"
      );
    } else if (identifyResult.classification === "mixed") {
      label = createLabel("AI", "Label--warning", "This user has been flagged as mixed by AgentScan");
    } else {
    }
    if (label) {
      authorEl.insertAdjacentElement("afterend", label);
    }
  }
  function createLabel(text, labelClass, description) {
    const label = document.createElement("span");
    label.className = "tooltipped tooltipped-n";
    label.ariaLabel = description;
    label.dataset.viewComponent = "true";
    const child = document.createElement("span");
    child.className = ["ml-1 Label", labelClass].filter(Boolean).join(" ");
    child.textContent = text;
    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 username = header.querySelector("a.author")?.textContent?.trim();
      if (!username) continue;
      const userId = header.querySelector('img[src^="https://avatars.githubusercontent.com/u/"]')?.getAttribute("src")?.match(/\/u\/(\d+)\?/)?.[1];
      if (!userId) continue;
      buildReportIssueUrl(username, userId).then((url) => {
        const reportButton = button.cloneNode();
        reportButton.href = url;
        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);
      }).catch((err) => {
        console.error("Error building report issue URL for user", username, err);
      });
    }
  }
  async function buildReportIssueUrl(username, userId) {
    const result = await getIdentifyResult(username);
    const reason = `AgentScan classified this account as possible "${result.classification}" (score ${result.score}/100).`;
    const evidenceLines = [`- Flagged in: ${withoutBacklink(location.href)}`];
    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", reason);
    url.searchParams.set("evidence", evidenceLines.join("\n"));
    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
  run();
  document.addEventListener("pjax:end", () => run());
  document.addEventListener("turbo:render", () => run());
  var observer = new MutationObserver(debounce(run, 200));
  observer.observe(document.body, { childList: true, subtree: true });
  function run() {
    identify();
    report();
  }
})();