pydantic-claude-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydantic_ai_claude_code/__init__.py +35 -0
- pydantic_ai_claude_code/__main__.py +19 -0
- pydantic_ai_claude_code/auth.py +83 -0
- pydantic_ai_claude_code/config.py +44 -0
- pydantic_ai_claude_code/credentials.py +72 -0
- pydantic_ai_claude_code/flow.py +238 -0
- pydantic_ai_claude_code/model.py +40 -0
- pydantic_ai_claude_code/provider.py +106 -0
- pydantic_ai_claude_code/py.typed +0 -0
- pydantic_ai_claude_code/storage.py +50 -0
- pydantic_claude_code-0.1.0.dist-info/METADATA +141 -0
- pydantic_claude_code-0.1.0.dist-info/RECORD +14 -0
- pydantic_claude_code-0.1.0.dist-info/WHEEL +4 -0
- pydantic_claude_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Use your Claude Code subscription from a Pydantic AI Agent.
|
|
2
|
+
|
|
3
|
+
Quick start:
|
|
4
|
+
|
|
5
|
+
from pydantic_ai import Agent
|
|
6
|
+
from pydantic_ai_claude_code import ClaudeCodeProvider
|
|
7
|
+
|
|
8
|
+
provider = ClaudeCodeProvider()
|
|
9
|
+
agent = Agent(provider.model('claude-sonnet-4-5'))
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .credentials import ClaudeCodeCredentials
|
|
13
|
+
from .flow import (
|
|
14
|
+
ClaudeCodeOAuthFlow,
|
|
15
|
+
exchange_code,
|
|
16
|
+
login,
|
|
17
|
+
parse_pasteback,
|
|
18
|
+
refresh_credentials,
|
|
19
|
+
)
|
|
20
|
+
from .model import ClaudeCodeModel
|
|
21
|
+
from .provider import ClaudeCodeProvider
|
|
22
|
+
from .storage import ClaudeCodeTokenStore, default_auth_path
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ClaudeCodeCredentials",
|
|
26
|
+
"ClaudeCodeModel",
|
|
27
|
+
"ClaudeCodeOAuthFlow",
|
|
28
|
+
"ClaudeCodeProvider",
|
|
29
|
+
"ClaudeCodeTokenStore",
|
|
30
|
+
"default_auth_path",
|
|
31
|
+
"exchange_code",
|
|
32
|
+
"login",
|
|
33
|
+
"parse_pasteback",
|
|
34
|
+
"refresh_credentials",
|
|
35
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Run `python -m pydantic_ai_claude_code login` to authenticate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import login
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
if "login" not in sys.argv[1:]:
|
|
13
|
+
print("Usage: python -m pydantic_ai_claude_code login", file=sys.stderr)
|
|
14
|
+
sys.exit(2)
|
|
15
|
+
asyncio.run(login())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
main()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""httpx2 auth shim that authenticates Messages API calls with Claude Code tokens."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
import httpx2
|
|
10
|
+
|
|
11
|
+
from pydantic_ai.exceptions import UserError
|
|
12
|
+
|
|
13
|
+
from . import config
|
|
14
|
+
from .credentials import ClaudeCodeCredentials
|
|
15
|
+
from .flow import refresh_credentials
|
|
16
|
+
|
|
17
|
+
CredentialsRefreshCallback = Callable[[ClaudeCodeCredentials], Awaitable[None]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClaudeCodeCredentialsPersistenceError(UserError):
|
|
21
|
+
"""Raised after refreshed in-memory credentials could not be persisted."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _expires_soon(credentials: ClaudeCodeCredentials) -> bool:
|
|
25
|
+
if credentials.expires_at is None:
|
|
26
|
+
return False
|
|
27
|
+
return time.time() + 30 >= credentials.expires_at.timestamp()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _ClaudeCodeAuth(httpx2.Auth):
|
|
31
|
+
requires_response_body = True
|
|
32
|
+
|
|
33
|
+
def __init__(self, credentials: ClaudeCodeCredentials, callback: CredentialsRefreshCallback | None = None) -> None:
|
|
34
|
+
self.credentials = credentials
|
|
35
|
+
self.callback = callback
|
|
36
|
+
self.revision = 0
|
|
37
|
+
self.lock = asyncio.Lock()
|
|
38
|
+
self.refresh_client = httpx2.AsyncClient()
|
|
39
|
+
|
|
40
|
+
async def _refresh(self, used_revision: int) -> None:
|
|
41
|
+
async with self.lock:
|
|
42
|
+
if self.revision != used_revision:
|
|
43
|
+
return
|
|
44
|
+
updated = await refresh_credentials(self.credentials, http_client=self.refresh_client)
|
|
45
|
+
self.credentials = updated
|
|
46
|
+
self.revision += 1
|
|
47
|
+
if self.callback is not None:
|
|
48
|
+
try:
|
|
49
|
+
await self.callback(updated)
|
|
50
|
+
except Exception as exc: # noqa: BLE001 - surface the persistence failure to the caller
|
|
51
|
+
raise ClaudeCodeCredentialsPersistenceError(
|
|
52
|
+
"Claude Code credentials refreshed in memory, but the persistence callback failed."
|
|
53
|
+
) from exc
|
|
54
|
+
|
|
55
|
+
def _apply(self, request: httpx2.Request) -> int:
|
|
56
|
+
# Subscription tokens are `Authorization: Bearer` credentials, not API keys.
|
|
57
|
+
# The SDK injects `x-api-key` from the placeholder `api_key`, so it must be
|
|
58
|
+
# removed or the server validates it first and rejects it as an API key.
|
|
59
|
+
if "x-api-key" in request.headers:
|
|
60
|
+
del request.headers["x-api-key"]
|
|
61
|
+
request.headers["Authorization"] = f"Bearer {self.credentials.token}"
|
|
62
|
+
request.headers["x-app"] = config.X_APP
|
|
63
|
+
request.headers["user-agent"] = config.USER_AGENT
|
|
64
|
+
# Anthropic's SDK manages its own betas; ours must be merged, not replaced.
|
|
65
|
+
existing_beta = request.headers.get("anthropic-beta")
|
|
66
|
+
if config.ANTHROPIC_BETA not in (existing_beta or ""):
|
|
67
|
+
request.headers["anthropic-beta"] = ", ".join(filter(None, [existing_beta, config.ANTHROPIC_BETA]))
|
|
68
|
+
|
|
69
|
+
return self.revision
|
|
70
|
+
|
|
71
|
+
async def async_auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]:
|
|
72
|
+
revision = self._apply(request)
|
|
73
|
+
if _expires_soon(self.credentials):
|
|
74
|
+
await self._refresh(revision)
|
|
75
|
+
revision = self._apply(request)
|
|
76
|
+
response = yield request
|
|
77
|
+
if response.status_code != 401:
|
|
78
|
+
return
|
|
79
|
+
await response.aread()
|
|
80
|
+
await response.aclose()
|
|
81
|
+
await self._refresh(revision)
|
|
82
|
+
self._apply(request)
|
|
83
|
+
yield request
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""OAuth endpoint and client configuration for Claude Code authentication.
|
|
2
|
+
|
|
3
|
+
These constants were verified against the Claude Code CLI 2.1.263 binary
|
|
4
|
+
(embedded config) and its published client metadata:
|
|
5
|
+
|
|
6
|
+
- `https://claude.ai/oauth/claude-code-client-metadata` (dynamic client registration)
|
|
7
|
+
- Authorization server: `https://claude.com/cai/oauth/authorize`
|
|
8
|
+
- Token endpoint: `https://platform.claude.com/v1/oauth/token`
|
|
9
|
+
|
|
10
|
+
The shared `9d1c250a...` client id is the public client the official CLI uses,
|
|
11
|
+
so our flow presents the same credentials to Anthropic's authorization server.
|
|
12
|
+
See `flow.py` for the authorization-code flow, and `provider.py` for the API client.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# OAuth endpoints and the shared public client id used by the Claude Code CLI.
|
|
16
|
+
AUTH_URL = "https://claude.ai/oauth/authorize"
|
|
17
|
+
TOKEN_URL = "https://platform.claude.com/v1/oauth/token"
|
|
18
|
+
CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
19
|
+
# Same scope set the plugin in `code_puppy_core_plugins` uses. The binary's
|
|
20
|
+
# `user:ccr_inference` scope is not recognized by the authorization server.
|
|
21
|
+
SCOPES = "org:create_api_key user:profile user:inference"
|
|
22
|
+
|
|
23
|
+
# The subscription tokens minted by this flow are valid against api.anthropic.com.
|
|
24
|
+
API_BASE_URL = "https://api.anthropic.com"
|
|
25
|
+
|
|
26
|
+
# Must open the system context when authenticating with Claude Code tokens. This
|
|
27
|
+
# is the exact persona string the official CLI sends; the subscription backend
|
|
28
|
+
# expects it at position 0.
|
|
29
|
+
CLAUDE_CODE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude."
|
|
30
|
+
|
|
31
|
+
# Redirect handling. We host a short-lived callback server on localhost. The
|
|
32
|
+
# authorization server accepts `http://localhost:<any port>/callback` but
|
|
33
|
+
# rejects `127.0.0.1` variants, so the host must stay `localhost`.
|
|
34
|
+
REDIRECT_HOST = "http://localhost"
|
|
35
|
+
REDIRECT_PATH = "callback"
|
|
36
|
+
CALLBACK_PORT_RANGE = (8765, 8795)
|
|
37
|
+
CALLBACK_TIMEOUT = 180
|
|
38
|
+
PASTEBACK_SCHEMES = ("claude://",)
|
|
39
|
+
|
|
40
|
+
# Request headers that must accompany Claude Code subscription tokens. `anthropic-beta` is
|
|
41
|
+
# appended (never replaced) by the auth layer so feature betas set by pydantic-ai survive.
|
|
42
|
+
ANTHROPIC_BETA = "oauth-2025-04-20"
|
|
43
|
+
USER_AGENT = "claude-cli/2.1.263 (external, cli)"
|
|
44
|
+
X_APP = "cli"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Credentials for Claude Code subscription authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, SecretStr
|
|
9
|
+
|
|
10
|
+
from pydantic_ai.exceptions import UserError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClaudeCodeCredentials(BaseModel):
|
|
14
|
+
"""Credentials minted by the Claude Code OAuth flow.
|
|
15
|
+
|
|
16
|
+
`access_token` is the bearer token sent to the Messages API. `refresh_token`
|
|
17
|
+
is used by the auth layer to mint a new pair before expiry.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
access_token: SecretStr
|
|
21
|
+
refresh_token: SecretStr
|
|
22
|
+
expires_at: datetime | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def token(self) -> str:
|
|
26
|
+
"""The plaintext access token."""
|
|
27
|
+
return self.access_token.get_secret_value()
|
|
28
|
+
|
|
29
|
+
def to_wire_dict(self) -> dict[str, object]:
|
|
30
|
+
"""A JSON-serializable, plain-string form of the credentials for storage.
|
|
31
|
+
|
|
32
|
+
Pydantic 2.13 masks `SecretStr` in some serialization modes, so the stored
|
|
33
|
+
form is spelled out here instead of trusting `model_dump`.
|
|
34
|
+
"""
|
|
35
|
+
return {
|
|
36
|
+
"access_token": self.token,
|
|
37
|
+
"refresh_token": self.refresh_token.get_secret_value(),
|
|
38
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at is not None else None,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_token_response(cls, data: object, previous: ClaudeCodeCredentials | None = None) -> ClaudeCodeCredentials:
|
|
43
|
+
"""Build credentials from a token-endpoint response.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
data: The parsed JSON body of a token response.
|
|
47
|
+
previous: Prior credentials whose refresh token is reused when the
|
|
48
|
+
issuer omits a new one, as Anthropic's refresh responses do.
|
|
49
|
+
"""
|
|
50
|
+
root = data if isinstance(data, dict) else None
|
|
51
|
+
access_token = root.get("access_token") if root is not None else None
|
|
52
|
+
refresh_token = root.get("refresh_token") if root is not None else None
|
|
53
|
+
if not isinstance(access_token, str):
|
|
54
|
+
raise UserError("Claude Code token response did not contain a string `access_token`.")
|
|
55
|
+
if not isinstance(refresh_token, str):
|
|
56
|
+
if previous is None:
|
|
57
|
+
raise UserError("Claude Code token response did not contain a string `refresh_token`.")
|
|
58
|
+
refresh_token = previous.refresh_token.get_secret_value()
|
|
59
|
+
expires_at = None
|
|
60
|
+
if expires_in := root.get("expires_in"):
|
|
61
|
+
if isinstance(expires_in, (int, float)) and not isinstance(expires_in, bool):
|
|
62
|
+
expires_at = datetime.fromtimestamp(time.time() + float(expires_in), tz=UTC)
|
|
63
|
+
return cls(
|
|
64
|
+
access_token=SecretStr(access_token),
|
|
65
|
+
refresh_token=SecretStr(refresh_token),
|
|
66
|
+
expires_at=expires_at,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_token_file(cls, data: object) -> ClaudeCodeCredentials:
|
|
71
|
+
"""Parse a stored token file (the saved output of a previous exchange)."""
|
|
72
|
+
return cls.model_validate(data)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Authorization-code PKCE flow for Claude Code credentials.
|
|
2
|
+
|
|
3
|
+
The browser flow hosts a short-lived localhost callback server, the same shape as
|
|
4
|
+
the Codex provider's `_REDIRECT_URI`. Both the `claude://` pasteback scheme and a
|
|
5
|
+
plain localhost redirect are supported.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
import secrets
|
|
13
|
+
import threading
|
|
14
|
+
import webbrowser
|
|
15
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
16
|
+
from typing import Any
|
|
17
|
+
from urllib.parse import parse_qs, urlencode
|
|
18
|
+
|
|
19
|
+
import httpx2
|
|
20
|
+
|
|
21
|
+
from pydantic_ai.exceptions import UserError
|
|
22
|
+
|
|
23
|
+
from . import config
|
|
24
|
+
from .credentials import ClaudeCodeCredentials
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClaudeCodeOAuthFlow:
|
|
28
|
+
"""A side-effect-free authorization-code PKCE flow context."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, redirect_uri: str | None = None) -> None:
|
|
31
|
+
self.redirect_uri = redirect_uri
|
|
32
|
+
self.state = secrets.token_urlsafe(32)
|
|
33
|
+
self.code_verifier = secrets.token_urlsafe(64)
|
|
34
|
+
digest = hashlib.sha256(self.code_verifier.encode()).digest()
|
|
35
|
+
self.code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
|
|
36
|
+
|
|
37
|
+
def authorization_url(self) -> str:
|
|
38
|
+
query = urlencode(
|
|
39
|
+
{
|
|
40
|
+
"response_type": "code",
|
|
41
|
+
"client_id": config.CLIENT_ID,
|
|
42
|
+
"redirect_uri": self.redirect_uri or "",
|
|
43
|
+
"scope": config.SCOPES,
|
|
44
|
+
"state": self.state,
|
|
45
|
+
"code": "true",
|
|
46
|
+
"code_challenge": self.code_challenge,
|
|
47
|
+
"code_challenge_method": "S256",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
return f"{config.AUTH_URL}?{query}"
|
|
51
|
+
|
|
52
|
+
async def exchange_code(self, code: str, *, http_client: httpx2.AsyncClient | None = None) -> ClaudeCodeCredentials:
|
|
53
|
+
"""Exchange an authorization code for credentials."""
|
|
54
|
+
return await exchange_code(
|
|
55
|
+
code, self.code_verifier, self.redirect_uri, state=self.state, http_client=http_client
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def exchange_code(
|
|
60
|
+
code: str,
|
|
61
|
+
code_verifier: str,
|
|
62
|
+
redirect_uri: str | None,
|
|
63
|
+
*,
|
|
64
|
+
state: str | None = None,
|
|
65
|
+
http_client: httpx2.AsyncClient | None = None,
|
|
66
|
+
) -> ClaudeCodeCredentials:
|
|
67
|
+
"""Exchange an authorization code for Claude Code credentials."""
|
|
68
|
+
async with _client_context(http_client) as client:
|
|
69
|
+
response = await client.post(
|
|
70
|
+
config.TOKEN_URL,
|
|
71
|
+
json={
|
|
72
|
+
"grant_type": "authorization_code",
|
|
73
|
+
"client_id": config.CLIENT_ID,
|
|
74
|
+
"code": code,
|
|
75
|
+
"redirect_uri": redirect_uri,
|
|
76
|
+
"code_verifier": code_verifier,
|
|
77
|
+
"state": state,
|
|
78
|
+
},
|
|
79
|
+
headers=_token_headers(),
|
|
80
|
+
)
|
|
81
|
+
_raise_for_token_error(response)
|
|
82
|
+
return ClaudeCodeCredentials.from_token_response(response.json())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def refresh_credentials(
|
|
86
|
+
credentials: ClaudeCodeCredentials, *, http_client: httpx2.AsyncClient | None = None
|
|
87
|
+
) -> ClaudeCodeCredentials:
|
|
88
|
+
"""Refresh Claude Code credentials without persisting them."""
|
|
89
|
+
async with _client_context(http_client) as client:
|
|
90
|
+
response = await client.post(
|
|
91
|
+
config.TOKEN_URL,
|
|
92
|
+
json={
|
|
93
|
+
"grant_type": "refresh_token",
|
|
94
|
+
"client_id": config.CLIENT_ID,
|
|
95
|
+
"refresh_token": credentials.refresh_token.get_secret_value(),
|
|
96
|
+
},
|
|
97
|
+
headers=_token_headers(),
|
|
98
|
+
)
|
|
99
|
+
_raise_for_token_error(response)
|
|
100
|
+
return ClaudeCodeCredentials.from_token_response(response.json(), previous=credentials)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _raise_for_token_error(response: httpx2.Response) -> None:
|
|
104
|
+
"""Raise a `UserError` that includes the token server's error body.
|
|
105
|
+
|
|
106
|
+
The token endpoints return sparse HTTP codes, so the reason lives in the
|
|
107
|
+
JSON body; without it a 400 tells us nothing.
|
|
108
|
+
"""
|
|
109
|
+
if response.status_code < 400:
|
|
110
|
+
return
|
|
111
|
+
body = response.text[:500]
|
|
112
|
+
raise UserError(f"Claude Code token endpoint returned {response.status_code}: {body}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _token_headers() -> dict[str, str]:
|
|
116
|
+
return {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
"Accept": "application/json",
|
|
119
|
+
"anthropic-beta": config.ANTHROPIC_BETA,
|
|
120
|
+
"User-Agent": config.USER_AGENT,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_pasteback(raw: str) -> tuple[str, str] | None:
|
|
125
|
+
"""Parse a `claude://oauth/callback?...` paste back into `(code, state)`.
|
|
126
|
+
|
|
127
|
+
Returns `None` when the input isn't a pasteback URL.
|
|
128
|
+
"""
|
|
129
|
+
if not raw.startswith(config.PASTEBACK_SCHEMES):
|
|
130
|
+
return None
|
|
131
|
+
try:
|
|
132
|
+
query = raw.partition("?")[2]
|
|
133
|
+
params = parse_qs(query)
|
|
134
|
+
except ValueError:
|
|
135
|
+
return None
|
|
136
|
+
code = params.get("code", [""])[0]
|
|
137
|
+
state = params.get("state", [""])[0]
|
|
138
|
+
if not code:
|
|
139
|
+
return None
|
|
140
|
+
return code, state
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _CallbackServer(ThreadingHTTPServer):
|
|
144
|
+
"""Localhost server that captures the OAuth redirect query string."""
|
|
145
|
+
|
|
146
|
+
def __init__(self) -> None:
|
|
147
|
+
self.query: str | None = None
|
|
148
|
+
self.received = threading.Event()
|
|
149
|
+
super().__init__(("127.0.0.1", 0), _CallbackHandler)
|
|
150
|
+
self.daemon_threads = True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
154
|
+
server: _CallbackServer
|
|
155
|
+
|
|
156
|
+
def do_GET(self) -> None: # noqa: N802
|
|
157
|
+
if not self.server.received.is_set():
|
|
158
|
+
self.server.query = self.path
|
|
159
|
+
self.server.received.set()
|
|
160
|
+
self.send_response(200)
|
|
161
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
162
|
+
self.end_headers()
|
|
163
|
+
self.wfile.write(
|
|
164
|
+
b"<html><body><h1>pydantic-ai-claude-code</h1><p>Auth successful. Return to your terminal.</p></body></html>"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def log_message(self, format: str, *args: object) -> None: # noqa: A002 - stdlib signature
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def start_login_callback_server() -> _CallbackServer:
|
|
172
|
+
"""Start a callback server plus a daemon thread serving it."""
|
|
173
|
+
server = _CallbackServer()
|
|
174
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
175
|
+
return server
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def login(*, store: Any = None) -> ClaudeCodeCredentials:
|
|
179
|
+
"""Run the full OAuth flow and persist the minted credentials.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
store: The token store to write to. Defaults to the standard store.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The credentials that were persisted.
|
|
186
|
+
"""
|
|
187
|
+
from .storage import ClaudeCodeTokenStore
|
|
188
|
+
|
|
189
|
+
if store is None:
|
|
190
|
+
store = ClaudeCodeTokenStore()
|
|
191
|
+
server = start_login_callback_server()
|
|
192
|
+
try:
|
|
193
|
+
redirect_uri = f"{config.REDIRECT_HOST}:{server.server_address[1]}/{config.REDIRECT_PATH}"
|
|
194
|
+
flow = ClaudeCodeOAuthFlow(redirect_uri=redirect_uri)
|
|
195
|
+
url = flow.authorization_url()
|
|
196
|
+
print(f"Open this URL in your browser if it did not open automatically:\n{url}")
|
|
197
|
+
# Launch the browser off the event path: on some macOS setups `webbrowser.open`
|
|
198
|
+
# hangs until the UI settles, which would starve the callback wait below.
|
|
199
|
+
threading.Thread(target=_open_browser, args=(url,), daemon=True).start()
|
|
200
|
+
if not server.received.wait(config.CALLBACK_TIMEOUT):
|
|
201
|
+
raise UserError("Claude Code OAuth callback timed out.")
|
|
202
|
+
code, state = _parse_callback_query(server.query)
|
|
203
|
+
if state and state != flow.state:
|
|
204
|
+
raise UserError("Claude Code OAuth state mismatch; the redirect may be a replay.")
|
|
205
|
+
if not code:
|
|
206
|
+
raise UserError(f"Claude Code OAuth redirect did not include a `code` parameter: {server.query!r}")
|
|
207
|
+
credentials = await flow.exchange_code(code)
|
|
208
|
+
finally:
|
|
209
|
+
server.shutdown()
|
|
210
|
+
server.server_close()
|
|
211
|
+
store.save(credentials)
|
|
212
|
+
return credentials
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _open_browser(url: str) -> None:
|
|
216
|
+
try:
|
|
217
|
+
webbrowser.open(url)
|
|
218
|
+
except Exception: # noqa: BLE001 - best-effort: the URL is printed for manual use
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _parse_callback_query(raw: str | None) -> tuple[str, str]:
|
|
223
|
+
"""Extract `(code, state)` from a captured redirect path like `/callback?code=...`."""
|
|
224
|
+
params = parse_qs((raw or "").split("?", 1)[-1])
|
|
225
|
+
return params.get("code", [""])[0], params.get("state", [""])[0]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _client_context:
|
|
229
|
+
def __init__(self, client: httpx2.AsyncClient | None) -> None:
|
|
230
|
+
self.client = client or httpx2.AsyncClient()
|
|
231
|
+
self.owned = client is None
|
|
232
|
+
|
|
233
|
+
async def __aenter__(self) -> httpx2.AsyncClient:
|
|
234
|
+
return self.client
|
|
235
|
+
|
|
236
|
+
async def __aexit__(self, *_: object) -> None:
|
|
237
|
+
if self.owned:
|
|
238
|
+
await self.client.aclose()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""The Claude Code model: an `AnthropicModel` with the Claude Code persona prepended."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic_ai.messages import ModelMessage, ModelRequest, SystemPromptPart
|
|
6
|
+
from pydantic_ai.models.anthropic import AnthropicModel
|
|
7
|
+
|
|
8
|
+
from . import config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClaudeCodeModel(AnthropicModel):
|
|
12
|
+
"""AnthropicModel whose system prompt opens with the Claude Code persona.
|
|
13
|
+
|
|
14
|
+
The subscription backend expects the Claude Code persona at position 0 of
|
|
15
|
+
the system context. Prepending a `SystemPromptPart` here is idempotent:
|
|
16
|
+
once it's in history it stays, and repeated `prepare_messages` calls don't
|
|
17
|
+
duplicate it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def prepare_messages(
|
|
21
|
+
self,
|
|
22
|
+
messages: list[ModelMessage],
|
|
23
|
+
model_request_parameters=None,
|
|
24
|
+
) -> list[ModelMessage]:
|
|
25
|
+
return super().prepare_messages(_prepend_persona(messages), model_request_parameters)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _prepend_persona(messages: list[ModelMessage]) -> list[ModelMessage]:
|
|
29
|
+
"""Return `messages` with the persona added first, unless it's already there."""
|
|
30
|
+
for index, message in enumerate(messages):
|
|
31
|
+
if not isinstance(message, ModelRequest):
|
|
32
|
+
continue
|
|
33
|
+
if any(
|
|
34
|
+
isinstance(part, SystemPromptPart) and part.content.strip().startswith(config.CLAUDE_CODE_SYSTEM_PROMPT)
|
|
35
|
+
for part in message.parts
|
|
36
|
+
):
|
|
37
|
+
return messages
|
|
38
|
+
updated = ModelRequest([SystemPromptPart(config.CLAUDE_CODE_SYSTEM_PROMPT), *message.parts])
|
|
39
|
+
return [*messages[:index], updated, *messages[index + 1 :]]
|
|
40
|
+
return messages
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Pydantic AI provider backed by a Claude Code subscription."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx2
|
|
8
|
+
|
|
9
|
+
from anthropic import AsyncAnthropic
|
|
10
|
+
|
|
11
|
+
from pydantic_ai.exceptions import UserError
|
|
12
|
+
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
13
|
+
|
|
14
|
+
from . import config
|
|
15
|
+
from .model import ClaudeCodeModel
|
|
16
|
+
from .auth import _ClaudeCodeAuth
|
|
17
|
+
from .credentials import ClaudeCodeCredentials
|
|
18
|
+
from .storage import ClaudeCodeTokenStore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ClaudeCodeProvider(AnthropicProvider):
|
|
22
|
+
"""Anthropic provider that authenticates with Claude Code subscription tokens.
|
|
23
|
+
|
|
24
|
+
Mirrors the `OpenAICodexProvider` pattern from pydantic-ai: the provider owns
|
|
25
|
+
an httpx2 auth shim that injects the bearer token, refreshes it before expiry,
|
|
26
|
+
and retries once on 401.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
return "claude-code"
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
credentials: ClaudeCodeCredentials | None = None,
|
|
36
|
+
*,
|
|
37
|
+
store: ClaudeCodeTokenStore | None = None,
|
|
38
|
+
on_credentials_refresh: Any = None,
|
|
39
|
+
http_client: httpx2.AsyncClient | None = None,
|
|
40
|
+
base_url: str | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Create a Claude Code provider.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
credentials: Credentials to use. When omitted, they are loaded from
|
|
46
|
+
the store (which defaults to the standard token file).
|
|
47
|
+
store: Token store used to persist refreshed credentials. When
|
|
48
|
+
credentials are loaded from the store, refreshes are persisted
|
|
49
|
+
back to it automatically.
|
|
50
|
+
on_credentials_refresh: Optional callback in place of automatic
|
|
51
|
+
persistence. Takes precedence over `store` when both are given.
|
|
52
|
+
http_client: An existing httpx2 client to use; its `auth` is replaced.
|
|
53
|
+
base_url: Override for the Anthropic API base URL.
|
|
54
|
+
"""
|
|
55
|
+
if credentials is None:
|
|
56
|
+
if store is None:
|
|
57
|
+
store = ClaudeCodeTokenStore()
|
|
58
|
+
creds = store.load()
|
|
59
|
+
if creds is None:
|
|
60
|
+
raise UserError(
|
|
61
|
+
"No Claude Code credentials found. Run `asyncio.run(pydantic_ai_claude_code.login())` "
|
|
62
|
+
"to authenticate, or pass credentials to the provider."
|
|
63
|
+
)
|
|
64
|
+
credentials = creds
|
|
65
|
+
if on_credentials_refresh is None and store is not None:
|
|
66
|
+
|
|
67
|
+
async def persist(updated: ClaudeCodeCredentials) -> None:
|
|
68
|
+
store.save(updated)
|
|
69
|
+
|
|
70
|
+
on_credentials_refresh = persist
|
|
71
|
+
auth = _ClaudeCodeAuth(credentials, on_credentials_refresh)
|
|
72
|
+
if http_client is None:
|
|
73
|
+
http_client = httpx2.AsyncClient(auth=auth)
|
|
74
|
+
else:
|
|
75
|
+
http_client.auth = auth # type: ignore[assignment]
|
|
76
|
+
self._claude_code_auth = auth
|
|
77
|
+
self._client = AsyncAnthropic(
|
|
78
|
+
api_key="unused", # the auth shim supplies the real credentials
|
|
79
|
+
base_url=base_url or config.API_BASE_URL,
|
|
80
|
+
http_client=http_client,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def credentials(self) -> ClaudeCodeCredentials:
|
|
85
|
+
"""The current credentials; refreshed in place by the auth shim."""
|
|
86
|
+
return self._claude_code_auth.credentials
|
|
87
|
+
|
|
88
|
+
def model(self, model_name: str) -> ClaudeCodeModel:
|
|
89
|
+
"""Build a `ClaudeCodeModel` bound to this provider.
|
|
90
|
+
|
|
91
|
+
This is the object-only entry point agreed for the wheel: pass its result
|
|
92
|
+
to `Agent(model=...)` without modifying pydantic-ai. The persona is
|
|
93
|
+
prepended to the system context automatically.
|
|
94
|
+
"""
|
|
95
|
+
return ClaudeCodeModel(model_name, provider=self)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_token_store(
|
|
99
|
+
cls,
|
|
100
|
+
*,
|
|
101
|
+
store: ClaudeCodeTokenStore | None = None,
|
|
102
|
+
http_client: httpx2.AsyncClient | None = None,
|
|
103
|
+
base_url: str | None = None,
|
|
104
|
+
) -> "ClaudeCodeProvider":
|
|
105
|
+
"""Build a provider from the stored token file."""
|
|
106
|
+
return cls(store=store, http_client=http_client, base_url=base_url)
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Persistent token storage for Claude Code credentials.
|
|
2
|
+
|
|
3
|
+
The file lives under the user's data directory, is created with `0o600`
|
|
4
|
+
permissions, and its path can be overridden with the `CLAUDE_CODE_AUTH_FILE`
|
|
5
|
+
environment variable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from pydantic_ai.exceptions import UserError
|
|
15
|
+
|
|
16
|
+
from .credentials import ClaudeCodeCredentials
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def default_auth_path() -> Path:
|
|
20
|
+
"""The token file path, honoring the `CLAUDE_CODE_AUTH_FILE` override."""
|
|
21
|
+
env_path = os.environ.get("CLAUDE_CODE_AUTH_FILE")
|
|
22
|
+
if env_path:
|
|
23
|
+
return Path(env_path).expanduser()
|
|
24
|
+
data_dir = Path(os.environ.get("XDG_DATA_HOME", "") or (Path.home() / ".local" / "share"))
|
|
25
|
+
return data_dir / "pydantic-ai-claude-code" / "auth.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClaudeCodeTokenStore:
|
|
29
|
+
"""Read and write `ClaudeCodeCredentials` to a single JSON file."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, path: str | Path | None = None) -> None:
|
|
32
|
+
self.path = Path(path) if path is not None else default_auth_path()
|
|
33
|
+
|
|
34
|
+
def load(self) -> ClaudeCodeCredentials | None:
|
|
35
|
+
"""Return stored credentials, or `None` when nothing is stored yet."""
|
|
36
|
+
try:
|
|
37
|
+
data = json.loads(self.path.read_text())
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
return None
|
|
40
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
41
|
+
raise UserError(f"Unable to read Claude Code credentials from {str(self.path)!r}.") from exc
|
|
42
|
+
return ClaudeCodeCredentials.from_token_file(data)
|
|
43
|
+
|
|
44
|
+
def save(self, credentials: ClaudeCodeCredentials) -> None:
|
|
45
|
+
"""Persist credentials atomically with restrictive permissions."""
|
|
46
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
tmp = self.path.with_suffix(".tmp")
|
|
48
|
+
tmp.write_text(json.dumps(credentials.to_wire_dict(), indent=2))
|
|
49
|
+
tmp.chmod(0o600)
|
|
50
|
+
os.replace(tmp, self.path)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pydantic-claude-code
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Use your Claude Code subscription from a Pydantic AI Agent, with full tool support
|
|
5
|
+
Project-URL: Repository, https://github.com/mpfaffenberger/pydantic-ai-claude-code
|
|
6
|
+
Project-URL: Homepage, https://pypi.org/project/pydantic-claude-code/
|
|
7
|
+
Author: Michael Pfaffenberger
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: <3.15,>=3.11
|
|
17
|
+
Requires-Dist: pydantic-ai-slim[anthropic]<3,>=2.31.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23.1; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.3.4; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff<0.16,>=0.15; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pydantic-claude-code
|
|
25
|
+
|
|
26
|
+
Use your Claude Code subscription from a plain pydantic-ai `Agent`, with full
|
|
27
|
+
pydantic-ai tool support. No API key, no separate billing: if Claude Code works
|
|
28
|
+
from your terminal, this wheel works too.
|
|
29
|
+
|
|
30
|
+
The repo is `mpfaffenberger/pydantic-ai-claude-code` and the import is
|
|
31
|
+
`pydantic_ai_claude_code`; the PyPI project is `pydantic-claude-code`
|
|
32
|
+
(`pip install pydantic-claude-code`).
|
|
33
|
+
|
|
34
|
+
## Why
|
|
35
|
+
|
|
36
|
+
pydantic-ai gained a Codex OAuth path where `openai-codex:gpt-6-astra` just
|
|
37
|
+
works against a ChatGPT subscription. This wheel brings the same experience to
|
|
38
|
+
Claude: authenticate once, then run pydantic-ai agents against your Claude
|
|
39
|
+
subscription, using `claude-sonnet-4-5`, `claude-opus-5`, or whatever model you
|
|
40
|
+
subscribe to.
|
|
41
|
+
|
|
42
|
+
Two deliberate design choices distinguish this from a fork of pydantic-ai:
|
|
43
|
+
|
|
44
|
+
1. **pydantic-ai owns the loop.** We don't hand the whole agent loop to the
|
|
45
|
+
Claude Code CLI. pydantic-ai's own `Agent` machinery drives the conversation,
|
|
46
|
+
executes your tools, and validates structured output. The wheel is a model
|
|
47
|
+
+ provider, not a second agent fighting for control.
|
|
48
|
+
2. **Object-only resolution.** Instead of a `claude-code:` model-name string
|
|
49
|
+
(which would require patching pydantic-ai's internals), pass the model
|
|
50
|
+
object you build from the provider.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
|
|
57
|
+
from pydantic_ai import Agent
|
|
58
|
+
|
|
59
|
+
from pydantic_ai_claude_code import ClaudeCodeProvider, login
|
|
60
|
+
|
|
61
|
+
async def main() -> None:
|
|
62
|
+
# One-time: opens your browser, mints tokens, stores them
|
|
63
|
+
# (only needs to run again when tokens are revoked).
|
|
64
|
+
await login()
|
|
65
|
+
|
|
66
|
+
provider = ClaudeCodeProvider() # loads the stored tokens
|
|
67
|
+
agent = Agent(provider.model('claude-sonnet-4-5'))
|
|
68
|
+
|
|
69
|
+
result = await agent.run('Say hi in three words.')
|
|
70
|
+
print(result.data)
|
|
71
|
+
|
|
72
|
+
asyncio.run(main())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### With tools
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from pydantic_ai import Agent
|
|
79
|
+
|
|
80
|
+
provider = ClaudeCodeProvider()
|
|
81
|
+
agent = Agent(provider.model('claude-sonnet-4-5'))
|
|
82
|
+
|
|
83
|
+
@agent.tool_plain
|
|
84
|
+
def add(a: int, b: int) -> int:
|
|
85
|
+
"""Add two numbers."""
|
|
86
|
+
return a + b
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Tools defined on the agent are passed to the API as standard Anthropic tool
|
|
90
|
+
definitions, and structured output works the same way as with the built-in
|
|
91
|
+
`anthropic` provider. That's the whole point of the wheel.
|
|
92
|
+
|
|
93
|
+
## How auth works
|
|
94
|
+
|
|
95
|
+
The flow uses the same shared OAuth client the Claude Code CLI uses:
|
|
96
|
+
|
|
97
|
+
- Authorization URL: `https://claude.ai/oauth/authorize`
|
|
98
|
+
- Token URL: `https://platform.claude.com/v1/oauth/token`
|
|
99
|
+
- Scopes: `org:create_api_key user:profile user:inference`
|
|
100
|
+
|
|
101
|
+
Tokens are stored in (overridable via `CLAUDE_CODE_AUTH_FILE`):
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
~/.local/share/pydantic-ai-claude-code/auth.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The file is written with `0o600` permissions and only ever contains what the
|
|
108
|
+
issuer gave us. We never read the CLI's own credential files.
|
|
109
|
+
|
|
110
|
+
Refreshes happen automatically in the background: the auth shim refreshes
|
|
111
|
+
before expiry and retries once on a 401, exactly like the codex provider does.
|
|
112
|
+
|
|
113
|
+
Requests identify as Claude Code: `"You are Claude Code, Anthropic's official
|
|
114
|
+
CLI for Claude."` is prepended to the system context (position 0), the same
|
|
115
|
+
persona the CLI sends. The subscription backend expects it and rate-gates
|
|
116
|
+
premium models without it.
|
|
117
|
+
|
|
118
|
+
## Security and scope
|
|
119
|
+
|
|
120
|
+
This is a plain Anthropic Messages API client authenticated by your Claude
|
|
121
|
+
subscription tokens. It does not run the Claude Code CLI in a subprocess, so it
|
|
122
|
+
does not inherit Claude Code's sandboxing, permission prompts, or hooks. Treat
|
|
123
|
+
it like any code-executing agent: only give it tools you trust.
|
|
124
|
+
|
|
125
|
+
Projects using this are responsible for following Anthropic's rules for using
|
|
126
|
+
Claude Code credentials in their own products.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv sync --extra dev # or: source .venv/bin/activate && pip install -e ".[dev]"
|
|
132
|
+
ruff check src tests
|
|
133
|
+
pytest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Prior art
|
|
137
|
+
|
|
138
|
+
The OAuth mechanics (shared client id, PKCE, token storage and refresh) are
|
|
139
|
+
lifted from the `claude_code_oauth` plugin in
|
|
140
|
+
[`code_puppy_core_plugins`](https://github.com/mpfaffenberger/code_puppy_core_plugins),
|
|
141
|
+
cleaned up and reshaped around the provider pattern in pydantic-ai.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pydantic_ai_claude_code/__init__.py,sha256=oEoYiQ_uyGv4GTdwEgyO2oNAoeA12e7sakHJXIpwAA4,832
|
|
2
|
+
pydantic_ai_claude_code/__main__.py,sha256=bFQey7CDpTvkqXpE_wWgf_kQlv977NFLo0t8SzTdw_I,377
|
|
3
|
+
pydantic_ai_claude_code/auth.py,sha256=iv4LpXTU5MIJDCiDOYKce6BidqkEFANqzvnIH_QI4Gs,3373
|
|
4
|
+
pydantic_ai_claude_code/config.py,sha256=MX2M9zc4Alv4c1KbvdjShsCpqwDNepc9wlrUhKEmpWY,2171
|
|
5
|
+
pydantic_ai_claude_code/credentials.py,sha256=3-PVMx-H7SyXMzJNBITdMgMZjZ4k18KJiaft_7fa8Z0,2939
|
|
6
|
+
pydantic_ai_claude_code/flow.py,sha256=XlLeGtUcdvNJP6bKXWZNE_nh18VOZahuLcly2-fq2BI,8424
|
|
7
|
+
pydantic_ai_claude_code/model.py,sha256=jikHE7bY5YMVVfMqUTUPknQpRVFGOtckGaSWLVPe_Ik,1580
|
|
8
|
+
pydantic_ai_claude_code/provider.py,sha256=kRtyUF_zevW90DIUcL9An1GLp-4KzlIwOmdvfMb8gRY,4012
|
|
9
|
+
pydantic_ai_claude_code/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
pydantic_ai_claude_code/storage.py,sha256=IB271p5VZtjXlch11p3i5LpXwJ5q1kO39hmTOeFxDuQ,1891
|
|
11
|
+
pydantic_claude_code-0.1.0.dist-info/METADATA,sha256=IGx_no9XK0gvSd-w-jzUeaKde4xY0GnlilgTY0JaES8,5049
|
|
12
|
+
pydantic_claude_code-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
13
|
+
pydantic_claude_code-0.1.0.dist-info/licenses/LICENSE,sha256=ANk1gpICbuBKisJscuk8cBJ6SQ6nA3yEi4soa2qSNT8,1078
|
|
14
|
+
pydantic_claude_code-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Pfaffenberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|