@mandujs/core 0.45.0 → 0.46.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -25,6 +25,7 @@
25
25
  "./content/llms-txt": "./src/content/llms-txt.ts",
26
26
  "./content/schema": "./src/content/schema.ts",
27
27
  "./db": "./src/db/index.ts",
28
+ "./deploy": "./src/deploy/index.ts",
28
29
  "./design": "./src/design/index.ts",
29
30
  "./desktop": "./src/desktop/index.ts",
30
31
  "./desktop/worker": "./src/desktop/worker.ts",
@@ -155,7 +156,6 @@
155
156
  "fast-glob": "^3.3.2",
156
157
  "glob": "^13.0.0",
157
158
  "minimatch": "^10.1.1",
158
- "ollama": "^0.6.3",
159
159
  "zod": "^3.23.8"
160
160
  }
161
161
  }
@@ -4,10 +4,13 @@
4
4
  * Priority order under `adapter: "auto"`:
5
5
  * 1. openai-oauth when token present
6
6
  * 2. anthropic-oauth when token present
7
- * 3. ollama when daemon reachable
8
- * 4. template otherwise
7
+ * 3. template otherwise with `needsLogin: true` so interactive
8
+ * CLIs prompt `mandu brain login --provider=openai` instead of
9
+ * degrading silently.
9
10
  *
10
- * `telemetryOptOut: true` disables every cloud tier.
11
+ * `telemetryOptOut: true` disables every cloud tier and clears
12
+ * `needsLogin` (the user opted out, so prompting login would be
13
+ * incorrect).
11
14
  */
12
15
 
13
16
  import { describe, it, expect } from "bun:test";
@@ -23,7 +26,7 @@ function anthropicToken(): StoredToken {
23
26
  }
24
27
 
25
28
  describe("resolveBrainAdapter — priority order", () => {
26
- it("picks openai first when both cloud tokens + ollama are available", async () => {
29
+ it("picks openai first when both cloud tokens are available", async () => {
27
30
  const store = makeMemoryStore({
28
31
  openai: openaiToken(),
29
32
  anthropic: anthropicToken(),
@@ -31,11 +34,11 @@ describe("resolveBrainAdapter — priority order", () => {
31
34
  const res = await resolveBrainAdapter({
32
35
  adapter: "auto",
33
36
  credentialStore: store,
34
- probeOllama: async () => true,
35
37
  probeChatGPTAuth: () => ({ authenticated: false, path: null }),
36
38
  });
37
39
  expect(res.resolved).toBe("openai");
38
40
  expect(res.adapter.name).toBe("openai-oauth");
41
+ expect(res.needsLogin).toBe(false);
39
42
  });
40
43
 
41
44
  it("falls to anthropic when only anthropic token present", async () => {
@@ -43,35 +46,35 @@ describe("resolveBrainAdapter — priority order", () => {
43
46
  const res = await resolveBrainAdapter({
44
47
  adapter: "auto",
45
48
  credentialStore: store,
46
- probeOllama: async () => true,
47
49
  probeChatGPTAuth: () => ({ authenticated: false, path: null }),
48
50
  });
49
51
  expect(res.resolved).toBe("anthropic");
50
52
  expect(res.adapter.name).toBe("anthropic-oauth");
53
+ expect(res.needsLogin).toBe(false);
51
54
  });
52
55
 
53
- it("falls to ollama when no cloud tokens but daemon is alive", async () => {
56
+ it("falls to template with needsLogin=true when no cloud tokens", async () => {
54
57
  const store = makeMemoryStore();
55
58
  const res = await resolveBrainAdapter({
56
59
  adapter: "auto",
57
60
  credentialStore: store,
58
- probeOllama: async () => true,
59
61
  probeChatGPTAuth: () => ({ authenticated: false, path: null }),
60
62
  });
61
- expect(res.resolved).toBe("ollama");
62
- expect(res.adapter.name).toBe("ollama");
63
+ expect(res.resolved).toBe("template");
64
+ expect(res.adapter.name).toBe("noop");
65
+ expect(res.needsLogin).toBe(true);
66
+ expect(res.reason).toContain("mandu brain login");
63
67
  });
64
68
 
65
- it("falls to template when nothing is reachable", async () => {
69
+ it("picks openai via ChatGPT session token when keychain is empty", async () => {
66
70
  const store = makeMemoryStore();
67
71
  const res = await resolveBrainAdapter({
68
72
  adapter: "auto",
69
73
  credentialStore: store,
70
- probeOllama: async () => false,
71
- probeChatGPTAuth: () => ({ authenticated: false, path: null }),
74
+ probeChatGPTAuth: () => ({ authenticated: true, path: "/tmp/auth.json" }),
72
75
  });
73
- expect(res.resolved).toBe("template");
74
- expect(res.adapter.name).toBe("noop");
76
+ expect(res.resolved).toBe("openai");
77
+ expect(res.needsLogin).toBe(false);
75
78
  });
76
79
  });
77
80
 
@@ -85,25 +88,26 @@ describe("resolveBrainAdapter — telemetryOptOut", () => {
85
88
  adapter: "auto",
86
89
  telemetryOptOut: true,
87
90
  credentialStore: store,
88
- probeOllama: async () => true,
89
91
  });
90
- expect(res.resolved).toBe("ollama");
92
+ expect(res.resolved).toBe("template");
93
+ expect(res.needsLogin).toBe(false);
94
+ expect(res.reason).toContain("telemetryOptOut");
91
95
  });
92
96
 
93
- it("falls to template when telemetryOptOut is true and ollama is down", async () => {
94
- const store = makeMemoryStore({ openai: openaiToken() });
97
+ it("falls to template without prompting login (needsLogin=false)", async () => {
98
+ const store = makeMemoryStore();
95
99
  const res = await resolveBrainAdapter({
96
100
  adapter: "auto",
97
101
  telemetryOptOut: true,
98
102
  credentialStore: store,
99
- probeOllama: async () => false,
100
103
  });
101
104
  expect(res.resolved).toBe("template");
105
+ expect(res.needsLogin).toBe(false);
102
106
  });
103
107
  });
104
108
 
105
109
  describe("resolveBrainAdapter — explicit pins degrade gracefully", () => {
106
- it("explicit 'openai' without a token degrades to template (does not throw)", async () => {
110
+ it("explicit 'openai' without a token degrades to template + needsLogin=true", async () => {
107
111
  const store = makeMemoryStore();
108
112
  const res = await resolveBrainAdapter({
109
113
  adapter: "openai",
@@ -111,6 +115,7 @@ describe("resolveBrainAdapter — explicit pins degrade gracefully", () => {
111
115
  probeChatGPTAuth: () => ({ authenticated: false, path: null }),
112
116
  });
113
117
  expect(res.resolved).toBe("template");
118
+ expect(res.needsLogin).toBe(true);
114
119
  expect(res.reason).toContain("no token");
115
120
  });
116
121
 
@@ -122,6 +127,17 @@ describe("resolveBrainAdapter — explicit pins degrade gracefully", () => {
122
127
  credentialStore: store,
123
128
  });
124
129
  expect(res.resolved).toBe("template");
130
+ expect(res.needsLogin).toBe(false);
125
131
  expect(res.reason).toContain("telemetryOptOut");
126
132
  });
133
+
134
+ it("explicit 'template' is the only no-prompt zero-cost path", async () => {
135
+ const store = makeMemoryStore({ openai: openaiToken() });
136
+ const res = await resolveBrainAdapter({
137
+ adapter: "template",
138
+ credentialStore: store,
139
+ });
140
+ expect(res.resolved).toBe("template");
141
+ expect(res.needsLogin).toBe(false);
142
+ });
127
143
  });
@@ -17,12 +17,13 @@ import type {
17
17
  * Base LLM Adapter Interface
18
18
  *
19
19
  * Implementations:
20
- * - OllamaAdapter: Local sLLM via Ollama
21
- * - (Future) OpenAIAdapter, AnthropicAdapter, etc.
20
+ * - OpenAIOAuthAdapter: OpenAI via ChatGPT session token / OAuth
21
+ * - AnthropicOAuthAdapter: Anthropic via Claude OAuth
22
+ * - NoopAdapter: template fallback (no LLM)
22
23
  */
23
24
  export interface LLMAdapter {
24
25
  /**
25
- * Adapter name (e.g., "ollama", "openai")
26
+ * Adapter name (e.g., "openai", "anthropic", "template")
26
27
  */
27
28
  readonly name: string;
28
29