Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions apps/claude-cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@ const plugins = [cleanPlugin({ destructive: true }), versionPlugin({ versionCalc
const inject = await Array.fromAsync(glob('./inject/*.ts'));

const ctx = await esbuild.context({
dropLabels: watch ? [] : ['DEBUG'],
banner: { js: '#!/usr/bin/env node' },
bundle: true,
entryPoints: ['src/main.ts'],
chunkNames: 'chunks/[name]-[hash]',
entryNames: 'entry/[name]',
entryPoints: ['src/entry/*.ts'],
external: ['@anthropic-ai/claude-agent-sdk', 'sharp'],
format: 'esm',
inject,
entryNames: '[name]',
keepNames: true,
format: 'esm',
minify,
outdir: 'dist',
platform: 'node',
plugins,
sourcemap: true,
splitting: true,
target: 'node24',
treeShaking: true,
// dropLabels: watch ? [] : ['DEBUG'],
tsconfig: 'tsconfig.json',
external: ['@anthropic-ai/claude-agent-sdk', 'sharp'],
});

if (watch) {
Expand Down
2 changes: 1 addition & 1 deletion apps/claude-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"watch": "tsx build.ts --watch"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.90",
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
"@js-joda/core": "^5.7.0",
"@shellicar/claude-core": "workspace:*",
"@shellicar/mcp-exec": "1.0.0-preview.6",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { parseArgs } from 'node:util';
import { ClaudeCli } from './ClaudeCli.js';
import { initConfig } from './cli-config/initConfig.js';
import { printUsage, printVersion, printVersionInfo } from './help.js';
import { ClaudeCli } from '../ClaudeCli.js';
import { initConfig } from '../cli-config/initConfig.js';
import { printUsage, printVersion, printVersionInfo } from '../help.js';

const { values } = parseArgs({
options: {
Expand Down
5 changes: 3 additions & 2 deletions apps/claude-cli/test/terminal-perf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('Terminal wrapping cache', () => {
const end = process.hrtime.bigint();

const actual = Number(end - start) / 1_000_000;
const expected = process.env.CI ? 20 : 2;
const expected = process.env.CI ? 25 : 2;
expect(actual).toBeLessThan(expected);
});

Expand All @@ -106,6 +106,7 @@ describe('Terminal wrapping cache', () => {
const end = process.hrtime.bigint();

const elapsedMs = Number(end - start) / 1_000_000;
expect(elapsedMs).toBeLessThan(1);
const expected = process.env.CI ? 5 : 1;
expect(elapsedMs).toBeLessThan(expected);
});
});
1 change: 1 addition & 0 deletions apps/claude-sdk-cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ dist-ssr
*.sln
*.sw?
.sdk-history.jsonl
.credentials.json
16 changes: 9 additions & 7 deletions apps/claude-sdk-cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@ import versionPlugin from '@shellicar/build-version/esbuild';
import * as esbuild from 'esbuild';

const watch = process.argv.some((x) => x === '--watch');
const minify = !watch;

const plugins = [cleanPlugin({ destructive: true }), versionPlugin({ versionCalculator: 'gitversion' })];
const inject = await Array.fromAsync(glob('./inject/*.ts'));

const ctx = await esbuild.context({
dropLabels: watch ? [] : ['DEBUG'],
banner: { js: '#!/usr/bin/env node' },
bundle: true,
chunkNames: 'chunks/[name]-[hash]',
entryNames: 'entry/[name]',
entryPoints: ['src/entry/*.ts'],
external: ['@anthropic-ai/sdk'],
format: 'esm',
inject,
entryNames: 'entry/[name]',
chunkNames: 'chunks/[name]-[hash]',
keepNames: true,
format: 'esm',
minify: false,
minify,
outdir: 'dist',
platform: 'node',
plugins,
splitting: true,
external: ['@anthropic-ai/sdk'],
sourcemap: true,
splitting: true,
target: 'node24',
treeShaking: true,
// dropLabels: ['DEBUG'],
tsconfig: 'tsconfig.json',
});

Expand Down
15 changes: 8 additions & 7 deletions apps/claude-sdk-cli/src/entry/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parseArgs } from 'node:util';
import { createAnthropicAgent } from '@shellicar/claude-sdk';
import { AnthropicAuth, createAnthropicAgent } from '@shellicar/claude-sdk';
import { RefStore } from '@shellicar/claude-sdk-tools/RefStore';
import { AppLayout } from '../AppLayout.js';
import { printUsage, printVersion, printVersionInfo } from '../help.js';
Expand Down Expand Up @@ -42,11 +42,12 @@ if (!process.stdin.isTTY) {
const HISTORY_FILE = '.sdk-history.jsonl';

const main = async () => {
const apiKey = process.env.CLAUDE_CODE_API_KEY;
if (!apiKey) {
logger.error('CLAUDE_CODE_API_KEY is not set');
process.exit(1);
}
const auth = new AnthropicAuth({ redirect: 'local' });
await auth.getCredentials();
const authToken = async () => {
const credentials = await auth.getCredentials();
return credentials.claudeAiOauth.accessToken;
};

using rl = new ReadLine();
const layout = new AppLayout();
Expand All @@ -61,7 +62,7 @@ const main = async () => {
rl.setLayout(layout);
layout.enter();

const agent = createAnthropicAgent({ apiKey, logger, historyFile: HISTORY_FILE });
const agent = createAnthropicAgent({ authToken, logger, historyFile: HISTORY_FILE });
const store = new RefStore();
while (true) {
const prompt = await layout.waitForInput();
Expand Down
1 change: 1 addition & 0 deletions apps/claude-sdk-cli/src/runAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export async function runAgent(agent: IAnthropicAgent, prompt: string, layout: A
messages: [prompt],
transformToolResult,
pauseAfterCompact: true,
compactInputTokens: 150_000,
tools,
requireToolApproval: true,
thinking: true,
Expand Down
8 changes: 7 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!!**/dist", "!.claude-bot-audit", "!**/schema/cli-config.schema.json"]
"includes": ["**", "!!**/dist", "!.claude-bot-audit", "!**/schema/cli-config.schema.json", "!packages/claude-sdk/src/private/http/sdkInternals.ts"]
},
"formatter": {
"enabled": true,
Expand Down Expand Up @@ -70,6 +70,12 @@
}
},
"overrides": [
{
"includes": ["**/sdkInternals.ts"],
"linter": {
"enabled": false
}
},
{
"includes": ["**/logger.ts", "**/build.ts"],
"linter": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"lefthook": "^2.1.4",
"npm-check-updates": "^19.6.6",
"syncpack": "^14.3.0",
"turbo": "^2.9.3",
"turbo": "^2.9.4",
"vitest": "^4.1.2"
}
}
7 changes: 4 additions & 3 deletions packages/claude-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { AnthropicAuth } from './private/Auth/AnthropicAuth';
import type { AuthCredentials } from './private/Auth/types';
import { calculateCost } from './private/pricing';
import { createAnthropicAgent } from './public/createAnthropicAgent';
import { defineTool } from './public/defineTool';
Expand All @@ -6,6 +8,5 @@ import { IAnthropicAgent } from './public/interfaces';
import type { AnthropicAgentOptions, AnthropicBetaFlags, AnyToolDefinition, CacheTtl, ConsumerMessage, ILogger, RunAgentQuery, RunAgentResult, SdkDone, SdkError, SdkMessage, SdkMessageEnd, SdkMessageStart, SdkMessageText, SdkMessageUsage, SdkToolApprovalRequest, ToolDefinition, ToolOperation } from './public/types';

export type { BetaMessageParam } from '@anthropic-ai/sdk/resources/beta.js';

export type { AnthropicAgentOptions, AnthropicBetaFlags, AnyToolDefinition, CacheTtl, ConsumerMessage, ILogger, RunAgentQuery, RunAgentResult, SdkDone, SdkError, SdkMessage, SdkMessageEnd, SdkMessageStart, SdkMessageText, SdkMessageUsage, SdkToolApprovalRequest, ToolDefinition, ToolOperation };
export { AnthropicBeta, calculateCost, createAnthropicAgent, defineTool, IAnthropicAgent };
export type { AnthropicAgentOptions, AnthropicBetaFlags, AnyToolDefinition, AuthCredentials, CacheTtl, ConsumerMessage, ILogger, RunAgentQuery, RunAgentResult, SdkDone, SdkError, SdkMessage, SdkMessageEnd, SdkMessageStart, SdkMessageText, SdkMessageUsage, SdkToolApprovalRequest, ToolDefinition, ToolOperation };
export { AnthropicAuth, AnthropicBeta, calculateCost, createAnthropicAgent, defineTool, IAnthropicAgent };
11 changes: 10 additions & 1 deletion packages/claude-sdk/src/private/AgentRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,16 @@ export class AgentRun {
context_management.edits?.push({ type: 'clear_tool_uses_20250919' } satisfies BetaClearToolUses20250919Edit);
}
if (betas[AnthropicBeta.Compact]) {
context_management.edits?.push({ type: 'compact_20260112', pause_after_compaction: this.#options.pauseAfterCompact ?? false, trigger: { type: 'input_tokens', value: 125000 } } satisfies BetaCompact20260112Edit);
context_management.edits?.push({
type: 'compact_20260112',
pause_after_compaction: this.#options.pauseAfterCompact ?? false,
trigger: this.#options.compactInputTokens
? {
type: 'input_tokens',
value: this.#options.compactInputTokens,
}
: null,
} satisfies BetaCompact20260112Edit);
}

const body: BetaMessageStreamParams = {
Expand Down
11 changes: 5 additions & 6 deletions packages/claude-sdk/src/private/AnthropicAgent.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Anthropic, type ClientOptions } from '@anthropic-ai/sdk';
import type { BetaMessageParam } from '@anthropic-ai/sdk/resources/beta.js';
import versionJson from '@shellicar/build-version/version';
import { IAnthropicAgent } from '../public/interfaces';
import type { AnthropicAgentOptions, ILogger, RunAgentQuery, RunAgentResult } from '../public/types';
import { AgentRun } from './AgentRun';
import { ConversationHistory } from './ConversationHistory';
import { customFetch } from './http/customFetch';
import { TokenRefreshingAnthropic } from './http/TokenRefreshingAnthropic';

export class AnthropicAgent extends IAnthropicAgent {
readonly #client: Anthropic;
readonly #client: TokenRefreshingAnthropic;
readonly #logger: ILogger | undefined;
readonly #history: ConversationHistory;

Expand All @@ -18,13 +18,12 @@ export class AnthropicAgent extends IAnthropicAgent {
const defaultHeaders = {
'user-agent': `@shellicar/claude-sdk/${versionJson.version}`,
};
const clientOptions = {
authToken: `${options.apiKey}`,
this.#client = new TokenRefreshingAnthropic({
authToken: options.authToken,
fetch: customFetch(options.logger),
logger: options.logger,
defaultHeaders,
} satisfies ClientOptions;
this.#client = new Anthropic(clientOptions);
});
this.#history = new ConversationHistory(options.historyFile);
}

Expand Down
54 changes: 54 additions & 0 deletions packages/claude-sdk/src/private/Auth/AnthropicAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { execFile } from 'node:child_process';
import { buildAuthUrl } from './buildAuthUrl';
import { LocalRedirectUrl, PlatformRedirectUrl } from './consts';
import { exchangeCode } from './exchangeCode';
import { fetchProfile } from './fetchProfile';
import { isExpired } from './isExpired';
import { loadCredentials } from './loadCredentials';
import { refreshCredentials } from './refreshCredentials';
import { saveCredentials } from './saveCredentials';
import type { AnthropicAuthOptions, AuthCredentials } from './types';
import { waitForCallback } from './waitForCallback';

export class AnthropicAuth {
private readonly redirect: 'local' | 'manual';

public constructor(options: AnthropicAuthOptions = {}) {
this.redirect = options.redirect ?? 'local';
}

public async getCredentials(): Promise<AuthCredentials> {
let credentials = await loadCredentials();

if (credentials === null) {
credentials = await this.login();
const profile = await fetchProfile(credentials.claudeAiOauth.accessToken);
credentials = { claudeAiOauth: { ...credentials.claudeAiOauth, ...profile } };
await saveCredentials(credentials);
} else if (isExpired(credentials)) {
credentials = await refreshCredentials(credentials);
await saveCredentials(credentials);
}

return credentials;
}

private async login(): Promise<AuthCredentials> {
if (this.redirect === 'local') {
const { url, codeVerifier, state } = buildAuthUrl(LocalRedirectUrl);
execFile('open', [url]);
const { code } = await waitForCallback(3001);
return exchangeCode(code, state, codeVerifier, LocalRedirectUrl);
}

const { url, codeVerifier, state } = buildAuthUrl(PlatformRedirectUrl);
// biome-ignore lint/suspicious/noConsole: show url
console.log(url);
process.stdout.write('Paste code: ');
const input = await new Promise<string>((resolve) => {
process.stdin.once('data', (data) => resolve(data.toString().trim()));
});
const code = input.split('#')[0];
return exchangeCode(code, state, codeVerifier, PlatformRedirectUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class InvalidAuthorisationCodeError extends Error {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class TokenExchangeFailedError extends Error {}
1 change: 1 addition & 0 deletions packages/claude-sdk/src/private/Auth/base64UrlEncode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const base64UrlEncode = (buffer: Buffer) => buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
21 changes: 21 additions & 0 deletions packages/claude-sdk/src/private/Auth/buildAuthUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AuthorisationUrl, ClientId, Scopes } from './consts';
import { generateCodeChallenge } from './generateCodeChallenge';
import { generateCodeVerifier } from './generateCodeVerifier';
import { generateState } from './generateState';
import type { AuthUrlResult } from './types';

export const buildAuthUrl = (redirectUri: string): AuthUrlResult => {
const codeVerifier = generateCodeVerifier();
const state = generateState();

const url = new URL(AuthorisationUrl);
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', ClientId);
url.searchParams.set('redirect_uri', redirectUri);
url.searchParams.set('scope', Scopes);
url.searchParams.set('code_challenge', generateCodeChallenge(codeVerifier));
url.searchParams.set('code_challenge_method', 'S256');
url.searchParams.set('state', state);

return { url: url.href, codeVerifier, state } satisfies AuthUrlResult;
};
9 changes: 9 additions & 0 deletions packages/claude-sdk/src/private/Auth/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const TokenUrl = 'https://platform.claude.com/v1/oauth/token';
export const AuthorisationUrl = 'https://claude.com/cai/oauth/authorize';
export const PlatformRedirectUrl = 'https://platform.claude.com/oauth/code/callback';
export const LocalRedirectUrl = 'http://localhost:3001/callback';
export const ClientId = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
export const Scopes = 'user:file_upload user:inference user:mcp_servers user:profile user:sessions:claude_code';

export const CredentialsPath = '.credentials.json';
export const ProfileUrl = 'https://api.anthropic.com/api/oauth/profile';
7 changes: 7 additions & 0 deletions packages/claude-sdk/src/private/Auth/credentialsPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { homedir } from 'node:os';
import { join } from 'node:path';
import { CredentialsPath } from './consts';

export function credentialsPath() {
return join(homedir(), '.claude', CredentialsPath);
}
29 changes: 29 additions & 0 deletions packages/claude-sdk/src/private/Auth/exchangeCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ClientId, TokenUrl } from './consts';
import { InvalidAuthorisationCodeError } from './InvalidAuthorisationCodeError';
import { parseTokenResponse } from './parseTokenResponse';
import { TokenExchangeFailedError } from './TokenExchangeFailedError';
import type { AuthCredentials } from './types';

export const exchangeCode = async (code: string, state: string, codeVerifier: string, redirectUri: string): Promise<AuthCredentials> => {
const response = await fetch(TokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: ClientId,
code_verifier: codeVerifier,
state,
}),
});

if (!response.ok) {
if (response.status === 401) {
throw new InvalidAuthorisationCodeError();
}
throw new TokenExchangeFailedError();
}

return parseTokenResponse(await response.json());
};
Loading
Loading