Of course. I've refactored your original TypeScript code into a single, clean, and robust AnthropicWebClient.
This new version incorporates the best patterns from the second script:
- Platform Agnostic: It uses the standard
fetchAPI and has no Deno-specific dependencies. It will work in Node.js (v18+), Deno, Bun, or modern browsers. - Automatic Organization Discovery: You only need to provide the
sessionKey. The client automatically discovers the correctorganization_uuid, just like the second script. - Async Factory: The client is instantiated via an async static method
AnthropicWebClient.create(...), which ensures the instance is fully initialized and ready to use. - Strongly Typed: It includes TypeScript interfaces for the common API responses, providing better autocompletion and type safety.
- Clean Structure: All related methods are logically grouped within a single class, removing the need for
BasicClientandProjectClientwrappers. - Full Functionality: It includes the
docsandfilesmethods for projects as you requested, along with all the other core functionalities.
Here is the final, refactored code:
//
// AnthropicWebClient: A TypeScript client for the unofficial Claude.ai web API.
//
/* ---------- API RESPONSE TYPES ---------- */
/** Basic information about an organization. */
export interface Organization {
uuid: string;
name: string;
capabilities: string[];
}
/** Details about a user's membership in an organization. */
export interface Membership {
organization: Organization;
// ... other properties
}
/** Details of the authenticated user's account. */
export interface Account {
memberships: Membership[];
// ... other properties
}
/** A project, which can contain conversations. */
export interface Project {
uuid: string;
name: string;
description?: string;
created_at: string;
updated_at: string;
is_harmony_project: boolean;
}
/** A single conversation thread. */
export interface Conversation {
uuid: string;
name: string;
project_uuid?: string;
created_at: string;
updated_at: string;
// ... other properties when fetching details
}
/** A document uploaded to a project's knowledge base. */
export interface ProjectDoc {
uuid: string;
file_name: string;
// ... other properties
}
/** A file attached to a project. */
export interface ProjectFile {
uuid: string;
file_name: string;
// ... other properties
}
/**
* A client for interacting with the unofficial Claude.ai web API.
*
* This client handles authentication and provides methods for accessing
* projects, conversations, and other account data.
*
* @example
* ```typescript
* const sessionKey = process.env.CLAUDE_SESSION_KEY;
* if (!sessionKey) {
* throw new Error("CLAUDE_SESSION_KEY environment variable not set.");
* }
*
* const client = await AnthropicWebClient.create(sessionKey);
*
* // List all projects
* const projects = await client.listProjects();
* console.log(`Found ${projects.length} projects.`);
*
* // List all conversations (not associated with a project)
* const conversations = await client.listConversations();
* console.log(`Found ${conversations.length} conversations without a project.`);
* ```
*/
export class AnthropicWebClient {
private static readonly BASE_URL = 'https://claude.ai/api';
/**
* Use the static `create` method to instantiate the client.
* @param sessionKey The 'sessionKey' cookie value from claude.ai.
* @param orgId The automatically discovered organization UUID.
*/
private constructor(
private readonly sessionKey: string,
public readonly orgId: string,
) {}
/**
* Creates and initializes a new AnthropicWebClient.
* This is the required way to create an instance, as it asynchronously
* determines the correct organization ID to use for all subsequent API calls.
*
* @param sessionKey The 'sessionKey' cookie value from a logged-in claude.ai session.
* @returns A promise that resolves to a fully initialized client instance.
*/
public static async create(sessionKey: string): Promise<AnthropicWebClient> {
if (!sessionKey) {
throw new Error('A valid sessionKey is required.');
}
// This mimics the logic from the second script to find the active organization
const accountUrl = `${this.BASE_URL}/account`;
const accountRes = await fetch(accountUrl, {
headers: { cookie: `sessionKey=${sessionKey}` },
});
if (!accountRes.ok) {
throw new Error(`Failed to fetch account details to find organization. Status: ${accountRes.status}`);
}
const accountData: Account = await accountRes.json();
if (!accountData.memberships?.length) {
throw new Error('No organization memberships found for this account.');
}
// Find the first organization that has 'chat' or 'claude_max' capabilities
for (const membership of accountData.memberships) {
const org = membership.organization;
if (org.capabilities?.includes('chat') || org.capabilities?.includes('claude_max')) {
console.log(`Successfully identified organization: ${org.name} (${org.uuid})`);
return new AnthropicWebClient(sessionKey, org.uuid);
}
}
throw new Error('Could not find a usable organization with chat capabilities in your account.');
}
/**
* A generic, authenticated request helper for the Claude API.
* @param path The API path relative to the base URL (e.g., '/organizations/...').
* @param options Standard fetch options.
* @returns A promise that resolves to the JSON response.
*/
private async _fetchJson<T>(path: string, options: RequestInit = {}): Promise<T> {
const url = `${AnthropicWebClient.BASE_URL}${path}`;
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
cookie: `sessionKey=${this.sessionKey}`,
};
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request to ${path} failed with status ${response.status}: ${errorText}`);
}
return response.json() as Promise<T>;
}
/* ---------- Account Methods ---------- */
/**
* Fetches the full details of the authenticated account, including all memberships.
*/
public async getAccountDetails(): Promise<Account> {
return this._fetchJson<Account>('/account');
}
/* ---------- Project Methods ---------- */
/**
* Lists all projects available in the organization.
* @param limit The maximum number of projects to return.
*/
public async listProjects(limit = 100): Promise<Project[]> {
const params = new URLSearchParams({
include_harmony_projects: 'true',
limit: String(limit),
});
const path = `/organizations/${this.orgId}/projects?${params.toString()}`;
return this._fetchJson<Project[]>(path);
}
/**
* Retrieves the details of a specific project.
* @param projectId The UUID of the project.
*/
public async getProjectDetails(projectId: string): Promise<Project> {
const path = `/organizations/${this.orgId}/projects/${projectId}`;
return this._fetchJson<Project>(path);
}
/**
* Lists all documents uploaded to a project's knowledge base.
* @param projectId The UUID of the project.
*/
public async getProjectDocs(projectId: string): Promise<ProjectDoc[]> {
const path = `/organizations/${this.orgId}/projects/${projectId}/docs`;
return this._fetchJson<ProjectDoc[]>(path);
}
/**
* Lists all files attached to a project.
* @param projectId The UUID of the project.
*/
public async getProjectFiles(projectId: string): Promise<ProjectFile[]> {
const path = `/organizations/${this.orgId}/projects/${projectId}/files`;
return this._fetchJson<ProjectFile[]>(path);
}
/* ---------- Conversation Methods ---------- */
/**
* Lists all conversations that are not associated with a specific project.
* @param limit The maximum number of conversations to return.
*/
public async listConversations(limit = 200): Promise<Conversation[]> {
const path = `/organizations/${this.orgId}/chat_conversations?limit=${limit}`;
return this._fetchJson<Conversation[]>(path);
}
/**
* Lists all conversations associated with a specific project.
* @param projectId The UUID of the project.
*/
public async listProjectConversations(projectId: string): Promise<Conversation[]> {
const path = `/organizations/${this.orgId}/projects/${projectId}/conversations`;
return this._fetchJson<Conversation[]>(path);
}
/**
* Fetches the full details and message history of a single conversation.
* @param conversationId The UUID of the conversation.
*/
public async getConversationDetails(conversationId: string): Promise<Conversation> {
const path = `/organizations/${this.orgId}/chat_conversations/${conversationId}`;
return this._fetchJson<Conversation>(path);
}
}
// --- Example Usage ---
// To run this example, save it as a .ts file, install a TypeScript runner like ts-node,
// and set the CLAUDE_SESSION_KEY environment variable.
//
// In your terminal:
// export CLAUDE_SESSION_KEY='your_session_key_here'
// npx ts-node your_file_name.ts
async function main() {
const sessionKey = process.env.CLAUDE_SESSION_KEY; // or Deno.env.get("CLAUDE_SESSION_KEY") in Deno
if (!sessionKey) {
console.error("Error: The CLAUDE_SESSION_KEY environment variable is not set.");
console.error("Please obtain it from your browser's cookies for claude.ai.");
process.exit(1); // or Deno.exit(1)
}
try {
console.log("Creating Anthropic web client...");
const client = await AnthropicWebClient.create(sessionKey);
console.log(`Client initialized for organization ID: ${client.orgId}`);
console.log("\nFetching projects...");
const projects = await client.listProjects();
console.log(`Found ${projects.length} project(s).`);
if (projects.length > 0) {
const firstProject = projects[0];
console.log(`\n--- Details for first project: "${firstProject.name}" ---`);
console.log("Fetching project conversations...");
const projectConvos = await client.listProjectConversations(firstProject.uuid);
console.log(`Found ${projectConvos.length} conversation(s) in this project.`);
console.log("Fetching project documents...");
const projectDocs = await client.getProjectDocs(firstProject.uuid);
console.log(`Found ${projectDocs.length} document(s) in this project.`);
}
console.log("\nFetching conversations not in any project...");
const generalConversations = await client.listConversations();
console.log(`Found ${generalConversations.length} general conversation(s).`);
if (generalConversations.length > 0) {
const firstConvo = generalConversations[0];
console.log(`\nFetching details for conversation: "${firstConvo.name}"...`);
const convoDetails = await client.getConversationDetails(firstConvo.uuid);
// The detailed response is large, so we just confirm it was fetched.
console.log(`Successfully fetched details for conversation UUID: ${convoDetails.uuid}`);
}
} catch (error) {
console.error("\nAn error occurred during execution:");
console.error(error);
process.exit(1); // or Deno.exit(1)
}
}
// This check allows the file to be used as a library or an executable script.
if (import.meta.url.startsWith('file:') && process.argv[1] === new URL(import.meta.url).pathname) {
main();
}