@clinebot/core 0.0.22 → 0.0.24

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.
Files changed (260) hide show
  1. package/dist/ClineCore.d.ts +110 -0
  2. package/dist/ClineCore.d.ts.map +1 -0
  3. package/dist/account/cline-account-service.d.ts +2 -1
  4. package/dist/account/cline-account-service.d.ts.map +1 -1
  5. package/dist/account/index.d.ts +1 -1
  6. package/dist/account/index.d.ts.map +1 -1
  7. package/dist/account/rpc.d.ts +3 -1
  8. package/dist/account/rpc.d.ts.map +1 -1
  9. package/dist/account/types.d.ts +3 -0
  10. package/dist/account/types.d.ts.map +1 -1
  11. package/dist/agents/plugin-loader.d.ts.map +1 -1
  12. package/dist/agents/plugin-sandbox-bootstrap.js +17 -17
  13. package/dist/auth/client.d.ts +1 -1
  14. package/dist/auth/client.d.ts.map +1 -1
  15. package/dist/auth/cline.d.ts +1 -1
  16. package/dist/auth/cline.d.ts.map +1 -1
  17. package/dist/auth/codex.d.ts +1 -1
  18. package/dist/auth/codex.d.ts.map +1 -1
  19. package/dist/auth/oca.d.ts +1 -1
  20. package/dist/auth/oca.d.ts.map +1 -1
  21. package/dist/auth/utils.d.ts +2 -2
  22. package/dist/auth/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +50 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +949 -0
  26. package/dist/providers/local-provider-service.d.ts +4 -4
  27. package/dist/providers/local-provider-service.d.ts.map +1 -1
  28. package/dist/runtime/runtime-builder.d.ts +1 -0
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/session-runtime.d.ts +2 -1
  31. package/dist/runtime/session-runtime.d.ts.map +1 -1
  32. package/dist/runtime/team-runtime-registry.d.ts +13 -0
  33. package/dist/runtime/team-runtime-registry.d.ts.map +1 -0
  34. package/dist/session/default-session-manager.d.ts +2 -2
  35. package/dist/session/default-session-manager.d.ts.map +1 -1
  36. package/dist/session/rpc-runtime-ensure.d.ts +53 -0
  37. package/dist/session/rpc-runtime-ensure.d.ts.map +1 -0
  38. package/dist/session/session-config-builder.d.ts +2 -3
  39. package/dist/session/session-config-builder.d.ts.map +1 -1
  40. package/dist/session/session-host.d.ts +8 -18
  41. package/dist/session/session-host.d.ts.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -1
  43. package/dist/session/session-manager.d.ts.map +1 -1
  44. package/dist/session/session-manifest.d.ts +1 -2
  45. package/dist/session/session-manifest.d.ts.map +1 -1
  46. package/dist/session/unified-session-persistence-service.d.ts +2 -2
  47. package/dist/session/unified-session-persistence-service.d.ts.map +1 -1
  48. package/dist/session/utils/helpers.d.ts +1 -1
  49. package/dist/session/utils/helpers.d.ts.map +1 -1
  50. package/dist/session/utils/types.d.ts +1 -1
  51. package/dist/session/utils/types.d.ts.map +1 -1
  52. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  53. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
  54. package/dist/telemetry/distinct-id.d.ts +2 -0
  55. package/dist/telemetry/distinct-id.d.ts.map +1 -0
  56. package/dist/telemetry/{opentelemetry.d.ts → index.d.ts} +1 -1
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/index.js +28 -0
  59. package/dist/tools/constants.d.ts +1 -1
  60. package/dist/tools/constants.d.ts.map +1 -1
  61. package/dist/tools/definitions.d.ts +3 -3
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/executors/apply-patch.d.ts +1 -1
  64. package/dist/tools/executors/apply-patch.d.ts.map +1 -1
  65. package/dist/tools/executors/bash.d.ts +1 -1
  66. package/dist/tools/executors/bash.d.ts.map +1 -1
  67. package/dist/tools/executors/editor.d.ts +1 -1
  68. package/dist/tools/executors/editor.d.ts.map +1 -1
  69. package/dist/tools/executors/file-read.d.ts +1 -1
  70. package/dist/tools/executors/file-read.d.ts.map +1 -1
  71. package/dist/tools/executors/index.d.ts +14 -14
  72. package/dist/tools/executors/index.d.ts.map +1 -1
  73. package/dist/tools/executors/search.d.ts +1 -1
  74. package/dist/tools/executors/search.d.ts.map +1 -1
  75. package/dist/tools/executors/web-fetch.d.ts +1 -1
  76. package/dist/tools/executors/web-fetch.d.ts.map +1 -1
  77. package/dist/tools/helpers.d.ts +1 -1
  78. package/dist/tools/helpers.d.ts.map +1 -1
  79. package/dist/tools/index.d.ts +10 -10
  80. package/dist/tools/index.d.ts.map +1 -1
  81. package/dist/tools/model-tool-routing.d.ts +1 -1
  82. package/dist/tools/model-tool-routing.d.ts.map +1 -1
  83. package/dist/tools/presets.d.ts +1 -1
  84. package/dist/tools/presets.d.ts.map +1 -1
  85. package/dist/types/common.d.ts +17 -8
  86. package/dist/types/common.d.ts.map +1 -1
  87. package/dist/types/config.d.ts +4 -3
  88. package/dist/types/config.d.ts.map +1 -1
  89. package/dist/types/provider-settings.d.ts +1 -1
  90. package/dist/types/provider-settings.d.ts.map +1 -1
  91. package/dist/types.d.ts +5 -2
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/package.json +44 -38
  96. package/src/ClineCore.ts +137 -0
  97. package/src/account/cline-account-service.test.ts +101 -0
  98. package/src/account/cline-account-service.ts +300 -0
  99. package/src/account/featurebase-token.test.ts +175 -0
  100. package/src/account/index.ts +23 -0
  101. package/src/account/rpc.test.ts +63 -0
  102. package/src/account/rpc.ts +185 -0
  103. package/src/account/types.ts +102 -0
  104. package/src/agents/agent-config-loader.test.ts +236 -0
  105. package/src/agents/agent-config-loader.ts +108 -0
  106. package/src/agents/agent-config-parser.ts +198 -0
  107. package/src/agents/hooks-config-loader.test.ts +20 -0
  108. package/src/agents/hooks-config-loader.ts +118 -0
  109. package/src/agents/index.ts +85 -0
  110. package/src/agents/plugin-config-loader.test.ts +140 -0
  111. package/src/agents/plugin-config-loader.ts +97 -0
  112. package/src/agents/plugin-loader.test.ts +210 -0
  113. package/src/agents/plugin-loader.ts +175 -0
  114. package/src/agents/plugin-sandbox-bootstrap.ts +448 -0
  115. package/src/agents/plugin-sandbox.test.ts +296 -0
  116. package/src/agents/plugin-sandbox.ts +341 -0
  117. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  118. package/src/agents/unified-config-file-watcher.ts +483 -0
  119. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  120. package/src/agents/user-instruction-config-loader.ts +438 -0
  121. package/src/auth/client.test.ts +40 -0
  122. package/src/auth/client.ts +25 -0
  123. package/src/auth/cline.test.ts +130 -0
  124. package/src/auth/cline.ts +420 -0
  125. package/src/auth/codex.test.ts +170 -0
  126. package/src/auth/codex.ts +491 -0
  127. package/src/auth/oca.test.ts +215 -0
  128. package/src/auth/oca.ts +573 -0
  129. package/src/auth/server.ts +216 -0
  130. package/src/auth/types.ts +81 -0
  131. package/src/auth/utils.test.ts +128 -0
  132. package/src/auth/utils.ts +247 -0
  133. package/src/chat/chat-schema.ts +82 -0
  134. package/src/index.ts +479 -0
  135. package/src/input/file-indexer.d.ts +11 -0
  136. package/src/input/file-indexer.test.ts +127 -0
  137. package/src/input/file-indexer.ts +327 -0
  138. package/src/input/index.ts +7 -0
  139. package/src/input/mention-enricher.test.ts +85 -0
  140. package/src/input/mention-enricher.ts +122 -0
  141. package/src/mcp/config-loader.test.ts +238 -0
  142. package/src/mcp/config-loader.ts +219 -0
  143. package/src/mcp/index.ts +26 -0
  144. package/src/mcp/manager.test.ts +106 -0
  145. package/src/mcp/manager.ts +262 -0
  146. package/src/mcp/types.ts +88 -0
  147. package/src/providers/local-provider-registry.ts +232 -0
  148. package/src/providers/local-provider-service.test.ts +783 -0
  149. package/src/providers/local-provider-service.ts +471 -0
  150. package/src/runtime/commands.test.ts +98 -0
  151. package/src/runtime/commands.ts +83 -0
  152. package/src/runtime/hook-file-hooks.test.ts +237 -0
  153. package/src/runtime/hook-file-hooks.ts +859 -0
  154. package/src/runtime/index.ts +37 -0
  155. package/src/runtime/rules.ts +34 -0
  156. package/src/runtime/runtime-builder.team-persistence.test.ts +245 -0
  157. package/src/runtime/runtime-builder.test.ts +371 -0
  158. package/src/runtime/runtime-builder.ts +631 -0
  159. package/src/runtime/runtime-parity.test.ts +143 -0
  160. package/src/runtime/sandbox/subprocess-sandbox.ts +231 -0
  161. package/src/runtime/session-runtime.ts +49 -0
  162. package/src/runtime/skills.ts +44 -0
  163. package/src/runtime/team-runtime-registry.ts +46 -0
  164. package/src/runtime/tool-approval.ts +104 -0
  165. package/src/runtime/workflows.test.ts +119 -0
  166. package/src/runtime/workflows.ts +45 -0
  167. package/src/session/default-session-manager.e2e.test.ts +384 -0
  168. package/src/session/default-session-manager.test.ts +1931 -0
  169. package/src/session/default-session-manager.ts +1422 -0
  170. package/src/session/file-session-service.ts +280 -0
  171. package/src/session/index.ts +45 -0
  172. package/src/session/rpc-runtime-ensure.ts +521 -0
  173. package/src/session/rpc-session-service.ts +107 -0
  174. package/src/session/rpc-spawn-lease.test.ts +49 -0
  175. package/src/session/rpc-spawn-lease.ts +122 -0
  176. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  177. package/src/session/runtime-oauth-token-manager.ts +272 -0
  178. package/src/session/session-agent-events.ts +248 -0
  179. package/src/session/session-artifacts.ts +106 -0
  180. package/src/session/session-config-builder.ts +113 -0
  181. package/src/session/session-graph.ts +92 -0
  182. package/src/session/session-host.test.ts +89 -0
  183. package/src/session/session-host.ts +205 -0
  184. package/src/session/session-manager.ts +69 -0
  185. package/src/session/session-manifest.ts +29 -0
  186. package/src/session/session-service.team-persistence.test.ts +48 -0
  187. package/src/session/session-service.ts +673 -0
  188. package/src/session/session-team-coordination.ts +229 -0
  189. package/src/session/session-telemetry.ts +100 -0
  190. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  191. package/src/session/unified-session-persistence-service.test.ts +85 -0
  192. package/src/session/unified-session-persistence-service.ts +994 -0
  193. package/src/session/utils/helpers.ts +139 -0
  194. package/src/session/utils/types.ts +57 -0
  195. package/src/session/utils/usage.ts +32 -0
  196. package/src/session/workspace-manager.ts +98 -0
  197. package/src/session/workspace-manifest.ts +100 -0
  198. package/src/storage/artifact-store.ts +1 -0
  199. package/src/storage/file-team-store.ts +257 -0
  200. package/src/storage/index.ts +11 -0
  201. package/src/storage/provider-settings-legacy-migration.test.ts +424 -0
  202. package/src/storage/provider-settings-legacy-migration.ts +826 -0
  203. package/src/storage/provider-settings-manager.test.ts +191 -0
  204. package/src/storage/provider-settings-manager.ts +152 -0
  205. package/src/storage/session-store.ts +1 -0
  206. package/src/storage/sqlite-session-store.ts +275 -0
  207. package/src/storage/sqlite-team-store.ts +454 -0
  208. package/src/storage/team-store.ts +40 -0
  209. package/src/team/index.ts +4 -0
  210. package/src/team/projections.ts +285 -0
  211. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  212. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  213. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  214. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  215. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  216. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  217. package/src/telemetry/OpenTelemetryProvider.ts +325 -0
  218. package/src/telemetry/TelemetryService.test.ts +134 -0
  219. package/src/telemetry/TelemetryService.ts +141 -0
  220. package/src/telemetry/core-events.ts +400 -0
  221. package/src/telemetry/distinct-id.test.ts +57 -0
  222. package/src/telemetry/distinct-id.ts +58 -0
  223. package/src/telemetry/index.ts +20 -0
  224. package/src/tools/constants.ts +35 -0
  225. package/src/tools/definitions.test.ts +704 -0
  226. package/src/tools/definitions.ts +709 -0
  227. package/src/tools/executors/apply-patch-parser.ts +520 -0
  228. package/src/tools/executors/apply-patch.ts +359 -0
  229. package/src/tools/executors/bash.test.ts +87 -0
  230. package/src/tools/executors/bash.ts +207 -0
  231. package/src/tools/executors/editor.test.ts +35 -0
  232. package/src/tools/executors/editor.ts +219 -0
  233. package/src/tools/executors/file-read.test.ts +49 -0
  234. package/src/tools/executors/file-read.ts +110 -0
  235. package/src/tools/executors/index.ts +87 -0
  236. package/src/tools/executors/search.ts +278 -0
  237. package/src/tools/executors/web-fetch.ts +259 -0
  238. package/src/tools/helpers.ts +130 -0
  239. package/src/tools/index.ts +169 -0
  240. package/src/tools/model-tool-routing.test.ts +86 -0
  241. package/src/tools/model-tool-routing.ts +132 -0
  242. package/src/tools/presets.test.ts +62 -0
  243. package/src/tools/presets.ts +168 -0
  244. package/src/tools/schemas.ts +327 -0
  245. package/src/tools/types.ts +329 -0
  246. package/src/types/common.ts +26 -0
  247. package/src/types/config.ts +86 -0
  248. package/src/types/events.ts +74 -0
  249. package/src/types/index.ts +24 -0
  250. package/src/types/provider-settings.ts +43 -0
  251. package/src/types/sessions.ts +16 -0
  252. package/src/types/storage.ts +64 -0
  253. package/src/types/workspace.ts +7 -0
  254. package/src/types.ts +132 -0
  255. package/src/version.ts +3 -0
  256. package/dist/index.node.d.ts +0 -47
  257. package/dist/index.node.d.ts.map +0 -1
  258. package/dist/index.node.js +0 -948
  259. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  260. package/dist/telemetry/opentelemetry.js +0 -27
@@ -0,0 +1,783 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import * as LlmsModels from "@clinebot/llms/models";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { ProviderSettingsManager } from "../storage/provider-settings-manager";
7
+ import {
8
+ addLocalProvider,
9
+ getLocalProviderModels,
10
+ listLocalProviders,
11
+ normalizeOAuthProvider,
12
+ resolveLocalClineAuthToken,
13
+ saveLocalProviderSettings,
14
+ } from "./local-provider-service";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function makeTempManager(): {
21
+ manager: ProviderSettingsManager;
22
+ cleanup: () => void;
23
+ } {
24
+ const dir = mkdtempSync(
25
+ path.join(os.tmpdir(), "local-provider-service-test-"),
26
+ );
27
+ const manager = new ProviderSettingsManager({
28
+ filePath: path.join(dir, "providers.json"),
29
+ });
30
+ return {
31
+ manager,
32
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
33
+ };
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Shared state reset
38
+ // ---------------------------------------------------------------------------
39
+
40
+ afterEach(() => {
41
+ LlmsModels.resetRegistry();
42
+ vi.restoreAllMocks();
43
+ vi.unstubAllGlobals();
44
+ });
45
+
46
+ // ===========================================================================
47
+ // extractModelIdsFromPayload — tested indirectly via addLocalProvider
48
+ // ===========================================================================
49
+
50
+ describe("addLocalProvider – model ID parsing via modelsSourceUrl", () => {
51
+ let manager: ProviderSettingsManager;
52
+ let cleanup: () => void;
53
+
54
+ beforeEach(() => {
55
+ ({ manager, cleanup } = makeTempManager());
56
+ });
57
+
58
+ afterEach(() => cleanup());
59
+
60
+ it("parses a flat array payload from modelsSourceUrl", async () => {
61
+ const mockFetch = vi.fn().mockResolvedValue({
62
+ ok: true,
63
+ json: async () => ["alpha", "beta", "gamma"],
64
+ });
65
+ vi.stubGlobal("fetch", mockFetch);
66
+
67
+ const result = await addLocalProvider(manager, {
68
+ providerId: "flat-array-provider",
69
+ name: "Flat Array",
70
+ baseUrl: "https://example.invalid/v1",
71
+ models: [],
72
+ modelsSourceUrl: "https://example.invalid/models",
73
+ });
74
+
75
+ expect(result.modelsCount).toBe(3);
76
+ const { models } = await getLocalProviderModels("flat-array-provider");
77
+ expect(models.map((m) => m.id).sort()).toEqual(["alpha", "beta", "gamma"]);
78
+ });
79
+
80
+ it("parses a { data: [...] } payload from modelsSourceUrl", async () => {
81
+ vi.stubGlobal(
82
+ "fetch",
83
+ vi.fn().mockResolvedValue({
84
+ ok: true,
85
+ json: async () => ({ data: [{ id: "model-x" }, { id: "model-y" }] }),
86
+ }),
87
+ );
88
+
89
+ await addLocalProvider(manager, {
90
+ providerId: "data-array-provider",
91
+ name: "Data Array",
92
+ baseUrl: "https://example.invalid/v1",
93
+ models: [],
94
+ modelsSourceUrl: "https://example.invalid/models",
95
+ });
96
+
97
+ const { models } = await getLocalProviderModels("data-array-provider");
98
+ expect(models.map((m) => m.id).sort()).toEqual(["model-x", "model-y"]);
99
+ });
100
+
101
+ it("parses a { models: { id1: {}, id2: {} } } object-keyed payload", async () => {
102
+ vi.stubGlobal(
103
+ "fetch",
104
+ vi.fn().mockResolvedValue({
105
+ ok: true,
106
+ json: async () => ({ models: { "key-a": {}, "key-b": {} } }),
107
+ }),
108
+ );
109
+
110
+ await addLocalProvider(manager, {
111
+ providerId: "obj-keys-provider",
112
+ name: "Object Keys",
113
+ baseUrl: "https://example.invalid/v1",
114
+ models: [],
115
+ modelsSourceUrl: "https://example.invalid/models",
116
+ });
117
+
118
+ const { models } = await getLocalProviderModels("obj-keys-provider");
119
+ expect(models.map((m) => m.id).sort()).toEqual(["key-a", "key-b"]);
120
+ });
121
+
122
+ it("parses a { providers: { <id>: { models: [...] } } } nested payload", async () => {
123
+ vi.stubGlobal(
124
+ "fetch",
125
+ vi.fn().mockResolvedValue({
126
+ ok: true,
127
+ json: async () => ({
128
+ providers: {
129
+ "nested-provider": { models: ["nested-a", "nested-b"] },
130
+ },
131
+ }),
132
+ }),
133
+ );
134
+
135
+ await addLocalProvider(manager, {
136
+ providerId: "nested-provider",
137
+ name: "Nested Provider",
138
+ baseUrl: "https://example.invalid/v1",
139
+ models: [],
140
+ modelsSourceUrl: "https://example.invalid/models",
141
+ });
142
+
143
+ const { models } = await getLocalProviderModels("nested-provider");
144
+ expect(models.map((m) => m.id).sort()).toEqual(["nested-a", "nested-b"]);
145
+ });
146
+
147
+ it("merges manually specified models with fetched models and deduplicates", async () => {
148
+ vi.stubGlobal(
149
+ "fetch",
150
+ vi.fn().mockResolvedValue({
151
+ ok: true,
152
+ json: async () => ["fetched-1", "fetched-2", "manual-1"],
153
+ }),
154
+ );
155
+
156
+ const result = await addLocalProvider(manager, {
157
+ providerId: "merged-provider",
158
+ name: "Merged",
159
+ baseUrl: "https://example.invalid/v1",
160
+ models: ["manual-1", "manual-2"],
161
+ modelsSourceUrl: "https://example.invalid/models",
162
+ });
163
+
164
+ // manual-1, manual-2, fetched-1, fetched-2, manual-1 → Set → 4
165
+ expect(result.modelsCount).toBe(4);
166
+ const { models } = await getLocalProviderModels("merged-provider");
167
+ const ids = models.map((m) => m.id).sort();
168
+ expect(ids).toEqual(["fetched-1", "fetched-2", "manual-1", "manual-2"]);
169
+ });
170
+
171
+ it("throws when fetch returns non-OK status", async () => {
172
+ vi.stubGlobal(
173
+ "fetch",
174
+ vi.fn().mockResolvedValue({
175
+ ok: false,
176
+ status: 404,
177
+ }),
178
+ );
179
+
180
+ await expect(
181
+ addLocalProvider(manager, {
182
+ providerId: "fail-fetch-provider",
183
+ name: "Fail",
184
+ baseUrl: "https://example.invalid/v1",
185
+ models: [],
186
+ modelsSourceUrl: "https://example.invalid/models",
187
+ }),
188
+ ).rejects.toThrow("HTTP 404");
189
+ });
190
+
191
+ it("ignores empty string entries in array payloads", async () => {
192
+ vi.stubGlobal(
193
+ "fetch",
194
+ vi.fn().mockResolvedValue({
195
+ ok: true,
196
+ json: async () => ["good-model", "", " "],
197
+ }),
198
+ );
199
+
200
+ await addLocalProvider(manager, {
201
+ providerId: "empty-strings-provider",
202
+ name: "Empty Strings",
203
+ baseUrl: "https://example.invalid/v1",
204
+ models: [],
205
+ modelsSourceUrl: "https://example.invalid/models",
206
+ });
207
+
208
+ const { models } = await getLocalProviderModels("empty-strings-provider");
209
+ expect(models.map((m) => m.id)).toEqual(["good-model"]);
210
+ });
211
+
212
+ it("ignores non-array, non-object payload shapes and falls back to manual models", async () => {
213
+ vi.stubGlobal(
214
+ "fetch",
215
+ vi.fn().mockResolvedValue({
216
+ ok: true,
217
+ json: async () => "just a string",
218
+ }),
219
+ );
220
+
221
+ await addLocalProvider(manager, {
222
+ providerId: "fallback-provider",
223
+ name: "Fallback",
224
+ baseUrl: "https://example.invalid/v1",
225
+ models: ["fallback-model"],
226
+ modelsSourceUrl: "https://example.invalid/models",
227
+ });
228
+
229
+ const { models } = await getLocalProviderModels("fallback-provider");
230
+ expect(models.map((m) => m.id)).toEqual(["fallback-model"]);
231
+ });
232
+ });
233
+
234
+ // ===========================================================================
235
+ // addLocalProvider – validation guards
236
+ // ===========================================================================
237
+
238
+ describe("addLocalProvider – validation", () => {
239
+ let manager: ProviderSettingsManager;
240
+ let cleanup: () => void;
241
+
242
+ beforeEach(() => {
243
+ ({ manager, cleanup } = makeTempManager());
244
+ });
245
+
246
+ afterEach(() => cleanup());
247
+
248
+ it("throws when providerId is empty", async () => {
249
+ await expect(
250
+ addLocalProvider(manager, {
251
+ providerId: " ",
252
+ name: "X",
253
+ baseUrl: "https://example.invalid",
254
+ models: ["m"],
255
+ }),
256
+ ).rejects.toThrow("providerId is required");
257
+ });
258
+
259
+ it("throws when name is empty", async () => {
260
+ await expect(
261
+ addLocalProvider(manager, {
262
+ providerId: "my-provider",
263
+ name: " ",
264
+ baseUrl: "https://example.invalid",
265
+ models: ["m"],
266
+ }),
267
+ ).rejects.toThrow("name is required");
268
+ });
269
+
270
+ it("throws when baseUrl is empty", async () => {
271
+ await expect(
272
+ addLocalProvider(manager, {
273
+ providerId: "my-provider2",
274
+ name: "My Provider",
275
+ baseUrl: " ",
276
+ models: ["m"],
277
+ }),
278
+ ).rejects.toThrow("baseUrl is required");
279
+ });
280
+
281
+ it("throws when no models are provided and no modelsSourceUrl", async () => {
282
+ await expect(
283
+ addLocalProvider(manager, {
284
+ providerId: "no-models-provider",
285
+ name: "No Models",
286
+ baseUrl: "https://example.invalid",
287
+ models: [],
288
+ }),
289
+ ).rejects.toThrow("at least one model is required");
290
+ });
291
+
292
+ it("throws when provider already exists", async () => {
293
+ // Register a provider first
294
+ await addLocalProvider(manager, {
295
+ providerId: "duplicate-provider",
296
+ name: "First",
297
+ baseUrl: "https://example.invalid/v1",
298
+ models: ["m1"],
299
+ });
300
+
301
+ await expect(
302
+ addLocalProvider(manager, {
303
+ providerId: "duplicate-provider",
304
+ name: "Second",
305
+ baseUrl: "https://example.invalid/v1",
306
+ models: ["m2"],
307
+ }),
308
+ ).rejects.toThrow('"duplicate-provider" already exists');
309
+ });
310
+ });
311
+
312
+ // ===========================================================================
313
+ // addLocalProvider – defaultModelId selection
314
+ // ===========================================================================
315
+
316
+ describe("addLocalProvider – defaultModelId selection", () => {
317
+ let manager: ProviderSettingsManager;
318
+ let cleanup: () => void;
319
+
320
+ beforeEach(() => {
321
+ ({ manager, cleanup } = makeTempManager());
322
+ });
323
+
324
+ afterEach(() => cleanup());
325
+
326
+ it("uses explicit defaultModelId when it is in the model list", async () => {
327
+ await addLocalProvider(manager, {
328
+ providerId: "default-model-provider",
329
+ name: "Test",
330
+ baseUrl: "https://example.invalid/v1",
331
+ models: ["model-a", "model-b", "model-c"],
332
+ defaultModelId: "model-b",
333
+ });
334
+
335
+ const settings = manager.getProviderSettings("default-model-provider");
336
+ expect(settings?.model).toBe("model-b");
337
+ });
338
+
339
+ it("falls back to the first model when defaultModelId is not in the list", async () => {
340
+ await addLocalProvider(manager, {
341
+ providerId: "fallback-default-provider",
342
+ name: "Test",
343
+ baseUrl: "https://example.invalid/v1",
344
+ models: ["model-a", "model-b"],
345
+ defaultModelId: "not-in-list",
346
+ });
347
+
348
+ const settings = manager.getProviderSettings("fallback-default-provider");
349
+ expect(settings?.model).toBe("model-a");
350
+ });
351
+ });
352
+
353
+ // ===========================================================================
354
+ // addLocalProvider – capabilities → vision / reasoning flags
355
+ // ===========================================================================
356
+
357
+ describe("addLocalProvider – capabilities", () => {
358
+ let manager: ProviderSettingsManager;
359
+ let cleanup: () => void;
360
+
361
+ beforeEach(() => {
362
+ ({ manager, cleanup } = makeTempManager());
363
+ });
364
+
365
+ afterEach(() => cleanup());
366
+
367
+ it("sets supportsVision and supportsAttachments when capability is 'vision'", async () => {
368
+ await addLocalProvider(manager, {
369
+ providerId: "vision-provider",
370
+ name: "Vision",
371
+ baseUrl: "https://example.invalid/v1",
372
+ models: ["vis-model"],
373
+ capabilities: ["vision"],
374
+ });
375
+
376
+ const { models } = await getLocalProviderModels("vision-provider");
377
+ expect(models).toHaveLength(1);
378
+ expect(models[0].supportsVision).toBe(true);
379
+ expect(models[0].supportsAttachments).toBe(true);
380
+ });
381
+
382
+ it("sets supportsReasoning when capability is 'reasoning'", async () => {
383
+ await addLocalProvider(manager, {
384
+ providerId: "reasoning-provider",
385
+ name: "Reasoning",
386
+ baseUrl: "https://example.invalid/v1",
387
+ models: ["r-model"],
388
+ capabilities: ["reasoning"],
389
+ });
390
+
391
+ const { models } = await getLocalProviderModels("reasoning-provider");
392
+ expect(models[0].supportsReasoning).toBe(true);
393
+ expect(models[0].supportsVision).toBeFalsy();
394
+ });
395
+
396
+ it("does not set vision/reasoning flags when capabilities are absent", async () => {
397
+ await addLocalProvider(manager, {
398
+ providerId: "plain-provider",
399
+ name: "Plain",
400
+ baseUrl: "https://example.invalid/v1",
401
+ models: ["plain-model"],
402
+ });
403
+
404
+ const { models } = await getLocalProviderModels("plain-provider");
405
+ expect(models[0].supportsVision).toBeFalsy();
406
+ expect(models[0].supportsReasoning).toBeFalsy();
407
+ });
408
+
409
+ it("merges LiteLLM private models into the provider model listing when auth is configured", async () => {
410
+ manager.saveProviderSettings(
411
+ {
412
+ provider: "litellm",
413
+ apiKey: "test-key-catalog",
414
+ baseUrl: "http://localhost:4010",
415
+ model: "gpt-4o",
416
+ },
417
+ { setLastUsed: false },
418
+ );
419
+ vi.stubGlobal(
420
+ "fetch",
421
+ vi.fn().mockResolvedValue({
422
+ ok: true,
423
+ json: async () => ({
424
+ data: [
425
+ {
426
+ model_name: "private-proxy-model",
427
+ litellm_params: { model: "openai/gpt-4o-mini" },
428
+ model_info: {
429
+ supports_vision: true,
430
+ supports_reasoning: true,
431
+ },
432
+ },
433
+ ],
434
+ }),
435
+ }),
436
+ );
437
+
438
+ const { models } = await getLocalProviderModels(
439
+ "litellm",
440
+ manager.getProviderConfig("litellm"),
441
+ );
442
+
443
+ expect(models.map((model) => model.id)).toContain("private-proxy-model");
444
+ expect(models.map((model) => model.id)).toContain("openai/gpt-4o-mini");
445
+ expect(
446
+ models.find((model) => model.id === "private-proxy-model"),
447
+ ).toMatchObject({
448
+ supportsVision: true,
449
+ supportsReasoning: true,
450
+ });
451
+ });
452
+ });
453
+
454
+ // ===========================================================================
455
+ // saveLocalProviderSettings
456
+ // ===========================================================================
457
+
458
+ describe("saveLocalProviderSettings", () => {
459
+ let manager: ProviderSettingsManager;
460
+ let cleanup: () => void;
461
+
462
+ beforeEach(async () => {
463
+ ({ manager, cleanup } = makeTempManager());
464
+ // Seed a provider so there is something to operate on
465
+ await addLocalProvider(manager, {
466
+ providerId: "test-provider",
467
+ name: "Test",
468
+ baseUrl: "https://example.invalid/v1",
469
+ models: ["m1"],
470
+ });
471
+ });
472
+
473
+ afterEach(() => cleanup());
474
+
475
+ it("disabling a provider removes it from settings", () => {
476
+ const result = saveLocalProviderSettings(manager, {
477
+ providerId: "test-provider",
478
+ enabled: false,
479
+ });
480
+
481
+ expect(result.enabled).toBe(false);
482
+ expect(manager.getProviderSettings("test-provider")).toBeUndefined();
483
+ });
484
+
485
+ it("updates apiKey", () => {
486
+ saveLocalProviderSettings(manager, {
487
+ providerId: "test-provider",
488
+ enabled: true,
489
+ apiKey: "new-key",
490
+ });
491
+
492
+ expect(manager.getProviderSettings("test-provider")?.apiKey).toBe(
493
+ "new-key",
494
+ );
495
+ });
496
+
497
+ it("clears apiKey when empty string is provided", () => {
498
+ // First set a key
499
+ saveLocalProviderSettings(manager, {
500
+ providerId: "test-provider",
501
+ enabled: true,
502
+ apiKey: "some-key",
503
+ });
504
+ // Then clear it
505
+ saveLocalProviderSettings(manager, {
506
+ providerId: "test-provider",
507
+ enabled: true,
508
+ apiKey: "",
509
+ });
510
+
511
+ const settings = manager.getProviderSettings("test-provider");
512
+ expect(settings).not.toHaveProperty("apiKey");
513
+ });
514
+
515
+ it("merges auth object rather than replacing it", () => {
516
+ saveLocalProviderSettings(manager, {
517
+ providerId: "test-provider",
518
+ enabled: true,
519
+ auth: { accessToken: "tok1" },
520
+ });
521
+ saveLocalProviderSettings(manager, {
522
+ providerId: "test-provider",
523
+ enabled: true,
524
+ auth: { refreshToken: "ref1" },
525
+ });
526
+
527
+ const settings = manager.getProviderSettings("test-provider") as Record<
528
+ string,
529
+ unknown
530
+ >;
531
+ const auth = settings?.auth as Record<string, unknown>;
532
+ expect(auth?.accessToken).toBe("tok1");
533
+ expect(auth?.refreshToken).toBe("ref1");
534
+ });
535
+
536
+ it("passes through scalar fields like maxTokens and timeout", () => {
537
+ saveLocalProviderSettings(manager, {
538
+ providerId: "test-provider",
539
+ enabled: true,
540
+ maxTokens: 4096,
541
+ timeout: 30_000,
542
+ });
543
+
544
+ const settings = manager.getProviderSettings("test-provider") as Record<
545
+ string,
546
+ unknown
547
+ >;
548
+ expect(settings?.maxTokens).toBe(4096);
549
+ expect(settings?.timeout).toBe(30_000);
550
+ });
551
+
552
+ it("disabling a last-used provider also clears lastUsedProvider", async () => {
553
+ // Set test-provider as last used
554
+ manager.saveProviderSettings(
555
+ { provider: "test-provider", model: "m1" },
556
+ { setLastUsed: true },
557
+ );
558
+ expect(manager.getLastUsedProviderSettings()?.provider).toBe(
559
+ "test-provider",
560
+ );
561
+
562
+ saveLocalProviderSettings(manager, {
563
+ providerId: "test-provider",
564
+ enabled: false,
565
+ });
566
+
567
+ expect(manager.getLastUsedProviderSettings()).toBeUndefined();
568
+ });
569
+ });
570
+
571
+ // ===========================================================================
572
+ // listLocalProviders
573
+ // ===========================================================================
574
+
575
+ describe("listLocalProviders", () => {
576
+ let manager: ProviderSettingsManager;
577
+ let cleanup: () => void;
578
+
579
+ beforeEach(() => {
580
+ ({ manager, cleanup } = makeTempManager());
581
+ });
582
+
583
+ afterEach(() => cleanup());
584
+
585
+ it("includes all registered providers", async () => {
586
+ await addLocalProvider(manager, {
587
+ providerId: "list-provider-a",
588
+ name: "Provider A",
589
+ baseUrl: "https://example.invalid/a",
590
+ models: ["ma1"],
591
+ });
592
+ await addLocalProvider(manager, {
593
+ providerId: "list-provider-b",
594
+ name: "Provider B",
595
+ baseUrl: "https://example.invalid/b",
596
+ models: ["mb1"],
597
+ });
598
+
599
+ const { providers } = await listLocalProviders(manager);
600
+ const ids = providers.map((p) => p.id);
601
+ expect(ids).toContain("list-provider-a");
602
+ expect(ids).toContain("list-provider-b");
603
+ });
604
+
605
+ it("marks enabled providers correctly", async () => {
606
+ await addLocalProvider(manager, {
607
+ providerId: "enabled-check-provider",
608
+ name: "Enabled Check",
609
+ baseUrl: "https://example.invalid/v1",
610
+ models: ["m1"],
611
+ });
612
+
613
+ const { providers } = await listLocalProviders(manager);
614
+ const p = providers.find((x) => x.id === "enabled-check-provider");
615
+ expect(p?.enabled).toBe(true);
616
+ });
617
+
618
+ it("exposes model count", async () => {
619
+ await addLocalProvider(manager, {
620
+ providerId: "count-provider",
621
+ name: "Count",
622
+ baseUrl: "https://example.invalid/v1",
623
+ models: ["x", "y", "z"],
624
+ });
625
+
626
+ const { providers } = await listLocalProviders(manager);
627
+ const p = providers.find((x) => x.id === "count-provider");
628
+ expect(p?.models).toBe(3);
629
+ });
630
+
631
+ it("generates a stable color and letter for each provider", async () => {
632
+ await addLocalProvider(manager, {
633
+ providerId: "color-letter-provider",
634
+ name: "Color Letter",
635
+ baseUrl: "https://example.invalid/v1",
636
+ models: ["m1"],
637
+ });
638
+
639
+ const { providers } = await listLocalProviders(manager);
640
+ const p = providers.find((x) => x.id === "color-letter-provider");
641
+ expect(p?.color).toMatch(/^#[0-9a-f]{6}$/i);
642
+ expect(p?.letter).toBeTruthy();
643
+ });
644
+
645
+ it("includes built-in model lists in the provider catalog path", async () => {
646
+ manager.saveProviderSettings(
647
+ {
648
+ provider: "openai-native",
649
+ apiKey: "test-key",
650
+ baseUrl: "https://api.openai.com/v1",
651
+ model: "gpt-5.3-codex",
652
+ },
653
+ { setLastUsed: false },
654
+ );
655
+
656
+ const { providers } = await listLocalProviders(manager);
657
+ const openai = providers.find(
658
+ (provider) => provider.id === "openai-native",
659
+ );
660
+
661
+ expect(openai?.modelList?.length).toBeGreaterThan(0);
662
+ expect(
663
+ openai?.modelList?.some((model) => model.id === "gpt-5.3-codex"),
664
+ ).toBe(true);
665
+ });
666
+
667
+ it("does not eagerly fetch LiteLLM private models while listing providers", async () => {
668
+ manager.saveProviderSettings(
669
+ {
670
+ provider: "litellm",
671
+ apiKey: "test-key",
672
+ baseUrl: "http://localhost:4000",
673
+ model: "gpt-4o",
674
+ },
675
+ { setLastUsed: false },
676
+ );
677
+ const fetchMock = vi.fn().mockResolvedValue({
678
+ ok: true,
679
+ json: async () => ({
680
+ data: [
681
+ {
682
+ model_name: "team-private-model",
683
+ litellm_params: { model: "team/private-model" },
684
+ model_info: {},
685
+ },
686
+ ],
687
+ }),
688
+ });
689
+ vi.stubGlobal("fetch", fetchMock);
690
+
691
+ const { providers } = await listLocalProviders(manager);
692
+ const litellm = providers.find((provider) => provider.id === "litellm");
693
+
694
+ expect(fetchMock).not.toHaveBeenCalled();
695
+ expect(litellm?.modelList).toEqual([]);
696
+ expect(
697
+ litellm?.modelList?.some((model) => model.id === "team/private-model"),
698
+ ).toBe(false);
699
+ });
700
+ });
701
+
702
+ // ===========================================================================
703
+ // normalizeOAuthProvider
704
+ // ===========================================================================
705
+
706
+ describe("normalizeOAuthProvider", () => {
707
+ it("normalizes 'cline' to 'cline'", () => {
708
+ expect(normalizeOAuthProvider("cline")).toBe("cline");
709
+ expect(normalizeOAuthProvider(" CLINE ")).toBe("cline");
710
+ });
711
+
712
+ it("normalizes 'oca' to 'oca'", () => {
713
+ expect(normalizeOAuthProvider("oca")).toBe("oca");
714
+ expect(normalizeOAuthProvider("OCA")).toBe("oca");
715
+ });
716
+
717
+ it("normalizes 'codex' and 'openai-codex' to 'openai-codex'", () => {
718
+ expect(normalizeOAuthProvider("codex")).toBe("openai-codex");
719
+ expect(normalizeOAuthProvider("openai-codex")).toBe("openai-codex");
720
+ expect(normalizeOAuthProvider("OPENAI-CODEX")).toBe("openai-codex");
721
+ });
722
+
723
+ it("throws for unsupported providers", () => {
724
+ expect(() => normalizeOAuthProvider("anthropic")).toThrow(
725
+ "does not support OAuth login",
726
+ );
727
+ expect(() => normalizeOAuthProvider("")).toThrow();
728
+ });
729
+ });
730
+
731
+ // ===========================================================================
732
+ // resolveLocalClineAuthToken
733
+ // ===========================================================================
734
+
735
+ describe("resolveLocalClineAuthToken", () => {
736
+ it("returns undefined when settings is undefined", () => {
737
+ expect(resolveLocalClineAuthToken(undefined)).toBeUndefined();
738
+ });
739
+
740
+ it("returns accessToken when present", () => {
741
+ expect(
742
+ resolveLocalClineAuthToken({
743
+ provider: "cline" as never,
744
+ auth: { accessToken: "tok123" },
745
+ }),
746
+ ).toBe("tok123");
747
+ });
748
+
749
+ it("falls back to apiKey when accessToken is absent", () => {
750
+ expect(
751
+ resolveLocalClineAuthToken({
752
+ provider: "cline" as never,
753
+ apiKey: "api-key-456",
754
+ }),
755
+ ).toBe("api-key-456");
756
+ });
757
+
758
+ it("prefers accessToken over apiKey", () => {
759
+ expect(
760
+ resolveLocalClineAuthToken({
761
+ provider: "cline" as never,
762
+ apiKey: "api-key",
763
+ auth: { accessToken: "access-token" },
764
+ }),
765
+ ).toBe("access-token");
766
+ });
767
+
768
+ it("returns undefined when both accessToken and apiKey are empty strings", () => {
769
+ expect(
770
+ resolveLocalClineAuthToken({
771
+ provider: "cline" as never,
772
+ apiKey: " ",
773
+ auth: { accessToken: " " },
774
+ }),
775
+ ).toBeUndefined();
776
+ });
777
+
778
+ it("returns undefined when both fields are absent", () => {
779
+ expect(
780
+ resolveLocalClineAuthToken({ provider: "cline" as never }),
781
+ ).toBeUndefined();
782
+ });
783
+ });