ccnew 0.1.10

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 (62) hide show
  1. package/README.md +107 -0
  2. package/build/icon.ico +0 -0
  3. package/build/icon.png +0 -0
  4. package/core/apply.js +152 -0
  5. package/core/backup.js +53 -0
  6. package/core/constants.js +78 -0
  7. package/core/desktop-service.js +403 -0
  8. package/core/desktop-state.js +1021 -0
  9. package/core/index.js +1468 -0
  10. package/core/paths.js +99 -0
  11. package/core/presets.js +171 -0
  12. package/core/probe.js +70 -0
  13. package/core/routing.js +334 -0
  14. package/core/store.js +218 -0
  15. package/core/utils.js +225 -0
  16. package/core/writers/codex.js +102 -0
  17. package/core/writers/index.js +16 -0
  18. package/core/writers/openclaw.js +93 -0
  19. package/core/writers/opencode.js +91 -0
  20. package/desktop/assets/fml-icon.png +0 -0
  21. package/desktop/assets/march-mark.svg +26 -0
  22. package/desktop/main.js +275 -0
  23. package/desktop/preload.cjs +67 -0
  24. package/desktop/preload.js +49 -0
  25. package/desktop/renderer/app.js +327 -0
  26. package/desktop/renderer/index.html +130 -0
  27. package/desktop/renderer/styles.css +490 -0
  28. package/package.json +111 -0
  29. package/scripts/build-web.mjs +95 -0
  30. package/scripts/desktop-dev.mjs +90 -0
  31. package/scripts/desktop-pack-win.mjs +81 -0
  32. package/scripts/postinstall.mjs +49 -0
  33. package/scripts/prepublish-check.mjs +57 -0
  34. package/scripts/serve-site.mjs +51 -0
  35. package/site/app.js +10 -0
  36. package/site/assets/fml-icon.png +0 -0
  37. package/site/assets/march-mark.svg +26 -0
  38. package/site/index.html +337 -0
  39. package/site/styles.css +840 -0
  40. package/src/App.tsx +1557 -0
  41. package/src/components/layout/app-sidebar.tsx +103 -0
  42. package/src/components/layout/top-toolbar.tsx +44 -0
  43. package/src/components/layout/workspace-tabs.tsx +32 -0
  44. package/src/components/providers/inspector-panel.tsx +84 -0
  45. package/src/components/providers/metric-strip.tsx +26 -0
  46. package/src/components/providers/provider-editor.tsx +87 -0
  47. package/src/components/providers/provider-table.tsx +85 -0
  48. package/src/components/ui/logo-mark.tsx +32 -0
  49. package/src/features/mcp/mcp-view.tsx +45 -0
  50. package/src/features/prompts/prompts-view.tsx +40 -0
  51. package/src/features/providers/providers-view.tsx +40 -0
  52. package/src/features/providers/types.ts +26 -0
  53. package/src/features/skills/skills-view.tsx +44 -0
  54. package/src/hooks/use-control-workspace.ts +235 -0
  55. package/src/index.css +22 -0
  56. package/src/lib/client.ts +726 -0
  57. package/src/lib/query-client.ts +3 -0
  58. package/src/lib/workspace-sections.ts +34 -0
  59. package/src/main.tsx +14 -0
  60. package/src/types.ts +137 -0
  61. package/src/vite-env.d.ts +64 -0
  62. package/src-tauri/README.md +11 -0
package/core/store.js ADDED
@@ -0,0 +1,218 @@
1
+ import { DEFAULT_PRIMARY_MODEL } from "./constants.js";
2
+ import { getMarchDir, getMarchStorePath } from "./paths.js";
3
+ import {
4
+ ensureDir,
5
+ findByName,
6
+ findByNameOrId,
7
+ generateId,
8
+ readJson,
9
+ writeJson
10
+ } from "./utils.js";
11
+
12
+ function createEmptyStore() {
13
+ return {
14
+ providers: [],
15
+ currentProviderId: null
16
+ };
17
+ }
18
+
19
+ export function loadPlatformStore(platform) {
20
+ const storePath = getMarchStorePath(platform);
21
+ const loaded = readJson(storePath, createEmptyStore());
22
+
23
+ return {
24
+ providers: Array.isArray(loaded?.providers) ? loaded.providers : [],
25
+ currentProviderId: typeof loaded?.currentProviderId === "string" ? loaded.currentProviderId : null
26
+ };
27
+ }
28
+
29
+ export function savePlatformStore(platform, store) {
30
+ ensureDir(getMarchDir());
31
+ writeJson(getMarchStorePath(platform), {
32
+ providers: Array.isArray(store.providers) ? store.providers : [],
33
+ currentProviderId: store.currentProviderId || null
34
+ });
35
+ }
36
+
37
+ export function listProviders(platform) {
38
+ return loadPlatformStore(platform).providers;
39
+ }
40
+
41
+ export function getCurrentProvider(platform) {
42
+ const store = loadPlatformStore(platform);
43
+ return store.providers.find((provider) => provider.id === store.currentProviderId) || null;
44
+ }
45
+
46
+ function ensureUniqueProviderName(providers, name, exceptId = null) {
47
+ const target = name.trim().toLowerCase();
48
+ const conflict = providers.find(
49
+ (provider) =>
50
+ provider.id !== exceptId && provider.name.trim().toLowerCase() === target
51
+ );
52
+
53
+ if (conflict) {
54
+ throw new Error(`Provider name already exists: ${name}`);
55
+ }
56
+ }
57
+
58
+ export function upsertProvider(platform, input, options = {}) {
59
+ const store = loadPlatformStore(platform);
60
+ const name = input.name.trim();
61
+ const now = new Date().toISOString();
62
+ const existing = findByName(store.providers, name);
63
+
64
+ let provider;
65
+
66
+ if (existing) {
67
+ provider = {
68
+ ...existing,
69
+ baseUrl: input.baseUrl,
70
+ apiKey: input.apiKey,
71
+ model: input.model || existing.model || DEFAULT_PRIMARY_MODEL,
72
+ updatedAt: now
73
+ };
74
+ store.providers = store.providers.map((item) => (item.id === provider.id ? provider : item));
75
+ } else {
76
+ provider = {
77
+ id: generateId(platform),
78
+ name,
79
+ baseUrl: input.baseUrl,
80
+ apiKey: input.apiKey,
81
+ model: input.model || DEFAULT_PRIMARY_MODEL,
82
+ createdAt: now,
83
+ updatedAt: now
84
+ };
85
+ store.providers.push(provider);
86
+ }
87
+
88
+ if (options.activate !== false) {
89
+ store.currentProviderId = provider.id;
90
+ }
91
+
92
+ savePlatformStore(platform, store);
93
+ return provider;
94
+ }
95
+
96
+ export function updateProvider(platform, nameOrId, updates, options = {}) {
97
+ const store = loadPlatformStore(platform);
98
+ const current = findByNameOrId(store.providers, nameOrId);
99
+
100
+ if (!current) {
101
+ throw new Error(`Provider not found: ${nameOrId}`);
102
+ }
103
+
104
+ const nextName = `${updates?.name ?? current.name}`.trim();
105
+ if (!nextName) {
106
+ throw new Error("Provider name cannot be empty");
107
+ }
108
+
109
+ ensureUniqueProviderName(store.providers, nextName, current.id);
110
+
111
+ const nextProvider = {
112
+ ...current,
113
+ name: nextName,
114
+ baseUrl: `${updates?.baseUrl ?? current.baseUrl}`.trim() || current.baseUrl,
115
+ apiKey: `${updates?.apiKey ?? current.apiKey}`.trim() || current.apiKey,
116
+ model: `${updates?.model ?? current.model ?? DEFAULT_PRIMARY_MODEL}`.trim() || DEFAULT_PRIMARY_MODEL,
117
+ updatedAt: new Date().toISOString()
118
+ };
119
+
120
+ store.providers = store.providers.map((provider) => (provider.id === current.id ? nextProvider : provider));
121
+
122
+ if (options.activate === true) {
123
+ store.currentProviderId = nextProvider.id;
124
+ }
125
+
126
+ savePlatformStore(platform, store);
127
+ return nextProvider;
128
+ }
129
+
130
+ export function cloneProvider(platform, nameOrId, input, options = {}) {
131
+ const store = loadPlatformStore(platform);
132
+ const source = findByNameOrId(store.providers, nameOrId);
133
+
134
+ if (!source) {
135
+ throw new Error(`Provider not found: ${nameOrId}`);
136
+ }
137
+
138
+ const nextName = `${input?.name || ""}`.trim();
139
+ if (!nextName) {
140
+ throw new Error("Clone name cannot be empty");
141
+ }
142
+
143
+ ensureUniqueProviderName(store.providers, nextName);
144
+ const now = new Date().toISOString();
145
+ const cloned = {
146
+ ...source,
147
+ id: generateId(platform),
148
+ name: nextName,
149
+ baseUrl: `${input?.baseUrl ?? source.baseUrl}`.trim() || source.baseUrl,
150
+ apiKey: `${input?.apiKey ?? source.apiKey}`.trim() || source.apiKey,
151
+ model: `${input?.model ?? source.model ?? DEFAULT_PRIMARY_MODEL}`.trim() || DEFAULT_PRIMARY_MODEL,
152
+ clonedFrom: source.id,
153
+ createdAt: now,
154
+ updatedAt: now
155
+ };
156
+
157
+ store.providers.push(cloned);
158
+ if (options.activate === true) {
159
+ store.currentProviderId = cloned.id;
160
+ }
161
+
162
+ savePlatformStore(platform, store);
163
+ return cloned;
164
+ }
165
+
166
+ export function removeProvider(platform, nameOrId, options = {}) {
167
+ const store = loadPlatformStore(platform);
168
+ const target = findByNameOrId(store.providers, nameOrId);
169
+
170
+ if (!target) {
171
+ throw new Error(`Provider not found: ${nameOrId}`);
172
+ }
173
+
174
+ store.providers = store.providers.filter((provider) => provider.id !== target.id);
175
+ let nextCurrent = null;
176
+
177
+ if (store.currentProviderId === target.id) {
178
+ if (options.activateFallback === false || store.providers.length === 0) {
179
+ store.currentProviderId = null;
180
+ } else {
181
+ nextCurrent = store.providers[store.providers.length - 1];
182
+ store.currentProviderId = nextCurrent.id;
183
+ }
184
+ } else {
185
+ nextCurrent = store.providers.find((provider) => provider.id === store.currentProviderId) || null;
186
+ }
187
+
188
+ savePlatformStore(platform, store);
189
+
190
+ return {
191
+ removedProvider: target,
192
+ currentProvider: nextCurrent
193
+ };
194
+ }
195
+
196
+ export function switchProvider(platform, nameOrId) {
197
+ const store = loadPlatformStore(platform);
198
+ const provider = findByNameOrId(store.providers, nameOrId);
199
+
200
+ if (!provider) {
201
+ throw new Error(`Provider not found: ${nameOrId}`);
202
+ }
203
+
204
+ store.currentProviderId = provider.id;
205
+ savePlatformStore(platform, store);
206
+ return provider;
207
+ }
208
+
209
+ export function resolveStoredProvider(platform, nameOrId) {
210
+ const store = loadPlatformStore(platform);
211
+ const provider = findByNameOrId(store.providers, nameOrId);
212
+
213
+ if (!provider) {
214
+ throw new Error(`Provider not found: ${nameOrId}`);
215
+ }
216
+
217
+ return provider;
218
+ }
package/core/utils.js ADDED
@@ -0,0 +1,225 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+
5
+ export function isRecord(value) {
6
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+
9
+ export function ensureDir(dirPath) {
10
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
11
+ }
12
+
13
+ export function readJson(filePath, fallback = null) {
14
+ if (!fs.existsSync(filePath)) {
15
+ return fallback;
16
+ }
17
+
18
+ try {
19
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
20
+ } catch {
21
+ return fallback;
22
+ }
23
+ }
24
+
25
+ export function writeJson(filePath, value) {
26
+ ensureDir(path.dirname(filePath));
27
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
28
+ }
29
+
30
+ export function readText(filePath, fallback = "") {
31
+ if (!fs.existsSync(filePath)) {
32
+ return fallback;
33
+ }
34
+
35
+ try {
36
+ return fs.readFileSync(filePath, "utf8");
37
+ } catch {
38
+ return fallback;
39
+ }
40
+ }
41
+
42
+ export function writeText(filePath, value) {
43
+ ensureDir(path.dirname(filePath));
44
+ fs.writeFileSync(filePath, value, { mode: 0o600 });
45
+ }
46
+
47
+ export function pathExists(targetPath) {
48
+ return fs.existsSync(targetPath);
49
+ }
50
+
51
+ export function deepMerge(base, patch) {
52
+ if (!isRecord(base)) {
53
+ return cloneValue(patch);
54
+ }
55
+
56
+ if (!isRecord(patch)) {
57
+ return cloneValue(base);
58
+ }
59
+
60
+ const result = { ...base };
61
+
62
+ for (const [key, value] of Object.entries(patch)) {
63
+ if (isRecord(value) && isRecord(result[key])) {
64
+ result[key] = deepMerge(result[key], value);
65
+ continue;
66
+ }
67
+
68
+ result[key] = cloneValue(value);
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ function cloneValue(value) {
75
+ if (Array.isArray(value)) {
76
+ return value.map((item) => cloneValue(item));
77
+ }
78
+
79
+ if (isRecord(value)) {
80
+ return Object.fromEntries(
81
+ Object.entries(value).map(([key, nested]) => [key, cloneValue(nested)])
82
+ );
83
+ }
84
+
85
+ return value;
86
+ }
87
+
88
+ export function normalizeBaseUrl(url) {
89
+ return url.trim().replace(/\/+$/, "");
90
+ }
91
+
92
+ export function resolveHomePath(input) {
93
+ const raw = `${input ?? ""}`.trim();
94
+ if (!raw) {
95
+ return "";
96
+ }
97
+
98
+ if (raw === "~") {
99
+ return os.homedir();
100
+ }
101
+
102
+ if (raw.startsWith("~/")) {
103
+ return path.join(os.homedir(), raw.slice(2));
104
+ }
105
+
106
+ return path.resolve(raw);
107
+ }
108
+
109
+ export function toTildePath(targetPath) {
110
+ const resolved = path.resolve(targetPath);
111
+ const homeDir = path.resolve(os.homedir());
112
+
113
+ if (resolved === homeDir) {
114
+ return "~";
115
+ }
116
+
117
+ if (resolved.startsWith(`${homeDir}${path.sep}`)) {
118
+ return `~${path.sep}${resolved.slice(homeDir.length + 1)}`;
119
+ }
120
+
121
+ return resolved;
122
+ }
123
+
124
+ export function buildOpenClawBaseUrl(baseUrl) {
125
+ return `${normalizeBaseUrl(baseUrl)}/v1`;
126
+ }
127
+
128
+ export function dedupeStrings(values) {
129
+ const seen = new Set();
130
+ const result = [];
131
+
132
+ for (const rawValue of values) {
133
+ const value = typeof rawValue === "string" ? rawValue.trim() : "";
134
+ if (!value) {
135
+ continue;
136
+ }
137
+
138
+ const key = value.toLowerCase();
139
+ if (seen.has(key)) {
140
+ continue;
141
+ }
142
+
143
+ seen.add(key);
144
+ result.push(value);
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ export function parseCommaList(value) {
151
+ if (!value) {
152
+ return [];
153
+ }
154
+
155
+ return dedupeStrings(value.split(",").map((item) => item.trim()).filter(Boolean));
156
+ }
157
+
158
+ export function generateId(prefix) {
159
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
160
+ }
161
+
162
+ export function slugify(value, fallback = "item") {
163
+ const normalized = `${value ?? ""}`
164
+ .trim()
165
+ .toLowerCase()
166
+ .replace(/[^a-z0-9]+/g, "-")
167
+ .replace(/^-+|-+$/g, "");
168
+
169
+ return normalized || fallback;
170
+ }
171
+
172
+ export function findByName(items, name) {
173
+ const target = name.trim().toLowerCase();
174
+ return items.find((item) => item.name.trim().toLowerCase() === target);
175
+ }
176
+
177
+ export function findByNameOrId(items, value) {
178
+ const trimmed = value.trim();
179
+ const lowered = trimmed.toLowerCase();
180
+ return items.find((item) => item.id === trimmed || item.name.trim().toLowerCase() === lowered);
181
+ }
182
+
183
+ export function replaceCaseInsensitiveKey(record, nextKey, nextValue) {
184
+ const result = isRecord(record) ? { ...record } : {};
185
+ const target = nextKey.toLowerCase();
186
+
187
+ for (const key of Object.keys(result)) {
188
+ if (key.toLowerCase() === target) {
189
+ delete result[key];
190
+ }
191
+ }
192
+
193
+ result[nextKey] = nextValue;
194
+ return result;
195
+ }
196
+
197
+ export function providerKeyFromName(name) {
198
+ const normalized = name
199
+ .trim()
200
+ .toLowerCase()
201
+ .replace(/[^a-z0-9]+/g, "-")
202
+ .replace(/^-+|-+$/g, "");
203
+
204
+ return normalized || "fhl";
205
+ }
206
+
207
+ export function maskApiKey(apiKey) {
208
+ if (!apiKey) {
209
+ return "(empty)";
210
+ }
211
+
212
+ if (apiKey.length <= 10) {
213
+ return `${apiKey.slice(0, 2)}***${apiKey.slice(-2)}`;
214
+ }
215
+
216
+ return `${apiKey.slice(0, 4)}***${apiKey.slice(-4)}`;
217
+ }
218
+
219
+ export function formatLatency(result) {
220
+ if (!result.ok) {
221
+ return result.error || "failed";
222
+ }
223
+
224
+ return `${result.latency} ms`;
225
+ }
@@ -0,0 +1,102 @@
1
+ import TOML from "@iarna/toml";
2
+ import { DEFAULT_PRIMARY_MODEL, DEFAULT_PROVIDER_NAME } from "../constants.js";
3
+ import { getCodexAuthPath, getCodexConfigPath, getCodexDir } from "../paths.js";
4
+ import {
5
+ deepMerge,
6
+ ensureDir,
7
+ isRecord,
8
+ providerKeyFromName,
9
+ readJson,
10
+ readText,
11
+ replaceCaseInsensitiveKey,
12
+ writeJson,
13
+ writeText
14
+ } from "../utils.js";
15
+
16
+ const DEFAULT_CODEX_CONFIG = {
17
+ model: DEFAULT_PRIMARY_MODEL,
18
+ model_reasoning_effort: "xhigh",
19
+ disable_response_storage: true,
20
+ sandbox_mode: "danger-full-access",
21
+ windows_wsl_setup_acknowledged: true,
22
+ approval_policy: "never",
23
+ profile: "auto-max",
24
+ file_opener: "vscode",
25
+ web_search: "cached",
26
+ suppress_unstable_features_warning: true,
27
+ history: {
28
+ persistence: "save-all"
29
+ },
30
+ tui: {
31
+ notifications: true
32
+ },
33
+ shell_environment_policy: {
34
+ inherit: "all",
35
+ ignore_default_excludes: false
36
+ },
37
+ sandbox_workspace_write: {
38
+ network_access: true
39
+ },
40
+ features: {
41
+ plan_tool: true,
42
+ apply_patch_freeform: true,
43
+ view_image_tool: true
44
+ },
45
+ profiles: {
46
+ "auto-max": {
47
+ approval_policy: "never",
48
+ sandbox_mode: "workspace-write"
49
+ }
50
+ }
51
+ };
52
+
53
+ function loadCodexConfig(configPath) {
54
+ const raw = readText(configPath, "");
55
+ if (!raw.trim()) {
56
+ return {};
57
+ }
58
+
59
+ try {
60
+ return TOML.parse(raw);
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ function buildManagedProvider(provider) {
67
+ return {
68
+ name: provider.name || DEFAULT_PROVIDER_NAME,
69
+ base_url: provider.baseUrl,
70
+ wire_api: "responses",
71
+ requires_openai_auth: true
72
+ };
73
+ }
74
+
75
+ export function writeCodexConfig(provider, options = {}) {
76
+ ensureDir(getCodexDir());
77
+
78
+ const configPath = getCodexConfigPath();
79
+ const authPath = getCodexAuthPath();
80
+ const providerKey = providerKeyFromName(provider.name);
81
+ const mode = options.mode === "overwrite" ? "overwrite" : "merge";
82
+ const baseConfig = mode === "overwrite" ? {} : loadCodexConfig(configPath);
83
+ const merged = deepMerge(DEFAULT_CODEX_CONFIG, isRecord(baseConfig) ? baseConfig : {});
84
+ const existingProviders =
85
+ mode === "overwrite" || !isRecord(merged.model_providers) ? {} : merged.model_providers;
86
+
87
+ merged.model = provider.model || DEFAULT_PRIMARY_MODEL;
88
+ merged.model_provider = providerKey;
89
+ merged.model_providers = replaceCaseInsensitiveKey(
90
+ existingProviders,
91
+ providerKey,
92
+ buildManagedProvider(provider)
93
+ );
94
+
95
+ writeText(configPath, TOML.stringify(merged));
96
+
97
+ const existingAuth = mode === "overwrite" ? {} : readJson(authPath, {});
98
+ writeJson(authPath, {
99
+ ...existingAuth,
100
+ OPENAI_API_KEY: provider.apiKey
101
+ });
102
+ }
@@ -0,0 +1,16 @@
1
+ import { writeCodexConfig } from "./codex.js";
2
+ import { writeOpenClawConfig } from "./openclaw.js";
3
+ import { writeOpenCodeConfig } from "./opencode.js";
4
+
5
+ export function writePlatformConfig(platform, provider, options = {}) {
6
+ switch (platform) {
7
+ case "codex":
8
+ return writeCodexConfig(provider, options);
9
+ case "opencode":
10
+ return writeOpenCodeConfig(provider, options);
11
+ case "openclaw":
12
+ return writeOpenClawConfig(provider, options);
13
+ default:
14
+ throw new Error(`Unsupported platform: ${platform}`);
15
+ }
16
+ }
@@ -0,0 +1,93 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_PRIMARY_MODEL, DEFAULT_PROVIDER_NAME, MODEL_DEFINITIONS } from "../constants.js";
3
+ import { getHomeDir, getOpenClawConfigPath, getOpenClawDir, getOpenClawModelsPath } from "../paths.js";
4
+ import {
5
+ ensureDir,
6
+ isRecord,
7
+ providerKeyFromName,
8
+ readJson,
9
+ replaceCaseInsensitiveKey,
10
+ writeJson
11
+ } from "../utils.js";
12
+
13
+ function buildOpenClawModels() {
14
+ return MODEL_DEFINITIONS.map((model) => ({
15
+ id: model.id,
16
+ name: model.id,
17
+ api: "openai-responses",
18
+ reasoning: model.reasoning,
19
+ input: ["text", "image"],
20
+ cost: {
21
+ input: 0,
22
+ output: 0,
23
+ cacheRead: 0,
24
+ cacheWrite: 0
25
+ },
26
+ contextWindow: model.contextWindow,
27
+ maxTokens: model.maxTokens
28
+ }));
29
+ }
30
+
31
+ function buildProviderConfig(provider) {
32
+ return {
33
+ baseUrl: provider.baseUrl,
34
+ apiKey: provider.apiKey,
35
+ api: "openai-responses",
36
+ authHeader: true,
37
+ headers: {
38
+ "User-Agent": "codex-rs/1.0.7"
39
+ },
40
+ models: buildOpenClawModels()
41
+ };
42
+ }
43
+
44
+ export function writeOpenClawConfig(provider, options = {}) {
45
+ ensureDir(getOpenClawDir());
46
+ ensureDir(path.dirname(getOpenClawModelsPath()));
47
+
48
+ const configPath = getOpenClawConfigPath();
49
+ const modelsPath = getOpenClawModelsPath();
50
+ const mode = options.mode === "overwrite" ? "overwrite" : "merge";
51
+ const providerName = providerKeyFromName(provider.name || DEFAULT_PROVIDER_NAME);
52
+ const providerConfig = buildProviderConfig(provider);
53
+ const existingConfig = mode === "overwrite" ? {} : readJson(configPath, {});
54
+ const existingModels = mode === "overwrite" ? {} : readJson(modelsPath, {});
55
+ const existingConfigProviders =
56
+ mode === "overwrite" || !isRecord(existingConfig?.models?.providers)
57
+ ? {}
58
+ : existingConfig.models.providers;
59
+ const existingModelProviders =
60
+ mode === "overwrite" || !isRecord(existingModels?.providers) ? {} : existingModels.providers;
61
+
62
+ const configPayload = {
63
+ ...(isRecord(existingConfig) ? existingConfig : {}),
64
+ models: {
65
+ mode: "merge",
66
+ ...(isRecord(existingConfig?.models) ? existingConfig.models : {}),
67
+ providers: replaceCaseInsensitiveKey(existingConfigProviders, providerName, providerConfig)
68
+ },
69
+ agents: {
70
+ ...(isRecord(existingConfig?.agents) ? existingConfig.agents : {}),
71
+ defaults: {
72
+ ...(isRecord(existingConfig?.agents?.defaults) ? existingConfig.agents.defaults : {}),
73
+ workspace: getHomeDir(),
74
+ imageModel: `${providerName}/${provider.model || DEFAULT_PRIMARY_MODEL}`,
75
+ model: {
76
+ ...(isRecord(existingConfig?.agents?.defaults?.model)
77
+ ? existingConfig.agents.defaults.model
78
+ : {}),
79
+ primary: `${providerName}/${provider.model || DEFAULT_PRIMARY_MODEL}`
80
+ },
81
+ thinkingDefault: "xhigh"
82
+ }
83
+ }
84
+ };
85
+
86
+ const modelsPayload = {
87
+ ...(isRecord(existingModels) ? existingModels : {}),
88
+ providers: replaceCaseInsensitiveKey(existingModelProviders, providerName, providerConfig)
89
+ };
90
+
91
+ writeJson(configPath, configPayload);
92
+ writeJson(modelsPath, modelsPayload);
93
+ }