GitHub AgentScan

Show AgentScan info for GitHub users

Verzia zo dňa 27.08.2026. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu budete musieť nainštalovať rozšírenie, ako je napríklad Tampermonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         GitHub AgentScan
// @version      0.1.4
// @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 fromUrl = new URL(authorEl.href);
    const fromUrlPathnameParts = fromUrl.pathname.split("/");
    if (fromUrl.search === "" && fromUrlPathnameParts.length === 2) return fromUrlPathnameParts[1];
    const fromText = authorEl.textContent?.trim();
    if (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;
    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;
      buildReportIssueUrl(username, userId).then((url) => {
        if (!url) return;
        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);
    if (!result) return null;
    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
  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 });
})();