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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +553 -0
  3. package/cli.js +431 -0
  4. package/config-loader.js +294 -0
  5. package/hooks/activity-track.sh +56 -0
  6. package/hooks/codex-workstream.sh +44 -0
  7. package/hooks/gemini-workstream.sh +44 -0
  8. package/hooks/workstream-inject.sh +20 -0
  9. package/lib/activity.js +283 -0
  10. package/lib/apply.js +344 -0
  11. package/lib/cli.js +267 -0
  12. package/lib/config.js +171 -0
  13. package/lib/constants.js +55 -0
  14. package/lib/env.js +114 -0
  15. package/lib/index.js +47 -0
  16. package/lib/init.js +122 -0
  17. package/lib/mcps.js +139 -0
  18. package/lib/memory.js +201 -0
  19. package/lib/projects.js +138 -0
  20. package/lib/registry.js +83 -0
  21. package/lib/utils.js +129 -0
  22. package/lib/workstreams.js +652 -0
  23. package/package.json +80 -0
  24. package/scripts/capture-screenshots.js +142 -0
  25. package/scripts/postinstall.js +122 -0
  26. package/scripts/release.sh +71 -0
  27. package/scripts/sync-version.js +77 -0
  28. package/scripts/tauri-prepare.js +328 -0
  29. package/shared/mcp-registry.json +76 -0
  30. package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
  31. package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
  32. package/ui/dist/icons/icon-192.svg +16 -0
  33. package/ui/dist/icons/icon-512.svg +16 -0
  34. package/ui/dist/index.html +39 -0
  35. package/ui/dist/manifest.json +25 -0
  36. package/ui/dist/sw.js +24 -0
  37. package/ui/dist/tutorial/claude-settings.png +0 -0
  38. package/ui/dist/tutorial/header.png +0 -0
  39. package/ui/dist/tutorial/mcp-registry.png +0 -0
  40. package/ui/dist/tutorial/memory-view.png +0 -0
  41. package/ui/dist/tutorial/permissions.png +0 -0
  42. package/ui/dist/tutorial/plugins-view.png +0 -0
  43. package/ui/dist/tutorial/project-explorer.png +0 -0
  44. package/ui/dist/tutorial/projects-view.png +0 -0
  45. package/ui/dist/tutorial/sidebar.png +0 -0
  46. package/ui/dist/tutorial/tutorial-view.png +0 -0
  47. package/ui/dist/tutorial/workstreams-view.png +0 -0
  48. package/ui/routes/activity.js +58 -0
  49. package/ui/routes/commands.js +74 -0
  50. package/ui/routes/configs.js +329 -0
  51. package/ui/routes/env.js +40 -0
  52. package/ui/routes/file-explorer.js +668 -0
  53. package/ui/routes/index.js +41 -0
  54. package/ui/routes/mcp-discovery.js +235 -0
  55. package/ui/routes/memory.js +385 -0
  56. package/ui/routes/package.json +3 -0
  57. package/ui/routes/plugins.js +466 -0
  58. package/ui/routes/projects.js +198 -0
  59. package/ui/routes/registry.js +30 -0
  60. package/ui/routes/rules.js +74 -0
  61. package/ui/routes/search.js +125 -0
  62. package/ui/routes/settings.js +381 -0
  63. package/ui/routes/subprojects.js +208 -0
  64. package/ui/routes/tool-sync.js +127 -0
  65. package/ui/routes/updates.js +339 -0
  66. package/ui/routes/workstreams.js +224 -0
  67. package/ui/server.cjs +773 -0
  68. 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
+ };