Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ It is not a Gittensor explorer, public leaderboard, reward-farming bot, or auton
## Install MCP

```sh
npm install -g @jsonbored/gittensory-mcp
npm install -g @jsonbored/gittensory-mcp@latest
gittensory-mcp login
gittensory-mcp doctor
gittensory-mcp --stdio
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface TerminalScene {

const DEFAULT_SCENES: TerminalScene[] = [
{
prompt: "npx -y @jsonbored/gittensory-mcp login",
prompt: "npx -y @jsonbored/gittensory-mcp@latest login",
output: "→ GitHub Device Flow opened… authorized as octocat",
},
{
Expand Down
141 changes: 101 additions & 40 deletions apps/gittensory-ui/src/components/site/app-panels/commands-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { apiFetch } from "@/lib/api/request";
import { getApiOrigin } from "@/lib/api/origin";
import { useApiResource } from "@/lib/api/use-api-resource";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";

type CommandSample = {
id: string;
Expand All @@ -32,15 +33,24 @@ type CommandPreviewResponse = {
export function CommandsPanel() {
const commands = useApiResource<CommandsResponse>("/v1/app/commands", "Command catalog");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [repoFullName, setRepoFullName] = useState("");
const [pullNumber, setPullNumber] = useState("");
const [preview, setPreview] = useState<CommandPreviewResponse | null>(null);
const selected =
commands.status === "ready"
? (commands.data.commands.find((command) => command.id === selectedId) ??
commands.data.commands[0])
: null;
const parsedPullNumber = Number(pullNumber);
const validContext =
/^[^/\s]+\/[^/\s]+$/.test(repoFullName.trim()) &&
Number.isInteger(parsedPullNumber) &&
parsedPullNumber > 0;

useEffect(() => {
if (!selected) return;
setPreview(null);
if (!selected || !validContext) return;
let active = true;
const origin = getApiOrigin().replace(/\/$/, "");
void apiFetch<CommandPreviewResponse>(`${origin}/v1/app/commands/preview`, {
method: "POST",
Expand All @@ -49,14 +59,17 @@ export function CommandsPanel() {
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
command: selected.id,
repoFullName: "jsonbored/gittensory",
pullNumber: 1218,
repoFullName: repoFullName.trim(),
pullNumber: parsedPullNumber,
}),
silentStatus: true,
}).then((result) => {
if (result.ok) setPreview(result.data);
if (active && result.ok) setPreview(result.data);
});
}, [selected]);
return () => {
active = false;
};
}, [parsedPullNumber, repoFullName, selected, validContext]);

return (
<StateBoundary
Expand All @@ -71,40 +84,74 @@ export function CommandsPanel() {
errorDescription={commands.status === "error" ? commands.error : undefined}
>
{commands.status === "ready" && selected ? (
<div className="grid gap-6 lg:grid-cols-[300px_1fr]">
<ul className="space-y-2">
{commands.data.commands.map((command) => {
const active = command.id === selected.id;
return (
<li key={command.id}>
<button
type="button"
onClick={() => setSelectedId(command.id)}
className={cn(
"w-full rounded-token border-hairline p-3 text-left transition-all duration-150 focus-ring motion-reduce:transition-none motion-reduce:active:scale-100 active:scale-[0.99]",
active
? "border-strong bg-mint/[0.04]"
: "hover:border-strong hover:bg-muted/40",
)}
>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-token-xs text-foreground">
{command.command}
</span>
<StatusPill status={command.boundary === "public" ? "ready" : "info"}>
{command.audience}
</StatusPill>
</div>
<p className="mt-1 text-token-xs text-muted-foreground">
{command.description}
</p>
</button>
</li>
);
})}
</ul>
<div className="space-y-4">
<div className="grid gap-3 rounded-token border-hairline bg-card p-4 sm:grid-cols-[minmax(0,1fr)_12rem]">
<label className="block">
<span className="font-mono text-token-2xs uppercase tracking-wider text-muted-foreground">
Repository
</span>
<Input
value={repoFullName}
onChange={(event) => setRepoFullName(event.target.value)}
placeholder="owner/repo"
className="mt-1 font-mono text-token-xs"
autoComplete="off"
/>
</label>
<label className="block">
<span className="font-mono text-token-2xs uppercase tracking-wider text-muted-foreground">
Pull request
</span>
<Input
value={pullNumber}
onChange={(event) => setPullNumber(event.target.value)}
placeholder="123"
inputMode="numeric"
className="mt-1 font-mono text-token-xs"
/>
</label>
</div>

<PrThread sample={selected} preview={preview?.preview ?? null} />
<div className="grid gap-6 lg:grid-cols-[300px_1fr]">
<ul className="space-y-2">
{commands.data.commands.map((command) => {
const active = command.id === selected.id;
return (
<li key={command.id}>
<button
type="button"
onClick={() => setSelectedId(command.id)}
className={cn(
"w-full rounded-token border-hairline p-3 text-left transition-all duration-150 focus-ring motion-reduce:transition-none motion-reduce:active:scale-100 active:scale-[0.99]",
active
? "border-strong bg-mint/[0.04]"
: "hover:border-strong hover:bg-muted/40",
)}
>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-token-xs text-foreground">
{command.command}
</span>
<StatusPill status={command.boundary === "public" ? "ready" : "info"}>
{command.audience}
</StatusPill>
</div>
<p className="mt-1 text-token-xs text-muted-foreground">
{command.description}
</p>
</button>
</li>
);
})}
</ul>

<PrThread
sample={selected}
preview={preview?.preview ?? null}
repoFullName={repoFullName.trim()}
pullNumber={validContext ? parsedPullNumber : null}
/>
</div>
</div>
) : null}
</StateBoundary>
Expand All @@ -114,24 +161,38 @@ export function CommandsPanel() {
function PrThread({
sample,
preview,
repoFullName,
pullNumber,
}: {
sample: CommandSample;
preview: CommandPreviewResponse["preview"] | null;
repoFullName: string;
pullNumber: number | null;
}) {
const hasContext = Boolean(repoFullName && pullNumber);
return (
<div className="overflow-hidden rounded-token border-hairline bg-card">
<div className="flex items-center gap-2 border-b-hairline bg-background/40 px-4 py-2.5">
<GitPullRequestArrow className="size-4 text-mint" />
<div className="font-mono text-token-xs text-foreground/90">
jsonbored/gittensory <span className="text-muted-foreground">·</span> PR #1218
{hasContext ? repoFullName : "Enter repo context"}{" "}
<span className="text-muted-foreground">·</span>{" "}
{pullNumber ? `PR #${pullNumber}` : "PR #"}
</div>
<span className="ml-auto rounded-full border-hairline bg-mint/10 px-2 py-0.5 font-mono text-token-2xs uppercase tracking-wider text-mint">
private preview
</span>
</div>

<div className="space-y-4 p-4">
<Comment author="maintainer" body={sample.command} muted />
{hasContext ? (
<Comment author="maintainer" body={sample.command} muted />
) : (
<div className="rounded-token border-hairline bg-background/40 p-3 text-token-xs text-muted-foreground">
Enter a repository and pull request number to preview this command against live API
context.
</div>
)}
<AnimatePresence mode="wait">
<motion.div
key={sample.id}
Expand Down
59 changes: 22 additions & 37 deletions apps/gittensory-ui/src/components/site/mcp-version-badge.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
import { useQuery } from "@tanstack/react-query";
import { Package, ExternalLink, ChevronDown } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "motion/react";

import { cn } from "@/lib/utils";
import { apiFetch, notifyApiFailure, notifyApiRecovered } from "@/lib/api/request";

type NpmPackage = {
"dist-tags": { latest: string };
time: Record<string, string>;
versions: Record<string, unknown>;
};

async function fetchNpm(): Promise<NpmPackage> {
const result = await apiFetch<NpmPackage>(
"https://registry.npmjs.org/@jsonbored/gittensory-mcp",
{ label: "MCP package version", timeoutMs: 6000, silentStatus: true },
);
if (!result.ok) throw new Error(result.message);
return result.data;
}
import { notifyApiFailure, notifyApiRecovered } from "@/lib/api/request";
import {
MCP_PACKAGE_NAME,
MCP_PACKAGE_NPM_URL,
getLatestMcpVersion,
getRecentMcpVersions,
useMcpPackageMetadata,
} from "@/lib/mcp-package";

export function McpVersionBadge({ className }: { className?: string }) {
const [open, setOpen] = useState(false);
const { data, isLoading, isError, isFetching, refetch } = useQuery({
queryKey: ["npm", "@jsonbored/gittensory-mcp"],
queryFn: fetchNpm,
staleTime: 1000 * 60 * 30,
retry: 1,
});
const { data, isError, isFetching, refetch } = useMcpPackageMetadata();

const wasError = useRef(false);
useEffect(() => {
Expand All @@ -48,12 +34,8 @@ export function McpVersionBadge({ className }: { className?: string }) {
}
}, [isError, isFetching, data, refetch]);

const latest = data?.["dist-tags"].latest;
const versions = data
? Object.keys(data.versions)
.sort((a, b) => (data.time[b] ?? "").localeCompare(data.time[a] ?? ""))
.slice(0, 6)
: [];
const latest = getLatestMcpVersion(data);
const versions = getRecentMcpVersions(data);

return (
<div className={cn("relative", className)}>
Expand All @@ -70,15 +52,13 @@ export function McpVersionBadge({ className }: { className?: string }) {
</span>
<Package className="size-3 shrink-0 opacity-70" />
<span>mcp</span>
<span className="truncate text-foreground">
{isLoading ? "…" : isError ? "offline" : `v${latest}`}
</span>
<span className="truncate text-foreground">v{latest}</span>
<ChevronDown
className={`size-2.5 shrink-0 opacity-50 transition-transform duration-150 motion-reduce:transition-none ${open ? "rotate-180" : ""}`}
/>
</button>
<AnimatePresence>
{open && data && (
{open && (
<motion.div
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
Expand All @@ -91,14 +71,14 @@ export function McpVersionBadge({ className }: { className?: string }) {
npm package
</div>
<div className="mt-0.5 font-mono text-token-sm text-foreground">
@jsonbored/gittensory-mcp
{MCP_PACKAGE_NAME}
</div>
</div>
<ul className="max-h-64 overflow-auto p-2 text-token-sm">
{versions.map((v) => (
<li key={v}>
<a
href={`https://www.npmjs.com/package/@jsonbored/gittensory-mcp/v/${v}`}
href={`${MCP_PACKAGE_NPM_URL}/v/${v}`}
target="_blank"
rel="noreferrer"
className="flex items-center justify-between rounded-token px-3 py-1.5 text-muted-foreground hover:bg-accent hover:text-foreground"
Expand All @@ -112,14 +92,19 @@ export function McpVersionBadge({ className }: { className?: string }) {
)}
</span>
<span className="text-token-2xs">
{new Date(data.time[v] ?? "").toLocaleDateString()}
{data?.time[v] ? new Date(data.time[v]).toLocaleDateString() : "cached"}
</span>
</a>
</li>
))}
</ul>
{isError && (
<div className="border-t border-border px-4 py-2 text-token-2xs text-muted-foreground">
npm is unreachable; showing the last known latest version.
</div>
)}
<a
href="https://www.npmjs.com/package/@jsonbored/gittensory-mcp"
href={MCP_PACKAGE_NPM_URL}
target="_blank"
rel="noreferrer"
className="flex items-center justify-between border-t border-border px-4 py-2.5 text-token-xs text-muted-foreground hover:text-foreground"
Expand Down
Loading