@mandujs/core 0.39.3 → 0.40.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 +1 -1
- package/src/brain/__tests__/redactor.test.ts +94 -0
- package/src/brain/adapters/__tests__/_helpers.ts +64 -0
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -0
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +202 -0
- package/src/brain/adapters/__tests__/resolver.test.ts +122 -0
- package/src/brain/adapters/anthropic-oauth.ts +420 -0
- package/src/brain/adapters/index.ts +290 -2
- package/src/brain/adapters/oauth-flow.ts +439 -0
- package/src/brain/adapters/openai-oauth.ts +459 -0
- package/src/brain/consent.ts +240 -0
- package/src/brain/credentials.ts +396 -0
- package/src/brain/index.ts +39 -1
- package/src/brain/redactor.ts +196 -0
- package/src/config/mandu.ts +39 -0
- package/src/config/validate.ts +49 -0
|
@@ -1,8 +1,296 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Brain v0.
|
|
2
|
+
* Brain v0.2 - LLM Adapters (resolver + factory).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* `createBrainAdapter(config)` picks the right adapter based on
|
|
5
|
+
* declarative config + runtime signals. Resolution order when
|
|
6
|
+
* `adapter: "auto"` (or `brain` config omitted entirely):
|
|
7
|
+
*
|
|
8
|
+
* 1. openai-oauth — token present in the keychain
|
|
9
|
+
* 2. anthropic-oauth — token present in the keychain
|
|
10
|
+
* 3. ollama — daemon reachable at localhost:11434
|
|
11
|
+
* 4. template — always works (returns NoopAdapter; Brain
|
|
12
|
+
* gracefully falls back to template analysis)
|
|
13
|
+
*
|
|
14
|
+
* `telemetryOptOut: true` disables every cloud tier — the resolver
|
|
15
|
+
* skips straight to ollama/template regardless of stored tokens.
|
|
16
|
+
*
|
|
17
|
+
* Explicit `adapter: "openai"` / `"anthropic"` / `"ollama"` /
|
|
18
|
+
* `"template"` pins the choice; the resolver still degrades to the
|
|
19
|
+
* NoopAdapter when the chosen provider is unreachable, so the caller
|
|
20
|
+
* never explodes on a missing dependency.
|
|
5
21
|
*/
|
|
6
22
|
|
|
7
23
|
export * from "./base";
|
|
8
24
|
export * from "./ollama";
|
|
25
|
+
export * from "./openai-oauth";
|
|
26
|
+
export * from "./anthropic-oauth";
|
|
27
|
+
export * from "./oauth-flow";
|
|
28
|
+
|
|
29
|
+
import { type LLMAdapter, NoopAdapter } from "./base";
|
|
30
|
+
import { OllamaAdapter, createOllamaAdapter } from "./ollama";
|
|
31
|
+
import {
|
|
32
|
+
OpenAIOAuthAdapter,
|
|
33
|
+
createOpenAIOAuthAdapter,
|
|
34
|
+
type OpenAIOAuthAdapterOptions,
|
|
35
|
+
} from "./openai-oauth";
|
|
36
|
+
import {
|
|
37
|
+
AnthropicOAuthAdapter,
|
|
38
|
+
createAnthropicOAuthAdapter,
|
|
39
|
+
type AnthropicOAuthAdapterOptions,
|
|
40
|
+
} from "./anthropic-oauth";
|
|
41
|
+
import {
|
|
42
|
+
getCredentialStore,
|
|
43
|
+
type CredentialStore,
|
|
44
|
+
type StoredToken,
|
|
45
|
+
} from "../credentials";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalised Brain config shape consumed by the resolver. Mirrors
|
|
49
|
+
* `ManduConfig.brain` but with every field required-or-explicitly
|
|
50
|
+
* defaulted so downstream code does not repeat the same null checks.
|
|
51
|
+
*/
|
|
52
|
+
export interface BrainAdapterConfig {
|
|
53
|
+
adapter: "auto" | "openai" | "anthropic" | "ollama" | "template";
|
|
54
|
+
openai?: { model?: string };
|
|
55
|
+
anthropic?: { model?: string };
|
|
56
|
+
ollama?: { model?: string; baseUrl?: string };
|
|
57
|
+
/**
|
|
58
|
+
* When true, cloud adapters are disabled entirely. The resolver
|
|
59
|
+
* falls to ollama/template regardless of stored tokens.
|
|
60
|
+
*/
|
|
61
|
+
telemetryOptOut?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Project root — required for consent scoping + redaction audit log.
|
|
64
|
+
* Defaults to `process.cwd()` when omitted.
|
|
65
|
+
*/
|
|
66
|
+
projectRoot?: string;
|
|
67
|
+
/** Credential store override — tests inject an in-memory one. */
|
|
68
|
+
credentialStore?: CredentialStore;
|
|
69
|
+
/** Override OpenAI-specific adapter options (tests only). */
|
|
70
|
+
openaiOptions?: OpenAIOAuthAdapterOptions;
|
|
71
|
+
/** Override Anthropic-specific adapter options (tests only). */
|
|
72
|
+
anthropicOptions?: AnthropicOAuthAdapterOptions;
|
|
73
|
+
/**
|
|
74
|
+
* Override the ollama-reachability probe. When omitted we call
|
|
75
|
+
* `OllamaAdapter.isServerRunning()` which is the production path.
|
|
76
|
+
*/
|
|
77
|
+
probeOllama?: (adapter: OllamaAdapter) => Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Override the keychain probe used by the auto-resolver. Returns
|
|
80
|
+
* the stored token or null. Tests inject a deterministic stub; the
|
|
81
|
+
* default consults `credentialStore.load(provider)`.
|
|
82
|
+
*/
|
|
83
|
+
probeToken?: (provider: "openai" | "anthropic") => Promise<StoredToken | null>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Result of the resolver — returned so callers can log which tier
|
|
88
|
+
* won (surfaced in `mandu brain status`).
|
|
89
|
+
*/
|
|
90
|
+
export interface BrainAdapterResolution {
|
|
91
|
+
adapter: LLMAdapter;
|
|
92
|
+
/** Which tier the resolver picked. */
|
|
93
|
+
resolved: "openai" | "anthropic" | "ollama" | "template";
|
|
94
|
+
/** Which tier the caller asked for (config value). */
|
|
95
|
+
requested: BrainAdapterConfig["adapter"];
|
|
96
|
+
/** Human-readable reason — useful for `mandu brain status`. */
|
|
97
|
+
reason: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve + construct a Brain adapter.
|
|
102
|
+
*
|
|
103
|
+
* Callers typically use the convenience re-export
|
|
104
|
+
* `createBrainAdapter(config)`. The full `resolveBrainAdapter()`
|
|
105
|
+
* surface returns the metadata record so CLI status commands can
|
|
106
|
+
* explain which tier won.
|
|
107
|
+
*/
|
|
108
|
+
export async function resolveBrainAdapter(
|
|
109
|
+
config: Partial<BrainAdapterConfig> = {},
|
|
110
|
+
): Promise<BrainAdapterResolution> {
|
|
111
|
+
const requested = config.adapter ?? "auto";
|
|
112
|
+
const telemetryOptOut = config.telemetryOptOut ?? false;
|
|
113
|
+
|
|
114
|
+
const store = config.credentialStore ?? getCredentialStore();
|
|
115
|
+
const projectRoot = config.projectRoot ?? process.cwd();
|
|
116
|
+
|
|
117
|
+
const probeToken =
|
|
118
|
+
config.probeToken ?? ((provider) => store.load(provider));
|
|
119
|
+
|
|
120
|
+
const probeOllama =
|
|
121
|
+
config.probeOllama ??
|
|
122
|
+
(async (ollama: OllamaAdapter) => ollama.isServerRunning());
|
|
123
|
+
|
|
124
|
+
// Explicit template — skip every other check.
|
|
125
|
+
if (requested === "template") {
|
|
126
|
+
return {
|
|
127
|
+
adapter: new NoopAdapter(),
|
|
128
|
+
resolved: "template",
|
|
129
|
+
requested,
|
|
130
|
+
reason: "Explicit adapter: 'template' in config",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Explicit openai — only honored when telemetry is allowed AND a
|
|
135
|
+
// token exists. Falls back to template otherwise so Core does not
|
|
136
|
+
// explode.
|
|
137
|
+
if (requested === "openai") {
|
|
138
|
+
if (telemetryOptOut) {
|
|
139
|
+
return {
|
|
140
|
+
adapter: new NoopAdapter(),
|
|
141
|
+
resolved: "template",
|
|
142
|
+
requested,
|
|
143
|
+
reason:
|
|
144
|
+
"adapter: 'openai' requested but telemetryOptOut=true — forcing template",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const token = await probeToken("openai");
|
|
148
|
+
if (!token) {
|
|
149
|
+
return {
|
|
150
|
+
adapter: new NoopAdapter(),
|
|
151
|
+
resolved: "template",
|
|
152
|
+
requested,
|
|
153
|
+
reason:
|
|
154
|
+
"adapter: 'openai' requested but no token in keychain — run `mandu brain login --provider=openai`",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
adapter: createOpenAIOAuthAdapter({
|
|
159
|
+
...(config.openaiOptions ?? {}),
|
|
160
|
+
model: config.openai?.model ?? config.openaiOptions?.model,
|
|
161
|
+
credentialStore: store,
|
|
162
|
+
projectRoot,
|
|
163
|
+
}),
|
|
164
|
+
resolved: "openai",
|
|
165
|
+
requested,
|
|
166
|
+
reason: "Explicit adapter: 'openai' + token present",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (requested === "anthropic") {
|
|
171
|
+
if (telemetryOptOut) {
|
|
172
|
+
return {
|
|
173
|
+
adapter: new NoopAdapter(),
|
|
174
|
+
resolved: "template",
|
|
175
|
+
requested,
|
|
176
|
+
reason:
|
|
177
|
+
"adapter: 'anthropic' requested but telemetryOptOut=true — forcing template",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const token = await probeToken("anthropic");
|
|
181
|
+
if (!token) {
|
|
182
|
+
return {
|
|
183
|
+
adapter: new NoopAdapter(),
|
|
184
|
+
resolved: "template",
|
|
185
|
+
requested,
|
|
186
|
+
reason:
|
|
187
|
+
"adapter: 'anthropic' requested but no token in keychain — run `mandu brain login --provider=anthropic`",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
adapter: createAnthropicOAuthAdapter({
|
|
192
|
+
...(config.anthropicOptions ?? {}),
|
|
193
|
+
model: config.anthropic?.model ?? config.anthropicOptions?.model,
|
|
194
|
+
credentialStore: store,
|
|
195
|
+
projectRoot,
|
|
196
|
+
}),
|
|
197
|
+
resolved: "anthropic",
|
|
198
|
+
requested,
|
|
199
|
+
reason: "Explicit adapter: 'anthropic' + token present",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (requested === "ollama") {
|
|
204
|
+
const ollama = createOllamaAdapter({
|
|
205
|
+
model: config.ollama?.model,
|
|
206
|
+
baseUrl: config.ollama?.baseUrl,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
adapter: ollama,
|
|
210
|
+
resolved: "ollama",
|
|
211
|
+
requested,
|
|
212
|
+
reason: "Explicit adapter: 'ollama'",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Auto — try cloud providers first (when allowed), then ollama,
|
|
217
|
+
// then template.
|
|
218
|
+
if (!telemetryOptOut) {
|
|
219
|
+
const openaiToken = await probeToken("openai");
|
|
220
|
+
if (openaiToken) {
|
|
221
|
+
return {
|
|
222
|
+
adapter: createOpenAIOAuthAdapter({
|
|
223
|
+
...(config.openaiOptions ?? {}),
|
|
224
|
+
model: config.openai?.model ?? config.openaiOptions?.model,
|
|
225
|
+
credentialStore: store,
|
|
226
|
+
projectRoot,
|
|
227
|
+
}),
|
|
228
|
+
resolved: "openai",
|
|
229
|
+
requested,
|
|
230
|
+
reason: "auto: OpenAI token found in keychain",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const anthropicToken = await probeToken("anthropic");
|
|
234
|
+
if (anthropicToken) {
|
|
235
|
+
return {
|
|
236
|
+
adapter: createAnthropicOAuthAdapter({
|
|
237
|
+
...(config.anthropicOptions ?? {}),
|
|
238
|
+
model: config.anthropic?.model ?? config.anthropicOptions?.model,
|
|
239
|
+
credentialStore: store,
|
|
240
|
+
projectRoot,
|
|
241
|
+
}),
|
|
242
|
+
resolved: "anthropic",
|
|
243
|
+
requested,
|
|
244
|
+
reason: "auto: Anthropic token found in keychain",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const ollama = createOllamaAdapter({
|
|
250
|
+
model: config.ollama?.model,
|
|
251
|
+
baseUrl: config.ollama?.baseUrl,
|
|
252
|
+
});
|
|
253
|
+
const ollamaAlive = await probeOllama(ollama).catch(() => false);
|
|
254
|
+
if (ollamaAlive) {
|
|
255
|
+
return {
|
|
256
|
+
adapter: ollama,
|
|
257
|
+
resolved: "ollama",
|
|
258
|
+
requested,
|
|
259
|
+
reason: telemetryOptOut
|
|
260
|
+
? "auto: telemetryOptOut=true, ollama daemon reachable"
|
|
261
|
+
: "auto: no cloud token, ollama daemon reachable",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
adapter: new NoopAdapter(),
|
|
267
|
+
resolved: "template",
|
|
268
|
+
requested,
|
|
269
|
+
reason: telemetryOptOut
|
|
270
|
+
? "auto: telemetryOptOut=true and no local LLM — using template"
|
|
271
|
+
: "auto: no cloud token, no ollama daemon — using template",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Convenience factory — returns just the adapter. Use
|
|
277
|
+
* `resolveBrainAdapter()` when you also need the resolution metadata
|
|
278
|
+
* (e.g. for `mandu brain status`).
|
|
279
|
+
*/
|
|
280
|
+
export async function createBrainAdapter(
|
|
281
|
+
config: Partial<BrainAdapterConfig> = {},
|
|
282
|
+
): Promise<LLMAdapter> {
|
|
283
|
+
const res = await resolveBrainAdapter(config);
|
|
284
|
+
return res.adapter;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Runtime guard — is the adapter a cloud connector? Used by CLI
|
|
289
|
+
* status to flag "may transmit data" lines.
|
|
290
|
+
*/
|
|
291
|
+
export function isCloudAdapter(adapter: LLMAdapter): boolean {
|
|
292
|
+
return (
|
|
293
|
+
adapter instanceof OpenAIOAuthAdapter ||
|
|
294
|
+
adapter instanceof AnthropicOAuthAdapter
|
|
295
|
+
);
|
|
296
|
+
}
|