@mandujs/core 0.41.2 → 0.43.0
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/package.json +21 -4
- package/src/auth/__tests__/login.test.ts +420 -419
- package/src/auth/__tests__/reset.test.ts +296 -296
- package/src/brain/adapters/anthropic-oauth.ts +421 -420
- package/src/brain/adapters/index.ts +2 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/adapters/openai-oauth.ts +534 -533
- package/src/brain/brain.ts +2 -1
- package/src/brain/redactor.ts +196 -196
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
- package/src/bundler/__tests__/cold-start.test.ts +504 -504
- package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
- package/src/bundler/__tests__/hdr.test.ts +1 -1
- package/src/bundler/analyzer.ts +958 -958
- package/src/bundler/build.ts +104 -14
- package/src/bundler/dev.ts +125 -0
- package/src/bundler/hmr-types.ts +1 -0
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
- package/src/bundler/plugins/index.ts +14 -0
- package/src/bundler/plugins/react-compiler-lint.ts +253 -0
- package/src/bundler/plugins/react-compiler.ts +162 -0
- package/src/bundler/types.ts +12 -0
- package/src/change/integrity.ts +2 -1
- package/src/client/index.ts +10 -0
- package/src/client/island.ts +38 -11
- package/src/client/router.ts +6 -1
- package/src/config/mandu.ts +57 -0
- package/src/config/validate.ts +42 -0
- package/src/content/collection.ts +844 -809
- package/src/content/content-layer.ts +316 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/digest.ts +133 -133
- package/src/content/generate-types.ts +168 -168
- package/src/content/index.ts +6 -1
- package/src/content/llms-txt.ts +277 -277
- package/src/contract/define.ts +474 -474
- package/src/contract/route-helpers.ts +2 -1
- package/src/contract/zod-utils.ts +158 -155
- package/src/db/index.ts +513 -513
- package/src/desktop/__tests__/smoke.test.ts +100 -100
- package/src/desktop/webview-fallback.ts +583 -583
- package/src/desktop/window.ts +3 -1
- package/src/dev-error-overlay/overlay-client.ts +300 -300
- package/src/devtools/ai/mcp-connector.ts +499 -498
- package/src/devtools/client/components/kitchen-root.tsx +7 -2
- package/src/email/resend.ts +163 -163
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/graph.ts +898 -898
- package/src/guard/index.ts +16 -0
- package/src/guard/statistics.ts +578 -578
- package/src/guard/tsgolint-bridge.ts +512 -0
- package/src/i18n/locale-resolver.ts +214 -214
- package/src/id/__tests__/id.test.ts +120 -120
- package/src/intent/index.ts +321 -321
- package/src/island/index.ts +39 -23
- package/src/kitchen/api/contract-api.ts +15 -8
- package/src/kitchen/kitchen-ui.ts +2137 -2137
- package/src/lockfile/index.ts +3 -2
- package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
- package/src/middleware/secure/index.ts +417 -417
- package/src/observability/event-bus.ts +2 -2
- package/src/observability/metrics.ts +334 -334
- package/src/observability/tracing.ts +694 -694
- package/src/openapi/generator.ts +1 -1
- package/src/perf/user-marks.ts +553 -553
- package/src/plugins/registry.ts +387 -387
- package/src/resource/ddl/diff.ts +392 -392
- package/src/resource/ddl/snapshot.ts +448 -447
- package/src/resource/generator-schema.ts +477 -476
- package/src/resource/parser.ts +4 -2
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.ts +422 -422
- package/src/runtime/fast-refresh-types.ts +126 -128
- package/src/runtime/image-handler.ts +206 -195
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +36 -19
- package/src/runtime/session-key.ts +328 -328
- package/src/scheduler/__tests__/scheduler.test.ts +514 -514
- package/src/seo/resolve/index.ts +353 -353
- package/src/spec/load.ts +1 -1
- package/src/testing/reporter.ts +676 -676
- package/src/testing/server.ts +196 -196
- package/src/testing/snapshot.ts +444 -444
- package/src/utils/__tests__/lru-cache.test.ts +186 -186
- package/src/utils/bun.ts +8 -8
|
@@ -1,420 +1,421 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brain — Anthropic OAuth adapter (Issue #235).
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the OpenAI adapter but targets Anthropic's Messages API
|
|
5
|
-
* (`POST /v1/messages`). Mandu does not own Anthropic API keys — the
|
|
6
|
-
* user's OAuth credentials live in the OS keychain and are forwarded
|
|
7
|
-
* on each request.
|
|
8
|
-
*
|
|
9
|
-
* Why the default model is `claude-haiku-4-5-20251001`: brain-doctor
|
|
10
|
-
* triage prompts are short and latency-sensitive. Haiku is the cheapest
|
|
11
|
-
* model in the family that still produces syntactically valid code
|
|
12
|
-
* suggestions in our exploratory tests. Users who prefer Sonnet /
|
|
13
|
-
* Opus override via `ManduConfig.brain.anthropic.model`.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { promises as fs } from "node:fs";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
|
|
19
|
-
import { BaseLLMAdapter } from "./base";
|
|
20
|
-
import type {
|
|
21
|
-
AdapterConfig,
|
|
22
|
-
AdapterStatus,
|
|
23
|
-
ChatMessage,
|
|
24
|
-
CompletionOptions,
|
|
25
|
-
CompletionResult,
|
|
26
|
-
} from "../types";
|
|
27
|
-
import {
|
|
28
|
-
CredentialStore
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/*
|
|
46
|
-
/*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
export const
|
|
56
|
-
export const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/*
|
|
74
|
-
/*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/*
|
|
92
|
-
/*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
private
|
|
98
|
-
private
|
|
99
|
-
private
|
|
100
|
-
private
|
|
101
|
-
private
|
|
102
|
-
private
|
|
103
|
-
private
|
|
104
|
-
private
|
|
105
|
-
private
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
...
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
let
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
this.refreshInFlight
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
| { ok:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
"
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
.
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
/*
|
|
381
|
-
/*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
.
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Brain — Anthropic OAuth adapter (Issue #235).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the OpenAI adapter but targets Anthropic's Messages API
|
|
5
|
+
* (`POST /v1/messages`). Mandu does not own Anthropic API keys — the
|
|
6
|
+
* user's OAuth credentials live in the OS keychain and are forwarded
|
|
7
|
+
* on each request.
|
|
8
|
+
*
|
|
9
|
+
* Why the default model is `claude-haiku-4-5-20251001`: brain-doctor
|
|
10
|
+
* triage prompts are short and latency-sensitive. Haiku is the cheapest
|
|
11
|
+
* model in the family that still produces syntactically valid code
|
|
12
|
+
* suggestions in our exploratory tests. Users who prefer Sonnet /
|
|
13
|
+
* Opus override via `ManduConfig.brain.anthropic.model`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { promises as fs } from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { BaseLLMAdapter } from "./base";
|
|
20
|
+
import type {
|
|
21
|
+
AdapterConfig,
|
|
22
|
+
AdapterStatus,
|
|
23
|
+
ChatMessage,
|
|
24
|
+
CompletionOptions,
|
|
25
|
+
CompletionResult,
|
|
26
|
+
} from "../types";
|
|
27
|
+
import type {
|
|
28
|
+
CredentialStore} from "../credentials";
|
|
29
|
+
import {
|
|
30
|
+
getCredentialStore,
|
|
31
|
+
type StoredToken,
|
|
32
|
+
} from "../credentials";
|
|
33
|
+
import {
|
|
34
|
+
ensureConsent,
|
|
35
|
+
type ConsentPromptDeps,
|
|
36
|
+
} from "../consent";
|
|
37
|
+
import { redactSecrets } from "../redactor";
|
|
38
|
+
import {
|
|
39
|
+
refreshAccessToken,
|
|
40
|
+
runAuthorizationCodeFlow,
|
|
41
|
+
type HttpClient,
|
|
42
|
+
type OAuthEndpoints,
|
|
43
|
+
} from "./oauth-flow";
|
|
44
|
+
|
|
45
|
+
/* -------------------------------------------------------------------- */
|
|
46
|
+
/* Defaults */
|
|
47
|
+
/* -------------------------------------------------------------------- */
|
|
48
|
+
|
|
49
|
+
export const ANTHROPIC_OAUTH_ENDPOINTS: OAuthEndpoints = {
|
|
50
|
+
authorizationUrl: "https://console.anthropic.com/oauth/authorize",
|
|
51
|
+
tokenUrl: "https://console.anthropic.com/oauth/token",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Public client id registered by Mandu — PKCE secures the exchange. */
|
|
55
|
+
export const ANTHROPIC_OAUTH_CLIENT_ID = "mandu-brain-cli";
|
|
56
|
+
export const ANTHROPIC_OAUTH_SCOPE = "messages:write";
|
|
57
|
+
export const ANTHROPIC_API_BASE = "https://api.anthropic.com/v1";
|
|
58
|
+
/** Must be bumped whenever Anthropic ships a breaking Messages API version. */
|
|
59
|
+
export const ANTHROPIC_API_VERSION = "2023-06-01";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Default model — Haiku 4.5. Fast, cheap, and good enough for the
|
|
63
|
+
* triage prompts brain-doctor issues.
|
|
64
|
+
*/
|
|
65
|
+
export const ANTHROPIC_DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
66
|
+
|
|
67
|
+
export const DEFAULT_ANTHROPIC_CONFIG: AdapterConfig = {
|
|
68
|
+
baseUrl: ANTHROPIC_API_BASE,
|
|
69
|
+
model: ANTHROPIC_DEFAULT_MODEL,
|
|
70
|
+
timeout: 60_000,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/* -------------------------------------------------------------------- */
|
|
74
|
+
/* Options */
|
|
75
|
+
/* -------------------------------------------------------------------- */
|
|
76
|
+
|
|
77
|
+
export interface AnthropicOAuthAdapterOptions extends Partial<AdapterConfig> {
|
|
78
|
+
httpClient?: HttpClient;
|
|
79
|
+
endpoints?: OAuthEndpoints;
|
|
80
|
+
clientId?: string;
|
|
81
|
+
scope?: string;
|
|
82
|
+
credentialStore?: CredentialStore;
|
|
83
|
+
projectRoot?: string;
|
|
84
|
+
skipConsent?: boolean;
|
|
85
|
+
consentDeps?: ConsentPromptDeps;
|
|
86
|
+
/** Anthropic API version header — override only for enterprise proxies. */
|
|
87
|
+
apiVersion?: string;
|
|
88
|
+
strict?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* -------------------------------------------------------------------- */
|
|
92
|
+
/* Adapter */
|
|
93
|
+
/* -------------------------------------------------------------------- */
|
|
94
|
+
|
|
95
|
+
export class AnthropicOAuthAdapter extends BaseLLMAdapter {
|
|
96
|
+
readonly name = "anthropic-oauth";
|
|
97
|
+
private httpClient: HttpClient;
|
|
98
|
+
private endpoints: OAuthEndpoints;
|
|
99
|
+
private clientId: string;
|
|
100
|
+
private scope: string;
|
|
101
|
+
private credentialStore: CredentialStore;
|
|
102
|
+
private projectRoot: string;
|
|
103
|
+
private skipConsent: boolean;
|
|
104
|
+
private consentDeps?: ConsentPromptDeps;
|
|
105
|
+
private apiVersion: string;
|
|
106
|
+
private strict: boolean;
|
|
107
|
+
private refreshInFlight: Promise<StoredToken | null> | null = null;
|
|
108
|
+
|
|
109
|
+
constructor(options: AnthropicOAuthAdapterOptions = {}) {
|
|
110
|
+
super({
|
|
111
|
+
...DEFAULT_ANTHROPIC_CONFIG,
|
|
112
|
+
...options,
|
|
113
|
+
});
|
|
114
|
+
this.httpClient =
|
|
115
|
+
options.httpClient ?? globalThis.fetch.bind(globalThis);
|
|
116
|
+
this.endpoints = options.endpoints ?? ANTHROPIC_OAUTH_ENDPOINTS;
|
|
117
|
+
this.clientId = options.clientId ?? ANTHROPIC_OAUTH_CLIENT_ID;
|
|
118
|
+
this.scope = options.scope ?? ANTHROPIC_OAUTH_SCOPE;
|
|
119
|
+
this.credentialStore = options.credentialStore ?? getCredentialStore();
|
|
120
|
+
this.projectRoot = options.projectRoot ?? process.cwd();
|
|
121
|
+
this.skipConsent = options.skipConsent ?? false;
|
|
122
|
+
this.consentDeps = options.consentDeps;
|
|
123
|
+
this.apiVersion = options.apiVersion ?? ANTHROPIC_API_VERSION;
|
|
124
|
+
this.strict = options.strict ?? false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async checkStatus(): Promise<AdapterStatus> {
|
|
128
|
+
const token = await this.credentialStore.load("anthropic");
|
|
129
|
+
if (!token) {
|
|
130
|
+
return {
|
|
131
|
+
available: false,
|
|
132
|
+
model: null,
|
|
133
|
+
error:
|
|
134
|
+
"No Anthropic OAuth token stored. Run `mandu brain login --provider=anthropic` first.",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
available: true,
|
|
139
|
+
model: this.config.model,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async login(
|
|
144
|
+
opts: {
|
|
145
|
+
onAuthUrl?: (url: string) => void;
|
|
146
|
+
openBrowser?: (url: string) => Promise<void> | void;
|
|
147
|
+
timeoutMs?: number;
|
|
148
|
+
} = {},
|
|
149
|
+
): Promise<StoredToken> {
|
|
150
|
+
const tokenResponse = await runAuthorizationCodeFlow({
|
|
151
|
+
endpoints: this.endpoints,
|
|
152
|
+
client: { clientId: this.clientId, scope: this.scope },
|
|
153
|
+
httpClient: this.httpClient,
|
|
154
|
+
onAuthUrl: opts.onAuthUrl,
|
|
155
|
+
openBrowser: opts.openBrowser,
|
|
156
|
+
timeoutMs: opts.timeoutMs,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const stored: StoredToken = {
|
|
160
|
+
access_token: tokenResponse.access_token,
|
|
161
|
+
refresh_token: tokenResponse.refresh_token,
|
|
162
|
+
expires_at:
|
|
163
|
+
typeof tokenResponse.expires_in === "number"
|
|
164
|
+
? Math.floor(Date.now() / 1000) + tokenResponse.expires_in
|
|
165
|
+
: undefined,
|
|
166
|
+
scope: tokenResponse.scope ?? this.scope,
|
|
167
|
+
default_model: this.config.model,
|
|
168
|
+
provider: "anthropic",
|
|
169
|
+
last_used_at: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
await this.credentialStore.save("anthropic", stored);
|
|
172
|
+
return stored;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async logout(): Promise<void> {
|
|
176
|
+
await this.credentialStore.delete("anthropic");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async complete(
|
|
180
|
+
messages: ChatMessage[],
|
|
181
|
+
options: CompletionOptions = {},
|
|
182
|
+
): Promise<CompletionResult> {
|
|
183
|
+
const token = await this.credentialStore.load("anthropic");
|
|
184
|
+
if (!token) {
|
|
185
|
+
if (this.strict) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
"AnthropicOAuthAdapter.complete() called without a stored token",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return emptyCompletion();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!this.skipConsent) {
|
|
194
|
+
const ok = await ensureConsent(
|
|
195
|
+
{
|
|
196
|
+
projectRoot: this.projectRoot,
|
|
197
|
+
provider: "anthropic",
|
|
198
|
+
model: this.config.model,
|
|
199
|
+
payloadDescription: describeChatPayload(messages),
|
|
200
|
+
},
|
|
201
|
+
this.consentDeps,
|
|
202
|
+
);
|
|
203
|
+
if (!ok) return emptyCompletion();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const redactedMessages: ChatMessage[] = [];
|
|
207
|
+
const audit: string[] = [];
|
|
208
|
+
for (const m of messages) {
|
|
209
|
+
const { redacted, hits } = redactSecrets(m.content);
|
|
210
|
+
redactedMessages.push({ role: m.role, content: redacted });
|
|
211
|
+
for (const hit of hits) {
|
|
212
|
+
audit.push(
|
|
213
|
+
JSON.stringify({
|
|
214
|
+
ts: new Date().toISOString(),
|
|
215
|
+
provider: "anthropic",
|
|
216
|
+
model: this.config.model,
|
|
217
|
+
role: m.role,
|
|
218
|
+
kind: hit.kind,
|
|
219
|
+
sample: hit.sample,
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (audit.length > 0) {
|
|
225
|
+
await appendRedactionLog(this.projectRoot, audit);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let attemptToken = token.access_token;
|
|
229
|
+
let result = await this.callMessagesApi(
|
|
230
|
+
attemptToken,
|
|
231
|
+
redactedMessages,
|
|
232
|
+
options,
|
|
233
|
+
);
|
|
234
|
+
if (result.status === 401 && token.refresh_token) {
|
|
235
|
+
const refreshed = await this.trySilentRefresh(token);
|
|
236
|
+
if (refreshed) {
|
|
237
|
+
attemptToken = refreshed.access_token;
|
|
238
|
+
result = await this.callMessagesApi(
|
|
239
|
+
attemptToken,
|
|
240
|
+
redactedMessages,
|
|
241
|
+
options,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (result.status === 401) {
|
|
246
|
+
await this.credentialStore.delete("anthropic");
|
|
247
|
+
return emptyCompletion();
|
|
248
|
+
}
|
|
249
|
+
if (!result.ok) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Anthropic request failed (${result.status}): ${result.bodySnippet}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
await this.credentialStore.touch("anthropic");
|
|
255
|
+
return result.completion;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async trySilentRefresh(
|
|
259
|
+
existing: StoredToken,
|
|
260
|
+
): Promise<StoredToken | null> {
|
|
261
|
+
if (!existing.refresh_token) return null;
|
|
262
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
263
|
+
this.refreshInFlight = (async () => {
|
|
264
|
+
try {
|
|
265
|
+
const refreshed = await refreshAccessToken({
|
|
266
|
+
endpoints: this.endpoints,
|
|
267
|
+
clientId: this.clientId,
|
|
268
|
+
refreshToken: existing.refresh_token!,
|
|
269
|
+
httpClient: this.httpClient,
|
|
270
|
+
scope: existing.scope ?? this.scope,
|
|
271
|
+
});
|
|
272
|
+
const stored: StoredToken = {
|
|
273
|
+
access_token: refreshed.access_token,
|
|
274
|
+
refresh_token: refreshed.refresh_token ?? existing.refresh_token,
|
|
275
|
+
expires_at:
|
|
276
|
+
typeof refreshed.expires_in === "number"
|
|
277
|
+
? Math.floor(Date.now() / 1000) + refreshed.expires_in
|
|
278
|
+
: undefined,
|
|
279
|
+
scope: refreshed.scope ?? existing.scope,
|
|
280
|
+
default_model: existing.default_model,
|
|
281
|
+
provider: "anthropic",
|
|
282
|
+
last_used_at: new Date().toISOString(),
|
|
283
|
+
};
|
|
284
|
+
await this.credentialStore.save("anthropic", stored);
|
|
285
|
+
return stored;
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
} finally {
|
|
289
|
+
this.refreshInFlight = null;
|
|
290
|
+
}
|
|
291
|
+
})();
|
|
292
|
+
return this.refreshInFlight;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Transform our shared ChatMessage shape into Anthropic's Messages
|
|
297
|
+
* API request body. Anthropic separates the system message from
|
|
298
|
+
* the conversation turns; we concatenate any leading system
|
|
299
|
+
* messages into one `system` string.
|
|
300
|
+
*/
|
|
301
|
+
private async callMessagesApi(
|
|
302
|
+
accessToken: string,
|
|
303
|
+
messages: ChatMessage[],
|
|
304
|
+
options: CompletionOptions,
|
|
305
|
+
): Promise<
|
|
306
|
+
| { ok: true; status: number; completion: CompletionResult }
|
|
307
|
+
| { ok: false; status: number; bodySnippet: string; completion: CompletionResult }
|
|
308
|
+
> {
|
|
309
|
+
const systemParts: string[] = [];
|
|
310
|
+
const conversation: Array<{
|
|
311
|
+
role: "user" | "assistant";
|
|
312
|
+
content: string;
|
|
313
|
+
}> = [];
|
|
314
|
+
for (const m of messages) {
|
|
315
|
+
if (m.role === "system") {
|
|
316
|
+
systemParts.push(m.content);
|
|
317
|
+
} else if (m.role === "user" || m.role === "assistant") {
|
|
318
|
+
conversation.push({ role: m.role, content: m.content });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const body: Record<string, unknown> = {
|
|
323
|
+
model: this.config.model,
|
|
324
|
+
messages: conversation,
|
|
325
|
+
max_tokens: options.maxTokens ?? 2048,
|
|
326
|
+
temperature: options.temperature ?? 0.2,
|
|
327
|
+
};
|
|
328
|
+
if (systemParts.length > 0) body.system = systemParts.join("\n\n");
|
|
329
|
+
if (options.stop && options.stop.length > 0) {
|
|
330
|
+
body.stop_sequences = options.stop;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const res = await this.httpClient(`${this.baseUrl}/messages`, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: {
|
|
336
|
+
authorization: `Bearer ${accessToken}`,
|
|
337
|
+
"content-type": "application/json",
|
|
338
|
+
accept: "application/json",
|
|
339
|
+
"anthropic-version": this.apiVersion,
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify(body),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
const txt = await res.text().catch(() => "");
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
status: res.status,
|
|
349
|
+
bodySnippet: txt.slice(0, 256),
|
|
350
|
+
completion: emptyCompletion(),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const json = (await res.json()) as {
|
|
355
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
356
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
357
|
+
};
|
|
358
|
+
const text =
|
|
359
|
+
json.content
|
|
360
|
+
?.filter((c) => c.type === "text")
|
|
361
|
+
.map((c) => c.text ?? "")
|
|
362
|
+
.join("") ?? "";
|
|
363
|
+
const input = json.usage?.input_tokens ?? 0;
|
|
364
|
+
const output = json.usage?.output_tokens ?? 0;
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
status: res.status,
|
|
368
|
+
completion: {
|
|
369
|
+
content: text,
|
|
370
|
+
usage: {
|
|
371
|
+
promptTokens: input,
|
|
372
|
+
completionTokens: output,
|
|
373
|
+
totalTokens: input + output,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* -------------------------------------------------------------------- */
|
|
381
|
+
/* Helpers */
|
|
382
|
+
/* -------------------------------------------------------------------- */
|
|
383
|
+
|
|
384
|
+
function emptyCompletion(): CompletionResult {
|
|
385
|
+
return {
|
|
386
|
+
content: "",
|
|
387
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function describeChatPayload(messages: ChatMessage[]): string {
|
|
392
|
+
const totalChars = messages.reduce((a, m) => a + m.content.length, 0);
|
|
393
|
+
const roleCounts = new Map<string, number>();
|
|
394
|
+
for (const m of messages) {
|
|
395
|
+
roleCounts.set(m.role, (roleCounts.get(m.role) ?? 0) + 1);
|
|
396
|
+
}
|
|
397
|
+
const roleSummary = [...roleCounts.entries()]
|
|
398
|
+
.map(([r, n]) => `${n} ${r}`)
|
|
399
|
+
.join(", ");
|
|
400
|
+
return `${messages.length} messages (${roleSummary}), ~${totalChars} chars`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function appendRedactionLog(
|
|
404
|
+
projectRoot: string,
|
|
405
|
+
entries: string[],
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
try {
|
|
408
|
+
const dir = path.join(projectRoot, ".mandu");
|
|
409
|
+
await fs.mkdir(dir, { recursive: true });
|
|
410
|
+
const file = path.join(dir, "brain-redactions.jsonl");
|
|
411
|
+
await fs.appendFile(file, `${entries.join("\n")}\n`, { mode: 0o600 });
|
|
412
|
+
} catch {
|
|
413
|
+
/* best-effort */
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function createAnthropicOAuthAdapter(
|
|
418
|
+
options: AnthropicOAuthAdapterOptions = {},
|
|
419
|
+
): AnthropicOAuthAdapter {
|
|
420
|
+
return new AnthropicOAuthAdapter(options);
|
|
421
|
+
}
|