@sharnix/agent 1.0.4 → 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.
- package/index.js +138 -16
- 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
|
|
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
|
+
}
|