diff --git a/README.md b/README.md index 8408d0d918..f967c660b3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apps/gittensory-ui/src/components/site/animated-terminal.tsx b/apps/gittensory-ui/src/components/site/animated-terminal.tsx index 64ca9a0860..bb511afe0a 100644 --- a/apps/gittensory-ui/src/components/site/animated-terminal.tsx +++ b/apps/gittensory-ui/src/components/site/animated-terminal.tsx @@ -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", }, { diff --git a/apps/gittensory-ui/src/components/site/app-panels/commands-panel.tsx b/apps/gittensory-ui/src/components/site/app-panels/commands-panel.tsx index e881ea840e..676a8d9459 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/commands-panel.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/commands-panel.tsx @@ -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; @@ -32,15 +33,24 @@ type CommandPreviewResponse = { export function CommandsPanel() { const commands = useApiResource("/v1/app/commands", "Command catalog"); const [selectedId, setSelectedId] = useState(null); + const [repoFullName, setRepoFullName] = useState(""); + const [pullNumber, setPullNumber] = useState(""); const [preview, setPreview] = useState(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(`${origin}/v1/app/commands/preview`, { method: "POST", @@ -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 ( {commands.status === "ready" && selected ? ( -
-
    - {commands.data.commands.map((command) => { - const active = command.id === selected.id; - return ( -
  • - -
  • - ); - })} -
+
+
+ + +
- +
+
    + {commands.data.commands.map((command) => { + const active = command.id === selected.id; + return ( +
  • + +
  • + ); + })} +
+ + +
) : null} @@ -114,16 +161,23 @@ 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 (
- jsonbored/gittensory · PR #1218 + {hasContext ? repoFullName : "Enter repo context"}{" "} + ·{" "} + {pullNumber ? `PR #${pullNumber}` : "PR #"}
private preview @@ -131,7 +185,14 @@ function PrThread({
- + {hasContext ? ( + + ) : ( +
+ Enter a repository and pull request number to preview this command against live API + context. +
+ )} ; - versions: Record; -}; - -async function fetchNpm(): Promise { - const result = await apiFetch( - "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(() => { @@ -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 (
@@ -70,15 +52,13 @@ export function McpVersionBadge({ className }: { className?: string }) { mcp - - {isLoading ? "…" : isError ? "offline" : `v${latest}`} - + v{latest} - {open && data && ( + {open && (
- @jsonbored/gittensory-mcp + {MCP_PACKAGE_NAME}
+ {isError && ( +
+ npm is unreachable; showing the last known latest version. +
+ )} => { - const r = await fetch(`https://registry.npmjs.org/${PKG}`); - if (!r.ok) throw new Error(`npm ${r.status}`); - const j = (await r.json()) as NpmMeta; - return j["dist-tags"]?.latest ?? ""; - }, - staleTime: 1000 * 60 * 30, - retry: 1, - }); -} +import { + MCP_PACKAGE_NAME, + getLatestMcpVersion, + getMcpInstallCommand, + useMcpPackageMetadata, +} from "@/lib/mcp-package"; /** * Install snippet for the MCP, pinned to the current npm latest. * Falls back gracefully when the npm registry is unreachable. */ export function NpmInstall({ className }: { className?: string }) { - const { data: version, isLoading, isError } = useNpmLatest(); + const { data, isError } = useMcpPackageMetadata(); const [copied, setCopied] = useState(false); - const command = version ? `npm i -g ${PKG}@${version}` : `npm i -g ${PKG}`; + const version = getLatestMcpVersion(data); + const command = getMcpInstallCommand(data && !isError ? version : undefined); const copy = async () => { try { @@ -51,16 +39,10 @@ export function NpmInstall({ className }: { className?: string }) {
- npm · {PKG} + npm · {MCP_PACKAGE_NAME} - {isLoading ? ( - - ) : isError || !version ? ( - "latest" - ) : ( - v{version} - )} + {isError || !version ? "latest" : v{version}}
diff --git a/apps/gittensory-ui/src/lib/mcp-package.ts b/apps/gittensory-ui/src/lib/mcp-package.ts new file mode 100644 index 0000000000..749d8fe434 --- /dev/null +++ b/apps/gittensory-ui/src/lib/mcp-package.ts @@ -0,0 +1,60 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiFetch } from "@/lib/api/request"; + +export const MCP_PACKAGE_NAME = "@jsonbored/gittensory-mcp"; +export const MCP_PACKAGE_ENCODED_NAME = "@jsonbored%2fgittensory-mcp"; +export const MCP_PACKAGE_REGISTRY_URL = `https://registry.npmjs.org/${MCP_PACKAGE_ENCODED_NAME}`; +export const MCP_PACKAGE_NPM_URL = `https://www.npmjs.com/package/${MCP_PACKAGE_NAME}`; +export const MCP_PACKAGE_KNOWN_LATEST_VERSION = "0.3.0"; +export const MCP_MINIMUM_SUPPORTED_VERSION = "0.2.0"; + +export type NpmPackageMetadata = { + "dist-tags": { latest?: string }; + time: Record; + versions: Record; +}; + +export function useMcpPackageMetadata() { + return useQuery({ + queryKey: ["npm", MCP_PACKAGE_NAME], + queryFn: fetchMcpPackageMetadata, + staleTime: 1000 * 60 * 30, + retry: 1, + }); +} + +export async function fetchMcpPackageMetadata(): Promise { + const result = await apiFetch(MCP_PACKAGE_REGISTRY_URL, { + label: "MCP package version", + timeoutMs: 6000, + silentStatus: true, + }); + if (!result.ok) throw new Error(result.message); + return result.data; +} + +export function getLatestMcpVersion(data: NpmPackageMetadata | undefined): string { + const latest = data?.["dist-tags"].latest; + return latest && isStableVersion(latest) ? latest : MCP_PACKAGE_KNOWN_LATEST_VERSION; +} + +export function getMcpInstallCommand(version?: string): string { + return `npm i -g ${MCP_PACKAGE_NAME}@${version && isStableVersion(version) ? version : "latest"}`; +} + +export function getMcpNpxPackage(version?: string): string { + return `${MCP_PACKAGE_NAME}@${version && isStableVersion(version) ? version : "latest"}`; +} + +export function getRecentMcpVersions(data: NpmPackageMetadata | undefined, limit = 6): string[] { + if (!data) return [MCP_PACKAGE_KNOWN_LATEST_VERSION]; + return Object.keys(data.versions) + .filter((version) => isStableVersion(version) && data.time[version]) + .sort((left, right) => data.time[right].localeCompare(data.time[left])) + .slice(0, limit); +} + +export function isStableVersion(version: string): boolean { + return /^\d+\.\d+\.\d+$/.test(version); +} diff --git a/apps/gittensory-ui/src/routes/agents.tsx b/apps/gittensory-ui/src/routes/agents.tsx index 4634bd0041..69fa209bd6 100644 --- a/apps/gittensory-ui/src/routes/agents.tsx +++ b/apps/gittensory-ui/src/routes/agents.tsx @@ -68,6 +68,9 @@ function AgentsPage() {

+
+ +
@@ -100,9 +103,11 @@ function AgentsPage() {

diff --git a/apps/gittensory-ui/src/routes/api.index.tsx b/apps/gittensory-ui/src/routes/api.index.tsx index ce13c07d56..b5749f3dce 100644 --- a/apps/gittensory-ui/src/routes/api.index.tsx +++ b/apps/gittensory-ui/src/routes/api.index.tsx @@ -4,6 +4,13 @@ import { ArrowRight, ShieldCheck } from "lucide-react"; import { CodeBlock, Callout, Eyebrow } from "@/components/site/primitives"; import { MethodPill, BoundaryBadge } from "@/components/site/control-primitives"; import { openapi } from "@/lib/openapi"; +import { + MCP_MINIMUM_SUPPORTED_VERSION, + MCP_PACKAGE_NAME, + getLatestMcpVersion, + getMcpInstallCommand, + useMcpPackageMetadata, +} from "@/lib/mcp-package"; export const Route = createFileRoute("/api/")({ component: ApiOverview, @@ -11,6 +18,8 @@ export const Route = createFileRoute("/api/")({ function ApiOverview() { const server = openapi.servers[0]?.url ?? ""; + const { data } = useMcpPackageMetadata(); + const latestMcpVersion = getLatestMcpVersion(data); return (
API reference @@ -49,6 +58,35 @@ function ApiOverview() {

+
+
+ MCP version contract +
+
+
+
+ Current npm latest +
+
+ {MCP_PACKAGE_NAME} v{latestMcpVersion} +
+
+
+
+ Minimum supported +
+
+ {MCP_MINIMUM_SUPPORTED_VERSION} +
+
+
+

+ Use npm dist-tags.latest for current-version display.{" "} + /v1/mcp/compatibility stays the API compatibility metadata source. +

+ +
+
Quick example diff --git a/apps/gittensory-ui/src/routes/changelog.tsx b/apps/gittensory-ui/src/routes/changelog.tsx index 0af0b58aa9..5d8cbefd93 100644 --- a/apps/gittensory-ui/src/routes/changelog.tsx +++ b/apps/gittensory-ui/src/routes/changelog.tsx @@ -1,9 +1,14 @@ import { createFileRoute } from "@tanstack/react-router"; -import { useQuery } from "@tanstack/react-query"; import { Rss } from "lucide-react"; import { Section, Eyebrow, Card } from "@/components/site/primitives"; import { cn } from "@/lib/utils"; +import { + MCP_PACKAGE_NAME, + MCP_PACKAGE_NPM_URL, + getLatestMcpVersion, + useMcpPackageMetadata, +} from "@/lib/mcp-package"; export const Route = createFileRoute("/changelog")({ head: () => ({ @@ -25,12 +30,6 @@ export const Route = createFileRoute("/changelog")({ component: Changelog, }); -type NpmPackage = { - "dist-tags": { latest: string }; - time: Record; - versions: Record; -}; - const DATE_FORMATTER = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", @@ -39,15 +38,8 @@ const DATE_FORMATTER = new Intl.DateTimeFormat("en-US", { }); function Changelog() { - const { data, isLoading, isError } = useQuery({ - queryKey: ["npm-full", "@jsonbored/gittensory-mcp"], - queryFn: async (): Promise => { - const r = await fetch("https://registry.npmjs.org/@jsonbored/gittensory-mcp"); - if (!r.ok) throw new Error("npm"); - return r.json(); - }, - staleTime: 1000 * 60 * 30, - }); + const { data, isLoading, isError } = useMcpPackageMetadata(); + const latestVersion = getLatestMcpVersion(data); const sortedVersions = data ? Object.keys(data.versions) @@ -64,12 +56,13 @@ function Changelog() { Releases

Changelog

- Live release history for @jsonbored/gittensory-mcp, - sourced directly from the npm registry. + Live release history for {MCP_PACKAGE_NAME}, sourced + directly from npm dist-tags.latest. Current release:{" "} + v{latestVersion}.

{sortedVersions.map((v) => { const id = `v${v.replace(/\./g, "-")}`; - const isLatest = v === data?.["dist-tags"].latest; + const isLatest = v === latestVersion; return (
  • Install the MCP. Global install or npx — see{" "} Quickstart.
  • diff --git a/apps/gittensory-ui/src/routes/docs.mcp-clients.tsx b/apps/gittensory-ui/src/routes/docs.mcp-clients.tsx index 6dd633c603..10bd2612db 100644 --- a/apps/gittensory-ui/src/routes/docs.mcp-clients.tsx +++ b/apps/gittensory-ui/src/routes/docs.mcp-clients.tsx @@ -52,7 +52,7 @@ gittensory-mcp init-client --print mcp`} lang="toml" code={`[mcp_servers.gittensory] command = "npx" -args = ["-y", "@jsonbored/gittensory-mcp", "--stdio"]`} +args = ["-y", "@jsonbored/gittensory-mcp@latest", "--stdio"]`} />

    Claude Desktop

    @@ -60,10 +60,10 @@ args = ["-y", "@jsonbored/gittensory-mcp", "--stdio"]`} filename="claude_desktop_config.json" lang="json" code={`{ - "mcpServers": { + "mcpServers": { "gittensory": { "command": "npx", - "args": ["-y", "@jsonbored/gittensory-mcp", "--stdio"] + "args": ["-y", "@jsonbored/gittensory-mcp@latest", "--stdio"] } } }`} @@ -74,10 +74,10 @@ args = ["-y", "@jsonbored/gittensory-mcp", "--stdio"]`} filename=".cursor/mcp.json" lang="json" code={`{ - "mcpServers": { + "mcpServers": { "gittensory": { "command": "npx", - "args": ["-y", "@jsonbored/gittensory-mcp", "--stdio"] + "args": ["-y", "@jsonbored/gittensory-mcp@latest", "--stdio"] } } }`} diff --git a/apps/gittensory-ui/src/routes/docs.quickstart.tsx b/apps/gittensory-ui/src/routes/docs.quickstart.tsx index e2ed31669b..ac49892712 100644 --- a/apps/gittensory-ui/src/routes/docs.quickstart.tsx +++ b/apps/gittensory-ui/src/routes/docs.quickstart.tsx @@ -39,10 +39,10 @@ function Quickstart() {

    2. Sign in (GitHub Device Flow)

    @@ -76,9 +76,9 @@ gittensory-mcp preflight --login your-login --json`}
    Beta onboarding.

    ); diff --git a/apps/gittensory-ui/src/routes/index.tsx b/apps/gittensory-ui/src/routes/index.tsx index 21f0326166..500c4e12b8 100644 --- a/apps/gittensory-ui/src/routes/index.tsx +++ b/apps/gittensory-ui/src/routes/index.tsx @@ -11,6 +11,7 @@ import { PrQuietCompare } from "@/components/site/home/pr-quiet-compare"; import { NpmInstall } from "@/components/site/npm-install"; import { TrustStrip } from "@/components/site/trust-strip"; import { describeApiStatus, pingHealth, useApiStatus } from "@/lib/api/status"; +import { MCP_PACKAGE_NAME, getLatestMcpVersion, useMcpPackageMetadata } from "@/lib/mcp-package"; export const Route = createFileRoute("/")({ head: () => ({ @@ -54,13 +55,16 @@ function Home() { } function Hero() { + const { data } = useMcpPackageMetadata(); + const latestMcpVersion = getLatestMcpVersion(data); + return (
    - v0.2 · deterministic base agent + MCP v{latestMcpVersion} · deterministic base agent

    Mine Gittensor like an engineer. @@ -106,7 +110,9 @@ function Hero() { function MetaStrip() { const { status, lastCheckedAt, connection } = useApiStatus(); + const { data } = useMcpPackageMetadata(); const [now, setNow] = useState(() => Date.now()); + const latestMcpVersion = getLatestMcpVersion(data); useEffect(() => { const t = window.setInterval(() => setNow(Date.now()), 30_000); @@ -139,7 +145,7 @@ function MetaStrip() { })(); const items = [ - { k: "MCP package", v: "@jsonbored/gittensory-mcp 0.2.x" }, + { k: "MCP package", v: `${MCP_PACKAGE_NAME} v${latestMcpVersion}` }, { k: "API", v: apiLabel }, { k: "Last checked", v: freshness }, { k: "Upstream drift", v: "monitored" }, @@ -445,7 +451,7 @@ const CLIENT_TABS: Array<{ audience: "Miners", filename: "terminal", lang: "bash", - snippet: `npm i -g @jsonbored/gittensory-mcp + snippet: `npm i -g @jsonbored/gittensory-mcp@latest gittensory-mcp login gittensory-mcp analyze-branch --login your-login --json`, }, @@ -457,7 +463,7 @@ gittensory-mcp analyze-branch --login your-login --json`, lang: "toml", snippet: `[mcp_servers.gittensory] command = "npx" -args = ["-y", "@jsonbored/gittensory-mcp", "--stdio"]`, +args = ["-y", "@jsonbored/gittensory-mcp@latest", "--stdio"]`, }, { id: "claude", @@ -469,7 +475,7 @@ args = ["-y", "@jsonbored/gittensory-mcp", "--stdio"]`, "mcpServers": { "gittensory": { "command": "npx", - "args": ["-y", "@jsonbored/gittensory-mcp", "--stdio"] + "args": ["-y", "@jsonbored/gittensory-mcp@latest", "--stdio"] } } }`, @@ -484,7 +490,7 @@ args = ["-y", "@jsonbored/gittensory-mcp", "--stdio"]`, "mcpServers": { "gittensory": { "command": "npx", - "args": ["-y", "@jsonbored/gittensory-mcp", "--stdio"] + "args": ["-y", "@jsonbored/gittensory-mcp@latest", "--stdio"] } } }`, diff --git a/apps/gittensory-ui/src/routes/roadmap.tsx b/apps/gittensory-ui/src/routes/roadmap.tsx index f4356c944c..3e971665f5 100644 --- a/apps/gittensory-ui/src/routes/roadmap.tsx +++ b/apps/gittensory-ui/src/routes/roadmap.tsx @@ -26,61 +26,86 @@ export const Route = createFileRoute("/roadmap")({ }); const COLUMNS = [ - { key: "shipping-soon", title: "Now", hint: "Shipping in the current cycle." }, - { key: "planned", title: "Next", hint: "Designed, queued for the next cycle." }, - { key: "exploring", title: "Later", hint: "Open questions, no commit date." }, + { key: "shipping-soon", title: "Now", hint: "Phase 0/1: stabilize and ship the miner loop." }, + { key: "planned", title: "Next", hint: "Phase 2/3: maintainer trust and repo-owner intake." }, + { key: "exploring", title: "Later", hint: "Phase 4/5: analytics, launch system, distribution." }, ] as const; -const LAST_UPDATED = "2026-05-30"; -const LAST_UPDATED_LABEL = "May 30, 2026"; +const LAST_UPDATED = "2026-06-01"; +const LAST_UPDATED_LABEL = "June 1, 2026"; const ROADMAP_ITEMS: Array<{ title: string; status: (typeof COLUMNS)[number]["key"]; description: string; + issue: number; }> = [ { - title: "@gittensory GitHub command agent", + title: "Phase 0: stabilize while shipping", status: "shipping-soon", - description: "Quiet, opt-in @-commands maintainers can use inside PR threads.", + issue: 233, + description: + "Current-version MCP display, stale PR queue triage, install/docs cleanup, and high-confidence polish before broader launch work.", }, { - title: "Product usage analytics", + title: "Phase 1: miner command center", status: "shipping-soon", - description: "Weekly value report and operator dashboard.", + issue: 234, + description: + "MCP doctor/status/init-client clarity, last-good decision packs, recommendation-change explanations, and command-copy flows for miners.", }, { - title: "Browser extension PR overlays", + title: "Phase 2: maintainer trust and browser extension", status: "planned", - description: "Private maintainer overlays on github.com, never shown to PR authors.", + issue: 235, + description: + "Maintainer trust checklist, install health next actions, screenshot-backed extension states, and private/public rendering checks.", }, { - title: "PWA maintainer digest", + title: "Phase 3: repo owner intake console", status: "planned", - description: "Mobile-friendly daily digest of reviewability and install health.", + issue: 236, + description: + "Guided registration readiness, config recommendations, repo onboarding packs, and source-quality next actions.", }, { - title: "Optional AI summaries", + title: "Phase 4: adoption analytics and launch system", status: "exploring", - description: "Strictly over deterministic signals; never replaces evidence.", + issue: 237, + description: + "Privacy-safe product events, role activation/retention metrics, weekly value reports, and operator export paths.", + }, + { + title: "Phase 5: ecosystem distribution", + status: "exploring", + issue: 238, + description: + "PWA/digest delivery, documentation distribution, launch loops, and ecosystem packaging once the core surfaces are durable.", }, ]; // Titles with live or self-hosted surfaces in the imported frontend. const BUILT_TITLES = new Set([ - "@gittensory GitHub command agent", - "Product usage analytics", - "Browser extension PR overlays", - "PWA maintainer digest", - "Optional AI summaries", + "Phase 0: stabilize while shipping", + "Phase 1: miner command center", + "Phase 2: maintainer trust and browser extension", + "Phase 3: repo owner intake console", + "Phase 4: adoption analytics and launch system", + "Phase 5: ecosystem distribution", ]); const LINK_MAP: Record = { - "@gittensory GitHub command agent": { to: "/app/commands", label: "Open command simulator" }, - "Product usage analytics": { to: "/app/analytics", label: "Open analytics" }, - "Browser extension PR overlays": { to: "/extension", label: "Open extension page" }, - "PWA maintainer digest": { to: "/app/digest", label: "Preview the digest" }, - "Optional AI summaries": { to: "/docs/ai-summaries", label: "Read the policy" }, + "Phase 1: miner command center": { to: "/app/miner", label: "Open miner dashboard" }, + "Phase 2: maintainer trust and browser extension": { + to: "/extension", + label: "Open extension page", + }, + "Phase 3: repo owner intake console": { to: "/app/repos", label: "Open repos console" }, + "Phase 4: adoption analytics and launch system": { + to: "/app/analytics", + label: "Open analytics", + }, + "Phase 5: ecosystem distribution": { to: "/app/digest", label: "Preview the digest" }, }; function RoadmapPage() { @@ -97,9 +122,17 @@ function RoadmapPage() { What's next for Gittensory

    - Each surface below maps to either live API-backed app wiring, a self-hosted package, or a - clearly scoped future lane. + This reflects the live roadmap issue #127 and phase epics #233-#238. Project-board linkage + waits until the GitHub project scope is available.

    + + Roadmap #127 → +
    Last updated @@ -143,11 +176,19 @@ function RoadmapPage() { {built && ( - Preview + Tracked )}

    {item.description}

    + + Issue #{item.issue} + {link && ( join(root, target)); + +const latest = process.env.GITTENSORY_MCP_LATEST_VERSION ?? (await fetchLatestVersion()); +const sourceLatest = readKnownLatestVersion(sourceLatestPath); +const failures = []; + +if (sourceLatest !== latest) { + failures.push( + `apps/gittensory-ui/src/lib/mcp-package.ts: known latest ${sourceLatest} does not match npm dist-tags.latest ${latest}`, + ); +} + +for (const file of targets.flatMap(collectSourceFiles)) { + const label = relative(root, file); + const text = readFileSync(file, "utf8"); + const lines = text.split(/\r?\n/); + + lines.forEach((line, index) => { + const lineNumber = index + 1; + if (/\bv0\.2(?:\.0)?\b/.test(line)) { + failures.push(`${label}:${lineNumber}: stale visible v0.2 version text`); + } + if (/\b0\.2\.x\b/.test(line)) { + failures.push(`${label}:${lineNumber}: stale 0.2.x package-version range`); + } + if (/\b0\.2\.0\b/.test(line) && !isMinimumSupportedContext(line)) { + failures.push(`${label}:${lineNumber}: 0.2.0 is only allowed as an explicit minimum-supported compatibility floor`); + } + if (/@jsonbored\/gittensory-mcp(?:\s+|@)v?\d+\.\d+\.\d+/.test(line)) { + failures.push(`${label}:${lineNumber}: hardcoded ${packageName} display version`); + } + if (/(?:npm (?:i|install) -g|npx -y)\s+@jsonbored\/gittensory-mcp(?!@)/.test(line)) { + failures.push(`${label}:${lineNumber}: install command must use ${packageName}@latest or resolved npm latest`); + } + if (/args\s*=\s*\[.*"@jsonbored\/gittensory-mcp"/.test(line) || /"args":\s*\[.*"@jsonbored\/gittensory-mcp"/.test(line)) { + failures.push(`${label}:${lineNumber}: MCP client args must use ${packageName}@latest or resolved npm latest`); + } + }); +} + +if (failures.length > 0) { + console.error(failures.join("\n")); + process.exit(1); +} + +console.log(`MCP UI version copy ok: npm latest ${latest}, scanned ${targets.length} target(s)`); + +function collectSourceFiles(path) { + const stat = statSync(path); + if (stat.isFile()) return isTextSource(path) ? [path] : []; + return readdirSync(path).flatMap((entry) => { + const next = join(path, entry); + if (entry === "node_modules" || entry === "dist" || entry === ".vitepress" || entry === "coverage") return []; + if (/routeTree\.gen\.ts$/.test(next) || /public\/openapi\.json$/.test(next)) return []; + return collectSourceFiles(next); + }); +} + +function isTextSource(path) { + return /\.(md|ts|tsx|js|jsx|json)$/.test(path); +} + +function isMinimumSupportedContext(line) { + return /minimum[_ -]?supported|MCP_MINIMUM_SUPPORTED_VERSION|MINIMUM_SUPPORTED_MCP_VERSION|compatibility floor|API minimum|supportedVersionRange/i.test(line); +} + +function readKnownLatestVersion(path) { + const text = readFileSync(path, "utf8"); + const match = /MCP_PACKAGE_KNOWN_LATEST_VERSION\s*=\s*"([^"]+)"/.exec(text); + if (!match) throw new Error("Could not find MCP_PACKAGE_KNOWN_LATEST_VERSION."); + return match[1]; +} + +function fetchLatestVersion() { + return new Promise((resolve, reject) => { + const request = get(registryUrl, { headers: { accept: "application/json" } }, (response) => { + let body = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { + body += chunk; + }); + response.on("end", () => { + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { + reject(new Error(`npm registry returned ${response.statusCode ?? "unknown"}`)); + return; + } + try { + const latest = JSON.parse(body)?.["dist-tags"]?.latest; + if (typeof latest !== "string" || !/^\d+\.\d+\.\d+$/.test(latest)) { + reject(new Error("npm registry did not return a stable latest version")); + return; + } + resolve(latest); + } catch (error) { + reject(error); + } + }); + }); + request.setTimeout(8000, () => { + request.destroy(new Error("npm registry timeout")); + }); + request.on("error", reject); + }); +}