Skip to content

Commit d367d60

Browse files
authored
Merge pull request #242 from paperclipinc/staging
Promote staging → main: guided credential connect (PR F)
2 parents 40ce60f + 40ebd7f commit d367d60

34 files changed

Lines changed: 1735 additions & 54 deletions

packages/adapter-utils/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type {
3636
StdoutLineParser,
3737
CLIAdapterModule,
3838
CreateConfigValues,
39+
AdapterCredentialOption,
40+
AdapterCredentialSetup,
3941
} from "./types.js";
4042
export type {
4143
SessionCompactionPolicy,

packages/adapter-utils/src/types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,3 +554,41 @@ export interface CreateConfigValues {
554554
headersJson?: string;
555555
password?: string;
556556
}
557+
558+
// ---------------------------------------------------------------------------
559+
// Adapter credential setup — declarative UI descriptors for guided credential configuration
560+
// ---------------------------------------------------------------------------
561+
562+
/**
563+
* A single credential option that an adapter accepts.
564+
*
565+
* Describes how to configure one environment variable (e.g., an API key or token),
566+
* with hints and optional setup commands/URLs to help users obtain and configure it.
567+
*/
568+
export interface AdapterCredentialOption {
569+
/** The environment variable name (e.g., "ANTHROPIC_API_KEY") */
570+
envKey: string;
571+
/** The kind of credential */
572+
kind: "api_key" | "subscription_token";
573+
/** Display label for the credential (e.g., "Anthropic API key") */
574+
label: string;
575+
/** Optional hint text explaining what the credential is for and how to use it */
576+
hint?: string;
577+
/** Optional command users can run to set up this credential (e.g., "claude setup-token") */
578+
setupCommand?: string;
579+
/** Optional URL where users can obtain/manage this credential (e.g., console URL) */
580+
setupUrl?: string;
581+
/** Optional placeholder text for the input field (e.g., "sk-ant-…") */
582+
placeholder?: string;
583+
}
584+
585+
/**
586+
* A complete credential setup descriptor for an adapter.
587+
*
588+
* Declares all environment variables an adapter accepts and provides
589+
* UI-friendly guidance for each one.
590+
*/
591+
export interface AdapterCredentialSetup {
592+
/** The list of credential options this adapter accepts */
593+
options: AdapterCredentialOption[];
594+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, it } from "vitest";
2+
import { claudeLocalCredentialSetup } from "./credential-setup.js";
3+
4+
describe("claudeLocalCredentialSetup", () => {
5+
it("exports credential options with correct envKeys", () => {
6+
const envKeys = claudeLocalCredentialSetup.options.map(o => o.envKey);
7+
expect(envKeys).toEqual(["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]);
8+
});
9+
10+
it("configures subscription_token option with setupCommand", () => {
11+
const tokenOption = claudeLocalCredentialSetup.options.find(o => o.envKey === "CLAUDE_CODE_OAUTH_TOKEN");
12+
expect(tokenOption).toBeDefined();
13+
expect(tokenOption?.kind).toBe("subscription_token");
14+
expect(tokenOption?.setupCommand).toBe("claude setup-token");
15+
});
16+
17+
it("includes hint text mentioning both quota and extra usage for subscription_token", () => {
18+
const tokenOption = claudeLocalCredentialSetup.options.find(o => o.envKey === "CLAUDE_CODE_OAUTH_TOKEN");
19+
expect(tokenOption?.hint).toContain("quota");
20+
expect(tokenOption?.hint).toContain("extra usage");
21+
});
22+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { AdapterCredentialSetup } from "@paperclipai/adapter-utils";
2+
3+
export const claudeLocalCredentialSetup: AdapterCredentialSetup = {
4+
options: [
5+
{
6+
envKey: "ANTHROPIC_API_KEY",
7+
kind: "api_key",
8+
label: "Anthropic API key",
9+
hint: "Create a key in the Anthropic Console. Usage bills to your Anthropic API account.",
10+
setupUrl: "https://console.anthropic.com/settings/keys",
11+
placeholder: "sk-ant-…",
12+
},
13+
{
14+
envKey: "CLAUDE_CODE_OAUTH_TOKEN",
15+
kind: "subscription_token",
16+
label: "Claude Pro/Max subscription token",
17+
hint: "Mint a long-lived token with `claude setup-token` on a machine where Claude Code is logged in. Token-only auth has no usage/quota reporting, and subscription usage in third-party contexts may draw from your Anthropic \"extra usage\" credits.",
18+
setupCommand: "claude setup-token",
19+
placeholder: "sk-ant-oat01-…",
20+
},
21+
],
22+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { parseClaudeStdoutLine } from "./parse-stdout.js";
22
export { buildClaudeLocalConfig } from "./build-config.js";
3+
export { claudeLocalCredentialSetup } from "./credential-setup.js";
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, expect, it } from "vitest";
2+
import { codexLocalCredentialSetup } from "./credential-setup.js";
3+
4+
describe("codexLocalCredentialSetup", () => {
5+
it("exports credential options with correct envKeys", () => {
6+
const envKeys = codexLocalCredentialSetup.options.map(o => o.envKey);
7+
expect(envKeys).toEqual(["OPENAI_API_KEY"]);
8+
});
9+
10+
it("configures api_key option with correct label and setupUrl", () => {
11+
const apiKeyOption = codexLocalCredentialSetup.options.find(o => o.envKey === "OPENAI_API_KEY");
12+
expect(apiKeyOption).toBeDefined();
13+
expect(apiKeyOption?.kind).toBe("api_key");
14+
expect(apiKeyOption?.label).toBe("OpenAI API key");
15+
expect(apiKeyOption?.setupUrl).toBe("https://platform.openai.com/api-keys");
16+
expect(apiKeyOption?.placeholder).toBe("sk-…");
17+
});
18+
19+
it("includes hint mentioning codex login and CODEX_HOME sync as ChatGPT-subscription alternative", () => {
20+
const apiKeyOption = codexLocalCredentialSetup.options.find(o => o.envKey === "OPENAI_API_KEY");
21+
expect(apiKeyOption?.hint).toContain("codex login");
22+
expect(apiKeyOption?.hint).toContain("CODEX_HOME");
23+
});
24+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { AdapterCredentialSetup } from "@paperclipai/adapter-utils";
2+
3+
export const codexLocalCredentialSetup: AdapterCredentialSetup = {
4+
options: [
5+
{
6+
envKey: "OPENAI_API_KEY",
7+
kind: "api_key",
8+
label: "OpenAI API key",
9+
hint: "Create a key in the OpenAI API console. Alternatively, you can log in with `codex login` locally and Paperclip's CODEX_HOME sync will ship the credential to remote runs.",
10+
setupUrl: "https://platform.openai.com/api-keys",
11+
placeholder: "sk-…",
12+
},
13+
],
14+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { parseCodexStdoutLine } from "./parse-stdout.js";
22
export { buildCodexLocalConfig } from "./build-config.js";
3+
export { codexLocalCredentialSetup } from "./credential-setup.js";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from "vitest";
2+
import { cursorLocalCredentialSetup } from "./credential-setup.js";
3+
4+
describe("cursorLocalCredentialSetup", () => {
5+
it("exports credential options with correct envKeys", () => {
6+
const envKeys = cursorLocalCredentialSetup.options.map(o => o.envKey);
7+
expect(envKeys).toEqual(["CURSOR_API_KEY"]);
8+
});
9+
10+
it("configures api_key option with correct label and setupUrl", () => {
11+
const apiKeyOption = cursorLocalCredentialSetup.options.find(o => o.envKey === "CURSOR_API_KEY");
12+
expect(apiKeyOption).toBeDefined();
13+
expect(apiKeyOption?.kind).toBe("api_key");
14+
expect(apiKeyOption?.label).toBe("Cursor API key");
15+
expect(apiKeyOption?.setupUrl).toBe("https://cursor.com/dashboard");
16+
expect(apiKeyOption?.placeholder).toBe("key_…");
17+
});
18+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { AdapterCredentialSetup } from "@paperclipai/adapter-utils";
2+
3+
export const cursorLocalCredentialSetup: AdapterCredentialSetup = {
4+
options: [
5+
{
6+
envKey: "CURSOR_API_KEY",
7+
kind: "api_key",
8+
label: "Cursor API key",
9+
setupUrl: "https://cursor.com/dashboard",
10+
placeholder: "key_…",
11+
},
12+
],
13+
};

0 commit comments

Comments
 (0)