@mandujs/core 0.45.1 → 0.46.1

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.
@@ -1,333 +1,319 @@
1
- /**
2
- * Brain v0.2 - LLM Adapters (resolver + factory).
3
- *
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.
21
- */
22
-
23
- export * from "./base";
24
- export * from "./ollama";
25
- export * from "./openai-oauth";
26
- export * from "./anthropic-oauth";
27
- export * from "./oauth-flow";
28
- export * from "./chatgpt-auth";
29
-
30
- import { type LLMAdapter, NoopAdapter } from "./base";
31
- import type { OllamaAdapter} from "./ollama";
32
- import { createOllamaAdapter } from "./ollama";
33
- import {
34
- OpenAIOAuthAdapter,
35
- createOpenAIOAuthAdapter,
36
- type OpenAIOAuthAdapterOptions,
37
- } from "./openai-oauth";
38
- import { ChatGPTAuth } from "./chatgpt-auth";
39
- import {
40
- AnthropicOAuthAdapter,
41
- createAnthropicOAuthAdapter,
42
- type AnthropicOAuthAdapterOptions,
43
- } from "./anthropic-oauth";
44
- import {
45
- getCredentialStore,
46
- type CredentialStore,
47
- type StoredToken,
48
- } from "../credentials";
49
-
50
- /**
51
- * Normalised Brain config shape consumed by the resolver. Mirrors
52
- * `ManduConfig.brain` but with every field required-or-explicitly
53
- * defaulted so downstream code does not repeat the same null checks.
54
- */
55
- export interface BrainAdapterConfig {
56
- adapter: "auto" | "openai" | "anthropic" | "ollama" | "template";
57
- openai?: { model?: string };
58
- anthropic?: { model?: string };
59
- ollama?: { model?: string; baseUrl?: string };
60
- /**
61
- * When true, cloud adapters are disabled entirely. The resolver
62
- * falls to ollama/template regardless of stored tokens.
63
- */
64
- telemetryOptOut?: boolean;
65
- /**
66
- * Project root — required for consent scoping + redaction audit log.
67
- * Defaults to `process.cwd()` when omitted.
68
- */
69
- projectRoot?: string;
70
- /** Credential store override — tests inject an in-memory one. */
71
- credentialStore?: CredentialStore;
72
- /** Override OpenAI-specific adapter options (tests only). */
73
- openaiOptions?: OpenAIOAuthAdapterOptions;
74
- /** Override Anthropic-specific adapter options (tests only). */
75
- anthropicOptions?: AnthropicOAuthAdapterOptions;
76
- /**
77
- * Override the ollama-reachability probe. When omitted we call
78
- * `OllamaAdapter.isServerRunning()` which is the production path.
79
- */
80
- probeOllama?: (adapter: OllamaAdapter) => Promise<boolean>;
81
- /**
82
- * Override the keychain probe used by the auto-resolver. Returns
83
- * the stored token or null. Tests inject a deterministic stub; the
84
- * default consults `credentialStore.load(provider)`.
85
- */
86
- probeToken?: (provider: "openai" | "anthropic") => Promise<StoredToken | null>;
87
- /**
88
- * Override the ChatGPT session-token probe. Default: instantiate
89
- * `new ChatGPTAuth()` and check its on-disk auth.json. Tests inject
90
- * a stub returning `false` so the developer's real `~/.codex/auth.json`
91
- * doesn't leak into unit-test expectations.
92
- */
93
- probeChatGPTAuth?: () => { authenticated: boolean; path: string | null };
94
- }
95
-
96
- /**
97
- * Result of the resolver — returned so callers can log which tier
98
- * won (surfaced in `mandu brain status`).
99
- */
100
- export interface BrainAdapterResolution {
101
- adapter: LLMAdapter;
102
- /** Which tier the resolver picked. */
103
- resolved: "openai" | "anthropic" | "ollama" | "template";
104
- /** Which tier the caller asked for (config value). */
105
- requested: BrainAdapterConfig["adapter"];
106
- /** Human-readable reason — useful for `mandu brain status`. */
107
- reason: string;
108
- }
109
-
110
- /**
111
- * Resolve + construct a Brain adapter.
112
- *
113
- * Callers typically use the convenience re-export
114
- * `createBrainAdapter(config)`. The full `resolveBrainAdapter()`
115
- * surface returns the metadata record so CLI status commands can
116
- * explain which tier won.
117
- */
118
- export async function resolveBrainAdapter(
119
- config: Partial<BrainAdapterConfig> = {},
120
- ): Promise<BrainAdapterResolution> {
121
- const requested = config.adapter ?? "auto";
122
- const telemetryOptOut = config.telemetryOptOut ?? false;
123
-
124
- const store = config.credentialStore ?? getCredentialStore();
125
- const projectRoot = config.projectRoot ?? process.cwd();
126
-
127
- const probeToken =
128
- config.probeToken ?? ((provider) => store.load(provider));
129
-
130
- const probeOllama =
131
- config.probeOllama ??
132
- (async (ollama: OllamaAdapter) => ollama.isServerRunning());
133
-
134
- const probeChatGPTAuth =
135
- config.probeChatGPTAuth ??
136
- (() => {
137
- const c = new ChatGPTAuth();
138
- return { authenticated: c.isAuthenticated(), path: c.locateAuthFile() };
139
- });
140
-
141
- // Explicit template — skip every other check.
142
- if (requested === "template") {
143
- return {
144
- adapter: new NoopAdapter(),
145
- resolved: "template",
146
- requested,
147
- reason: "Explicit adapter: 'template' in config",
148
- };
149
- }
150
-
151
- // Explicit openai — only honored when telemetry is allowed AND a
152
- // token exists. Falls back to template otherwise so Core does not
153
- // explode.
154
- if (requested === "openai") {
155
- if (telemetryOptOut) {
156
- return {
157
- adapter: new NoopAdapter(),
158
- resolved: "template",
159
- requested,
160
- reason:
161
- "adapter: 'openai' requested but telemetryOptOut=true — forcing template",
162
- };
163
- }
164
- // Primary: ChatGPT session token (written by `@openai/codex login`).
165
- const cg = probeChatGPTAuth();
166
- const hasChatGPT = cg.authenticated;
167
- const token = hasChatGPT ? null : await probeToken("openai");
168
- if (!hasChatGPT && !token) {
169
- return {
170
- adapter: new NoopAdapter(),
171
- resolved: "template",
172
- requested,
173
- reason:
174
- "adapter: 'openai' requested but no token found — run `mandu brain login --provider=openai`",
175
- };
176
- }
177
- return {
178
- adapter: createOpenAIOAuthAdapter({
179
- ...(config.openaiOptions ?? {}),
180
- model: config.openai?.model ?? config.openaiOptions?.model,
181
- credentialStore: store,
182
- projectRoot,
183
- }),
184
- resolved: "openai",
185
- requested,
186
- reason: hasChatGPT
187
- ? "Explicit adapter: 'openai' + ChatGPT session token present"
188
- : "Explicit adapter: 'openai' + keychain token present",
189
- };
190
- }
191
-
192
- if (requested === "anthropic") {
193
- if (telemetryOptOut) {
194
- return {
195
- adapter: new NoopAdapter(),
196
- resolved: "template",
197
- requested,
198
- reason:
199
- "adapter: 'anthropic' requested but telemetryOptOut=true — forcing template",
200
- };
201
- }
202
- const token = await probeToken("anthropic");
203
- if (!token) {
204
- return {
205
- adapter: new NoopAdapter(),
206
- resolved: "template",
207
- requested,
208
- reason:
209
- "adapter: 'anthropic' requested but no token in keychain — run `mandu brain login --provider=anthropic`",
210
- };
211
- }
212
- return {
213
- adapter: createAnthropicOAuthAdapter({
214
- ...(config.anthropicOptions ?? {}),
215
- model: config.anthropic?.model ?? config.anthropicOptions?.model,
216
- credentialStore: store,
217
- projectRoot,
218
- }),
219
- resolved: "anthropic",
220
- requested,
221
- reason: "Explicit adapter: 'anthropic' + token present",
222
- };
223
- }
224
-
225
- if (requested === "ollama") {
226
- const ollama = createOllamaAdapter({
227
- model: config.ollama?.model,
228
- baseUrl: config.ollama?.baseUrl,
229
- });
230
- return {
231
- adapter: ollama,
232
- resolved: "ollama",
233
- requested,
234
- reason: "Explicit adapter: 'ollama'",
235
- };
236
- }
237
-
238
- // Auto — try cloud providers first (when allowed), then ollama,
239
- // then template.
240
- if (!telemetryOptOut) {
241
- // Primary: ChatGPT session token (managed by `@openai/codex`).
242
- const cg2 = probeChatGPTAuth();
243
- if (cg2.authenticated) {
244
- return {
245
- adapter: createOpenAIOAuthAdapter({
246
- ...(config.openaiOptions ?? {}),
247
- model: config.openai?.model ?? config.openaiOptions?.model,
248
- credentialStore: store,
249
- projectRoot,
250
- }),
251
- resolved: "openai",
252
- requested,
253
- reason: `auto: ChatGPT session token at ${cg2.path ?? "(unknown)"}`,
254
- };
255
- }
256
- const openaiToken = await probeToken("openai");
257
- if (openaiToken) {
258
- return {
259
- adapter: createOpenAIOAuthAdapter({
260
- ...(config.openaiOptions ?? {}),
261
- model: config.openai?.model ?? config.openaiOptions?.model,
262
- credentialStore: store,
263
- projectRoot,
264
- }),
265
- resolved: "openai",
266
- requested,
267
- reason: "auto: OpenAI token found in keychain",
268
- };
269
- }
270
- const anthropicToken = await probeToken("anthropic");
271
- if (anthropicToken) {
272
- return {
273
- adapter: createAnthropicOAuthAdapter({
274
- ...(config.anthropicOptions ?? {}),
275
- model: config.anthropic?.model ?? config.anthropicOptions?.model,
276
- credentialStore: store,
277
- projectRoot,
278
- }),
279
- resolved: "anthropic",
280
- requested,
281
- reason: "auto: Anthropic token found in keychain",
282
- };
283
- }
284
- }
285
-
286
- const ollama = createOllamaAdapter({
287
- model: config.ollama?.model,
288
- baseUrl: config.ollama?.baseUrl,
289
- });
290
- const ollamaAlive = await probeOllama(ollama).catch(() => false);
291
- if (ollamaAlive) {
292
- return {
293
- adapter: ollama,
294
- resolved: "ollama",
295
- requested,
296
- reason: telemetryOptOut
297
- ? "auto: telemetryOptOut=true, ollama daemon reachable"
298
- : "auto: no cloud token, ollama daemon reachable",
299
- };
300
- }
301
-
302
- return {
303
- adapter: new NoopAdapter(),
304
- resolved: "template",
305
- requested,
306
- reason: telemetryOptOut
307
- ? "auto: telemetryOptOut=true and no local LLM — using template"
308
- : "auto: no cloud token, no ollama daemon — using template",
309
- };
310
- }
311
-
312
- /**
313
- * Convenience factory — returns just the adapter. Use
314
- * `resolveBrainAdapter()` when you also need the resolution metadata
315
- * (e.g. for `mandu brain status`).
316
- */
317
- export async function createBrainAdapter(
318
- config: Partial<BrainAdapterConfig> = {},
319
- ): Promise<LLMAdapter> {
320
- const res = await resolveBrainAdapter(config);
321
- return res.adapter;
322
- }
323
-
324
- /**
325
- * Runtime guard — is the adapter a cloud connector? Used by CLI
326
- * status to flag "may transmit data" lines.
327
- */
328
- export function isCloudAdapter(adapter: LLMAdapter): boolean {
329
- return (
330
- adapter instanceof OpenAIOAuthAdapter ||
331
- adapter instanceof AnthropicOAuthAdapter
332
- );
333
- }
1
+ /**
2
+ * Brain LLM Adapters (resolver + factory).
3
+ *
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 (or ChatGPT session) present
9
+ * 2. anthropic-oauth — token present in the keychain
10
+ * 3. template final fallback (returns NoopAdapter; Brain
11
+ * gracefully falls back to template analysis)
12
+ *
13
+ * The local-LLM (Ollama) tier was removed: Mandu standardised on
14
+ * cloud OAuth providers so every dev has the same baseline quality
15
+ * without managing a local daemon. CLI surfaces that *want* a brain
16
+ * (`mandu brain doctor`, `mandu deploy:plan --use-brain`) detect the
17
+ * `template` fallback and prompt the user to run
18
+ * `mandu brain login --provider=openai` instead of degrading silently.
19
+ *
20
+ * `telemetryOptOut: true` disables every cloud tier — the resolver
21
+ * skips straight to template regardless of stored tokens.
22
+ *
23
+ * Explicit `adapter: "openai"` / `"anthropic"` / `"template"` pins
24
+ * the choice; the resolver still degrades to the NoopAdapter when
25
+ * the chosen provider is unreachable, so the caller never explodes
26
+ * on a missing dependency.
27
+ */
28
+
29
+ export * from "./base";
30
+ export * from "./openai-oauth";
31
+ export * from "./anthropic-oauth";
32
+ export * from "./oauth-flow";
33
+ export * from "./chatgpt-auth";
34
+
35
+ import { type LLMAdapter, NoopAdapter } from "./base";
36
+ import {
37
+ OpenAIOAuthAdapter,
38
+ createOpenAIOAuthAdapter,
39
+ type OpenAIOAuthAdapterOptions,
40
+ } from "./openai-oauth";
41
+ import { ChatGPTAuth } from "./chatgpt-auth";
42
+ import {
43
+ AnthropicOAuthAdapter,
44
+ createAnthropicOAuthAdapter,
45
+ type AnthropicOAuthAdapterOptions,
46
+ } from "./anthropic-oauth";
47
+ import {
48
+ getCredentialStore,
49
+ type CredentialStore,
50
+ type StoredToken,
51
+ } from "../credentials";
52
+
53
+ /**
54
+ * Normalised Brain config shape consumed by the resolver. Mirrors
55
+ * `ManduConfig.brain` but with every field required-or-explicitly
56
+ * defaulted so downstream code does not repeat the same null checks.
57
+ */
58
+ export interface BrainAdapterConfig {
59
+ adapter: "auto" | "openai" | "anthropic" | "template";
60
+ openai?: { model?: string };
61
+ anthropic?: { model?: string };
62
+ /**
63
+ * When true, cloud adapters are disabled entirely. The resolver
64
+ * falls to template regardless of stored tokens.
65
+ */
66
+ telemetryOptOut?: boolean;
67
+ /**
68
+ * Project root — required for consent scoping + redaction audit log.
69
+ * Defaults to `process.cwd()` when omitted.
70
+ */
71
+ projectRoot?: string;
72
+ /** Credential store override tests inject an in-memory one. */
73
+ credentialStore?: CredentialStore;
74
+ /** Override OpenAI-specific adapter options (tests only). */
75
+ openaiOptions?: OpenAIOAuthAdapterOptions;
76
+ /** Override Anthropic-specific adapter options (tests only). */
77
+ anthropicOptions?: AnthropicOAuthAdapterOptions;
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
+ * Override the ChatGPT session-token probe. Default: instantiate
86
+ * `new ChatGPTAuth()` and check its on-disk auth.json. Tests inject
87
+ * a stub returning `false` so the developer's real `~/.codex/auth.json`
88
+ * doesn't leak into unit-test expectations.
89
+ */
90
+ probeChatGPTAuth?: () => { authenticated: boolean; path: string | null };
91
+ }
92
+
93
+ /**
94
+ * Result of the resolver — returned so callers can log which tier
95
+ * won (surfaced in `mandu brain status`) and detect the
96
+ * "needs-login" state.
97
+ */
98
+ export interface BrainAdapterResolution {
99
+ adapter: LLMAdapter;
100
+ /** Which tier the resolver picked. */
101
+ resolved: "openai" | "anthropic" | "template";
102
+ /** Which tier the caller asked for (config value). */
103
+ requested: BrainAdapterConfig["adapter"];
104
+ /** Human-readable reason useful for `mandu brain status`. */
105
+ reason: string;
106
+ /**
107
+ * True when the resolver fell back to template ONLY because the
108
+ * user has no cloud token. Interactive CLIs should prompt
109
+ * `mandu brain login --provider=openai` instead of using the noop
110
+ * adapter. False when the user explicitly opted out (`telemetryOptOut`)
111
+ * or asked for `template` directly.
112
+ */
113
+ needsLogin: boolean;
114
+ }
115
+
116
+ /**
117
+ * Resolve + construct a Brain adapter.
118
+ *
119
+ * Callers typically use the convenience re-export
120
+ * `createBrainAdapter(config)`. The full `resolveBrainAdapter()`
121
+ * surface returns the metadata record so CLI status commands can
122
+ * explain which tier won.
123
+ */
124
+ export async function resolveBrainAdapter(
125
+ config: Partial<BrainAdapterConfig> = {},
126
+ ): Promise<BrainAdapterResolution> {
127
+ const requested = config.adapter ?? "auto";
128
+ const telemetryOptOut = config.telemetryOptOut ?? false;
129
+
130
+ const store = config.credentialStore ?? getCredentialStore();
131
+ const projectRoot = config.projectRoot ?? process.cwd();
132
+
133
+ const probeToken =
134
+ config.probeToken ?? ((provider) => store.load(provider));
135
+
136
+ const probeChatGPTAuth =
137
+ config.probeChatGPTAuth ??
138
+ (() => {
139
+ const c = new ChatGPTAuth();
140
+ return { authenticated: c.isAuthenticated(), path: c.locateAuthFile() };
141
+ });
142
+
143
+ // Explicit template — skip every other check.
144
+ if (requested === "template") {
145
+ return {
146
+ adapter: new NoopAdapter(),
147
+ resolved: "template",
148
+ requested,
149
+ reason: "Explicit adapter: 'template' in config",
150
+ needsLogin: false,
151
+ };
152
+ }
153
+
154
+ // Explicit openai — only honored when telemetry is allowed AND a
155
+ // token exists. Falls back to template otherwise so Core does not
156
+ // explode.
157
+ if (requested === "openai") {
158
+ if (telemetryOptOut) {
159
+ return {
160
+ adapter: new NoopAdapter(),
161
+ resolved: "template",
162
+ requested,
163
+ reason:
164
+ "adapter: 'openai' requested but telemetryOptOut=true forcing template",
165
+ needsLogin: false,
166
+ };
167
+ }
168
+ // Primary: ChatGPT session token (written by `@openai/codex login`).
169
+ const cg = probeChatGPTAuth();
170
+ const hasChatGPT = cg.authenticated;
171
+ const token = hasChatGPT ? null : await probeToken("openai");
172
+ if (!hasChatGPT && !token) {
173
+ return {
174
+ adapter: new NoopAdapter(),
175
+ resolved: "template",
176
+ requested,
177
+ reason:
178
+ "adapter: 'openai' requested but no token found — run `mandu brain login --provider=openai`",
179
+ needsLogin: true,
180
+ };
181
+ }
182
+ return {
183
+ adapter: createOpenAIOAuthAdapter({
184
+ ...(config.openaiOptions ?? {}),
185
+ model: config.openai?.model ?? config.openaiOptions?.model,
186
+ credentialStore: store,
187
+ projectRoot,
188
+ }),
189
+ resolved: "openai",
190
+ requested,
191
+ reason: hasChatGPT
192
+ ? "Explicit adapter: 'openai' + ChatGPT session token present"
193
+ : "Explicit adapter: 'openai' + keychain token present",
194
+ needsLogin: false,
195
+ };
196
+ }
197
+
198
+ if (requested === "anthropic") {
199
+ if (telemetryOptOut) {
200
+ return {
201
+ adapter: new NoopAdapter(),
202
+ resolved: "template",
203
+ requested,
204
+ reason:
205
+ "adapter: 'anthropic' requested but telemetryOptOut=true — forcing template",
206
+ needsLogin: false,
207
+ };
208
+ }
209
+ const token = await probeToken("anthropic");
210
+ if (!token) {
211
+ return {
212
+ adapter: new NoopAdapter(),
213
+ resolved: "template",
214
+ requested,
215
+ reason:
216
+ "adapter: 'anthropic' requested but no token in keychain — run `mandu brain login --provider=anthropic`",
217
+ needsLogin: true,
218
+ };
219
+ }
220
+ return {
221
+ adapter: createAnthropicOAuthAdapter({
222
+ ...(config.anthropicOptions ?? {}),
223
+ model: config.anthropic?.model ?? config.anthropicOptions?.model,
224
+ credentialStore: store,
225
+ projectRoot,
226
+ }),
227
+ resolved: "anthropic",
228
+ requested,
229
+ reason: "Explicit adapter: 'anthropic' + token present",
230
+ needsLogin: false,
231
+ };
232
+ }
233
+
234
+ // Auto try cloud providers first (when allowed), then template.
235
+ if (!telemetryOptOut) {
236
+ // Primary: ChatGPT session token (managed by `@openai/codex`).
237
+ const cg2 = probeChatGPTAuth();
238
+ if (cg2.authenticated) {
239
+ return {
240
+ adapter: createOpenAIOAuthAdapter({
241
+ ...(config.openaiOptions ?? {}),
242
+ model: config.openai?.model ?? config.openaiOptions?.model,
243
+ credentialStore: store,
244
+ projectRoot,
245
+ }),
246
+ resolved: "openai",
247
+ requested,
248
+ reason: `auto: ChatGPT session token at ${cg2.path ?? "(unknown)"}`,
249
+ needsLogin: false,
250
+ };
251
+ }
252
+ const openaiToken = await probeToken("openai");
253
+ if (openaiToken) {
254
+ return {
255
+ adapter: createOpenAIOAuthAdapter({
256
+ ...(config.openaiOptions ?? {}),
257
+ model: config.openai?.model ?? config.openaiOptions?.model,
258
+ credentialStore: store,
259
+ projectRoot,
260
+ }),
261
+ resolved: "openai",
262
+ requested,
263
+ reason: "auto: OpenAI token found in keychain",
264
+ needsLogin: false,
265
+ };
266
+ }
267
+ const anthropicToken = await probeToken("anthropic");
268
+ if (anthropicToken) {
269
+ return {
270
+ adapter: createAnthropicOAuthAdapter({
271
+ ...(config.anthropicOptions ?? {}),
272
+ model: config.anthropic?.model ?? config.anthropicOptions?.model,
273
+ credentialStore: store,
274
+ projectRoot,
275
+ }),
276
+ resolved: "anthropic",
277
+ requested,
278
+ reason: "auto: Anthropic token found in keychain",
279
+ needsLogin: false,
280
+ };
281
+ }
282
+ }
283
+
284
+ // Final fallback. `needsLogin` distinguishes "user opted out" from
285
+ // "user has no token" so interactive CLIs can prompt login only in
286
+ // the latter case.
287
+ return {
288
+ adapter: new NoopAdapter(),
289
+ resolved: "template",
290
+ requested,
291
+ reason: telemetryOptOut
292
+ ? "auto: telemetryOptOut=true — using template"
293
+ : "auto: no cloud token — run `mandu brain login --provider=openai`",
294
+ needsLogin: !telemetryOptOut,
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Convenience factory — returns just the adapter. Use
300
+ * `resolveBrainAdapter()` when you also need the resolution metadata
301
+ * (e.g. for `mandu brain status`).
302
+ */
303
+ export async function createBrainAdapter(
304
+ config: Partial<BrainAdapterConfig> = {},
305
+ ): Promise<LLMAdapter> {
306
+ const res = await resolveBrainAdapter(config);
307
+ return res.adapter;
308
+ }
309
+
310
+ /**
311
+ * Runtime guard — is the adapter a cloud connector? Used by CLI
312
+ * status to flag "may transmit data" lines.
313
+ */
314
+ export function isCloudAdapter(adapter: LLMAdapter): boolean {
315
+ return (
316
+ adapter instanceof OpenAIOAuthAdapter ||
317
+ adapter instanceof AnthropicOAuthAdapter
318
+ );
319
+ }