@ai-sdk/harness-claude-code 1.0.110 → 1.0.112
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.
- package/CHANGELOG.md +21 -0
- package/dist/index.js +208 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/claude-code-auth.ts +3 -0
- package/src/claude-code-harness.ts +15 -4
- package/src/claude-code-subscription.ts +258 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-claude-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.112",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.108",
|
|
30
|
+
"@ai-sdk/provider-utils": "5.0.40",
|
|
31
31
|
"ws": "^8.21.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
package/src/claude-code-auth.ts
CHANGED
|
@@ -17,6 +17,7 @@ export const CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
|
|
|
17
17
|
'AI_GATEWAY_API_KEY',
|
|
18
18
|
'ANTHROPIC_API_KEY',
|
|
19
19
|
'ANTHROPIC_AUTH_TOKEN',
|
|
20
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
20
21
|
] as const;
|
|
21
22
|
|
|
22
23
|
export function createClaudeCodeRequestTransformations({
|
|
@@ -154,8 +155,10 @@ function pickAnthropic({
|
|
|
154
155
|
const helperKey = readApiKey();
|
|
155
156
|
const apiKey = processEnv.ANTHROPIC_API_KEY ?? helperKey;
|
|
156
157
|
const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ?? helperKey;
|
|
158
|
+
const oauthToken = processEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
157
159
|
if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
|
|
158
160
|
if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
|
|
161
|
+
if (oauthToken) env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
159
162
|
const baseUrl = processEnv.ANTHROPIC_BASE_URL;
|
|
160
163
|
if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
|
|
161
164
|
return env;
|
|
@@ -59,9 +59,12 @@ import {
|
|
|
59
59
|
CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
|
|
60
60
|
createClaudeCodeRequestTransformations,
|
|
61
61
|
resolveClaudeCodeAuthenticationMode,
|
|
62
|
-
resolveClaudeCodeEnv,
|
|
63
62
|
type ClaudeCodeAuthenticationMode,
|
|
64
63
|
} from './claude-code-auth';
|
|
64
|
+
import {
|
|
65
|
+
createClaudeCodeSubscriptionRequestTransformations,
|
|
66
|
+
resolveClaudeCodeAuthentication,
|
|
67
|
+
} from './claude-code-subscription';
|
|
65
68
|
import {
|
|
66
69
|
outboundMessageSchema,
|
|
67
70
|
type InboundMessage,
|
|
@@ -849,7 +852,9 @@ export function createClaudeCode(
|
|
|
849
852
|
const authenticationMode = resolveClaudeCodeAuthenticationMode(
|
|
850
853
|
settings.auth,
|
|
851
854
|
);
|
|
852
|
-
const resolvedAuthEnvironment =
|
|
855
|
+
const resolvedAuthEnvironment = await resolveClaudeCodeAuthentication({
|
|
856
|
+
auth: settings.auth,
|
|
857
|
+
});
|
|
853
858
|
const claudeEnvironment = {
|
|
854
859
|
...resolvedAuthEnvironment,
|
|
855
860
|
/*
|
|
@@ -888,11 +893,17 @@ export function createClaudeCode(
|
|
|
888
893
|
...claudeEnvironment,
|
|
889
894
|
...sandboxCredentialEnvironment,
|
|
890
895
|
};
|
|
891
|
-
const
|
|
896
|
+
const transformationSources = {
|
|
892
897
|
env: claudeEnvironment,
|
|
893
898
|
sandboxEnv: sandboxClaudeEnvironment,
|
|
894
899
|
auth: authenticationMode,
|
|
895
|
-
}
|
|
900
|
+
};
|
|
901
|
+
const requestTransformations = [
|
|
902
|
+
...createClaudeCodeRequestTransformations(transformationSources),
|
|
903
|
+
...createClaudeCodeSubscriptionRequestTransformations(
|
|
904
|
+
transformationSources,
|
|
905
|
+
),
|
|
906
|
+
];
|
|
896
907
|
if (requestTransformations.length > 0) {
|
|
897
908
|
await sandboxSession.addRequestTransformations(
|
|
898
909
|
requestTransformations,
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import type {
|
|
6
|
+
HarnessV1RequestTransformation,
|
|
7
|
+
HarnessV1RequestTransformationSources,
|
|
8
|
+
} from '@ai-sdk/harness';
|
|
9
|
+
import {
|
|
10
|
+
createCredentialRequestTransformation,
|
|
11
|
+
isAccessTokenExpiringSoon,
|
|
12
|
+
isHarnessAuthenticationEnvironment,
|
|
13
|
+
isMacOS,
|
|
14
|
+
readMacOSKeychainPassword,
|
|
15
|
+
refreshOAuthAccessToken,
|
|
16
|
+
shouldResolveNativeSubscription,
|
|
17
|
+
} from '@ai-sdk/harness/utils';
|
|
18
|
+
import { isRecord, safeParseJSON } from '@ai-sdk/provider-utils';
|
|
19
|
+
import {
|
|
20
|
+
resolveClaudeCodeEnv,
|
|
21
|
+
type ClaudeCodeAuthenticationMode,
|
|
22
|
+
type ClaudeCodeResolvedAuthenticationMode,
|
|
23
|
+
type ResolveClaudeCodeEnvOptions,
|
|
24
|
+
} from './claude-code-auth';
|
|
25
|
+
|
|
26
|
+
const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
27
|
+
const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
28
|
+
|
|
29
|
+
export function createClaudeCodeSubscriptionRequestTransformations({
|
|
30
|
+
env: environment,
|
|
31
|
+
sandboxEnv: sandboxEnvironment,
|
|
32
|
+
auth: authenticationMode,
|
|
33
|
+
}: HarnessV1RequestTransformationSources<ClaudeCodeResolvedAuthenticationMode>): HarnessV1RequestTransformation[] {
|
|
34
|
+
if (
|
|
35
|
+
environment.CLAUDE_CODE_OAUTH_TOKEN == null ||
|
|
36
|
+
sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN == null
|
|
37
|
+
) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
createCredentialRequestTransformation({
|
|
42
|
+
matchUrl:
|
|
43
|
+
authenticationMode === 'ai-gateway'
|
|
44
|
+
? environment.ANTHROPIC_BASE_URL
|
|
45
|
+
: (environment.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'),
|
|
46
|
+
matchHeaders: {
|
|
47
|
+
Authorization: `Bearer ${sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN}`,
|
|
48
|
+
},
|
|
49
|
+
transformHeaders: {
|
|
50
|
+
Authorization: `Bearer ${environment.CLAUDE_CODE_OAUTH_TOKEN}`,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function resolveClaudeCodeAuthentication({
|
|
57
|
+
auth,
|
|
58
|
+
processEnv = process.env,
|
|
59
|
+
options = {},
|
|
60
|
+
readSubscription = readClaudeCodeSubscription,
|
|
61
|
+
}: {
|
|
62
|
+
auth: ClaudeCodeAuthenticationMode | undefined;
|
|
63
|
+
processEnv?: Record<string, string | undefined>;
|
|
64
|
+
options?: ResolveClaudeCodeEnvOptions;
|
|
65
|
+
readSubscription?: () => Promise<Record<string, string> | undefined>;
|
|
66
|
+
}): Promise<Record<string, string>> {
|
|
67
|
+
const environmentWithoutHelper = resolveClaudeCodeEnv(auth, processEnv, {
|
|
68
|
+
readApiKeyHelper: () => undefined,
|
|
69
|
+
});
|
|
70
|
+
if (isHarnessAuthenticationEnvironment(auth)) {
|
|
71
|
+
return environmentWithoutHelper;
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
!shouldResolveNativeSubscription({
|
|
75
|
+
auth,
|
|
76
|
+
env: environmentWithoutHelper,
|
|
77
|
+
hasDirectCredential:
|
|
78
|
+
environmentWithoutHelper.ANTHROPIC_API_KEY != null ||
|
|
79
|
+
environmentWithoutHelper.ANTHROPIC_AUTH_TOKEN != null ||
|
|
80
|
+
environmentWithoutHelper.CLAUDE_CODE_OAUTH_TOKEN != null,
|
|
81
|
+
})
|
|
82
|
+
) {
|
|
83
|
+
return environmentWithoutHelper;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
(await readSubscription()) ??
|
|
88
|
+
resolveClaudeCodeEnv(auth, processEnv, options)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function readClaudeCodeSubscription({
|
|
93
|
+
env = process.env,
|
|
94
|
+
homeDirectory = homedir(),
|
|
95
|
+
platform = process.platform,
|
|
96
|
+
fetch,
|
|
97
|
+
}: {
|
|
98
|
+
env?: Record<string, string | undefined>;
|
|
99
|
+
homeDirectory?: string;
|
|
100
|
+
platform?: NodeJS.Platform;
|
|
101
|
+
fetch?: typeof globalThis.fetch;
|
|
102
|
+
} = {}): Promise<Record<string, string> | undefined> {
|
|
103
|
+
const configDirectory = resolve(
|
|
104
|
+
env.CLAUDE_CONFIG_DIR ?? join(homeDirectory, '.claude'),
|
|
105
|
+
);
|
|
106
|
+
const credentialPath = join(configDirectory, '.credentials.json');
|
|
107
|
+
const stored = await readClaudeCredentialStore({
|
|
108
|
+
credentialPath,
|
|
109
|
+
homeDirectory,
|
|
110
|
+
configDirectory,
|
|
111
|
+
platform,
|
|
112
|
+
});
|
|
113
|
+
if (stored == null) return undefined;
|
|
114
|
+
const oauth = getClaudeOAuthCredential({ value: stored.value });
|
|
115
|
+
if (oauth == null) return undefined;
|
|
116
|
+
const { accessToken, refreshToken, expiresAt } = oauth;
|
|
117
|
+
|
|
118
|
+
let resolvedAccessToken = accessToken;
|
|
119
|
+
if (isAccessTokenExpiringSoon({ expiresAt })) {
|
|
120
|
+
const refreshed = await refreshOAuthAccessToken({
|
|
121
|
+
tokenUrl: CLAUDE_TOKEN_URL,
|
|
122
|
+
clientId: CLAUDE_CLIENT_ID,
|
|
123
|
+
refreshToken,
|
|
124
|
+
requestFormat: 'json',
|
|
125
|
+
...(fetch == null ? {} : { fetch }),
|
|
126
|
+
});
|
|
127
|
+
resolvedAccessToken = refreshed.accessToken;
|
|
128
|
+
await stored.write({
|
|
129
|
+
...stored.value,
|
|
130
|
+
claudeAiOauth: {
|
|
131
|
+
...oauth,
|
|
132
|
+
accessToken: refreshed.accessToken,
|
|
133
|
+
refreshToken: refreshed.refreshToken ?? refreshToken,
|
|
134
|
+
expiresAt: refreshed.expiresAt,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
CLAUDE_CODE_OAUTH_TOKEN: resolvedAccessToken,
|
|
141
|
+
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type ClaudeCredentialFile = {
|
|
146
|
+
readonly claudeAiOauth?: unknown;
|
|
147
|
+
readonly [key: string]: unknown;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type ClaudeOAuthCredential = Record<string, unknown> & {
|
|
151
|
+
readonly accessToken: string;
|
|
152
|
+
readonly refreshToken: string;
|
|
153
|
+
readonly expiresAt: number;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
async function readClaudeCredentialStore({
|
|
157
|
+
credentialPath,
|
|
158
|
+
homeDirectory,
|
|
159
|
+
configDirectory,
|
|
160
|
+
platform,
|
|
161
|
+
}: {
|
|
162
|
+
credentialPath: string;
|
|
163
|
+
homeDirectory: string;
|
|
164
|
+
configDirectory: string;
|
|
165
|
+
platform: NodeJS.Platform;
|
|
166
|
+
}): Promise<
|
|
167
|
+
| {
|
|
168
|
+
value: ClaudeCredentialFile;
|
|
169
|
+
write(value: ClaudeCredentialFile): Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
| undefined
|
|
172
|
+
> {
|
|
173
|
+
const text = await readFile(credentialPath, 'utf8').catch(() => undefined);
|
|
174
|
+
if (text != null) {
|
|
175
|
+
const value = await parseClaudeCredentialFile(text);
|
|
176
|
+
if (value != null && getClaudeOAuthCredential({ value }) != null) {
|
|
177
|
+
return {
|
|
178
|
+
value,
|
|
179
|
+
write: updated =>
|
|
180
|
+
writeClaudeCredentialFile({ credentialPath, value: updated }),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!isMacOS(platform)) return undefined;
|
|
186
|
+
if (configDirectory !== resolve(join(homeDirectory, '.claude'))) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const service = 'Claude Code-credentials';
|
|
190
|
+
const keychainText = await readMacOSKeychainPassword({
|
|
191
|
+
service,
|
|
192
|
+
account: process.env.USER ?? '',
|
|
193
|
+
});
|
|
194
|
+
if (keychainText == null) return undefined;
|
|
195
|
+
const value = await parseClaudeCredentialFile(keychainText);
|
|
196
|
+
if (value == null) return undefined;
|
|
197
|
+
return {
|
|
198
|
+
value,
|
|
199
|
+
write: async updated => {
|
|
200
|
+
execFileSync(
|
|
201
|
+
'/usr/bin/security',
|
|
202
|
+
[
|
|
203
|
+
'add-generic-password',
|
|
204
|
+
'-U',
|
|
205
|
+
'-s',
|
|
206
|
+
service,
|
|
207
|
+
'-a',
|
|
208
|
+
process.env.USER ?? '',
|
|
209
|
+
'-w',
|
|
210
|
+
JSON.stringify(updated),
|
|
211
|
+
],
|
|
212
|
+
{ stdio: 'ignore' },
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getClaudeOAuthCredential({
|
|
219
|
+
value,
|
|
220
|
+
}: {
|
|
221
|
+
value: ClaudeCredentialFile;
|
|
222
|
+
}): ClaudeOAuthCredential | undefined {
|
|
223
|
+
const oauth = value.claudeAiOauth;
|
|
224
|
+
if (
|
|
225
|
+
!isRecord(oauth) ||
|
|
226
|
+
typeof oauth.accessToken !== 'string' ||
|
|
227
|
+
typeof oauth.refreshToken !== 'string' ||
|
|
228
|
+
typeof oauth.expiresAt !== 'number'
|
|
229
|
+
) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
return oauth as ClaudeOAuthCredential;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function parseClaudeCredentialFile(
|
|
236
|
+
text: string,
|
|
237
|
+
): Promise<ClaudeCredentialFile | undefined> {
|
|
238
|
+
const parsed = await safeParseJSON({ text });
|
|
239
|
+
return parsed.success && isRecord(parsed.value)
|
|
240
|
+
? (parsed.value as ClaudeCredentialFile)
|
|
241
|
+
: undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function writeClaudeCredentialFile({
|
|
245
|
+
credentialPath,
|
|
246
|
+
value,
|
|
247
|
+
}: {
|
|
248
|
+
credentialPath: string;
|
|
249
|
+
value: ClaudeCredentialFile;
|
|
250
|
+
}): Promise<void> {
|
|
251
|
+
await mkdir(dirname(credentialPath), { recursive: true });
|
|
252
|
+
const temporaryPath = `${credentialPath}.${process.pid}.tmp`;
|
|
253
|
+
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
254
|
+
mode: 0o600,
|
|
255
|
+
});
|
|
256
|
+
await chmod(temporaryPath, 0o600);
|
|
257
|
+
await rename(temporaryPath, credentialPath);
|
|
258
|
+
}
|