50c 1.5.0 → 2.0.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/bin/50c.js CHANGED
@@ -1,289 +1,241 @@
1
1
  #!/usr/bin/env node
2
- const https = require('https');
3
- const readline = require('readline');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
-
8
- const API_ENDPOINT = process.env.FIFTYC_ENDPOINT || 'https://50c.ai';
9
- const MCP_ENDPOINT = API_ENDPOINT + '/mcp';
10
- const API_KEY = process.env.FIFTYC_API_KEY;
11
-
12
- // Version
13
- if (process.argv.includes('--version') || process.argv.includes('-v')) {
14
- console.log(require('../package.json').version);
15
- process.exit(0);
16
- }
17
-
18
- // Install MCP to IDE
19
- if (process.argv.includes('--install') || process.argv.includes('install')) {
20
- installMCP();
21
- process.exit(0);
22
- }
2
+ /**
3
+ * 50c CLI & MCP Server
4
+ * The AI toolkit - one package, all tools, works everywhere
5
+ */
23
6
 
24
- // Help
25
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
26
- showHelp();
27
- process.exit(0);
28
- }
7
+ const readline = require('readline');
8
+ const lib = require('../lib');
29
9
 
30
- function installMCP() {
31
- const home = os.homedir();
32
- const isWin = process.platform === 'win32';
33
- const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
34
- const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
35
-
36
- const ides = [
37
- // Desktop IDEs
38
- { name: 'Claude Desktop', path: isWin ? path.join(appData, 'Claude', 'claude_desktop_config.json') : path.join(home, '.claude', 'claude_desktop_config.json'), key: 'mcpServers' },
39
- { name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json'), key: 'mcpServers' },
40
- { name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), key: 'mcpServers' },
41
- { name: 'VS Code', path: path.join(home, '.vscode', 'mcp.json'), key: 'servers' },
42
- { name: 'VS Code Insiders', path: path.join(home, '.vscode-insiders', 'mcp.json'), key: 'servers' },
43
- { name: 'VSCodium', path: path.join(home, '.vscodium', 'mcp.json'), key: 'servers' },
44
- { name: 'Verdent', path: path.join(home, '.verdent', 'mcp.json'), key: 'mcpServers' },
45
- // Roo Code / Continue / Cline
46
- { name: 'Roo Code', path: path.join(home, '.roo-code', 'mcp.json'), key: 'mcpServers' },
47
- { name: 'Roo Code (AppData)', path: isWin ? path.join(appData, 'Roo-Code', 'mcp.json') : null, key: 'mcpServers' },
48
- { name: 'Continue', path: path.join(home, '.continue', 'mcp.json'), key: 'mcpServers' },
49
- { name: 'Cline', path: path.join(home, '.cline', 'mcp.json'), key: 'mcpServers' },
50
- { name: 'Cline (VS Code)', path: isWin ? path.join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json') : path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), key: 'mcpServers' },
51
- // JetBrains IDEs
52
- { name: 'JetBrains', path: path.join(home, '.jb-mcp', 'mcp.json'), key: 'mcpServers' },
53
- { name: 'IntelliJ IDEA', path: isWin ? path.join(appData, 'JetBrains', 'IntelliJIdea', 'mcp.json') : path.join(home, '.config', 'JetBrains', 'mcp.json'), key: 'mcpServers' },
54
- // Augment
55
- { name: 'Augment', path: path.join(home, '.augment', 'mcp.json'), key: 'mcpServers' },
56
- // Zed
57
- { name: 'Zed', path: path.join(home, '.config', 'zed', 'settings.json'), key: 'context_servers', nested: true },
58
- ].filter(ide => ide.path); // Remove null paths
59
-
60
- const mcpEntry = {
61
- command: 'npx',
62
- args: ['-y', '50c'],
63
- env: { FIFTYC_API_KEY: API_KEY || '<YOUR_API_KEY>' }
64
- };
10
+ // MCP Protocol Handler
11
+ async function handleMCP(req) {
12
+ const { id, method, params } = req;
65
13
 
66
- // Zed uses different format
67
- const zedEntry = {
68
- command: { path: 'npx', args: ['-y', '50c'], env: { FIFTYC_API_KEY: API_KEY || '<YOUR_API_KEY>' } }
69
- };
70
-
71
- let installed = [];
72
- let hostedNote = false;
73
-
74
- for (const ide of ides) {
75
- try {
76
- let config = {};
77
- const dir = path.dirname(ide.path);
78
-
79
- if (fs.existsSync(ide.path)) {
80
- config = JSON.parse(fs.readFileSync(ide.path, 'utf8'));
81
- } else if (fs.existsSync(dir)) {
82
- // Dir exists but no config file - create it
83
- } else {
84
- continue; // IDE not installed
14
+ if (method === 'initialize') {
15
+ return {
16
+ jsonrpc: '2.0',
17
+ id,
18
+ result: {
19
+ protocolVersion: '2024-11-05',
20
+ capabilities: { tools: { listChanged: true } },
21
+ serverInfo: { name: '50c', version: '2.0.0' }
85
22
  }
86
-
87
- if (!config[ide.key]) config[ide.key] = {};
88
- if (config[ide.key]['50c']) {
89
- console.log(`[skip] ${ide.name} - already configured`);
90
- continue;
91
- }
92
-
93
- config[ide.key]['50c'] = ide.name === 'Zed' ? zedEntry : mcpEntry;
94
- fs.mkdirSync(dir, { recursive: true });
95
- fs.writeFileSync(ide.path, JSON.stringify(config, null, 2));
96
- installed.push(ide.name);
97
- console.log(`[done] ${ide.name} - added 50c`);
98
- } catch (e) {
99
- // Skip silently
100
- }
23
+ };
101
24
  }
102
25
 
103
- console.log('');
104
- if (installed.length > 0) {
105
- console.log(`Installed to: ${installed.join(', ')}`);
106
- console.log('');
107
- if (!API_KEY) {
108
- console.log('Next: Set FIFTYC_API_KEY in the config or as env var');
109
- console.log('Get key at: https://50c.ai');
110
- } else {
111
- console.log('Restart your IDE to activate 50c tools.');
112
- }
113
- } else {
114
- console.log('No local IDEs detected.');
26
+ if (method === 'notifications/initialized') {
27
+ return null;
115
28
  }
116
29
 
117
- console.log('');
118
- console.log('Hosted IDEs (manual setup via their MCP settings):');
119
- console.log(' Bolt.new, Replit, Lovable, Mocha, v0.dev');
120
- console.log(' Use: npx -y 50c (command) with FIFTYC_API_KEY env');
121
- }
122
-
123
- function showHelp() {
124
- console.log(`
125
- 50c - AI Augmentation CLI & MCP Server
126
-
127
- QUICK START:
128
- npx 50c install Auto-configure MCP for your IDE
129
-
130
- USAGE:
131
- 50c <command> <input> Direct CLI mode
132
- 50c MCP mode (JSON-RPC via stdin)
133
-
134
- COMMANDS:
135
- install Add 50c to all detected IDEs
136
- genius <problem> Deep problem solving ($0.50)
137
- hints <topic> 5 brutal 2-word hints ($0.05)
138
- hints+ <topic> 10 expanded hints ($0.10)
139
- vibe <working_on> 3 unconventional ideas ($0.05)
140
- one-liner <product> Elevator pitch in 8 words ($0.02)
141
- roast <code> Brutal code review ($0.05)
142
- name-it <does> 5 names + domain check ($0.03)
143
- price-it <product> SaaS pricing strategy ($0.05)
144
- compute <code> Execute Python code ($0.02)
145
-
146
- EXAMPLES:
147
- npx 50c install
148
- 50c genius "How do I scale PostgreSQL?"
149
- 50c hints "api design"
150
-
151
- ENVIRONMENT:
152
- FIFTYC_API_KEY Your API key (required)
153
-
154
- MCP MODE:
155
- Run without arguments for IDE integration (Claude Desktop, Cursor, etc.)
156
- `);
157
- }
158
-
159
- // CLI commands
160
- const COMMANDS = ['genius', 'hints', 'hints+', 'vibe', 'compute', 'one-liner', 'roast', 'name-it', 'price-it', 'mind-opener'];
161
- const command = process.argv[2];
162
- const input = process.argv.slice(3).join(' ');
163
-
164
- // Check API key (not needed for install/help/version)
165
- if (!API_KEY && command && COMMANDS.includes(command)) {
166
- console.error('Error: FIFTYC_API_KEY environment variable required');
167
- console.error('Get your key at: https://50c.ai');
168
- process.exit(1);
169
- }
170
-
171
- if (command && COMMANDS.includes(command)) {
172
- if (!input) {
173
- console.error(`Error: ${command} requires input`);
174
- process.exit(1);
30
+ if (method === 'tools/list') {
31
+ const tools = await lib.getTools();
32
+ return { jsonrpc: '2.0', id, result: { tools } };
175
33
  }
176
34
 
177
- runCLI(command, input).then(result => {
178
- console.log(result);
179
- process.exit(0);
180
- }).catch(err => {
181
- console.error('Error:', err.message);
182
- process.exit(1);
183
- });
184
- } else if (command) {
185
- startMCPMode();
186
- } else {
187
- startMCPMode();
188
- }
189
-
190
- async function runCLI(cmd, query) {
191
- // Map CLI commands to MCP tool names
192
- const toolMap = {
193
- 'genius': { name: 'genius', arg: 'problem' },
194
- 'hints': { name: 'hints', arg: 'query' },
195
- 'hints+': { name: 'hints_plus', arg: 'query' },
196
- 'vibe': { name: 'quick_vibe', arg: 'working_on' },
197
- 'compute': { name: 'compute', arg: 'code' },
198
- 'one-liner': { name: 'one_liner', arg: 'product' },
199
- 'roast': { name: 'roast', arg: 'code' },
200
- 'name-it': { name: 'name_it', arg: 'does' },
201
- 'price-it': { name: 'price_it', arg: 'product' },
202
- 'mind-opener': { name: 'mind_opener', arg: 'problem' }
203
- };
204
-
205
- const tool = toolMap[cmd];
206
- if (!tool) throw new Error(`Unknown command: ${cmd}`);
35
+ if (method === 'tools/call') {
36
+ const result = await lib.handleTool(params?.name, params?.arguments || {});
37
+ return {
38
+ jsonrpc: '2.0',
39
+ id,
40
+ result: {
41
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
42
+ }
43
+ };
44
+ }
207
45
 
208
- const request = {
46
+ return {
209
47
  jsonrpc: '2.0',
210
- id: 1,
211
- method: 'tools/call',
212
- params: {
213
- name: tool.name,
214
- arguments: { [tool.arg]: query }
215
- }
48
+ id,
49
+ error: { code: -32601, message: 'Method not found' }
216
50
  };
217
-
218
- const response = await callRemoteMCP(request);
219
-
220
- if (response.error) {
221
- throw new Error(response.error.message || JSON.stringify(response.error));
222
- }
223
-
224
- return response.result?.content?.[0]?.text || 'No output';
225
51
  }
226
52
 
227
- function startMCPMode() {
228
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
53
+ // MCP Server Mode
54
+ async function runMCP() {
55
+ const rl = readline.createInterface({
56
+ input: process.stdin,
57
+ output: process.stdout,
58
+ terminal: false
59
+ });
229
60
 
230
61
  rl.on('line', async (line) => {
231
62
  try {
232
- const clean = line.trim();
233
- if (!clean) return;
234
- const request = JSON.parse(clean);
235
- const response = await callRemoteMCP(request);
236
- process.stdout.write(JSON.stringify(response) + '\n');
237
- } catch (e) {
238
- process.stdout.write(JSON.stringify({
63
+ const req = JSON.parse(line);
64
+ const res = await handleMCP(req);
65
+ if (res) console.log(JSON.stringify(res));
66
+ } catch (err) {
67
+ console.log(JSON.stringify({
239
68
  jsonrpc: '2.0',
240
69
  id: null,
241
- error: { code: -32603, message: e.message }
242
- }) + '\n');
70
+ error: { code: -32700, message: 'Parse error' }
71
+ }));
243
72
  }
244
73
  });
245
74
  }
246
75
 
247
- function callRemoteMCP(request) {
248
- return new Promise((resolve, reject) => {
249
- const url = new URL(MCP_ENDPOINT);
250
- const postData = JSON.stringify(request);
251
-
252
- const options = {
253
- hostname: url.hostname,
254
- port: url.port || 443,
255
- path: url.pathname,
256
- method: 'POST',
257
- headers: {
258
- 'Content-Type': 'application/json',
259
- 'Content-Length': Buffer.byteLength(postData),
260
- 'Authorization': `Bearer ${API_KEY}`,
261
- 'User-Agent': '50c-cli/1.4.0'
76
+ // CLI Mode
77
+ async function runCLI(args) {
78
+ const cmd = args[0];
79
+ const cmdArgs = args.slice(1);
80
+
81
+ switch (cmd) {
82
+ case 'status':
83
+ console.log(JSON.stringify(await lib.getStatus(), null, 2));
84
+ break;
85
+
86
+ case 'discover':
87
+ console.log(JSON.stringify(await lib.packs.discover(), null, 2));
88
+ break;
89
+
90
+ case 'enable':
91
+ console.log(JSON.stringify(await lib.packs.enablePack(cmdArgs[0]), null, 2));
92
+ break;
93
+
94
+ case 'disable':
95
+ console.log(JSON.stringify(await lib.packs.disablePack(cmdArgs[0]), null, 2));
96
+ break;
97
+
98
+ case 'packs':
99
+ console.log(JSON.stringify(await lib.packs.listPacks(), null, 2));
100
+ break;
101
+
102
+ // Vault commands
103
+ case 'vault':
104
+ const vaultCmd = cmdArgs[0];
105
+ const vaultArgs = cmdArgs.slice(1);
106
+
107
+ if (vaultCmd === 'init') {
108
+ const passphrase = process.env.VAULT_PASSPHRASE || vaultArgs[0];
109
+ if (!passphrase) {
110
+ console.error('Usage: 50c vault init <passphrase>');
111
+ console.error('Or set VAULT_PASSPHRASE env var');
112
+ process.exit(1);
113
+ }
114
+ console.log(JSON.stringify(await lib.vault.init(passphrase), null, 2));
262
115
  }
263
- };
264
-
265
- const req = https.request(options, (res) => {
266
- let data = '';
267
- res.on('data', chunk => data += chunk);
268
- res.on('end', () => {
116
+ else if (vaultCmd === 'unlock') {
117
+ const passphrase = process.env.VAULT_PASSPHRASE || vaultArgs[0];
118
+ if (!passphrase) {
119
+ console.error('Usage: 50c vault unlock <passphrase>');
120
+ process.exit(1);
121
+ }
122
+ console.log(JSON.stringify(await lib.vault.unlock(passphrase), null, 2));
123
+ }
124
+ else if (vaultCmd === 'lock') {
125
+ console.log(JSON.stringify(lib.vault.lock(), null, 2));
126
+ }
127
+ else if (vaultCmd === 'yolo') {
128
+ const passphrase = process.env.VAULT_PASSPHRASE || vaultArgs[0];
129
+ if (!passphrase) {
130
+ console.error('Usage: 50c vault yolo <passphrase>');
131
+ process.exit(1);
132
+ }
133
+ console.log(JSON.stringify(await lib.vault.yolo(passphrase), null, 2));
134
+ }
135
+ else if (vaultCmd === 'add') {
136
+ const [name, ...valueParts] = vaultArgs;
137
+ const value = valueParts.join(' ');
138
+ if (!name || !value) {
139
+ console.error('Usage: 50c vault add <name> <value>');
140
+ process.exit(1);
141
+ }
142
+ console.log(JSON.stringify(await lib.vault.add(name, value), null, 2));
143
+ }
144
+ else if (vaultCmd === 'get') {
145
+ const name = vaultArgs[0];
146
+ if (!name) {
147
+ console.error('Usage: 50c vault get <name>');
148
+ process.exit(1);
149
+ }
269
150
  try {
270
- resolve(JSON.parse(data));
151
+ console.log(await lib.vault.get(name));
271
152
  } catch (e) {
272
- reject(new Error(`Invalid response: ${data.substring(0, 200)}`));
153
+ console.error(e.message);
154
+ process.exit(1);
273
155
  }
274
- });
275
- });
276
-
277
- req.setTimeout(120000, () => {
278
- req.destroy();
279
- reject(new Error('Request timeout'));
280
- });
281
-
282
- req.on('error', reject);
283
- req.write(postData);
284
- req.end();
285
- });
156
+ }
157
+ else if (vaultCmd === 'list') {
158
+ const namespace = vaultArgs[0];
159
+ const list = await lib.vault.list(namespace);
160
+ list.forEach(n => console.log(n));
161
+ }
162
+ else if (vaultCmd === 'delete' || vaultCmd === 'rm') {
163
+ const name = vaultArgs[0];
164
+ if (!name) {
165
+ console.error('Usage: 50c vault delete <name>');
166
+ process.exit(1);
167
+ }
168
+ console.log(JSON.stringify(await lib.vault.remove(name), null, 2));
169
+ }
170
+ else if (vaultCmd === 'status') {
171
+ console.log(JSON.stringify(await lib.vault.status(), null, 2));
172
+ }
173
+ else {
174
+ console.log(`50c vault commands:
175
+ init <passphrase> Initialize vault
176
+ unlock <passphrase> Unlock for session
177
+ lock Lock immediately
178
+ yolo <passphrase> Stay unlocked (dev mode)
179
+ add <name> <value> Add credential
180
+ get <name> Get credential
181
+ list [namespace] List credentials
182
+ delete <name> Delete credential
183
+ status Check vault status`);
184
+ }
185
+ break;
186
+
187
+ case 'help':
188
+ default:
189
+ console.log(`50c - The AI Toolkit
190
+
191
+ Usage:
192
+ 50c Start MCP server (for AI tools)
193
+ 50c status Show status
194
+ 50c discover Show available packs
195
+ 50c enable <pack> Enable a pack
196
+ 50c disable <pack> Disable a pack
197
+ 50c packs List packs
198
+ 50c vault <cmd> Vault commands
199
+
200
+ Packs:
201
+ vault Secure credentials (always on)
202
+ whm WHM/cPanel/SSH (39 tools)
203
+ cf Cloudflare (34 tools)
204
+ wp WordPress (14 tools)
205
+ ux UI/UX toolkit (17 tools)
206
+
207
+ MCP Config:
208
+ {
209
+ "mcpServers": {
210
+ "50c": {
211
+ "command": "50c",
212
+ "env": { "FIFTY_CENT_API_KEY": "cv_xxx" }
213
+ }
214
+ }
215
+ }
216
+
217
+ More info: https://50c.ai/docs`);
218
+ }
286
219
  }
287
220
 
288
- process.on('SIGINT', () => process.exit(130));
289
- process.on('SIGTERM', () => process.exit(143));
221
+ // Main
222
+ async function main() {
223
+ const args = process.argv.slice(2);
224
+
225
+ // MCP mode: --mcp flag or no args AND piped input
226
+ const isMCPMode = args[0] === '--mcp' || (args.length === 0 && !process.stdin.isTTY);
227
+
228
+ if (isMCPMode) {
229
+ await runMCP();
230
+ } else if (args.length === 0) {
231
+ // No args, show help
232
+ await runCLI(['help']);
233
+ } else {
234
+ await runCLI(args);
235
+ }
236
+ }
237
+
238
+ main().catch(err => {
239
+ console.error('Error:', err.message);
240
+ process.exit(1);
241
+ });
package/lib/config.js ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * 50c Config & Mode Detection
3
+ * Handles local vs cloud mode automatically
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const https = require('https');
10
+
11
+ // Storage locations by OS
12
+ function getLocalDir() {
13
+ if (process.platform === 'win32') {
14
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), '50c');
15
+ } else if (process.platform === 'darwin') {
16
+ return path.join(os.homedir(), 'Library', 'Application Support', '50c');
17
+ } else {
18
+ return path.join(os.homedir(), '.local', 'share', '50c');
19
+ }
20
+ }
21
+
22
+ const LOCAL_DIR = getLocalDir();
23
+ const CONFIG_FILE = path.join(LOCAL_DIR, 'config.json');
24
+ const VAULT_DIR = path.join(LOCAL_DIR, 'vault');
25
+ const API_URL = process.env.FIFTY_CENT_API_URL || 'https://api.50c.ai';
26
+
27
+ // Default config
28
+ const DEFAULT_CONFIG = {
29
+ api_key: '',
30
+ packs: {
31
+ vault: true, // Always on
32
+ whm: false,
33
+ cf: false,
34
+ wp: false,
35
+ ux: false
36
+ },
37
+ vault: {
38
+ yolo_mode: false,
39
+ session_ttl: 3600,
40
+ idle_timeout: 1800
41
+ }
42
+ };
43
+
44
+ // Detect mode: local or cloud
45
+ function detectMode() {
46
+ // Explicit override
47
+ if (process.env.FIFTY_CENT_MODE === 'cloud') return 'cloud';
48
+ if (process.env.FIFTY_CENT_MODE === 'local') return 'local';
49
+
50
+ // Cloud indicators
51
+ if (process.env.REPL_ID) return 'cloud'; // Replit
52
+ if (process.env.LOVABLE_PROJECT) return 'cloud'; // Lovable
53
+ if (process.env.CODESPACE_NAME) return 'cloud'; // GitHub Codespaces
54
+ if (process.env.CODESANDBOX_SSE) return 'cloud'; // CodeSandbox
55
+ if (process.env.GITPOD_WORKSPACE_ID) return 'cloud'; // Gitpod
56
+
57
+ // Check if we can write locally
58
+ try {
59
+ if (!fs.existsSync(LOCAL_DIR)) {
60
+ fs.mkdirSync(LOCAL_DIR, { recursive: true, mode: 0o700 });
61
+ }
62
+ const testFile = path.join(LOCAL_DIR, '.write-test');
63
+ fs.writeFileSync(testFile, 'test', { mode: 0o600 });
64
+ fs.unlinkSync(testFile);
65
+ return 'local';
66
+ } catch {
67
+ return 'cloud';
68
+ }
69
+ }
70
+
71
+ const MODE = detectMode();
72
+
73
+ // Ensure local directory exists
74
+ function ensureLocalDir() {
75
+ if (MODE !== 'local') return;
76
+ if (!fs.existsSync(LOCAL_DIR)) {
77
+ fs.mkdirSync(LOCAL_DIR, { recursive: true, mode: 0o700 });
78
+ }
79
+ if (!fs.existsSync(VAULT_DIR)) {
80
+ fs.mkdirSync(VAULT_DIR, { recursive: true, mode: 0o700 });
81
+ }
82
+ }
83
+
84
+ // Load config (local or cloud)
85
+ async function loadConfig() {
86
+ if (MODE === 'local') {
87
+ return loadConfigLocal();
88
+ } else {
89
+ return loadConfigCloud();
90
+ }
91
+ }
92
+
93
+ function loadConfigLocal() {
94
+ try {
95
+ if (fs.existsSync(CONFIG_FILE)) {
96
+ const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
97
+ return { ...DEFAULT_CONFIG, ...saved, packs: { ...DEFAULT_CONFIG.packs, ...saved.packs } };
98
+ }
99
+ } catch {}
100
+ return { ...DEFAULT_CONFIG };
101
+ }
102
+
103
+ async function loadConfigCloud() {
104
+ const apiKey = process.env.FIFTY_CENT_API_KEY || process.env.FIFTYC_API_KEY;
105
+ if (!apiKey) return { ...DEFAULT_CONFIG };
106
+
107
+ try {
108
+ const response = await apiRequest('GET', '/user/config');
109
+ if (response && response.packs) {
110
+ return { ...DEFAULT_CONFIG, ...response, packs: { ...DEFAULT_CONFIG.packs, ...response.packs } };
111
+ }
112
+ } catch {}
113
+ return { ...DEFAULT_CONFIG };
114
+ }
115
+
116
+ // Save config (local or cloud)
117
+ async function saveConfig(config) {
118
+ if (MODE === 'local') {
119
+ return saveConfigLocal(config);
120
+ } else {
121
+ return saveConfigCloud(config);
122
+ }
123
+ }
124
+
125
+ function saveConfigLocal(config) {
126
+ ensureLocalDir();
127
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
128
+ }
129
+
130
+ async function saveConfigCloud(config) {
131
+ const apiKey = process.env.FIFTY_CENT_API_KEY || process.env.FIFTYC_API_KEY;
132
+ if (!apiKey) throw new Error('API key required for cloud mode');
133
+
134
+ await apiRequest('PUT', '/user/config', { packs: config.packs, vault: config.vault });
135
+ }
136
+
137
+ // API request helper
138
+ function apiRequest(method, endpoint, body = null) {
139
+ return new Promise((resolve, reject) => {
140
+ const apiKey = process.env.FIFTY_CENT_API_KEY || process.env.FIFTYC_API_KEY || '';
141
+ const url = new URL(endpoint, API_URL);
142
+
143
+ const options = {
144
+ hostname: url.hostname,
145
+ port: url.port || 443,
146
+ path: url.pathname,
147
+ method: method,
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ 'Authorization': `Bearer ${apiKey}`,
151
+ 'User-Agent': '50c/2.0.0'
152
+ }
153
+ };
154
+
155
+ const req = https.request(options, (res) => {
156
+ let data = '';
157
+ res.on('data', chunk => data += chunk);
158
+ res.on('end', () => {
159
+ try {
160
+ resolve(JSON.parse(data));
161
+ } catch {
162
+ resolve({ raw: data });
163
+ }
164
+ });
165
+ });
166
+
167
+ req.on('error', reject);
168
+ if (body) req.write(JSON.stringify(body));
169
+ req.end();
170
+ });
171
+ }
172
+
173
+ module.exports = {
174
+ MODE,
175
+ LOCAL_DIR,
176
+ VAULT_DIR,
177
+ CONFIG_FILE,
178
+ API_URL,
179
+ DEFAULT_CONFIG,
180
+ detectMode,
181
+ ensureLocalDir,
182
+ loadConfig,
183
+ saveConfig,
184
+ apiRequest
185
+ };