@somfan2001/claudeenv 1.0.0

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 (2) hide show
  1. package/package.json +15 -0
  2. package/server.js +434 -0
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@somfan2001/claudeenv",
3
+ "version": "1.0.0",
4
+ "description": "Standalone CLI tool to edit Claude settings on macOS",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "claudeenv": "./server.js"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "engines": {
13
+ "node": ">=12.0.0"
14
+ }
15
+ }
package/server.js ADDED
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env node
2
+ const http = require('http');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const PORT = 20129;
8
+
9
+ // Đường dẫn file settings.json của Claude trên macOS
10
+ const getClaudeSettingsPath = () => {
11
+ const homeDir = os.homedir();
12
+ return path.join(homeDir, '.claude', 'settings.json');
13
+ };
14
+
15
+ // Đọc cài đặt
16
+ function readSettings() {
17
+ const settingsPath = getClaudeSettingsPath();
18
+ try {
19
+ if (fs.existsSync(settingsPath)) {
20
+ const data = fs.readFileSync(settingsPath, 'utf8');
21
+ return JSON.parse(data);
22
+ }
23
+ } catch (err) {
24
+ console.error('Error reading settings:', err);
25
+ }
26
+ return { hasCompletedOnboarding: true, env: {} };
27
+ }
28
+
29
+ // Lưu cài đặt
30
+ function saveSettings(settings) {
31
+ const settingsPath = getClaudeSettingsPath();
32
+ const dir = path.dirname(settingsPath);
33
+ try {
34
+ if (!fs.existsSync(dir)) {
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ }
37
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
38
+ return true;
39
+ } catch (err) {
40
+ console.error('Error writing settings:', err);
41
+ return false;
42
+ }
43
+ }
44
+
45
+ // Render giao diện HTML
46
+ const htmlTemplate = (envData, message = '') => {
47
+ // Danh sách các key env thường dùng để tiện hiển thị/nhập liệu
48
+ const defaultKeys = [
49
+ "ANTHROPIC_AUTH_TOKEN",
50
+ "ANTHROPIC_BASE_URL",
51
+ "API_TIMEOUT_MS",
52
+ "DISABLE_TELEMETRY",
53
+ "CLAUDE_CODE_DISABLE_1M_CONTEXT",
54
+ "CLAUDE_CODE_MAX_RETRIES",
55
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
56
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
57
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
58
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
59
+ "CLAUDE_CODE_SUBAGENT_MODEL"
60
+ ];
61
+
62
+ const env = envData || {};
63
+
64
+ // Lấy các key tùy biến khác mà user tự thêm
65
+ const allKeys = Array.from(new Set([...defaultKeys, ...Object.keys(env)]));
66
+
67
+ let rowsHtml = '';
68
+ allKeys.forEach(key => {
69
+ const value = env[key] || '';
70
+ rowsHtml += `
71
+ <div class="row">
72
+ <label for="env_${key}">${key}</label>
73
+ <div class="input-group">
74
+ <input type="text" id="env_${key}" name="env_${key}" value="${value}" placeholder="Chưa cấu hình">
75
+ <button type="button" class="btn-clear" onclick="document.getElementById('env_${key}').value = ''">Xóa</button>
76
+ </div>
77
+ </div>
78
+ `;
79
+ });
80
+
81
+ return `
82
+ <!DOCTYPE html>
83
+ <html lang="vi">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>Claude Settings Editor (macOS)</title>
88
+ <style>
89
+ body {
90
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
91
+ background-color: #f5f5f7;
92
+ color: #1d1d1f;
93
+ margin: 0;
94
+ padding: 40px 20px;
95
+ display: flex;
96
+ justify-content: center;
97
+ }
98
+ .container {
99
+ background: #ffffff;
100
+ border-radius: 12px;
101
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
102
+ max-width: 800px;
103
+ width: 100%;
104
+ padding: 30px;
105
+ box-sizing: border-box;
106
+ }
107
+ h1 {
108
+ font-size: 24px;
109
+ margin-top: 0;
110
+ margin-bottom: 8px;
111
+ color: #d97757;
112
+ }
113
+ .subtitle {
114
+ color: #86868b;
115
+ font-size: 14px;
116
+ margin-bottom: 24px;
117
+ }
118
+ .path-box {
119
+ background-color: #f5f5f7;
120
+ padding: 10px 14px;
121
+ border-radius: 6px;
122
+ font-family: monospace;
123
+ font-size: 13px;
124
+ margin-bottom: 20px;
125
+ word-break: break-all;
126
+ border: 1px solid #d2d2d7;
127
+ }
128
+ .row {
129
+ margin-bottom: 16px;
130
+ display: grid;
131
+ grid-template-columns: 320px 1fr;
132
+ align-items: center;
133
+ gap: 15px;
134
+ }
135
+ label {
136
+ font-weight: 500;
137
+ font-size: 14px;
138
+ color: #333;
139
+ word-break: break-all;
140
+ }
141
+ .input-group {
142
+ display: flex;
143
+ gap: 8px;
144
+ }
145
+ input[type="text"] {
146
+ flex: 1;
147
+ padding: 8px 12px;
148
+ border: 1px solid #ccc;
149
+ border-radius: 6px;
150
+ font-size: 14px;
151
+ font-family: monospace;
152
+ outline: none;
153
+ }
154
+ input[type="text"]:focus {
155
+ border-color: #d97757;
156
+ box-shadow: 0 0 0 2px rgba(217,119,87,0.2);
157
+ }
158
+ .btn-clear {
159
+ background-color: #e3e3e3;
160
+ border: none;
161
+ color: #333;
162
+ padding: 8px 12px;
163
+ border-radius: 6px;
164
+ font-size: 12px;
165
+ cursor: pointer;
166
+ transition: background 0.2s;
167
+ }
168
+ .btn-clear:hover {
169
+ background-color: #d5d5d5;
170
+ }
171
+ .actions {
172
+ margin-top: 30px;
173
+ padding-top: 20px;
174
+ border-top: 1px solid #e5e5e7;
175
+ display: flex;
176
+ gap: 12px;
177
+ }
178
+ .btn-save {
179
+ background-color: #d97757;
180
+ color: white;
181
+ border: none;
182
+ padding: 10px 20px;
183
+ border-radius: 6px;
184
+ font-weight: 600;
185
+ font-size: 15px;
186
+ cursor: pointer;
187
+ transition: opacity 0.2s;
188
+ }
189
+ .btn-save:hover {
190
+ opacity: 0.9;
191
+ }
192
+ .btn-add {
193
+ background-color: #0071e3;
194
+ color: white;
195
+ border: none;
196
+ padding: 10px 16px;
197
+ border-radius: 6px;
198
+ font-size: 14px;
199
+ cursor: pointer;
200
+ }
201
+ .btn-add:hover {
202
+ opacity: 0.9;
203
+ }
204
+ .btn-shutdown {
205
+ background-color: #ff3b30;
206
+ color: white;
207
+ border: none;
208
+ padding: 10px 16px;
209
+ border-radius: 6px;
210
+ font-size: 14px;
211
+ font-weight: 600;
212
+ cursor: pointer;
213
+ margin-left: auto;
214
+ }
215
+ .btn-shutdown:hover {
216
+ opacity: 0.9;
217
+ }
218
+ .message {
219
+ padding: 12px;
220
+ border-radius: 6px;
221
+ margin-bottom: 20px;
222
+ font-size: 14px;
223
+ font-weight: 500;
224
+ }
225
+ .success {
226
+ background-color: #e8f5e9;
227
+ color: #2e7d32;
228
+ border: 1px solid #c8e6c9;
229
+ }
230
+ .error {
231
+ background-color: #ffebee;
232
+ color: #c62828;
233
+ border: 1px solid #ffcdd2;
234
+ }
235
+ /* Modal for adding custom key */
236
+ #add-modal {
237
+ display: none;
238
+ position: fixed;
239
+ top: 0; left: 0; right: 0; bottom: 0;
240
+ background: rgba(0,0,0,0.5);
241
+ justify-content: center;
242
+ align-items: center;
243
+ z-index: 100;
244
+ }
245
+ .modal-content {
246
+ background: white;
247
+ padding: 24px;
248
+ border-radius: 12px;
249
+ width: 400px;
250
+ box-shadow: 0 4px 24px rgba(0,0,0,0.15);
251
+ }
252
+ .modal-content h3 {
253
+ margin-top: 0;
254
+ margin-bottom: 15px;
255
+ }
256
+ .modal-content input {
257
+ width: 93%;
258
+ padding: 8px 12px;
259
+ margin-bottom: 15px;
260
+ border: 1px solid #ccc;
261
+ border-radius: 6px;
262
+ }
263
+ .modal-actions {
264
+ display: flex;
265
+ justify-content: flex-end;
266
+ gap: 10px;
267
+ }
268
+ .btn-cancel {
269
+ background: #e5e5e7;
270
+ border: none;
271
+ padding: 8px 16px;
272
+ border-radius: 6px;
273
+ cursor: pointer;
274
+ }
275
+ .btn-confirm {
276
+ background: #0071e3;
277
+ color: white;
278
+ border: none;
279
+ padding: 8px 16px;
280
+ border-radius: 6px;
281
+ cursor: pointer;
282
+ }
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <div class="container">
287
+ <h1>Claude CLI Settings Editor</h1>
288
+ <div class="subtitle">Chỉnh sửa file cấu hình biến môi trường của Claude Code cho macOS</div>
289
+
290
+ <div class="path-box">
291
+ <strong>Đường dẫn file:</strong> ${getClaudeSettingsPath()}
292
+ </div>
293
+
294
+ ${message}
295
+
296
+ <form id="settings-form" method="POST" action="/save">
297
+ <div class="env-rows">
298
+ ${rowsHtml}
299
+ </div>
300
+
301
+ <div class="actions">
302
+ <button type="submit" class="btn-save">Lưu Cấu Hình</button>
303
+ <button type="button" class="btn-add" onclick="showModal()">+ Thêm Biến Custom</button>
304
+ <button type="button" class="btn-shutdown" onclick="shutdownServer()">Tắt Server</button>
305
+ </div>
306
+ </form>
307
+ </div>
308
+
309
+ <div id="add-modal">
310
+ <div class="modal-content">
311
+ <h3>Thêm Biến Môi Trường Custom</h3>
312
+ <input type="text" id="new-key-input" placeholder="Ví dụ: MY_CUSTOM_ENV_VAR" style="font-family: monospace;">
313
+ <div class="modal-actions">
314
+ <button class="btn-cancel" onclick="closeModal()">Hủy</button>
315
+ <button class="btn-confirm" onclick="addNewKey()">Thêm</button>
316
+ </div>
317
+ </div>
318
+ </div>
319
+
320
+ <script>
321
+ function shutdownServer() {
322
+ fetch('/shutdown', { method: 'POST' })
323
+ .then(() => {
324
+ // Gợi ý trình duyệt đóng tab ngay lập tức
325
+ window.close();
326
+ // Dự phòng trong trường hợp window.close() bị trình duyệt chặn (do tab không được mở bằng window.open)
327
+ setTimeout(() => {
328
+ document.body.innerHTML = '<div style="max-width: 600px; margin: 50px auto; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center;"><h2>Server đã tắt thành công!</h2><p>Tab này sẽ tự đóng hoặc bạn có thể đóng thủ công.</p></div>';
329
+ }, 100);
330
+ })
331
+ .catch(err => {
332
+ alert('Có lỗi xảy ra khi tắt server.');
333
+ });
334
+ }
335
+ function showModal() {
336
+ document.getElementById('add-modal').style.display = 'flex';
337
+ document.getElementById('new-key-input').focus();
338
+ }
339
+ function closeModal() {
340
+ document.getElementById('add-modal').style.display = 'none';
341
+ document.getElementById('new-key-input').value = '';
342
+ }
343
+ function addNewKey() {
344
+ const keyInput = document.getElementById('new-key-input').value.trim().toUpperCase();
345
+ if (!keyInput) return;
346
+
347
+ // Kiểm tra xem đã có field đó chưa
348
+ if (document.getElementById('env_' + keyInput)) {
349
+ alert('Biến này đã tồn tại!');
350
+ return;
351
+ }
352
+
353
+ const container = document.querySelector('.env-rows');
354
+ const newRow = document.createElement('div');
355
+ newRow.className = 'row';
356
+ newRow.innerHTML = \`
357
+ <label for="env_\${keyInput}">\${keyInput}</label>
358
+ <div class="input-group">
359
+ <input type="text" id="env_\${keyInput}" name="env_\${keyInput}" value="" placeholder="Nhập giá trị">
360
+ <button type="button" class="btn-clear" onclick="document.getElementById('env_\${keyInput}').value = ''">Xóa</button>
361
+ </div>
362
+ \`;
363
+ container.appendChild(newRow);
364
+ closeModal();
365
+
366
+ // Cuộn xuống
367
+ newRow.scrollIntoView({ behavior: 'smooth' });
368
+ }
369
+ </script>
370
+ </body>
371
+ </html>
372
+ `;
373
+ };
374
+
375
+ // Khởi tạo server
376
+ const server = http.createServer((req, res) => {
377
+ const url = req.url;
378
+
379
+ if (req.method === 'GET' && (url === '/' || url === '/index.html')) {
380
+ const settings = readSettings();
381
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
382
+ res.end(htmlTemplate(settings.env));
383
+ }
384
+ else if (req.method === 'POST' && url === '/save') {
385
+ let body = '';
386
+ req.on('data', chunk => {
387
+ body += chunk.toString();
388
+ });
389
+
390
+ req.on('end', () => {
391
+ // Parse application/x-www-form-urlencoded
392
+ const params = new URLSearchParams(body);
393
+ const newEnv = {};
394
+
395
+ for (const [key, value] of params.entries()) {
396
+ if (key.startsWith('env_')) {
397
+ const envKey = key.slice(4); // Lấy tên biến (bỏ 'env_')
398
+ if (value.trim() !== '') {
399
+ newEnv[envKey] = value.trim();
400
+ }
401
+ }
402
+ }
403
+
404
+ const settings = readSettings();
405
+ settings.env = newEnv;
406
+ settings.hasCompletedOnboarding = true;
407
+
408
+ const success = saveSettings(settings);
409
+
410
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
411
+ if (success) {
412
+ res.end(htmlTemplate(newEnv, `<div class="message success">Lưu cấu hình thành công! Đã lưu vào file config.</div>`));
413
+ } else {
414
+ res.end(htmlTemplate(newEnv, `<div class="message error">Lưu thất bại! Đã xảy ra lỗi khi ghi file settings.json.</div>`));
415
+ }
416
+ });
417
+ }
418
+ else if (req.method === 'POST' && url === '/shutdown') {
419
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
420
+ res.end('Server shutting down...');
421
+ console.log('Shutdown request received. Stopping server...');
422
+ setTimeout(() => {
423
+ process.exit(0);
424
+ }, 500);
425
+ }
426
+ else {
427
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
428
+ res.end('Not Found');
429
+ }
430
+ });
431
+
432
+ server.listen(PORT, () => {
433
+ console.log(`Server is running at http://localhost:${PORT}`);
434
+ });