coder-config 0.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +553 -0
- package/cli.js +431 -0
- package/config-loader.js +294 -0
- package/hooks/activity-track.sh +56 -0
- package/hooks/codex-workstream.sh +44 -0
- package/hooks/gemini-workstream.sh +44 -0
- package/hooks/workstream-inject.sh +20 -0
- package/lib/activity.js +283 -0
- package/lib/apply.js +344 -0
- package/lib/cli.js +267 -0
- package/lib/config.js +171 -0
- package/lib/constants.js +55 -0
- package/lib/env.js +114 -0
- package/lib/index.js +47 -0
- package/lib/init.js +122 -0
- package/lib/mcps.js +139 -0
- package/lib/memory.js +201 -0
- package/lib/projects.js +138 -0
- package/lib/registry.js +83 -0
- package/lib/utils.js +129 -0
- package/lib/workstreams.js +652 -0
- package/package.json +80 -0
- package/scripts/capture-screenshots.js +142 -0
- package/scripts/postinstall.js +122 -0
- package/scripts/release.sh +71 -0
- package/scripts/sync-version.js +77 -0
- package/scripts/tauri-prepare.js +328 -0
- package/shared/mcp-registry.json +76 -0
- package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
- package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
- package/ui/dist/icons/icon-192.svg +16 -0
- package/ui/dist/icons/icon-512.svg +16 -0
- package/ui/dist/index.html +39 -0
- package/ui/dist/manifest.json +25 -0
- package/ui/dist/sw.js +24 -0
- package/ui/dist/tutorial/claude-settings.png +0 -0
- package/ui/dist/tutorial/header.png +0 -0
- package/ui/dist/tutorial/mcp-registry.png +0 -0
- package/ui/dist/tutorial/memory-view.png +0 -0
- package/ui/dist/tutorial/permissions.png +0 -0
- package/ui/dist/tutorial/plugins-view.png +0 -0
- package/ui/dist/tutorial/project-explorer.png +0 -0
- package/ui/dist/tutorial/projects-view.png +0 -0
- package/ui/dist/tutorial/sidebar.png +0 -0
- package/ui/dist/tutorial/tutorial-view.png +0 -0
- package/ui/dist/tutorial/workstreams-view.png +0 -0
- package/ui/routes/activity.js +58 -0
- package/ui/routes/commands.js +74 -0
- package/ui/routes/configs.js +329 -0
- package/ui/routes/env.js +40 -0
- package/ui/routes/file-explorer.js +668 -0
- package/ui/routes/index.js +41 -0
- package/ui/routes/mcp-discovery.js +235 -0
- package/ui/routes/memory.js +385 -0
- package/ui/routes/package.json +3 -0
- package/ui/routes/plugins.js +466 -0
- package/ui/routes/projects.js +198 -0
- package/ui/routes/registry.js +30 -0
- package/ui/routes/rules.js +74 -0
- package/ui/routes/search.js +125 -0
- package/ui/routes/settings.js +381 -0
- package/ui/routes/subprojects.js +208 -0
- package/ui/routes/tool-sync.js +127 -0
- package/ui/routes/updates.js +339 -0
- package/ui/routes/workstreams.js +224 -0
- package/ui/server.cjs +773 -0
- package/ui/terminal-server.cjs +160 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get all rules
|
|
10
|
+
*/
|
|
11
|
+
function getRules(manager, projectDir) {
|
|
12
|
+
const configs = manager.findAllConfigs(projectDir);
|
|
13
|
+
return manager.collectFilesFromHierarchy(configs, 'rules');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get a single rule
|
|
18
|
+
*/
|
|
19
|
+
function getRule(fullPath) {
|
|
20
|
+
try {
|
|
21
|
+
return { content: fs.readFileSync(fullPath, 'utf8') };
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return { error: e.message };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Save a rule
|
|
29
|
+
*/
|
|
30
|
+
function saveRule(body) {
|
|
31
|
+
try {
|
|
32
|
+
fs.writeFileSync(body.path, body.content);
|
|
33
|
+
return { success: true };
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return { error: e.message };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a rule
|
|
41
|
+
*/
|
|
42
|
+
function createRule(body, projectDir) {
|
|
43
|
+
try {
|
|
44
|
+
const dir = body.dir || path.join(projectDir, '.claude', 'rules');
|
|
45
|
+
if (!fs.existsSync(dir)) {
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
const filePath = path.join(dir, body.name);
|
|
49
|
+
fs.writeFileSync(filePath, body.content || '');
|
|
50
|
+
return { success: true, path: filePath };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return { error: e.message };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Delete a rule
|
|
58
|
+
*/
|
|
59
|
+
function deleteRule(fullPath) {
|
|
60
|
+
try {
|
|
61
|
+
fs.unlinkSync(fullPath);
|
|
62
|
+
return { success: true };
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { error: e.message };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
getRules,
|
|
70
|
+
getRule,
|
|
71
|
+
saveRule,
|
|
72
|
+
createRule,
|
|
73
|
+
deleteRule,
|
|
74
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Routes (GitHub, npm)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const https = require('https');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Infer MCP args from GitHub repo
|
|
9
|
+
*/
|
|
10
|
+
function inferMcpArgs(repo) {
|
|
11
|
+
const name = repo.name.toLowerCase();
|
|
12
|
+
const fullName = repo.full_name.toLowerCase();
|
|
13
|
+
|
|
14
|
+
// Check for official MCP packages
|
|
15
|
+
if (fullName.includes('modelcontextprotocol/servers')) {
|
|
16
|
+
const serverName = name.replace('server-', '');
|
|
17
|
+
return ['-y', `@modelcontextprotocol/server-${serverName}`];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check for npm-style package names
|
|
21
|
+
if (repo.topics && repo.topics.includes('npm')) {
|
|
22
|
+
return ['-y', repo.full_name];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Default to running from GitHub
|
|
26
|
+
return ['-y', `github:${repo.full_name}`];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Search GitHub for MCP servers
|
|
31
|
+
*/
|
|
32
|
+
async function searchGithub(query) {
|
|
33
|
+
if (!query) {
|
|
34
|
+
return { results: [], error: 'Query required' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const searchQuery = encodeURIComponent(`${query} mcp server in:name,description,topics`);
|
|
38
|
+
const url = `https://api.github.com/search/repositories?q=${searchQuery}&per_page=20&sort=stars`;
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const req = https.get(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
'User-Agent': 'claude-config-ui/1.0',
|
|
44
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
45
|
+
}
|
|
46
|
+
}, (res) => {
|
|
47
|
+
let data = '';
|
|
48
|
+
res.on('data', chunk => data += chunk);
|
|
49
|
+
res.on('end', () => {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(data);
|
|
52
|
+
if (parsed.message) {
|
|
53
|
+
resolve({ results: [], error: parsed.message });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const results = (parsed.items || []).map(repo => ({
|
|
57
|
+
name: repo.name,
|
|
58
|
+
fullName: repo.full_name,
|
|
59
|
+
description: repo.description,
|
|
60
|
+
url: repo.html_url,
|
|
61
|
+
stars: repo.stargazers_count,
|
|
62
|
+
topics: repo.topics || [],
|
|
63
|
+
suggestedCommand: 'npx',
|
|
64
|
+
suggestedArgs: inferMcpArgs(repo)
|
|
65
|
+
}));
|
|
66
|
+
resolve({ results });
|
|
67
|
+
} catch (e) {
|
|
68
|
+
resolve({ results: [], error: e.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
req.on('error', (e) => resolve({ results: [], error: e.message }));
|
|
73
|
+
req.setTimeout(10000, () => {
|
|
74
|
+
req.destroy();
|
|
75
|
+
resolve({ results: [], error: 'Request timeout' });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Search npm for MCP packages
|
|
82
|
+
*/
|
|
83
|
+
async function searchNpm(query) {
|
|
84
|
+
if (!query) {
|
|
85
|
+
return { results: [], error: 'Query required' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const searchQuery = encodeURIComponent(`${query} mcp`);
|
|
89
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${searchQuery}&size=20`;
|
|
90
|
+
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
const req = https.get(url, (res) => {
|
|
93
|
+
let data = '';
|
|
94
|
+
res.on('data', chunk => data += chunk);
|
|
95
|
+
res.on('end', () => {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(data);
|
|
98
|
+
const results = (parsed.objects || []).map(obj => ({
|
|
99
|
+
name: obj.package.name,
|
|
100
|
+
description: obj.package.description,
|
|
101
|
+
version: obj.package.version,
|
|
102
|
+
url: `https://www.npmjs.com/package/${obj.package.name}`,
|
|
103
|
+
keywords: obj.package.keywords || [],
|
|
104
|
+
suggestedCommand: 'npx',
|
|
105
|
+
suggestedArgs: ['-y', obj.package.name]
|
|
106
|
+
}));
|
|
107
|
+
resolve({ results });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
resolve({ results: [], error: e.message });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
req.on('error', (e) => resolve({ results: [], error: e.message }));
|
|
114
|
+
req.setTimeout(10000, () => {
|
|
115
|
+
req.destroy();
|
|
116
|
+
resolve({ results: [], error: 'Request timeout' });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
searchGithub,
|
|
123
|
+
searchNpm,
|
|
124
|
+
inferMcpArgs,
|
|
125
|
+
};
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Routes (Claude Code, Gemini CLI, Codex CLI)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
// TOML parser for Codex CLI config
|
|
10
|
+
let TOML;
|
|
11
|
+
try {
|
|
12
|
+
TOML = require('@iarna/toml');
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// Fallback if TOML not installed yet
|
|
15
|
+
TOML = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get Claude Code settings from ~/.claude/settings.json
|
|
20
|
+
*/
|
|
21
|
+
function getClaudeSettings() {
|
|
22
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(settingsPath)) {
|
|
26
|
+
return {
|
|
27
|
+
path: settingsPath,
|
|
28
|
+
exists: false,
|
|
29
|
+
settings: { permissions: { allow: [], ask: [], deny: [] } }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
34
|
+
const settings = JSON.parse(content);
|
|
35
|
+
|
|
36
|
+
// Ensure permissions structure exists
|
|
37
|
+
if (!settings.permissions) {
|
|
38
|
+
settings.permissions = { allow: [], ask: [], deny: [] };
|
|
39
|
+
}
|
|
40
|
+
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
41
|
+
if (!settings.permissions.ask) settings.permissions.ask = [];
|
|
42
|
+
if (!settings.permissions.deny) settings.permissions.deny = [];
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
path: settingsPath,
|
|
46
|
+
exists: true,
|
|
47
|
+
settings
|
|
48
|
+
};
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return {
|
|
51
|
+
path: settingsPath,
|
|
52
|
+
error: e.message
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save Claude Code settings to ~/.claude/settings.json
|
|
59
|
+
*/
|
|
60
|
+
function saveClaudeSettings(body) {
|
|
61
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
62
|
+
const { settings, permissions } = body;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const claudeDir = path.dirname(settingsPath);
|
|
66
|
+
if (!fs.existsSync(claudeDir)) {
|
|
67
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let finalSettings = {};
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(settingsPath)) {
|
|
73
|
+
try {
|
|
74
|
+
finalSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
75
|
+
} catch (e) {
|
|
76
|
+
finalSettings = {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (settings) {
|
|
81
|
+
finalSettings = { ...finalSettings, ...settings };
|
|
82
|
+
}
|
|
83
|
+
if (permissions) {
|
|
84
|
+
finalSettings.permissions = permissions;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.writeFileSync(settingsPath, JSON.stringify(finalSettings, null, 2) + '\n', 'utf8');
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
path: settingsPath,
|
|
92
|
+
settings: finalSettings
|
|
93
|
+
};
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: e.message
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get Gemini CLI settings from ~/.gemini/settings.json
|
|
104
|
+
*/
|
|
105
|
+
function getGeminiSettings() {
|
|
106
|
+
const settingsPath = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(settingsPath)) {
|
|
110
|
+
return {
|
|
111
|
+
path: settingsPath,
|
|
112
|
+
exists: false,
|
|
113
|
+
settings: {}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
118
|
+
const settings = JSON.parse(content);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
path: settingsPath,
|
|
122
|
+
exists: true,
|
|
123
|
+
settings
|
|
124
|
+
};
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return {
|
|
127
|
+
path: settingsPath,
|
|
128
|
+
error: e.message
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Save Gemini CLI settings to ~/.gemini/settings.json
|
|
135
|
+
*/
|
|
136
|
+
function saveGeminiSettings(body) {
|
|
137
|
+
const settingsPath = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const geminiDir = path.dirname(settingsPath);
|
|
141
|
+
if (!fs.existsSync(geminiDir)) {
|
|
142
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let finalSettings = {};
|
|
146
|
+
if (fs.existsSync(settingsPath)) {
|
|
147
|
+
try {
|
|
148
|
+
finalSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
149
|
+
} catch (e) {
|
|
150
|
+
finalSettings = {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Merge with new settings (preserves mcpServers which is managed separately)
|
|
155
|
+
finalSettings = { ...finalSettings, ...body };
|
|
156
|
+
|
|
157
|
+
fs.writeFileSync(settingsPath, JSON.stringify(finalSettings, null, 2) + '\n', 'utf8');
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
path: settingsPath,
|
|
162
|
+
settings: finalSettings
|
|
163
|
+
};
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: e.message
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get Antigravity settings from ~/.gemini/antigravity/settings.json
|
|
174
|
+
*/
|
|
175
|
+
function getAntigravitySettings() {
|
|
176
|
+
const settingsPath = path.join(os.homedir(), '.gemini', 'antigravity', 'settings.json');
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
if (!fs.existsSync(settingsPath)) {
|
|
180
|
+
return {
|
|
181
|
+
path: settingsPath,
|
|
182
|
+
exists: false,
|
|
183
|
+
settings: {}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
188
|
+
const settings = JSON.parse(content);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
path: settingsPath,
|
|
192
|
+
exists: true,
|
|
193
|
+
settings
|
|
194
|
+
};
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return {
|
|
197
|
+
path: settingsPath,
|
|
198
|
+
error: e.message
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Save Antigravity settings to ~/.gemini/antigravity/settings.json
|
|
205
|
+
*/
|
|
206
|
+
function saveAntigravitySettings(body) {
|
|
207
|
+
const settingsPath = path.join(os.homedir(), '.gemini', 'antigravity', 'settings.json');
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const antigravityDir = path.dirname(settingsPath);
|
|
211
|
+
if (!fs.existsSync(antigravityDir)) {
|
|
212
|
+
fs.mkdirSync(antigravityDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let finalSettings = {};
|
|
216
|
+
if (fs.existsSync(settingsPath)) {
|
|
217
|
+
try {
|
|
218
|
+
finalSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
219
|
+
} catch (e) {
|
|
220
|
+
finalSettings = {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Merge with new settings
|
|
225
|
+
finalSettings = { ...finalSettings, ...body };
|
|
226
|
+
|
|
227
|
+
fs.writeFileSync(settingsPath, JSON.stringify(finalSettings, null, 2) + '\n', 'utf8');
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
path: settingsPath,
|
|
232
|
+
settings: finalSettings
|
|
233
|
+
};
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
error: e.message
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get Codex CLI settings from ~/.codex/config.toml
|
|
244
|
+
*/
|
|
245
|
+
function getCodexSettings() {
|
|
246
|
+
const settingsPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
if (!fs.existsSync(settingsPath)) {
|
|
250
|
+
return {
|
|
251
|
+
path: settingsPath,
|
|
252
|
+
exists: false,
|
|
253
|
+
settings: {}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
258
|
+
|
|
259
|
+
// Parse TOML if available, otherwise return raw content
|
|
260
|
+
if (TOML) {
|
|
261
|
+
const settings = TOML.parse(content);
|
|
262
|
+
return {
|
|
263
|
+
path: settingsPath,
|
|
264
|
+
exists: true,
|
|
265
|
+
settings,
|
|
266
|
+
raw: content
|
|
267
|
+
};
|
|
268
|
+
} else {
|
|
269
|
+
return {
|
|
270
|
+
path: settingsPath,
|
|
271
|
+
exists: true,
|
|
272
|
+
settings: {},
|
|
273
|
+
raw: content,
|
|
274
|
+
error: 'TOML parser not available'
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
return {
|
|
279
|
+
path: settingsPath,
|
|
280
|
+
error: e.message
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Save Codex CLI settings to ~/.codex/config.toml
|
|
287
|
+
*/
|
|
288
|
+
function saveCodexSettings(body) {
|
|
289
|
+
const settingsPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
290
|
+
const { settings, raw } = body;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const codexDir = path.dirname(settingsPath);
|
|
294
|
+
if (!fs.existsSync(codexDir)) {
|
|
295
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// If raw TOML is provided, use it directly
|
|
299
|
+
if (raw !== undefined) {
|
|
300
|
+
// Validate TOML if parser available
|
|
301
|
+
if (TOML) {
|
|
302
|
+
try {
|
|
303
|
+
TOML.parse(raw);
|
|
304
|
+
} catch (parseErr) {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: `Invalid TOML: ${parseErr.message}`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
fs.writeFileSync(settingsPath, raw, 'utf8');
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
path: settingsPath,
|
|
315
|
+
settings: TOML ? TOML.parse(raw) : {}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Convert JSON settings to TOML
|
|
320
|
+
if (!TOML) {
|
|
321
|
+
return {
|
|
322
|
+
success: false,
|
|
323
|
+
error: 'TOML parser not available for conversion'
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Read existing settings and merge
|
|
328
|
+
let finalSettings = {};
|
|
329
|
+
if (fs.existsSync(settingsPath)) {
|
|
330
|
+
try {
|
|
331
|
+
finalSettings = TOML.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
332
|
+
} catch (e) {
|
|
333
|
+
finalSettings = {};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Deep merge settings
|
|
338
|
+
finalSettings = deepMerge(finalSettings, settings);
|
|
339
|
+
|
|
340
|
+
// Convert to TOML and write
|
|
341
|
+
const tomlContent = TOML.stringify(finalSettings);
|
|
342
|
+
fs.writeFileSync(settingsPath, tomlContent, 'utf8');
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
success: true,
|
|
346
|
+
path: settingsPath,
|
|
347
|
+
settings: finalSettings
|
|
348
|
+
};
|
|
349
|
+
} catch (e) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
error: e.message
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Deep merge helper for nested objects
|
|
359
|
+
*/
|
|
360
|
+
function deepMerge(target, source) {
|
|
361
|
+
const result = { ...target };
|
|
362
|
+
for (const key of Object.keys(source)) {
|
|
363
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
364
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
365
|
+
} else {
|
|
366
|
+
result[key] = source[key];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
getClaudeSettings,
|
|
374
|
+
saveClaudeSettings,
|
|
375
|
+
getGeminiSettings,
|
|
376
|
+
saveGeminiSettings,
|
|
377
|
+
getAntigravitySettings,
|
|
378
|
+
saveAntigravitySettings,
|
|
379
|
+
getCodexSettings,
|
|
380
|
+
saveCodexSettings,
|
|
381
|
+
};
|