@sharnix/agent 1.0.4 → 1.0.6

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 (3) hide show
  1. package/README.md +67 -25
  2. package/index.js +138 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,80 +2,122 @@
2
2
 
3
3
  Instantly share your local app with anyone — no config, no port forwarding, no deployment.
4
4
 
5
+ ## First-time setup (one command)
6
+
5
7
  ```bash
6
- SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000
8
+ npx @sharnix/agent setup
7
9
  ```
8
10
 
9
- ## What it does
11
+ This is all a new user needs to run. It will:
10
12
 
11
- `@sharnix/agent` opens a secure tunnel from [relay.sharnix.com](https://relay.sharnix.com) to your local machine. Visitors access your app through a shareable link — you never expose a port publicly.
13
+ 1. Open your browser to relay.sharnix.com sign in if you haven't already
14
+ 2. Click **"Authorize & create key"** — one click
15
+ 3. The CLI captures your API key automatically
16
+ 4. Writes the MCP config to Claude Desktop, Cursor, and Windsurf automatically (whichever are installed)
17
+ 5. Prints your key and tells you to restart your editor
12
18
 
13
- - **Stable tunnel ID** per project directory the same tunnel URL survives restarts
14
- - **Auto-provisioned** — creates an agent credential on first run, stored in `~/.sharnix/`
15
- - **Auto-suspends** share links when you disconnect, reactivates when you reconnect
16
- - **Works with any framework** — Next.js, Vite, Django, Rails, anything on localhost
19
+ After that, your AI agent has full MCP access and you can start tunneling.
17
20
 
18
- ## Requirements
21
+ ## Tunneling your app
19
22
 
20
- - Node.js 18 or later
21
- - A free Sharnix account at [relay.sharnix.com](https://relay.sharnix.com)
22
- - An API key from **Settings → API Keys**
23
+ ```bash
24
+ npx @sharnix/agent --port 3000
25
+ ```
23
26
 
24
- ## Usage
27
+ The API key is read from `SHARNIX_API_KEY` in your environment, or saved automatically by `setup`.
25
28
 
26
29
  ```bash
27
30
  # Tunnel port 3000
28
31
  SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000
29
32
 
30
- # Tunnel a different port with a label
33
+ # Tunnel and immediately print a share link (no dashboard needed)
34
+ SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000 --share
35
+
36
+ # With a label
31
37
  SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 8080 --label "staging"
32
38
  ```
33
39
 
34
- Set the key in your shell profile so you don't repeat it:
40
+ Save the key to your shell profile so you don't repeat it:
35
41
 
36
42
  ```bash
37
43
  # ~/.bashrc or ~/.zshrc
38
44
  export SHARNIX_API_KEY=shx_...
39
45
  ```
40
46
 
41
- Then just:
47
+ ## What it does
42
48
 
43
- ```bash
44
- npx @sharnix/agent --port 3000
45
- ```
49
+ `@sharnix/agent` opens a secure tunnel from [relay.sharnix.com](https://relay.sharnix.com) to your local machine. Visitors access your app through a shareable link — you never expose a port publicly.
50
+
51
+ - **Stable tunnel ID** per project directory — same URL survives restarts
52
+ - **Auto-provisioned** — creates agent credentials on first run, stored in `~/.sharnix/`
53
+ - **Auto-suspends** share links when you disconnect, reactivates when you reconnect
54
+ - **Works with any framework** — Next.js, Vite, Django, Rails, anything on localhost
46
55
 
47
56
  ## Options
48
57
 
49
58
  | Flag | Default | Description |
50
59
  |------|---------|-------------|
60
+ | `setup` | — | First-time setup: creates API key and writes MCP config automatically |
51
61
  | `--port`, `-p` | `3000` | Local port to forward |
52
62
  | `--label`, `-l` | — | Human-readable label for this tunnel |
53
63
  | `--name` | `local-dev` | Agent name used on first-time setup |
64
+ | `--share` | — | Create a read-only share link on connect and print the URL |
54
65
  | `--help`, `-h` | — | Show help |
55
66
 
56
67
  ## Environment variables
57
68
 
58
69
  | Variable | Description |
59
70
  |----------|-------------|
60
- | `SHARNIX_API_KEY` | Your API key (required) |
71
+ | `SHARNIX_API_KEY` | Your API key set automatically by `setup`, or from Settings |
61
72
  | `SHARNIX_URL` | Override relay URL (default: `https://relay.sharnix.com`) |
62
73
 
74
+ ## What setup writes automatically
75
+
76
+ `npx @sharnix/agent setup` detects which editors are installed and writes the MCP config to:
77
+
78
+ | Editor | Config file |
79
+ |--------|-------------|
80
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
81
+ | Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
82
+ | Claude Desktop (Linux) | `~/.config/Claude/claude_desktop_config.json` |
83
+ | Cursor | `~/.cursor/mcp.json` |
84
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
85
+
86
+ It merges into the existing config — it will never overwrite other MCP servers you have configured.
87
+
88
+ The block it writes looks like this:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "sharnix": {
94
+ "command": "npx",
95
+ "args": ["-y", "@sharnix/mcp-server"],
96
+ "env": {
97
+ "SHARNIX_API_KEY": "shx_your_key_here"
98
+ }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
63
104
  ## Creating share links
64
105
 
65
106
  Once the agent is running, create share links from:
66
107
 
108
+ - **Terminal** — `npx @sharnix/agent --port 3000 --share` prints a URL immediately
109
+ - **AI agent** — ask Claude/Cursor: *"share my app with the client"* via [`@sharnix/mcp-server`](https://www.npmjs.com/package/@sharnix/mcp-server)
67
110
  - **Dashboard** — [relay.sharnix.com/app](https://relay.sharnix.com/app)
68
- - **AI agent** — via the [`@sharnix/mcp-server`](https://www.npmjs.com/package/@sharnix/mcp-server) MCP integration
69
- - **API** — `POST /api/v1/orgs/:slug/tunnels/:id/links`
70
111
 
71
112
  Share links support permissions (`read-only`, `full`), expiry dates, email restrictions, and one-time view mode.
72
113
 
73
114
  ## How it works
74
115
 
75
- 1. On first run, the agent calls the Sharnix API to create a credential pair (`agentId` + `secret`), saved to `~/.sharnix/agent.json`
76
- 2. A stable tunnel ID is generated for the current working directory and saved to `~/.sharnix/tunnel-<hash>.json`
77
- 3. The agent opens a WebSocket to the relay and registers the tunnel
78
- 4. When a visitor opens a share link, the relay forwards their HTTP request over the WebSocket to your local app and streams the response back
116
+ 1. `setup` creates an API key via browser auth and writes MCP config to your editors
117
+ 2. On first tunnel run, agent credentials (`agentId` + `secret`) are auto-provisioned via the API and saved to `~/.sharnix/agent.json`
118
+ 3. A stable tunnel ID is generated per working directory and saved to `~/.sharnix/tunnel-<hash>.json`
119
+ 4. The agent opens a WebSocket to the relay and registers the tunnel
120
+ 5. When a visitor opens a share link, the relay forwards their HTTP request over the WebSocket to your local app and streams the response back
79
121
 
80
122
  ## License
81
123
 
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.4",
3
+ "version": "1.0.6",
4
4
  "description": "Tunnel your local app through Sharnix — share previews with one command",
5
5
  "keywords": [
6
6
  "tunnel",