@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.
- package/README.md +67 -25
- package/index.js +138 -16
- 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
|
-
|
|
8
|
+
npx @sharnix/agent setup
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
This is all a new user needs to run. It will:
|
|
10
12
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
21
|
+
## Tunneling your app
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
```bash
|
|
24
|
+
npx @sharnix/agent --port 3000
|
|
25
|
+
```
|
|
23
26
|
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
## What it does
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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.
|
|
76
|
-
2.
|
|
77
|
-
3.
|
|
78
|
-
4.
|
|
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
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
console.error(
|
|
53
|
-
|
|
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
|
-
// ──
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
.
|
|
263
|
-
|
|
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
|
+
}
|