@sharnix/agent 1.0.3 → 1.0.5

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/index.js +138 -16
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -30,7 +30,8 @@ if (has('--help') || has('-h')) {
30
30
  sharnix — tunnel your local app through Sharnix
31
31
 
32
32
  Usage:
33
- npx @sharnix/agent --port <port> [options]
33
+ npx @sharnix/agent setup First-time setup (no API key needed)
34
+ npx @sharnix/agent --port <port> Tunnel a local port
34
35
  SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000
35
36
 
36
37
  Options:
@@ -41,22 +42,28 @@ if (has('--help') || has('-h')) {
41
42
  --help, -h Show this message
42
43
 
43
44
  Environment:
44
- SHARNIX_API_KEY API key from relay.sharnix.com/app/settings (required)
45
+ SHARNIX_API_KEY API key from relay.sharnix.com/app/settings
45
46
  SHARNIX_URL Override relay base URL
46
47
  `);
47
48
  process.exit(0);
48
49
  }
49
50
 
50
- if (!API_KEY) {
51
- console.error('\n Error: SHARNIX_API_KEY is not set.\n');
52
- console.error(' Get your key at: https://relay.sharnix.com/app/settings\n');
53
- process.exit(1);
51
+ // ── Setup command (device-flow bootstrap, no API key required) ────────────────
52
+ if (args[0] === 'setup') {
53
+ runSetup().catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
54
+ } else {
55
+ if (!API_KEY) {
56
+ console.error('\n Error: SHARNIX_API_KEY is not set.');
57
+ console.error(' Run: npx @sharnix/agent setup\n');
58
+ process.exit(1);
59
+ }
60
+ if (isNaN(port) || port < 1 || port > 65535) {
61
+ console.error(`\n Error: invalid port "${get('--port') || get('-p')}"\n`);
62
+ process.exit(1);
63
+ }
64
+ main();
54
65
  }
55
66
 
56
- if (isNaN(port) || port < 1 || port > 65535) {
57
- console.error(`\n Error: invalid port "${get('--port') || get('-p')}"\n`);
58
- process.exit(1);
59
- }
60
67
 
61
68
  // ── Config persistence ────────────────────────────────────────────────────────
62
69
  const CONFIG_DIR = path.join(os.homedir(), '.sharnix');
@@ -135,7 +142,7 @@ function connect() {
135
142
  const wsUrl = `${WS_BASE}?agentId=${encodeURIComponent(creds.agentId)}&secret=${encodeURIComponent(creds.secret)}`;
136
143
  ws = new WebSocket(wsUrl);
137
144
 
138
- ws.on('open', () => {
145
+ ws.on('open', async () => {
139
146
  clearTimeout(reconnectTimer);
140
147
  ws.send(JSON.stringify({ type: MSG.REGISTER, tunnelId, label: label || agentName }));
141
148
  startHeartbeat();
@@ -255,9 +262,124 @@ process.on('SIGINT', () => {
255
262
  process.exit(0);
256
263
  });
257
264
 
258
- // ── Main ──────────────────────────────────────────────────────────────────────
259
- console.log(`\n Sharnix Agent → localhost:${port}\n`);
265
+ // ── Tunnel main ───────────────────────────────────────────────────────────────
266
+ function main() {
267
+ console.log(`\n Sharnix Agent → localhost:${port}\n`);
268
+ bootstrap()
269
+ .then((c) => { creds = c; connect(); })
270
+ .catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
271
+ }
272
+
273
+ // ── Setup: device-flow bootstrap ─────────────────────────────────────────────
274
+ async function runSetup() {
275
+ const { execSync, spawn } = require('child_process');
276
+
277
+ console.log('\n Sharnix Setup\n');
260
278
 
261
- bootstrap()
262
- .then((c) => { creds = c; connect(); })
263
- .catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
279
+ // Start device flow
280
+ process.stdout.write(' Starting setup…');
281
+ const r = await fetch(`${RELAY_BASE}/api/v1/setup-cli`, { method: 'POST' });
282
+ if (!r.ok) throw new Error('Could not reach relay.sharnix.com. Check your internet connection.');
283
+ const { code, authUrl } = await r.json();
284
+ process.stdout.write(' done\n\n');
285
+
286
+ console.log(' Opening your browser to authorize…');
287
+ console.log(` URL: ${authUrl}\n`);
288
+
289
+ // Open browser cross-platform
290
+ try {
291
+ const cmd = process.platform === 'win32' ? 'start' :
292
+ process.platform === 'darwin' ? 'open' : 'xdg-open';
293
+ if (process.platform === 'win32') {
294
+ spawn('cmd', ['/c', 'start', '', authUrl], { detached: true, stdio: 'ignore' });
295
+ } else {
296
+ spawn(cmd, [authUrl], { detached: true, stdio: 'ignore' });
297
+ }
298
+ } catch {}
299
+
300
+ // Poll until authorized
301
+ process.stdout.write(' Waiting for browser authorization');
302
+ let apiKey = null;
303
+ const deadline = Date.now() + 10 * 60 * 1000;
304
+ while (Date.now() < deadline) {
305
+ await new Promise((r) => setTimeout(r, 2000));
306
+ process.stdout.write('.');
307
+ const poll = await fetch(`${RELAY_BASE}/api/v1/setup-cli/${code}`);
308
+ if (!poll.ok) { process.stdout.write('\n'); throw new Error('Setup session expired.'); }
309
+ const data = await poll.json();
310
+ if (data.done) { apiKey = data.key; break; }
311
+ }
312
+ process.stdout.write('\n');
313
+ if (!apiKey) throw new Error('Setup timed out. Run npx @sharnix/agent setup again.');
314
+
315
+ console.log('\n API key received!\n');
316
+
317
+ // Save key to ~/.sharnix/config.json
318
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
319
+ saveJson(path.join(CONFIG_DIR, 'sharnix-key.json'), { apiKey, createdAt: new Date().toISOString() });
320
+
321
+ // Detect and write MCP configs
322
+ const mcpBlock = {
323
+ sharnix: {
324
+ command: 'npx',
325
+ args: ['-y', '@sharnix/mcp-server'],
326
+ env: { SHARNIX_API_KEY: apiKey },
327
+ },
328
+ };
329
+
330
+ const configTargets = getMcpConfigTargets();
331
+ const written = [];
332
+
333
+ for (const { label: editorLabel, file, wrapper } of configTargets) {
334
+ if (!fs.existsSync(path.dirname(file))) continue;
335
+ try {
336
+ let config = loadJson(file) || {};
337
+ if (wrapper === 'mcpServers') {
338
+ config.mcpServers = { ...(config.mcpServers || {}), ...mcpBlock };
339
+ } else {
340
+ config = { ...config, ...mcpBlock };
341
+ }
342
+ fs.mkdirSync(path.dirname(file), { recursive: true });
343
+ fs.writeFileSync(file, JSON.stringify(config, null, 2));
344
+ written.push(editorLabel);
345
+ } catch {}
346
+ }
347
+
348
+ if (written.length) {
349
+ console.log(` MCP config written for: ${written.join(', ')}`);
350
+ console.log(' Restart your editor to pick up the changes.\n');
351
+ } else {
352
+ console.log(' Could not auto-detect an editor config. Add manually:\n');
353
+ console.log(' File: ~/Library/Application Support/Claude/claude_desktop_config.json');
354
+ console.log(' (or %APPDATA%\\Claude\\claude_desktop_config.json on Windows)\n');
355
+ console.log(' Content:');
356
+ console.log(JSON.stringify({ mcpServers: mcpBlock }, null, 2) + '\n');
357
+ }
358
+
359
+ console.log(' To start tunneling:');
360
+ console.log(` SHARNIX_API_KEY=${apiKey} npx @sharnix/agent --port 3000\n`);
361
+ console.log(' Or save the key to your shell profile:');
362
+ console.log(` export SHARNIX_API_KEY=${apiKey}\n`);
363
+ process.exit(0);
364
+ }
365
+
366
+ function getMcpConfigTargets() {
367
+ const home = os.homedir();
368
+ const appData = process.env.APPDATA || '';
369
+ const targets = [];
370
+
371
+ if (process.platform === 'win32') {
372
+ targets.push({ label: 'Claude Desktop', file: path.join(appData, 'Claude', 'claude_desktop_config.json'), wrapper: 'mcpServers' });
373
+ targets.push({ label: 'Cursor', file: path.join(appData, 'Cursor', 'User', 'globalStorage', 'cursor.mcp', 'mcp.json'), wrapper: 'mcpServers' });
374
+ } else if (process.platform === 'darwin') {
375
+ targets.push({ label: 'Claude Desktop', file: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), wrapper: 'mcpServers' });
376
+ targets.push({ label: 'Cursor', file: path.join(home, '.cursor', 'mcp.json'), wrapper: 'mcpServers' });
377
+ targets.push({ label: 'Windsurf', file: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), wrapper: 'mcpServers' });
378
+ } else {
379
+ targets.push({ label: 'Claude Desktop', file: path.join(home, '.config', 'Claude', 'claude_desktop_config.json'), wrapper: 'mcpServers' });
380
+ targets.push({ label: 'Cursor', file: path.join(home, '.cursor', 'mcp.json'), wrapper: 'mcpServers' });
381
+ targets.push({ label: 'Windsurf', file: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), wrapper: 'mcpServers' });
382
+ }
383
+
384
+ return targets;
385
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sharnix/agent",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Tunnel your local app through Sharnix — share previews with one command",
5
5
  "keywords": [
6
6
  "tunnel",