diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2200e11..a72d7ec 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "understand-anything", "description": "Multi-agent codebase analysis with interactive dashboard, guided tours, and skill commands", - "version": "1.3.0", + "version": "1.3.1", "source": "./understand-anything-plugin" } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index eae103b..b857341 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "1.3.0", + "version": "1.3.1", "author": { "name": "Lum1104" }, diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 5e189f6..19f3f2b 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "understand-anything", "displayName": "Understand Anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "1.3.0", + "version": "1.3.1", "author": { "name": "Lum1104" }, diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index 7ee16e9..f96d583 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@understand-anything/skill", - "version": "1.3.0", + "version": "1.3.1", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 2067b14..0a39b73 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useMemo } from "react"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { validateGraph } from "@understand-anything/core/schema"; import type { GraphIssue } from "@understand-anything/core/schema"; import { useDashboardStore } from "./store"; @@ -13,20 +13,57 @@ import PersonaSelector from "./components/PersonaSelector"; import ProjectOverview from "./components/ProjectOverview"; import KeyboardShortcutsHelp from "./components/KeyboardShortcutsHelp"; import WarningBanner from "./components/WarningBanner"; +import TokenGate from "./components/TokenGate"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts"; import { ThemeProvider } from "./themes/index.ts"; import { ThemePicker } from "./components/ThemePicker.tsx"; import type { ThemeConfig } from "./themes/index.ts"; -// Extract the access token from the URL so protected endpoints can be fetched. -const ACCESS_TOKEN = new URLSearchParams(window.location.search).get("token"); +const SESSION_TOKEN_KEY = "understand-anything-token"; -function tokenUrl(path: string): string { - return ACCESS_TOKEN ? `${path}?token=${ACCESS_TOKEN}` : path; +/** + * Resolve the access token from the URL query string or sessionStorage. + * If found in the URL, persist to sessionStorage and strip the param from the address bar. + */ +function resolveInitialToken(): string | null { + const params = new URLSearchParams(window.location.search); + const urlToken = params.get("token"); + if (urlToken) { + sessionStorage.setItem(SESSION_TOKEN_KEY, urlToken); + // Clean the URL + params.delete("token"); + const cleanSearch = params.toString(); + const newUrl = + window.location.pathname + (cleanSearch ? `?${cleanSearch}` : "") + window.location.hash; + window.history.replaceState(null, "", newUrl); + return urlToken; + } + return sessionStorage.getItem(SESSION_TOKEN_KEY); +} + +/** Build a URL with the token query param appended. */ +function tokenUrl(path: string, token: string | null): string { + return token ? `${path}?token=${encodeURIComponent(token)}` : path; } function App() { + const [accessToken, setAccessToken] = useState(resolveInitialToken); + + const handleTokenValid = useCallback((token: string) => { + sessionStorage.setItem(SESSION_TOKEN_KEY, token); + setAccessToken(token); + }, []); + + // Show the token gate when no token is available + if (accessToken === null) { + return ; + } + + return ; +} + +function Dashboard({ accessToken }: { accessToken: string }) { const graph = useDashboardStore((s) => s.graph); const setGraph = useDashboardStore((s) => s.setGraph); const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); @@ -41,7 +78,7 @@ function App() { const [metaTheme, setMetaTheme] = useState(null); useEffect(() => { - fetch(tokenUrl("/meta.json")) + fetch(tokenUrl("/meta.json", accessToken)) .then((r) => (r.ok ? r.json() : null)) .then((meta) => { if (meta?.theme) setMetaTheme(meta.theme); @@ -133,7 +170,7 @@ function App() { useKeyboardShortcuts(shortcuts); useEffect(() => { - fetch(tokenUrl("/knowledge-graph.json")) + fetch(tokenUrl("/knowledge-graph.json", accessToken)) .then((res) => res.json()) .then((data: unknown) => { const result = validateGraph(data); @@ -162,7 +199,7 @@ function App() { }, [setGraph]); useEffect(() => { - fetch(tokenUrl("/diff-overlay.json")) + fetch(tokenUrl("/diff-overlay.json", accessToken)) .then((res) => { if (!res.ok) return null; return res.json(); diff --git a/understand-anything-plugin/packages/dashboard/src/components/TokenGate.tsx b/understand-anything-plugin/packages/dashboard/src/components/TokenGate.tsx new file mode 100644 index 0000000..5d74d67 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/components/TokenGate.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; + +interface TokenGateProps { + onTokenValid: (token: string) => void; +} + +export default function TokenGate({ onTokenValid }: TokenGateProps) { + const [input, setInput] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const token = input.trim(); + if (!token) return; + + setLoading(true); + setError(null); + + try { + const res = await fetch(`/knowledge-graph.json?token=${encodeURIComponent(token)}`); + if (res.ok) { + onTokenValid(token); + } else if (res.status === 403) { + setError("Invalid token. Please check and try again."); + } else { + setError(`Unexpected response (${res.status}). Is the dashboard server running?`); + } + } catch (err) { + setError( + `Could not reach the server: ${err instanceof Error ? err.message : String(err)}` + ); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Heading */} +

+ Access Token Required +

+

+ Paste the access token from your terminal. Look for the{" "} + 🔑 line. +

+ + {/* Form */} +
+ { + setInput(e.target.value); + if (error) setError(null); + }} + placeholder="Paste token here..." + autoFocus + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded text-text-primary placeholder:text-text-muted/50 font-mono text-sm focus:outline-none focus:border-accent transition-colors" + /> + + {error && ( +

{error}

+ )} + + +
+
+
+ ); +}