Compare commits

...

1 Commits

Author SHA1 Message Date
Saoud Rizwan fa2ba4e904 fix(cli): handle cline account auth error code 2026-05-18 23:01:43 -07:00
12 changed files with 143 additions and 23 deletions
+13 -3
View File
@@ -15,7 +15,12 @@ const coreMocks = vi.hoisted(() => {
});
vi.mock("@cline/core", () => {
const SDK_ERROR_CODES = {
ClineAccountAuthRequired: "cline_account_auth_required",
} as const;
return {
SDK_ERROR_CODES,
ClineAccountService: class {
constructor(options: {
apiBaseUrl: string;
@@ -32,6 +37,11 @@ vi.mock("@cline/core", () => {
coreMocks.saveProviderSettings(settings, options);
}
},
createSdkCodedError: (code: string, message: string) => {
const error = new Error(message) as Error & { code: string };
error.code = code;
return error;
},
getValidClineCredentials: coreMocks.getValidClineCredentials,
};
});
@@ -129,8 +139,8 @@ describe("createClineAccountService", () => {
await expect(
createClineAccountService({ config: makeConfig() }),
).rejects.toThrow(
"Cline account requires re-authentication. Run cline auth cline.",
);
).rejects.toMatchObject({
code: "cline_account_auth_required",
});
});
});
+9 -10
View File
@@ -4,9 +4,11 @@ import {
type ClineAccountOrganizationBalance,
ClineAccountService,
type ClineAccountUser,
createSdkCodedError,
getValidClineCredentials,
type ProviderSettings,
ProviderSettingsManager,
SDK_ERROR_CODES,
} from "@cline/core";
import { getClineEnvironmentConfig } from "@cline/shared";
import { formatCreditBalance, normalizeCreditBalance } from "../utils/output";
@@ -30,11 +32,10 @@ export function formatClineCredits(value: number): string {
return formatCreditBalance(normalizeCreditBalance(value));
}
export function isClineAccountAuthErrorMessage(message: string): boolean {
const normalized = message.trim().toLowerCase();
return (
normalized === "no cline account auth token found" ||
normalized.includes("requires re-authentication")
function createClineAccountAuthError(): Error {
return createSdkCodedError(
SDK_ERROR_CODES.ClineAccountAuthRequired,
"Cline account authentication requires sign in.",
);
}
@@ -100,9 +101,7 @@ async function resolveValidClineAccountAuthToken(input: {
{ apiBaseUrl: input.apiBaseUrl },
);
if (!credentials) {
throw new Error(
"Cline account requires re-authentication. Run cline auth cline.",
);
throw createClineAccountAuthError();
}
const nextAccessToken = toProviderApiKey("cline", credentials);
if (
@@ -167,7 +166,7 @@ export async function loadClineAccountSnapshot(input: {
}): Promise<ClineAccountSnapshot> {
const service = await createClineAccountService(input);
if (!service) {
throw new Error("No Cline account auth token found");
throw createClineAccountAuthError();
}
const user = await service.fetchMe();
@@ -202,7 +201,7 @@ export async function switchClineAccount(input: {
}): Promise<void> {
const service = await createClineAccountService(input);
if (!service) {
throw new Error("No Cline account auth token found");
throw createClineAccountAuthError();
}
await service.switchAccount(input.organizationId);
}
@@ -347,6 +347,20 @@ export function ChatEntryView(props: {
);
case "error":
if (entry.help) {
return (
<box flexDirection="column">
<box flexDirection="row">
<text fg="red" content="* " />
<text fg="red" selectable content={entry.text} />
</box>
<box flexDirection="row">
<box width={2} />
<text fg="gray" selectable content={entry.help} />
</box>
</box>
);
}
return (
<box flexDirection="row">
<text fg="red" content="* " />
@@ -1,12 +1,15 @@
// @jsxImportSource @opentui/react
import type { ClineAccountOrganization } from "@cline/core";
import {
type ClineAccountOrganization,
getSdkErrorCode,
SDK_ERROR_CODES,
} from "@cline/core";
import type { ChoiceContext } from "@opentui-ui/dialog";
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
type ClineAccountSnapshot,
formatClineCredits,
isClineAccountAuthErrorMessage,
} from "../../cline-account";
import { palette } from "../../palette";
@@ -256,10 +259,10 @@ export function AccountDialogContent(
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (generation.current === currentGeneration) {
const accountAuthRequired =
getSdkErrorCode(error) === SDK_ERROR_CODES.ClineAccountAuthRequired;
setState({
status: isClineAccountAuthErrorMessage(message)
? "unauthenticated"
: "error",
status: accountAuthRequired ? "unauthenticated" : "error",
message,
});
setSelectedAction(0);
+11 -2
View File
@@ -1,4 +1,4 @@
import type { AgentEvent, TeamEvent } from "@cline/core";
import { type AgentEvent, SDK_ERROR_CODES, type TeamEvent } from "@cline/core";
import { useCallback, useRef } from "react";
import type {
PendingPromptSnapshot,
@@ -29,6 +29,11 @@ interface AgentEventDeps {
verbose: boolean;
}
const CLINE_ACCOUNT_AUTH_ERROR = {
text: "Sign in to your Cline account.",
help: "Run /account to sign in again, then retry your message.",
} as const;
export function useAgentEventHandlers(deps: AgentEventDeps) {
const {
appendEntry,
@@ -171,7 +176,11 @@ export function useAgentEventHandlers(deps: AgentEventDeps) {
turnErrorReportedRef.current = true;
onTurnErrorReported(true);
if (!event.recoverable || verbose) {
appendEntry({ kind: "error", text: event.error.message });
appendEntry(
event.errorCode === SDK_ERROR_CODES.ClineAccountAuthRequired
? { kind: "error", ...CLINE_ACCOUNT_AUTH_ERROR }
: { kind: "error", text: event.error.message },
);
}
break;
case "notice":
@@ -1,3 +1,4 @@
import { getSdkErrorCode, SDK_ERROR_CODES } from "@cline/core";
import { useCallback, useLayoutEffect, useRef, useState } from "react";
import { shouldShowCliUsageCost } from "../../utils/usage-cost-display";
import type { SlashCommandRegistry } from "../commands/slash-command-registry";
@@ -27,6 +28,11 @@ interface PastedImage {
dataUrl: string;
}
const CLINE_ACCOUNT_AUTH_ERROR = {
text: "Sign in to your Cline account.",
help: "Run /account to sign in again, then retry your message.",
} as const;
export function usePromptInputController(input: {
autocomplete: ReturnType<typeof useAutocomplete>;
slashCommandRegistry: SlashCommandRegistry;
@@ -365,9 +371,15 @@ export function usePromptInputController(input: {
}
} catch (error) {
if (!turnErrorReportedRef.current) {
const message =
error instanceof Error ? error.message : String(error);
const accountAuthError =
getSdkErrorCode(error) === SDK_ERROR_CODES.ClineAccountAuthRequired
? CLINE_ACCOUNT_AUTH_ERROR
: undefined;
session.appendEntry({
kind: "error",
text: error instanceof Error ? error.message : String(error),
...(accountAuthError ?? { text: message }),
});
}
} finally {
+1 -1
View File
@@ -41,7 +41,7 @@ export type ChatEntry =
error?: string;
};
}
| { kind: "error"; text: string }
| { kind: "error"; text: string; help?: string }
| { kind: "status"; text: string }
| { kind: "team"; text: string }
| { kind: "user_submitted"; text: string; delivery?: "queue" | "steer" }
+5
View File
@@ -48,6 +48,7 @@ export type {
ProviderOAuthLoginResponse,
RuntimeLoggerConfig,
SaveProviderSettingsActionRequest,
SdkErrorCode,
SdkTelemetryErrorComponent,
SdkTelemetryErrorSeverity,
SessionLineage,
@@ -80,14 +81,18 @@ export {
createClineTelemetryServiceConfig,
createClineTelemetryServiceMetadata,
createContributionRegistry,
createSdkCodedError,
createTool,
emptyWorkspaceManifest,
formatDisplayUserInput,
getSdkErrorCode,
isSdkErrorCode,
noopBasicLogger,
normalizeSdkError,
normalizeUserInput,
parseUserCommandEnvelope,
registerDisposable,
SDK_ERROR_CODES,
SDK_ERROR_TELEMETRY_EVENT,
} from "@cline/shared";
export * from "@cline/shared/storage";
@@ -7,10 +7,12 @@ import {
type AgentEvent,
type AgentResult,
captureSdkError,
createSdkCodedError,
createSessionId,
type ITelemetryService,
isLikelyAuthError,
normalizeUserInput,
SDK_ERROR_CODES,
} from "@cline/shared";
import { setHomeDirIfUnset } from "@cline/shared/storage";
import { createContextCompactionPrepareTurn } from "../../extensions/context/compaction";
@@ -1488,6 +1490,12 @@ export class LocalRuntimeHost implements RuntimeHost {
});
} catch (error) {
if (error instanceof OAuthReauthRequiredError) {
if (error.providerId === "cline") {
throw createSdkCodedError(
SDK_ERROR_CODES.ClineAccountAuthRequired,
"Cline account authentication requires sign in.",
);
}
throw new Error(`${error.providerId} requires re-authentication.`);
}
throw error;
@@ -21,6 +21,7 @@ import type {
AgentToolCallPart,
AgentUsage,
} from "@cline/shared";
import { createSdkCodedError, SDK_ERROR_CODES } from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
RuntimeEventAdapter,
@@ -715,6 +716,26 @@ describe("RuntimeEventAdapter — run lifecycle", () => {
iteration: 4,
});
});
it("maps run-failed coded errors", () => {
const err = createSdkCodedError(
SDK_ERROR_CODES.ClineAccountAuthRequired,
"auth required",
);
const out = adapter.translate({
type: "run-failed",
snapshot: makeSnapshot({ iteration: 4 }),
error: err,
});
expect(out[0]).toMatchObject({
type: "error",
error: err,
errorCode: SDK_ERROR_CODES.ClineAccountAuthRequired,
recoverable: false,
iteration: 4,
});
});
});
// ---------------------------------------------------------------------------
@@ -61,6 +61,7 @@ import type {
AgentUsage,
LegacyAgentUsage,
} from "@cline/shared";
import { getSdkErrorCode } from "@cline/shared";
// =============================================================================
// Helpers
@@ -234,15 +235,18 @@ export class RuntimeEventAdapter {
];
case "run-finished":
return this.translateRunFinished(event.result);
case "run-failed":
case "run-failed": {
const errorCode = getSdkErrorCode(event.error);
return [
{
type: "error",
error: event.error,
...(errorCode ? { errorCode } : {}),
recoverable: false,
iteration: event.snapshot.iteration,
},
];
}
default: {
const _exhaustive: never = event;
return _exhaustive;
+35
View File
@@ -56,6 +56,39 @@ export type AgentEvent =
export type AgentContentType = "text" | "reasoning" | "tool";
export const SDK_ERROR_CODES = {
ClineAccountAuthRequired: "cline_account_auth_required",
} as const;
export type SdkErrorCode =
(typeof SDK_ERROR_CODES)[keyof typeof SDK_ERROR_CODES];
export type SdkCodedError = Error & { code: SdkErrorCode };
export function isSdkErrorCode(value: unknown): value is SdkErrorCode {
return (
typeof value === "string" &&
(Object.values(SDK_ERROR_CODES) as readonly string[]).includes(value)
);
}
export function createSdkCodedError(
code: SdkErrorCode,
message: string,
): SdkCodedError {
const error = new Error(message) as SdkCodedError;
error.code = code;
return error;
}
export function getSdkErrorCode(error: unknown): SdkErrorCode | undefined {
if (typeof error !== "object" || error === null || !("code" in error)) {
return undefined;
}
const code = error.code;
return isSdkErrorCode(code) ? code : undefined;
}
export interface AgentEventMetadata {
/** Current ID */
agentId?: string;
@@ -182,6 +215,8 @@ export interface AgentErrorEvent extends AgentEventMetadata {
type: "error";
/** The error that occurred */
error: Error;
/** Stable error code for client-side handling */
errorCode?: SdkErrorCode;
/** Whether the error is recoverable */
recoverable: boolean;
/** Current iteration when error occurred */