Skip to content

Instantly share code, notes, and snippets.

@alkimiadev
Last active July 20, 2025 11:31
Show Gist options
  • Select an option

  • Save alkimiadev/d83b2deee9d315cddfdf5618489232ee to your computer and use it in GitHub Desktop.

Select an option

Save alkimiadev/d83b2deee9d315cddfdf5618489232ee to your computer and use it in GitHub Desktop.
This was written by Gemini 2.5 pro where I gave them my messy prototype. Most of the end points have been mapped out. Others can finish if they like. I no longer use Anthropic products.

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:

  1. Platform Agnostic: It uses the standard fetch API and has no Deno-specific dependencies. It will work in Node.js (v18+), Deno, Bun, or modern browsers.
  2. Automatic Organization Discovery: You only need to provide the sessionKey. The client automatically discovers the correct organization_uuid, just like the second script.
  3. Async Factory: The client is instantiated via an async static method AnthropicWebClient.create(...), which ensures the instance is fully initialized and ready to use.
  4. Strongly Typed: It includes TypeScript interfaces for the common API responses, providing better autocompletion and type safety.
  5. Clean Structure: All related methods are logically grouped within a single class, removing the need for BasicClient and ProjectClient wrappers.
  6. Full Functionality: It includes the docs and files methods 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();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment