Show AgentScan info for GitHub users
As of
// ==UserScript==
// @name GitHub AgentScan
// @version 0.1.1
// @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"]',
// 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 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;
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);
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 (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";
const child = document.createElement("a");
child.className = ["ml-1 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 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();
}
})();