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.
- package/README.md +107 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/core/apply.js +152 -0
- package/core/backup.js +53 -0
- package/core/constants.js +78 -0
- package/core/desktop-service.js +403 -0
- package/core/desktop-state.js +1021 -0
- package/core/index.js +1468 -0
- package/core/paths.js +99 -0
- package/core/presets.js +171 -0
- package/core/probe.js +70 -0
- package/core/routing.js +334 -0
- package/core/store.js +218 -0
- package/core/utils.js +225 -0
- package/core/writers/codex.js +102 -0
- package/core/writers/index.js +16 -0
- package/core/writers/openclaw.js +93 -0
- package/core/writers/opencode.js +91 -0
- package/desktop/assets/fml-icon.png +0 -0
- package/desktop/assets/march-mark.svg +26 -0
- package/desktop/main.js +275 -0
- package/desktop/preload.cjs +67 -0
- package/desktop/preload.js +49 -0
- package/desktop/renderer/app.js +327 -0
- package/desktop/renderer/index.html +130 -0
- package/desktop/renderer/styles.css +490 -0
- package/package.json +111 -0
- package/scripts/build-web.mjs +95 -0
- package/scripts/desktop-dev.mjs +90 -0
- package/scripts/desktop-pack-win.mjs +81 -0
- package/scripts/postinstall.mjs +49 -0
- package/scripts/prepublish-check.mjs +57 -0
- package/scripts/serve-site.mjs +51 -0
- package/site/app.js +10 -0
- package/site/assets/fml-icon.png +0 -0
- package/site/assets/march-mark.svg +26 -0
- package/site/index.html +337 -0
- package/site/styles.css +840 -0
- package/src/App.tsx +1557 -0
- package/src/components/layout/app-sidebar.tsx +103 -0
- package/src/components/layout/top-toolbar.tsx +44 -0
- package/src/components/layout/workspace-tabs.tsx +32 -0
- package/src/components/providers/inspector-panel.tsx +84 -0
- package/src/components/providers/metric-strip.tsx +26 -0
- package/src/components/providers/provider-editor.tsx +87 -0
- package/src/components/providers/provider-table.tsx +85 -0
- package/src/components/ui/logo-mark.tsx +32 -0
- package/src/features/mcp/mcp-view.tsx +45 -0
- package/src/features/prompts/prompts-view.tsx +40 -0
- package/src/features/providers/providers-view.tsx +40 -0
- package/src/features/providers/types.ts +26 -0
- package/src/features/skills/skills-view.tsx +44 -0
- package/src/hooks/use-control-workspace.ts +235 -0
- package/src/index.css +22 -0
- package/src/lib/client.ts +726 -0
- package/src/lib/query-client.ts +3 -0
- package/src/lib/workspace-sections.ts +34 -0
- package/src/main.tsx +14 -0
- package/src/types.ts +137 -0
- package/src/vite-env.d.ts +64 -0
- 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
|
+
}
|