cynarAI 3469165c92
Connect GitHub from a token alone (#195)
* Connect GitHub from a token alone, without a vendor account

Integrations only reached GitHub through Composio or Pipedream, which a
self-hosted instance does not have. A built-in GitHub row now opens
GitHub's prefilled token form, and the pasted token connects GitHub's
own MCP server through the existing installed-connector path. Web and
mobile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

* Sync web catalogs for the GitHub connect strings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 13:18:44 +02:00

131 lines
4.5 KiB
TypeScript

import { Trans, useLingui } from "@lingui/react/macro";
import type { CapabilityInstall } from "@rakazo/contracts";
import { GITHUB_MCP_SOURCE, GITHUB_TOKEN_URL, isGithubInstall } from "@rakazo/core";
import { Button, buttonVariants, cn, Input } from "@rakazo/ui-web";
import { useState } from "react";
import { rpc } from "../../lib/rpc";
/** GitHub's mark, monochrome like the rest of the app. */
function GithubMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.2c-3.3.7-4-1.4-4-1.4-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 0-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.6.2 2.8.1 3.2.8.8 1.2 1.9 1.2 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.1.7.8.6A12 12 0 0 0 12 .3" />
</svg>
);
}
/**
* GitHub without a vendor account or an app registration: a token from GitHub's prefilled form
* connects GitHub's own MCP server for every bot.
*/
export function GithubConnect({
sources,
onChanged,
}: {
sources: CapabilityInstall[];
onChanged: () => Promise<unknown>;
}) {
const { t } = useLingui();
const installed = sources.find(isGithubInstall);
const [open, setOpen] = useState(false);
const [token, setToken] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function run(task: () => Promise<unknown>) {
setPending(true);
setError(null);
try {
await task();
await onChanged();
} catch (err) {
setError(err instanceof Error ? err.message : t`Could not connect GitHub`);
} finally {
setPending(false);
}
}
return (
<div data-testid="github-connect" className="mb-6 rounded-xl border border-border px-3 py-2.5">
<div className="flex items-center gap-3">
<div className="grid size-9 shrink-0 place-items-center rounded-xl bg-accent text-foreground">
<GithubMark className="size-5" />
</div>
<div className="min-w-0 flex-1 truncate text-ui-body font-medium text-foreground">
GitHub
</div>
{installed ? (
<>
<span className="text-ui-body-sm text-muted-foreground">
<Trans>Connected</Trans>
</span>
<Button
type="button"
variant="ghost"
size="sm"
className="rounded-full"
disabled={pending}
onClick={() => void run(() => rpc.capabilities.remove({ id: installed.id }))}
>
<Trans>Disconnect</Trans>
</Button>
</>
) : open ? null : (
<Button
type="button"
variant="secondary"
size="sm"
className="rounded-full"
onClick={() => setOpen(true)}
>
<Trans>Connect</Trans>
</Button>
)}
</div>
{!installed && open ? (
<form
className="mt-3 flex flex-col gap-2"
onSubmit={(event) => {
event.preventDefault();
void run(async () => {
await rpc.capabilities.install({
kind: "mcp",
name: "GitHub",
source: GITHUB_MCP_SOURCE,
credential: token.trim(),
config: { preset: "github" },
});
setToken("");
setOpen(false);
});
}}
>
<a
href={GITHUB_TOKEN_URL}
target="_blank"
rel="noreferrer"
className={cn(buttonVariants({ variant: "outline" }), "rounded-full")}
>
<Trans>Create token on GitHub</Trans>
</a>
<div className="flex gap-2">
<Input
type="password"
autoComplete="off"
value={token}
onChange={(event) => setToken(event.target.value)}
aria-label={t`GitHub token`}
placeholder="ghp_…"
className="h-10 flex-1 rounded-full px-4"
/>
<Button type="submit" className="rounded-full" disabled={pending || !token.trim()}>
{pending ? <Trans>Connecting…</Trans> : <Trans>Connect</Trans>}
</Button>
</div>
</form>
) : null}
{error ? <p className="mt-2 text-ui-body-sm text-destructive">{error}</p> : null}
</div>
);
}