@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.
- package/README.md +43 -47
- package/index.js +107 -54
- 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
|
-
##
|
|
5
|
+
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @sharnix/agent
|
|
8
|
+
npx @sharnix/agent --port 3000
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
npx @sharnix/agent --port 8080 --label "staging"
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
## Key resolution order
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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` |
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
117
|
-
2.
|
|
118
|
-
3.
|
|
119
|
-
4.
|
|
120
|
-
5.
|
|
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
|
-
|
|
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
|
|
34
|
-
npx @sharnix/agent --port <port>
|
|
35
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
// ──
|
|
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
|
-
// ──
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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(
|
|
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
|
-
|
|
287
|
-
console.log(` URL: ${authUrl}\n`);
|
|
288
|
+
const headless = isHeadlessEnv();
|
|
288
289
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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,
|
|
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) {
|
|
316
|
+
if (data.done) { process.stdout.write(' ✓\n'); return data.key; }
|
|
311
317
|
}
|
|
312
318
|
process.stdout.write('\n');
|
|
313
|
-
|
|
319
|
+
throw new Error('Setup timed out after 10 minutes.');
|
|
320
|
+
}
|
|
314
321
|
|
|
315
|
-
|
|
322
|
+
function saveKeyToDisk(apiKey) {
|
|
323
|
+
saveJson(path.join(CONFIG_DIR, 'key.json'), { apiKey, createdAt: new Date().toISOString() });
|
|
324
|
+
}
|
|
316
325
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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.
|
|
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": "
|
|
20
|
-
"sharnix": "
|
|
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
|
}
|