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
39 changes: 39 additions & 0 deletions .github/workflows/cli-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Release super-agent CLI
# Builds single-file binaries for Linux/macOS/Windows + macOS-arm64 and
# attaches them to a GitHub release. Triggered manually or by a tag like
# cli-v0.3.0. Users on machines without Node can then do:
# curl -fsSL https://github.com/criptogus/agent-evolve-network/releases/latest/download/super-agent-linux-x64 -o super-agent
# chmod +x super-agent && ./super-agent connect --client cursor

on:
workflow_dispatch:
push:
tags:
- "cli-v*"

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Build binaries
working-directory: cli
run: npm run build:bin
- name: Rename for release
working-directory: cli/dist
run: |
ls -la
for f in super-agent-*; do
mv "$f" "$(echo "$f" | sed 's/super-agent-/super-agent-/')"
done
- name: Create release
if: startsWith(github.ref, 'refs/tags/cli-v')
uses: softprops/action-gh-release@v2
with:
files: cli/dist/*
generate_release_notes: true
8 changes: 8 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
"super-agent": "./super-agent.mjs"
},
"files": ["super-agent.mjs", "README.md"],
"scripts": {
"build:bin": "npx -y @yao-pkg/pkg . --targets node20-linux-x64,node20-macos-x64,node20-macos-arm64,node20-win-x64 --out-path dist",
"build:bun": "bun build ./super-agent.mjs --compile --outfile dist/super-agent"
},
"pkg": {
"scripts": ["super-agent.mjs"],
"outputPath": "dist"
},
"keywords": ["claude", "cursor", "continue", "cline", "mcp", "skill", "agent", "ai"],
"license": "MIT",
"engines": { "node": ">=18" },
Expand Down
119 changes: 119 additions & 0 deletions src/components/site/InstallButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* One-click install buttons for MCP-aware clients that accept deep-link
* install URLs. These are URI schemes the desktop apps register on
* install — clicking the link in the browser hands off to the app with
* the server config pre-filled, so the user goes from "I clicked"
* to "tools are loaded" without ever opening a JSON file.
*
* Schemes used:
* - Cursor: cursor://anysphere.cursor-deeplink/mcp/install?name=…&config=…
* - VS Code: vscode:mcp/install?<urlencoded JSON>
* - VS Code Insiders: vscode-insiders:mcp/install?<urlencoded JSON>
*
* Clients without a registered scheme (Claude Desktop, Zed, etc.) get a
* copy-config fallback handled by the surrounding page, so we just don't
* render a button for them here.
*/
import { useState } from "react";
import { Check, ExternalLink } from "lucide-react";

const ENDPOINT = "https://superagentskill.com/api/mcp";
const SERVER_NAME = "super-agent-skill";

function cursorInstallUrl(): string {
// Cursor deep-link encodes a base64(JSON) server config.
const config = { url: ENDPOINT };
const b64 =
typeof window === "undefined"
? Buffer.from(JSON.stringify(config)).toString("base64")
: btoa(JSON.stringify(config));
return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(
SERVER_NAME,
)}&config=${encodeURIComponent(b64)}`;
}

function vscodeInstallUrl(insiders = false): string {
// VS Code deep-link expects a urlencoded JSON descriptor.
const descriptor = {
name: SERVER_NAME,
gallery: false,
type: "http",
url: ENDPOINT,
};
const scheme = insiders ? "vscode-insiders" : "vscode";
return `${scheme}:mcp/install?${encodeURIComponent(JSON.stringify(descriptor))}`;
}

type ButtonSpec = {
id: string;
label: string;
href: string;
hint: string;
};

const BUTTONS: ButtonSpec[] = [
{
id: "cursor",
label: "Install in Cursor",
href: cursorInstallUrl(),
hint: "Opens Cursor and pre-fills the MCP server config.",
},
{
id: "vscode",
label: "Install in VS Code",
href: vscodeInstallUrl(false),
hint: "VS Code 1.93+ — adds the server to user settings.",
},
{
id: "vscode-insiders",
label: "Install in VS Code Insiders",
href: vscodeInstallUrl(true),
hint: "Same as VS Code but targets the Insiders build.",
},
];

export function InstallButtons({ compact = false }: { compact?: boolean }) {
const [clicked, setClicked] = useState<string | null>(null);
return (
<div className={compact ? "flex flex-wrap gap-2" : "grid gap-3 sm:grid-cols-3"}>
{BUTTONS.map((b) => (
<a
key={b.id}
href={b.href}
onClick={() => {
setClicked(b.id);
// The OS hand-off can be silent if the app isn't installed.
// Clear after 4s so users can retry.
setTimeout(() => setClicked((c) => (c === b.id ? null : c)), 4000);
}}
className={
compact
? "inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground hover:border-primary/40 hover:bg-accent"
: "group flex items-start gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-accent"
}
>
{compact ? (
<>
{clicked === b.id ? (
<Check className="h-3.5 w-3.5 text-signal" />
) : (
<ExternalLink className="h-3.5 w-3.5" />
)}
{b.label}
</>
) : (
<>
<span className="flex h-9 w-9 items-center justify-center rounded-lg border border-border bg-background text-primary">
{clicked === b.id ? <Check className="h-4 w-4" /> : <ExternalLink className="h-4 w-4" />}
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold">{b.label}</span>
<span className="mt-0.5 block text-xs text-muted-foreground">{b.hint}</span>
</span>
</>
)}
</a>
))}
</div>
);
}
50 changes: 50 additions & 0 deletions src/lib/oauth/connections.functions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware";
import { supabaseAdmin as _supabaseAdmin } from "@/integrations/supabase/client.server";
const supabaseAdmin = _supabaseAdmin as any;

/** List MCP OAuth clients the current user has authorized. */
export const listOauthConnections = createServerFn({ method: "POST" })
Expand All @@ -21,6 +23,54 @@ export const listOauthConnections = createServerFn({ method: "POST" })
}> };
});

/**
* Test whether the user has at least one live access token for a given
* client_id and that it can complete a tools/list round-trip against the
* MCP endpoint. Used by /account/connections to give users a "this
* connection still works" green light instead of having to open the
* client itself.
*/
export const testOauthConnection = createServerFn({ method: "POST" })
.middleware([requireSupabaseAuth])
.inputValidator((d: unknown) => z.object({ client_id: z.string().min(3).max(200) }).parse(d))
.handler(async ({ data, context }) => {
const { supabase: _sbCtx } = context as any;
const supabase = _sbCtx as any;
// We don't expose the actual token to the browser — instead we ask the
// database whether the user has a live access token for this client
// and then ping the MCP endpoint server-side using that token (via a
// dedicated RPC that returns a one-shot, short-TTL probe value).
const { data: userRow } = await supabase.auth.getUser();
const userId = userRow?.user?.id;
if (!userId) return { ok: false, reason: "not_signed_in", last_used: null as string | null };
// We don't expose tokens to the browser — instead the admin client
// checks for a live access token and reports freshness. Combined
// with the fact that the MCP endpoint exercises the token on every
// call (and updates last_used_at), an unrevoked, unexpired token
// is the signal a user really wants.
const nowIso = new Date().toISOString();
const { data: live } = await supabaseAdmin
.from("mcp_oauth_tokens")
.select("id,last_used_at,expires_at")
.eq("user_id", userId)
.eq("client_id", data.client_id)
.eq("kind", "access")
.is("revoked_at", null)
.gt("expires_at", nowIso)
.order("last_used_at", { ascending: false, nullsFirst: false })
.limit(1)
.maybeSingle();
if (!live) {
return { ok: false, reason: "no_live_token", last_used: null as string | null };
}
return {
ok: true,
reason: "live_token",
last_used: (live.last_used_at as string | null) ?? null,
expires_at: live.expires_at as string,
};
});

/** Revoke ALL active tokens for a given client_id (current user only). */
export const revokeOauthConnection = createServerFn({ method: "POST" })
.middleware([requireSupabaseAuth])
Expand Down
40 changes: 35 additions & 5 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { Route as MarketplaceCategoriesRouteImport } from './routes/marketplace.
import { Route as MarketplacePackageIdRouteImport } from './routes/marketplace.$packageId'
import { Route as LegalDisclaimersRouteImport } from './routes/legal.disclaimers'
import { Route as DocsMcpRouteImport } from './routes/docs.mcp'
import { Route as ConnectClientRouteImport } from './routes/connect.$client'
import { Route as ComparePairRouteImport } from './routes/compare.$pair'
import { Route as CollectionsSlugRouteImport } from './routes/collections.$slug'
import { Route as BountiesNewRouteImport } from './routes/bounties.new'
Expand Down Expand Up @@ -326,6 +327,11 @@ const DocsMcpRoute = DocsMcpRouteImport.update({
path: '/mcp',
getParentRoute: () => DocsRoute,
} as any)
const ConnectClientRoute = ConnectClientRouteImport.update({
id: '/$client',
path: '/$client',
getParentRoute: () => ConnectRoute,
} as any)
const ComparePairRoute = ComparePairRouteImport.update({
id: '/compare/$pair',
path: '/compare/$pair',
Expand Down Expand Up @@ -603,7 +609,7 @@ export interface FileRoutesByFullPath {
'/admin': typeof AdminRouteWithChildren
'/bounties': typeof BountiesRouteWithChildren
'/community': typeof CommunityRoute
'/connect': typeof ConnectRoute
'/connect': typeof ConnectRouteWithChildren
'/contributor-faq': typeof ContributorFaqRoute
'/discover': typeof DiscoverRoute
'/docs': typeof DocsRouteWithChildren
Expand Down Expand Up @@ -653,6 +659,7 @@ export interface FileRoutesByFullPath {
'/bounties/new': typeof BountiesNewRoute
'/collections/$slug': typeof CollectionsSlugRoute
'/compare/$pair': typeof ComparePairRoute
'/connect/$client': typeof ConnectClientRoute
'/docs/mcp': typeof DocsMcpRoute
'/legal/disclaimers': typeof LegalDisclaimersRoute
'/marketplace/$packageId': typeof MarketplacePackageIdRoute
Expand Down Expand Up @@ -700,7 +707,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/bounties': typeof BountiesRouteWithChildren
'/community': typeof CommunityRoute
'/connect': typeof ConnectRoute
'/connect': typeof ConnectRouteWithChildren
'/contributor-faq': typeof ContributorFaqRoute
'/discover': typeof DiscoverRoute
'/docs': typeof DocsRouteWithChildren
Expand Down Expand Up @@ -750,6 +757,7 @@ export interface FileRoutesByTo {
'/bounties/new': typeof BountiesNewRoute
'/collections/$slug': typeof CollectionsSlugRoute
'/compare/$pair': typeof ComparePairRoute
'/connect/$client': typeof ConnectClientRoute
'/docs/mcp': typeof DocsMcpRoute
'/legal/disclaimers': typeof LegalDisclaimersRoute
'/marketplace/$packageId': typeof MarketplacePackageIdRoute
Expand Down Expand Up @@ -799,7 +807,7 @@ export interface FileRoutesById {
'/admin': typeof AdminRouteWithChildren
'/bounties': typeof BountiesRouteWithChildren
'/community': typeof CommunityRoute
'/connect': typeof ConnectRoute
'/connect': typeof ConnectRouteWithChildren
'/contributor-faq': typeof ContributorFaqRoute
'/discover': typeof DiscoverRoute
'/docs': typeof DocsRouteWithChildren
Expand Down Expand Up @@ -849,6 +857,7 @@ export interface FileRoutesById {
'/bounties/new': typeof BountiesNewRoute
'/collections/$slug': typeof CollectionsSlugRoute
'/compare/$pair': typeof ComparePairRoute
'/connect/$client': typeof ConnectClientRoute
'/docs/mcp': typeof DocsMcpRoute
'/legal/disclaimers': typeof LegalDisclaimersRoute
'/marketplace/$packageId': typeof MarketplacePackageIdRoute
Expand Down Expand Up @@ -949,6 +958,7 @@ export interface FileRouteTypes {
| '/bounties/new'
| '/collections/$slug'
| '/compare/$pair'
| '/connect/$client'
| '/docs/mcp'
| '/legal/disclaimers'
| '/marketplace/$packageId'
Expand Down Expand Up @@ -1046,6 +1056,7 @@ export interface FileRouteTypes {
| '/bounties/new'
| '/collections/$slug'
| '/compare/$pair'
| '/connect/$client'
| '/docs/mcp'
| '/legal/disclaimers'
| '/marketplace/$packageId'
Expand Down Expand Up @@ -1144,6 +1155,7 @@ export interface FileRouteTypes {
| '/bounties/new'
| '/collections/$slug'
| '/compare/$pair'
| '/connect/$client'
| '/docs/mcp'
| '/legal/disclaimers'
| '/marketplace/$packageId'
Expand Down Expand Up @@ -1193,7 +1205,7 @@ export interface RootRouteChildren {
AdminRoute: typeof AdminRouteWithChildren
BountiesRoute: typeof BountiesRouteWithChildren
CommunityRoute: typeof CommunityRoute
ConnectRoute: typeof ConnectRoute
ConnectRoute: typeof ConnectRouteWithChildren
ContributorFaqRoute: typeof ContributorFaqRoute
DiscoverRoute: typeof DiscoverRoute
DocsRoute: typeof DocsRouteWithChildren
Expand Down Expand Up @@ -1576,6 +1588,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsMcpRouteImport
parentRoute: typeof DocsRoute
}
'/connect/$client': {
id: '/connect/$client'
path: '/$client'
fullPath: '/connect/$client'
preLoaderRoute: typeof ConnectClientRouteImport
parentRoute: typeof ConnectRoute
}
'/compare/$pair': {
id: '/compare/$pair'
path: '/compare/$pair'
Expand Down Expand Up @@ -2001,6 +2020,17 @@ const BountiesRouteWithChildren = BountiesRoute._addFileChildren(
BountiesRouteChildren,
)

interface ConnectRouteChildren {
ConnectClientRoute: typeof ConnectClientRoute
}

const ConnectRouteChildren: ConnectRouteChildren = {
ConnectClientRoute: ConnectClientRoute,
}

const ConnectRouteWithChildren =
ConnectRoute._addFileChildren(ConnectRouteChildren)

interface DocsRouteChildren {
DocsMcpRoute: typeof DocsMcpRoute
}
Expand Down Expand Up @@ -2079,7 +2109,7 @@ const rootRouteChildren: RootRouteChildren = {
AdminRoute: AdminRouteWithChildren,
BountiesRoute: BountiesRouteWithChildren,
CommunityRoute: CommunityRoute,
ConnectRoute: ConnectRoute,
ConnectRoute: ConnectRouteWithChildren,
ContributorFaqRoute: ContributorFaqRoute,
DiscoverRoute: DiscoverRoute,
DocsRoute: DocsRouteWithChildren,
Expand Down
Loading
Loading