@sharnix/agent 1.0.6 → 1.0.8

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 +43 -47
  2. package/index.js +107 -54
  3. package/package.json +6 -16
package/README.md CHANGED
@@ -2,47 +2,48 @@
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)
5
+ ## Usage
6
6
 
7
7
  ```bash
8
- npx @sharnix/agent setup
8
+ npx @sharnix/agent --port 3000
9
9
  ```
10
10
 
11
- This is all a new user needs to run. It will:
11
+ That's it. If this is your first time on this machine, the CLI will handle setup automatically — no separate setup step needed.
12
+
13
+ ## First-time setup (automatic)
12
14
 
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
15
+ When no key is found, the CLI starts a one-time device flow:
18
16
 
19
- After that, your AI agent has full MCP access and you can start tunneling.
17
+ 1. Generates a short-lived auth URL and prints it in the terminal
18
+ 2. You (or your AI agent) open the URL in any browser — phone, laptop, doesn't matter
19
+ 3. Sign in and click **"Authorize"** — one click
20
+ 4. The CLI receives your key automatically via polling — nothing to copy or paste
21
+ 5. Key saved to `~/.sharnix/key.json` for all future runs
22
+ 6. Tunnel connects immediately
23
+
24
+ **Every run after that is silent** — the CLI reads the saved key and connects with no prompts.
20
25
 
21
26
  ## Tunneling your app
22
27
 
23
28
  ```bash
29
+ # Tunnel port 3000 (key read from ~/.sharnix/key.json or env)
24
30
  npx @sharnix/agent --port 3000
25
- ```
26
31
 
27
- The API key is read from `SHARNIX_API_KEY` in your environment, or saved automatically by `setup`.
28
-
29
- ```bash
30
- # Tunnel port 3000
32
+ # Pass key explicitly
31
33
  SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000
32
34
 
33
- # Tunnel and immediately print a share link (no dashboard needed)
34
- SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000 --share
35
-
36
35
  # With a label
37
- SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 8080 --label "staging"
36
+ npx @sharnix/agent --port 8080 --label "staging"
38
37
  ```
39
38
 
40
- Save the key to your shell profile so you don't repeat it:
39
+ ## Key resolution order
41
40
 
42
- ```bash
43
- # ~/.bashrc or ~/.zshrc
44
- export SHARNIX_API_KEY=shx_...
45
- ```
41
+ The CLI checks for a key in this order:
42
+
43
+ 1. `SHARNIX_API_KEY` environment variable
44
+ 2. `SHARNIX_KEY` environment variable
45
+ 3. `~/.sharnix/key.json` (saved automatically after first setup)
46
+ 4. Device flow (auto-started if none of the above exist)
46
47
 
47
48
  ## What it does
48
49
 
@@ -57,35 +58,22 @@ export SHARNIX_API_KEY=shx_...
57
58
 
58
59
  | Flag | Default | Description |
59
60
  |------|---------|-------------|
60
- | `setup` | — | First-time setup: creates API key and writes MCP config automatically |
61
61
  | `--port`, `-p` | `3000` | Local port to forward |
62
62
  | `--label`, `-l` | — | Human-readable label for this tunnel |
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 |
63
+ | `--name` | `local-dev` | Agent name registered on first setup |
65
64
  | `--help`, `-h` | — | Show help |
66
65
 
67
66
  ## Environment variables
68
67
 
69
68
  | Variable | Description |
70
69
  |----------|-------------|
71
- | `SHARNIX_API_KEY` | Your API key — set automatically by `setup`, or from Settings |
70
+ | `SHARNIX_API_KEY` | API key — overrides saved key file |
71
+ | `SHARNIX_KEY` | Alias for `SHARNIX_API_KEY` |
72
72
  | `SHARNIX_URL` | Override relay URL (default: `https://relay.sharnix.com`) |
73
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.
74
+ ## MCP config for AI editors
87
75
 
88
- The block it writes looks like this:
76
+ To give your AI agent (Claude Desktop, Cursor, Windsurf) MCP access to Sharnix, add this to your editor's config file and restart:
89
77
 
90
78
  ```json
91
79
  {
@@ -101,11 +89,18 @@ The block it writes looks like this:
101
89
  }
102
90
  ```
103
91
 
92
+ | Editor | Config file |
93
+ |--------|-------------|
94
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
95
+ | Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
96
+ | Claude Desktop (Linux) | `~/.config/Claude/claude_desktop_config.json` |
97
+ | Cursor | `~/.cursor/mcp.json` |
98
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
99
+
104
100
  ## Creating share links
105
101
 
106
102
  Once the agent is running, create share links from:
107
103
 
108
- - **Terminal** — `npx @sharnix/agent --port 3000 --share` prints a URL immediately
109
104
  - **AI agent** — ask Claude/Cursor: *"share my app with the client"* via [`@sharnix/mcp-server`](https://www.npmjs.com/package/@sharnix/mcp-server)
110
105
  - **Dashboard** — [relay.sharnix.com/app](https://relay.sharnix.com/app)
111
106
 
@@ -113,11 +108,12 @@ Share links support permissions (`read-only`, `full`), expiry dates, email restr
113
108
 
114
109
  ## How it works
115
110
 
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
111
+ 1. On first run with no key, the CLI starts a device flow: prints a one-time auth URL and polls for completion
112
+ 2. You open the URL in any browser, sign in, click Authorize key is saved to `~/.sharnix/key.json` automatically
113
+ 3. Agent credentials (`agentId` + `secret`) are provisioned via the API and saved to `~/.sharnix/agent.json`
114
+ 4. A stable tunnel ID is generated per working directory and saved to `~/.sharnix/tunnel-<hash>.json`
115
+ 5. The agent opens a WebSocket to the relay and registers the tunnel
116
+ 6. 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
121
117
 
122
118
  ## License
123
119
 
package/index.js CHANGED
@@ -10,7 +10,11 @@ const { randomBytes, createHash } = require('crypto');
10
10
 
11
11
  const RELAY_BASE = (process.env.SHARNIX_URL || 'https://relay.sharnix.com').replace(/\/$/, '');
12
12
  const WS_BASE = RELAY_BASE.replace(/^http/, 'ws');
13
- const API_KEY = process.env.SHARNIX_API_KEY || '';
13
+ let API_KEY = process.env.SHARNIX_API_KEY || process.env.SHARNIX_KEY || '';
14
+
15
+ // ── Config persistence (declared early so dispatch below can rely on it) ──────
16
+ const CONFIG_DIR = path.join(os.homedir(), '.sharnix');
17
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
14
18
 
15
19
  // ── CLI args ──────────────────────────────────────────────────────────────────
16
20
  const args = process.argv.slice(2);
@@ -30,9 +34,10 @@ if (has('--help') || has('-h')) {
30
34
  sharnix — tunnel your local app through Sharnix
31
35
 
32
36
  Usage:
33
- npx @sharnix/agent setup First-time setup (no API key needed)
34
- npx @sharnix/agent --port <port> Tunnel a local port
35
- SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000
37
+ npx @sharnix/agent --port <port> Tunnel a local port (auto-runs setup if no key)
38
+ npx @sharnix/agent --port <port> --share Tunnel and print a share link on connect
39
+ npx @sharnix/agent setup Re-run setup explicitly (writes MCP config too)
40
+ npx @sharnix/agent setup --print-url Print auth URL and exit (legacy non-blocking mode)
36
41
 
37
42
  Options:
38
43
  --port, -p <n> Local port to forward (default: 3000)
@@ -42,21 +47,17 @@ if (has('--help') || has('-h')) {
42
47
  --help, -h Show this message
43
48
 
44
49
  Environment:
45
- SHARNIX_API_KEY API key from relay.sharnix.com/app/settings
46
- SHARNIX_URL Override relay base URL
50
+ SHARNIX_API_KEY API key (also: SHARNIX_KEY). If unset and ~/.sharnix/key.json
51
+ doesn't exist, the CLI will start the device flow automatically.
52
+ SHARNIX_URL Override relay base URL (default: https://relay.sharnix.com)
47
53
  `);
48
54
  process.exit(0);
49
55
  }
50
56
 
51
- // ── Setup command (device-flow bootstrap, no API key required) ────────────────
57
+ // ── Dispatch ──────────────────────────────────────────────────────────────────
52
58
  if (args[0] === 'setup') {
53
- runSetup().catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
59
+ runSetup(has('--print-url')).catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
54
60
  } 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
61
  if (isNaN(port) || port < 1 || port > 65535) {
61
62
  console.error(`\n Error: invalid port "${get('--port') || get('-p')}"\n`);
62
63
  process.exit(1);
@@ -65,10 +66,7 @@ if (args[0] === 'setup') {
65
66
  }
66
67
 
67
68
 
68
- // ── Config persistence ────────────────────────────────────────────────────────
69
- const CONFIG_DIR = path.join(os.homedir(), '.sharnix');
70
- if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
71
-
69
+ // ── Config persistence helpers ────────────────────────────────────────────────
72
70
  function loadJson(file) {
73
71
  try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
74
72
  }
@@ -262,61 +260,116 @@ process.on('SIGINT', () => {
262
260
  process.exit(0);
263
261
  });
264
262
 
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); });
263
+ // ── Key resolution + headless detection ──────────────────────────────────────
264
+ function resolveApiKey() {
265
+ if (API_KEY) return API_KEY;
266
+ for (const name of ['key.json', 'sharnix-key.json']) {
267
+ const data = loadJson(path.join(CONFIG_DIR, name));
268
+ if (data?.apiKey) return data.apiKey;
269
+ }
270
+ return null;
271
271
  }
272
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');
273
+ function isHeadlessEnv() {
274
+ if (!process.stdout.isTTY) return true;
275
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) return true;
276
+ if (process.env.CI) return true;
277
+ if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
278
+ return false;
279
+ }
278
280
 
279
- // Start device flow
280
- process.stdout.write(' Starting setup…');
281
+ // Device-flow bootstrap — returns the API key without writing MCP config.
282
+ // Shared by both auto-bootstrap (main path) and the explicit `setup` command.
283
+ async function obtainKeyViaDeviceFlow() {
281
284
  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.');
285
+ if (!r.ok) throw new Error(`Could not reach ${RELAY_BASE}. Check your connection.`);
283
286
  const { code, authUrl } = await r.json();
284
- process.stdout.write(' done\n\n');
285
287
 
286
- console.log(' Opening your browser to authorize…');
287
- console.log(` URL: ${authUrl}\n`);
288
+ const headless = isHeadlessEnv();
288
289
 
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 {}
290
+ console.log('\n ──────────────────────────────────────────────────────────');
291
+ console.log(' No Sharnix key found — let\'s set one up. (10-second flow.)');
292
+ console.log(' ──────────────────────────────────────────────────────────\n');
293
+ console.log(' Open this URL in any browser to authorize (your phone works fine):\n');
294
+ console.log(` ${authUrl}\n`);
295
+
296
+ if (!headless) {
297
+ try {
298
+ const { spawn } = require('child_process');
299
+ if (process.platform === 'win32') {
300
+ spawn('cmd', ['/c', 'start', '', authUrl], { detached: true, stdio: 'ignore' });
301
+ } else {
302
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
303
+ spawn(cmd, [authUrl], { detached: true, stdio: 'ignore' });
304
+ }
305
+ } catch {}
306
+ }
299
307
 
300
- // Poll until authorized
301
- process.stdout.write(' Waiting for browser authorization');
302
- let apiKey = null;
308
+ process.stdout.write(' Waiting for authorization');
303
309
  const deadline = Date.now() + 10 * 60 * 1000;
304
310
  while (Date.now() < deadline) {
305
- await new Promise((r) => setTimeout(r, 2000));
311
+ await new Promise((r) => setTimeout(r, 3000));
306
312
  process.stdout.write('.');
307
313
  const poll = await fetch(`${RELAY_BASE}/api/v1/setup-cli/${code}`);
308
314
  if (!poll.ok) { process.stdout.write('\n'); throw new Error('Setup session expired.'); }
309
315
  const data = await poll.json();
310
- if (data.done) { apiKey = data.key; break; }
316
+ if (data.done) { process.stdout.write(' ✓\n'); return data.key; }
311
317
  }
312
318
  process.stdout.write('\n');
313
- if (!apiKey) throw new Error('Setup timed out. Run npx @sharnix/agent setup again.');
319
+ throw new Error('Setup timed out after 10 minutes.');
320
+ }
314
321
 
315
- console.log('\n API key received!\n');
322
+ function saveKeyToDisk(apiKey) {
323
+ saveJson(path.join(CONFIG_DIR, 'key.json'), { apiKey, createdAt: new Date().toISOString() });
324
+ }
316
325
 
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() });
326
+ // ── Tunnel main ───────────────────────────────────────────────────────────────
327
+ async function main() {
328
+ console.log(`\n Sharnix Agent → localhost:${port}`);
329
+
330
+ // Resolve a key, or trigger device flow inline if none.
331
+ API_KEY = resolveApiKey();
332
+ if (!API_KEY) {
333
+ try {
334
+ API_KEY = await obtainKeyViaDeviceFlow();
335
+ saveKeyToDisk(API_KEY);
336
+ console.log(` ✓ Key saved to ${path.join(CONFIG_DIR, 'key.json')}\n`);
337
+ } catch (err) {
338
+ console.error(`\n Error: ${err.message}\n`);
339
+ process.exit(1);
340
+ }
341
+ }
342
+
343
+ try {
344
+ creds = await bootstrap();
345
+ connect();
346
+ } catch (err) {
347
+ console.error(`\n Error: ${err.message}\n`);
348
+ process.exit(1);
349
+ }
350
+ }
351
+
352
+ // ── Setup: explicit `setup` subcommand — bootstrap + MCP config write ────────
353
+ async function runSetup(printUrlOnly = false) {
354
+ console.log('\n Sharnix Setup\n');
355
+
356
+ if (printUrlOnly) {
357
+ // Legacy non-blocking mode — start device flow, print URL, exit.
358
+ process.stdout.write(' Starting setup…');
359
+ const r = await fetch(`${RELAY_BASE}/api/v1/setup-cli`, { method: 'POST' });
360
+ if (!r.ok) throw new Error('Could not reach relay.sharnix.com. Check your internet connection.');
361
+ const { authUrl } = await r.json();
362
+ process.stdout.write(' done\n\n');
363
+ console.log(' Authorization URL (open this in your browser):\n');
364
+ console.log(` ${authUrl}\n`);
365
+ console.log(' After authorizing, just run the tunnel — the CLI will read the saved key:');
366
+ console.log(' npx @sharnix/agent --port 3000\n');
367
+ process.exit(0);
368
+ }
369
+
370
+ const apiKey = await obtainKeyViaDeviceFlow();
371
+ console.log('\n API key received!\n');
372
+ saveKeyToDisk(apiKey);
320
373
 
321
374
  // Detect and write MCP configs
322
375
  const mcpBlock = {
package/package.json CHANGED
@@ -1,27 +1,17 @@
1
1
  {
2
2
  "name": "@sharnix/agent",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Tunnel your local app through Sharnix — share previews with one command",
5
- "keywords": [
6
- "tunnel",
7
- "preview",
8
- "sharing",
9
- "localhost",
10
- "sharnix"
11
- ],
5
+ "keywords": ["tunnel", "preview", "sharing", "localhost", "sharnix"],
12
6
  "homepage": "https://relay.sharnix.com",
13
7
  "license": "MIT",
14
- "engines": {
15
- "node": ">=18"
16
- },
8
+ "engines": { "node": ">=18" },
17
9
  "main": "./index.js",
18
10
  "bin": {
19
- "sharnix-agent": "./index.js",
20
- "sharnix": "./index.js"
11
+ "sharnix-agent": "index.js",
12
+ "sharnix": "index.js"
21
13
  },
22
- "files": [
23
- "index.js"
24
- ],
14
+ "files": ["index.js"],
25
15
  "dependencies": {
26
16
  "ws": "^8.18.0"
27
17
  }