mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
Add Claude subscription auth controls
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
ae59be5ead
commit
db1e46f3dc
8 files changed
+342
-72
No files matched your search
@@ -111,6 +111,7 @@ function buildLoginResult(input: {
|
||||
stdout: input.proc.stdout,
|
||||
stderr: input.proc.stderr,
|
||||
loginUrl: input.loginUrl,
|
||||
authUrl: input.loginUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -331,6 +332,18 @@ export async function runClaudeLogin(input: {
|
||||
context?: Record<string, unknown>;
|
||||
authToken?: string;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
}) {
|
||||
return runClaudeSubscriptionLogin(input);
|
||||
}
|
||||
|
||||
export async function runClaudeSubscriptionLogin(input: {
|
||||
runId: string;
|
||||
agent: AdapterExecutionContext["agent"];
|
||||
config: Record<string, unknown>;
|
||||
context?: Record<string, unknown>;
|
||||
authToken?: string;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
timeoutSec?: number;
|
||||
}) {
|
||||
const onLog = input.onLog ?? (async () => {});
|
||||
const runtime = await buildClaudeRuntimeConfig({
|
||||
@@ -341,13 +354,19 @@ export async function runClaudeLogin(input: {
|
||||
authToken: input.authToken,
|
||||
});
|
||||
|
||||
const proc = await runAdapterExecutionTargetProcess(input.runId, null, runtime.command, ["login"], {
|
||||
cwd: runtime.cwd,
|
||||
env: runtime.env,
|
||||
timeoutSec: runtime.timeoutSec,
|
||||
graceSec: runtime.graceSec,
|
||||
onLog,
|
||||
});
|
||||
const proc = await runAdapterExecutionTargetProcess(
|
||||
input.runId,
|
||||
null,
|
||||
runtime.command,
|
||||
["auth", "login", "--claudeai"],
|
||||
{
|
||||
cwd: runtime.cwd,
|
||||
env: runtime.env,
|
||||
timeoutSec: input.timeoutSec ?? runtime.timeoutSec,
|
||||
graceSec: runtime.graceSec,
|
||||
onLog,
|
||||
},
|
||||
);
|
||||
|
||||
const loginMeta = detectClaudeLoginRequired({
|
||||
parsed: null,
|
||||
@@ -361,6 +380,58 @@ export async function runClaudeLogin(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function runClaudeSubscriptionAuthUrl(input: {
|
||||
runId: string;
|
||||
agent: AdapterExecutionContext["agent"];
|
||||
config: Record<string, unknown>;
|
||||
context?: Record<string, unknown>;
|
||||
authToken?: string;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
timeoutSec?: number;
|
||||
}) {
|
||||
return runClaudeSubscriptionLogin({
|
||||
...input,
|
||||
timeoutSec: input.timeoutSec ?? 8,
|
||||
});
|
||||
}
|
||||
|
||||
export async function runClaudeLogout(input: {
|
||||
runId: string;
|
||||
agent: AdapterExecutionContext["agent"];
|
||||
config: Record<string, unknown>;
|
||||
context?: Record<string, unknown>;
|
||||
authToken?: string;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
}) {
|
||||
const onLog = input.onLog ?? (async () => {});
|
||||
const runtime = await buildClaudeRuntimeConfig({
|
||||
runId: input.runId,
|
||||
agent: input.agent,
|
||||
config: input.config,
|
||||
context: input.context ?? {},
|
||||
authToken: input.authToken,
|
||||
});
|
||||
|
||||
const proc = await runAdapterExecutionTargetProcess(
|
||||
input.runId,
|
||||
null,
|
||||
runtime.command,
|
||||
["auth", "logout"],
|
||||
{
|
||||
cwd: runtime.cwd,
|
||||
env: runtime.env,
|
||||
timeoutSec: runtime.timeoutSec,
|
||||
graceSec: runtime.graceSec,
|
||||
onLog,
|
||||
},
|
||||
);
|
||||
|
||||
return buildLoginResult({
|
||||
proc,
|
||||
loginUrl: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
|
||||
const executionTarget = readAdapterExecutionTarget({
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
export { claudeSessionCwdMatchesExecutionTarget, execute, runClaudeLogin } from "./execute.js";
|
||||
export {
|
||||
claudeSessionCwdMatchesExecutionTarget,
|
||||
execute,
|
||||
runClaudeLogin,
|
||||
runClaudeLogout,
|
||||
runClaudeSubscriptionAuthUrl,
|
||||
runClaudeSubscriptionLogin,
|
||||
} from "./execute.js";
|
||||
export { listClaudeSkills, syncClaudeSkills } from "./skills.js";
|
||||
export { listClaudeModels, refreshClaudeModels, resetClaudeModelsCacheForTests } from "./models.js";
|
||||
export { testEnvironment } from "./test.js";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
extractClaudeLoginUrl,
|
||||
extractClaudeRetryNotBefore,
|
||||
isClaudeTransientUpstreamError,
|
||||
isClaudePoisonedPreviousMessageIdError,
|
||||
@@ -7,6 +8,31 @@ import {
|
||||
isClaudeImageProcessingError,
|
||||
} from "./parse.js";
|
||||
|
||||
describe("extractClaudeLoginUrl", () => {
|
||||
it("extracts the subscription auth URL from Claude CLI login output", () => {
|
||||
const output = [
|
||||
"Opening browser to sign in...",
|
||||
"If the browser didn't open, visit: https://claude.com/cai/oauth/authorize?code=true&client_id=abc&redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback.",
|
||||
"Paste code here if prompted > ",
|
||||
].join("\n");
|
||||
|
||||
expect(extractClaudeLoginUrl(output)).toBe(
|
||||
"https://claude.com/cai/oauth/authorize?code=true&client_id=abc&redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers Claude auth URLs when other links are present", () => {
|
||||
const output = [
|
||||
"Docs: https://example.com/docs",
|
||||
"Open https://claude.com/cai/oauth/authorize?state=paperclip to continue.",
|
||||
].join("\n");
|
||||
|
||||
expect(extractClaudeLoginUrl(output)).toBe(
|
||||
"https://claude.com/cai/oauth/authorize?state=paperclip",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isClaudeTransientUpstreamError", () => {
|
||||
it("classifies the 'out of extra usage' subscription window failure as transient", () => {
|
||||
expect(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
const CLAUDE_AUTH_REQUIRED_RE = /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+`?claude\s+login`?|login\s+required|requires\s+login|unauthorized|authentication\s+required)/i;
|
||||
const URL_RE = /(https?:\/\/[^\s'"`<>()[\]{};,!?]+[^\s'"`<>()[\]{};,!.?:]+)/gi;
|
||||
const URL_RE = /(https?:\/\/[^\s'"`<>()[\]{}]+)/gi;
|
||||
|
||||
const CLAUDE_TRANSIENT_UPSTREAM_RE =
|
||||
/(?:rate[-\s]?limit(?:ed)?|rate_limit_error|too\s+many\s+requests|\b429\b|overloaded(?:_error)?|server\s+overloaded|service\s+unavailable|\b503\b|\b529\b|high\s+demand|try\s+again\s+later|temporarily\s+unavailable|throttl(?:ed|ing)|throttlingexception|servicequotaexceededexception|out\s+of\s+extra\s+usage|extra\s+usage\b|claude\s+usage\s+limit\s+reached|5[-\s]?hour\s+limit\s+reached|weekly\s+limit\s+reached|usage\s+limit\s+reached|usage\s+cap\s+reached)/i;
|
||||
|
||||
@@ -78,7 +78,11 @@ import { redactEventPayload } from "../redaction.js";
|
||||
import { redactCurrentUserValue } from "../log-redaction.js";
|
||||
import { renderOrgChartSvg, renderOrgChartPng, type OrgNode, type OrgChartStyle, ORG_CHART_STYLES } from "./org-chart-svg.js";
|
||||
import { instanceSettingsService } from "../services/instance-settings.js";
|
||||
import { runClaudeLogin } from "@paperclipai/adapter-claude-local/server";
|
||||
import {
|
||||
runClaudeLogin,
|
||||
runClaudeLogout,
|
||||
runClaudeSubscriptionAuthUrl,
|
||||
} from "@paperclipai/adapter-claude-local/server";
|
||||
import {
|
||||
DEFAULT_ACPX_LOCAL_AGENT,
|
||||
DEFAULT_ACPX_LOCAL_MODE,
|
||||
@@ -3258,20 +3262,31 @@ export function agentRoutes(
|
||||
res.status(202).json(run);
|
||||
});
|
||||
|
||||
router.post("/agents/:id/claude-login", async (req, res) => {
|
||||
type ClaudeLocalAgent = NonNullable<Awaited<ReturnType<typeof svc.getById>>>;
|
||||
const getClaudeLocalAgentForBoardAction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
actionLabel: string,
|
||||
): Promise<ClaudeLocalAgent | null> => {
|
||||
assertBoard(req);
|
||||
const id = req.params.id as string;
|
||||
const agent = await svc.getById(id);
|
||||
if (!agent) {
|
||||
res.status(404).json({ error: "Agent not found" });
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
await assertBoardCanManageAgentsForCompany(req, agent.companyId);
|
||||
assertCompanyAccess(req, agent.companyId);
|
||||
if (agent.adapterType !== "claude_local") {
|
||||
res.status(400).json({ error: "Login is only supported for claude_local agents" });
|
||||
return;
|
||||
res.status(400).json({ error: `${actionLabel} is only supported for claude_local agents` });
|
||||
return null;
|
||||
}
|
||||
return agent;
|
||||
};
|
||||
|
||||
router.post("/agents/:id/claude-login", async (req, res) => {
|
||||
const agent = await getClaudeLocalAgentForBoardAction(req, res, "Login");
|
||||
if (!agent) return;
|
||||
|
||||
const config = asRecord(agent.adapterConfig) ?? {};
|
||||
const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config);
|
||||
@@ -3290,6 +3305,48 @@ export function agentRoutes(
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.post("/agents/:id/claude-subscription-auth-url", async (req, res) => {
|
||||
const agent = await getClaudeLocalAgentForBoardAction(req, res, "Subscription auth URL");
|
||||
if (!agent) return;
|
||||
|
||||
const config = asRecord(agent.adapterConfig) ?? {};
|
||||
const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config);
|
||||
const result = await runClaudeSubscriptionAuthUrl({
|
||||
runId: `claude-subscription-auth-url-${randomUUID()}`,
|
||||
agent: {
|
||||
id: agent.id,
|
||||
companyId: agent.companyId,
|
||||
name: agent.name,
|
||||
adapterType: agent.adapterType,
|
||||
adapterConfig: agent.adapterConfig,
|
||||
},
|
||||
config: runtimeConfig,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.post("/agents/:id/claude-logout", async (req, res) => {
|
||||
const agent = await getClaudeLocalAgentForBoardAction(req, res, "Logout");
|
||||
if (!agent) return;
|
||||
|
||||
const config = asRecord(agent.adapterConfig) ?? {};
|
||||
const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config);
|
||||
const result = await runClaudeLogout({
|
||||
runId: `claude-logout-${randomUUID()}`,
|
||||
agent: {
|
||||
id: agent.id,
|
||||
companyId: agent.companyId,
|
||||
name: agent.name,
|
||||
adapterType: agent.adapterType,
|
||||
adapterConfig: agent.adapterConfig,
|
||||
},
|
||||
config: runtimeConfig,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/heartbeat-runs", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
|
||||
@@ -2887,6 +2887,24 @@ registry.registerPath({
|
||||
responses: { 200: r.ok(), 401: r.unauthorized },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/api/agents/{id}/claude-subscription-auth-url",
|
||||
tags: ["agents"],
|
||||
summary: "Get Claude subscription auth URL for agent",
|
||||
request: { params: z.object({ id: z.string() }) },
|
||||
responses: { 200: r.ok(), 401: r.unauthorized },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/api/agents/{id}/claude-logout",
|
||||
tags: ["agents"],
|
||||
summary: "Trigger Claude logout for agent",
|
||||
request: { params: z.object({ id: z.string() }) },
|
||||
responses: { 200: r.ok(), 401: r.unauthorized },
|
||||
});
|
||||
|
||||
// ─── Issue interactions & tree ───────────────────────────────────────────────
|
||||
|
||||
registry.registerPath({
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface ClaudeLoginResult {
|
||||
signal: string | null;
|
||||
timedOut: boolean;
|
||||
loginUrl: string | null;
|
||||
authUrl: string | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
@@ -228,6 +229,10 @@ export const agentsApi = {
|
||||
) => api.post<AgentWakeupResponse>(agentPath(id, companyId, "/wakeup"), data),
|
||||
loginWithClaude: (id: string, companyId?: string) =>
|
||||
api.post<ClaudeLoginResult>(agentPath(id, companyId, "/claude-login"), {}),
|
||||
getClaudeSubscriptionAuthUrl: (id: string, companyId?: string) =>
|
||||
api.post<ClaudeLoginResult>(agentPath(id, companyId, "/claude-subscription-auth-url"), {}),
|
||||
logoutFromClaude: (id: string, companyId?: string) =>
|
||||
api.post<ClaudeLoginResult>(agentPath(id, companyId, "/claude-logout"), {}),
|
||||
availableSkills: () =>
|
||||
api.get<{ skills: AvailableSkill[] }>("/skills/available"),
|
||||
};
|
||||
|
||||
+144
-58
@@ -67,12 +67,15 @@ import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
ArrowLeft,
|
||||
HelpCircle,
|
||||
FolderOpen,
|
||||
AlertTriangle,
|
||||
LogIn,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
@@ -1479,6 +1482,9 @@ function AgentConfigurePage({
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{agent.adapterType === "claude_local" && (
|
||||
<ClaudeAuthControls agentId={agent.id} companyId={companyId} />
|
||||
)}
|
||||
<ConfigurationTab
|
||||
agent={agent}
|
||||
onDirtyChange={onDirtyChange}
|
||||
@@ -1549,6 +1555,138 @@ function AgentConfigurePage({
|
||||
);
|
||||
}
|
||||
|
||||
function ClaudeAuthControls({
|
||||
agentId,
|
||||
companyId,
|
||||
authRequired = false,
|
||||
}: {
|
||||
agentId: string;
|
||||
companyId?: string;
|
||||
authRequired?: boolean;
|
||||
}) {
|
||||
const [claudeLoginResult, setClaudeLoginResult] = useState<ClaudeLoginResult | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setClaudeLoginResult(null);
|
||||
}, [agentId, companyId]);
|
||||
|
||||
const runClaudeLogin = useMutation({
|
||||
mutationFn: () => agentsApi.loginWithClaude(agentId, companyId),
|
||||
onSuccess: (data) => {
|
||||
setClaudeLoginResult(data);
|
||||
},
|
||||
});
|
||||
const getClaudeSubscriptionAuthUrl = useMutation({
|
||||
mutationFn: () => agentsApi.getClaudeSubscriptionAuthUrl(agentId, companyId),
|
||||
onSuccess: (data) => {
|
||||
setClaudeLoginResult(data);
|
||||
},
|
||||
});
|
||||
const runClaudeLogout = useMutation({
|
||||
mutationFn: () => agentsApi.logoutFromClaude(agentId, companyId),
|
||||
onSuccess: (data) => {
|
||||
setClaudeLoginResult(data);
|
||||
},
|
||||
});
|
||||
|
||||
const claudeAuthUrl = claudeLoginResult?.authUrl ?? claudeLoginResult?.loginUrl ?? null;
|
||||
const claudeAuthPending =
|
||||
runClaudeLogin.isPending || getClaudeSubscriptionAuthUrl.isPending || runClaudeLogout.isPending;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-md border border-border/70 bg-accent/20 px-3 py-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() => getClaudeSubscriptionAuthUrl.mutate()}
|
||||
disabled={claudeAuthPending}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
{getClaudeSubscriptionAuthUrl.isPending ? "Getting URL..." : "Get subscription auth URL"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() => runClaudeLogin.mutate()}
|
||||
disabled={claudeAuthPending}
|
||||
>
|
||||
<LogIn className="h-3.5 w-3.5" />
|
||||
{runClaudeLogin.isPending ? "Running login..." : "Login with subscription"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs text-destructive hover:text-destructive"
|
||||
onClick={() => runClaudeLogout.mutate()}
|
||||
disabled={claudeAuthPending}
|
||||
>
|
||||
<LogOut className="h-3.5 w-3.5" />
|
||||
{runClaudeLogout.isPending ? "Logging out..." : "Logout"}
|
||||
</Button>
|
||||
{claudeAuthUrl && (
|
||||
<Button asChild variant="link" size="sm" className="h-7 px-1 text-xs">
|
||||
<a href={claudeAuthUrl} target="_blank" rel="noreferrer">
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
Open auth URL
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{authRequired && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This run needs Claude subscription auth before it can be retried.
|
||||
</p>
|
||||
)}
|
||||
{(runClaudeLogin.isError || getClaudeSubscriptionAuthUrl.isError || runClaudeLogout.isError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{runClaudeLogin.error instanceof Error
|
||||
? runClaudeLogin.error.message
|
||||
: getClaudeSubscriptionAuthUrl.error instanceof Error
|
||||
? getClaudeSubscriptionAuthUrl.error.message
|
||||
: runClaudeLogout.error instanceof Error
|
||||
? runClaudeLogout.error.message
|
||||
: "Claude auth command failed"}
|
||||
</p>
|
||||
)}
|
||||
{claudeAuthUrl && (
|
||||
<p className="text-xs">
|
||||
Auth URL:
|
||||
<a
|
||||
href={claudeAuthUrl}
|
||||
className="text-blue-600 underline underline-offset-2 ml-1 break-all dark:text-blue-400"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{claudeAuthUrl}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{claudeLoginResult && (
|
||||
<>
|
||||
{claudeLoginResult.timedOut && claudeAuthUrl && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The auth command was stopped after capturing the browser URL.
|
||||
</p>
|
||||
)}
|
||||
{!!claudeLoginResult.stdout && (
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">
|
||||
{claudeLoginResult.stdout}
|
||||
</pre>
|
||||
)}
|
||||
{!!claudeLoginResult.stderr && (
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-700 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">
|
||||
{claudeLoginResult.stderr}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---- Configuration Tab ---- */
|
||||
|
||||
function ConfigurationTab({
|
||||
@@ -3122,11 +3260,6 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig }
|
||||
const run = hydratedRun ?? initialRun;
|
||||
const metrics = runMetrics(run);
|
||||
const [sessionOpen, setSessionOpen] = useState(false);
|
||||
const [claudeLoginResult, setClaudeLoginResult] = useState<ClaudeLoginResult | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setClaudeLoginResult(null);
|
||||
}, [run.id]);
|
||||
|
||||
const cancelRun = useMutation({
|
||||
mutationFn: () => heartbeatsApi.cancel(run.id),
|
||||
@@ -3224,13 +3357,6 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig }
|
||||
},
|
||||
});
|
||||
|
||||
const runClaudeLogin = useMutation({
|
||||
mutationFn: () => agentsApi.loginWithClaude(run.agentId, run.companyId),
|
||||
onSuccess: (data) => {
|
||||
setClaudeLoginResult(data);
|
||||
},
|
||||
});
|
||||
|
||||
const isRunning = run.status === "running" && !!run.startedAt && !run.finishedAt;
|
||||
const [elapsedSec, setElapsedSec] = useState<number>(() => {
|
||||
if (!run.startedAt) return 0;
|
||||
@@ -3361,52 +3487,12 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig }
|
||||
{run.errorCode && <span className="text-muted-foreground ml-1">({run.errorCode})</span>}
|
||||
</div>
|
||||
)}
|
||||
{run.errorCode === "claude_auth_required" && adapterType === "claude_local" && (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() => runClaudeLogin.mutate()}
|
||||
disabled={runClaudeLogin.isPending}
|
||||
>
|
||||
{runClaudeLogin.isPending ? "Running claude login..." : "Login to Claude Code"}
|
||||
</Button>
|
||||
{runClaudeLogin.isError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{runClaudeLogin.error instanceof Error
|
||||
? runClaudeLogin.error.message
|
||||
: "Failed to run Claude login"}
|
||||
</p>
|
||||
)}
|
||||
{claudeLoginResult?.loginUrl && (
|
||||
<p className="text-xs">
|
||||
Login URL:
|
||||
<a
|
||||
href={claudeLoginResult.loginUrl}
|
||||
className="text-blue-600 underline underline-offset-2 ml-1 break-all dark:text-blue-400"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{claudeLoginResult.loginUrl}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{claudeLoginResult && (
|
||||
<>
|
||||
{!!claudeLoginResult.stdout && (
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">
|
||||
{claudeLoginResult.stdout}
|
||||
</pre>
|
||||
)}
|
||||
{!!claudeLoginResult.stderr && (
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-700 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">
|
||||
{claudeLoginResult.stderr}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{adapterType === "claude_local" && (
|
||||
<ClaudeAuthControls
|
||||
agentId={run.agentId}
|
||||
companyId={run.companyId}
|
||||
authRequired={run.errorCode === "claude_auth_required"}
|
||||
/>
|
||||
)}
|
||||
{hasNonZeroExit && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400">
|
||||
|
||||
Reference in new issue
Block a user