linkshell-cli 0.2.57 → 0.2.59
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/dist/cli/src/auth.d.ts +23 -0
- package/dist/cli/src/auth.js +90 -0
- package/dist/cli/src/auth.js.map +1 -0
- package/dist/cli/src/commands/login.d.ts +10 -0
- package/dist/cli/src/commands/login.js +146 -0
- package/dist/cli/src/commands/login.js.map +1 -0
- package/dist/cli/src/commands/logout.d.ts +1 -0
- package/dist/cli/src/commands/logout.js +11 -0
- package/dist/cli/src/commands/logout.js.map +1 -0
- package/dist/cli/src/commands/upgrade.d.ts +1 -0
- package/dist/cli/src/commands/upgrade.js +70 -0
- package/dist/cli/src/commands/upgrade.js.map +1 -0
- package/dist/cli/src/index.js +94 -0
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +1 -0
- package/dist/cli/src/runtime/bridge-session.js +47 -2
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/screen-share.js +18 -5
- package/dist/cli/src/runtime/screen-share.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +42 -8
- package/dist/shared-protocol/src/index.js +10 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +2 -2
- package/src/auth.ts +110 -0
- package/src/commands/login.ts +178 -0
- package/src/commands/logout.ts +12 -0
- package/src/commands/upgrade.ts +74 -0
- package/src/index.ts +119 -0
- package/src/runtime/bridge-session.ts +47 -2
- package/src/runtime/screen-share.ts +21 -5
package/src/index.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { resolveProviderConfig } from "./providers.js";
|
|
|
5
5
|
import { loadConfig } from "./config.js";
|
|
6
6
|
import { runDoctor } from "./commands/doctor.js";
|
|
7
7
|
import { runSetup } from "./commands/setup.js";
|
|
8
|
+
import { runUpgrade } from "./commands/upgrade.js";
|
|
9
|
+
import { runLogin } from "./commands/login.js";
|
|
10
|
+
import { runLogout } from "./commands/logout.js";
|
|
8
11
|
import { getLanIp } from "./utils/lan-ip.js";
|
|
9
12
|
|
|
10
13
|
import { createRequire } from "node:module";
|
|
@@ -158,6 +161,13 @@ program
|
|
|
158
161
|
// Save PID for status/stop
|
|
159
162
|
daemon.savePid("bridge", process.pid);
|
|
160
163
|
|
|
164
|
+
// Load auth token if logged in
|
|
165
|
+
let authToken: string | undefined;
|
|
166
|
+
try {
|
|
167
|
+
const { getValidToken } = await import("./auth.js");
|
|
168
|
+
authToken = (await getValidToken()) ?? undefined;
|
|
169
|
+
} catch {}
|
|
170
|
+
|
|
161
171
|
const session = new BridgeSession({
|
|
162
172
|
gatewayUrl,
|
|
163
173
|
gatewayHttpUrl,
|
|
@@ -170,6 +180,7 @@ program
|
|
|
170
180
|
verbose: Boolean(options.verbose),
|
|
171
181
|
screen: Boolean(options.screen),
|
|
172
182
|
providerConfig,
|
|
183
|
+
authToken,
|
|
173
184
|
});
|
|
174
185
|
|
|
175
186
|
const cleanup = async () => {
|
|
@@ -352,6 +363,114 @@ program
|
|
|
352
363
|
await runSetup();
|
|
353
364
|
});
|
|
354
365
|
|
|
366
|
+
program
|
|
367
|
+
.command("upgrade")
|
|
368
|
+
.description("Upgrade LinkShell to the latest version")
|
|
369
|
+
.action(async () => {
|
|
370
|
+
await runUpgrade();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
program
|
|
374
|
+
.command("login")
|
|
375
|
+
.description("Log in to LinkShell (enables premium gateway)")
|
|
376
|
+
.action(async () => {
|
|
377
|
+
await runLogin();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
program
|
|
381
|
+
.command("logout")
|
|
382
|
+
.description("Log out of LinkShell")
|
|
383
|
+
.action(() => {
|
|
384
|
+
runLogout();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
program
|
|
388
|
+
.command("list")
|
|
389
|
+
.description("List your sessions on official gateways")
|
|
390
|
+
.action(async () => {
|
|
391
|
+
const { getValidToken, loadAuth, SUPABASE_URL, SUPABASE_ANON_KEY } =
|
|
392
|
+
await import("./auth.js");
|
|
393
|
+
const token = await getValidToken();
|
|
394
|
+
if (!token) {
|
|
395
|
+
process.stderr.write(
|
|
396
|
+
"\n Not logged in. Run: linkshell login\n\n",
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const auth = loadAuth();
|
|
402
|
+
process.stderr.write(`\n Logged in as ${auth?.email || auth?.userId || "unknown"}\n\n`);
|
|
403
|
+
|
|
404
|
+
// Fetch official gateways
|
|
405
|
+
let gateways: { url: string; name: string; region: string | null }[] = [];
|
|
406
|
+
try {
|
|
407
|
+
const res = await fetch(
|
|
408
|
+
`${SUPABASE_URL}/rest/v1/linkshell_official_gateways?enabled=eq.true&select=url,name,region`,
|
|
409
|
+
{
|
|
410
|
+
headers: {
|
|
411
|
+
Authorization: `Bearer ${token}`,
|
|
412
|
+
apikey: SUPABASE_ANON_KEY,
|
|
413
|
+
},
|
|
414
|
+
signal: AbortSignal.timeout(10_000),
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
if (res.ok) {
|
|
418
|
+
gateways = (await res.json()) as typeof gateways;
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
|
|
422
|
+
if (gateways.length === 0) {
|
|
423
|
+
process.stderr.write(" No official gateways available.\n\n");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
process.stderr.write(" Official Gateways:\n\n");
|
|
428
|
+
|
|
429
|
+
for (const gw of gateways) {
|
|
430
|
+
const label = gw.region ? `${gw.name} (${gw.region})` : gw.name;
|
|
431
|
+
// Fetch user's sessions on this gateway
|
|
432
|
+
let sessions: {
|
|
433
|
+
id: string;
|
|
434
|
+
provider: string | null;
|
|
435
|
+
projectName: string | null;
|
|
436
|
+
hasHost: boolean;
|
|
437
|
+
lastActivity: number;
|
|
438
|
+
}[] = [];
|
|
439
|
+
try {
|
|
440
|
+
const res = await fetch(`${gw.url}/sessions/mine`, {
|
|
441
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
442
|
+
signal: AbortSignal.timeout(5_000),
|
|
443
|
+
});
|
|
444
|
+
if (res.ok) {
|
|
445
|
+
const body = (await res.json()) as { sessions: typeof sessions };
|
|
446
|
+
sessions = body.sessions;
|
|
447
|
+
}
|
|
448
|
+
} catch {}
|
|
449
|
+
|
|
450
|
+
process.stderr.write(` \x1b[32m✓\x1b[0m ${label}\n`);
|
|
451
|
+
process.stderr.write(` ${gw.url}\n`);
|
|
452
|
+
|
|
453
|
+
if (sessions.length === 0) {
|
|
454
|
+
process.stderr.write(" (no active sessions)\n\n");
|
|
455
|
+
} else {
|
|
456
|
+
for (const s of sessions) {
|
|
457
|
+
const ago = Math.round((Date.now() - s.lastActivity) / 60_000);
|
|
458
|
+
const agoStr = ago < 1 ? "just now" : `${ago}m ago`;
|
|
459
|
+
const info = [s.provider, s.projectName].filter(Boolean).join(" · ");
|
|
460
|
+
const hostIcon = s.hasHost ? "\x1b[32m●\x1b[0m" : "\x1b[31m●\x1b[0m";
|
|
461
|
+
process.stderr.write(
|
|
462
|
+
` └ ${hostIcon} ${s.id.slice(0, 8)} — ${info || "unknown"} · ${agoStr}\n`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
process.stderr.write("\n");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
process.stderr.write(
|
|
470
|
+
" Connect: linkshell start --gateway <url> --provider claude\n\n",
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
355
474
|
program.parseAsync(process.argv).catch((error: unknown) => {
|
|
356
475
|
const message = error instanceof Error ? error.message : String(error);
|
|
357
476
|
process.stderr.write(`${message}\n`);
|
|
@@ -31,6 +31,7 @@ export interface BridgeSessionOptions {
|
|
|
31
31
|
verbose?: boolean;
|
|
32
32
|
screen?: boolean;
|
|
33
33
|
providerConfig: ProviderConfig;
|
|
34
|
+
authToken?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const HEARTBEAT_INTERVAL = 15_000;
|
|
@@ -158,9 +159,13 @@ export class BridgeSession {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
private async createPairing(): Promise<void> {
|
|
162
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
163
|
+
if (this.options.authToken) {
|
|
164
|
+
headers["Authorization"] = `Bearer ${this.options.authToken}`;
|
|
165
|
+
}
|
|
161
166
|
const res = await fetch(`${this.options.gatewayHttpUrl}/pairings`, {
|
|
162
167
|
method: "POST",
|
|
163
|
-
headers
|
|
168
|
+
headers,
|
|
164
169
|
body: JSON.stringify({}),
|
|
165
170
|
});
|
|
166
171
|
if (!res.ok) {
|
|
@@ -222,6 +227,9 @@ export class BridgeSession {
|
|
|
222
227
|
const url = new URL(this.options.gatewayUrl);
|
|
223
228
|
url.searchParams.set("sessionId", this.sessionId);
|
|
224
229
|
url.searchParams.set("role", "host");
|
|
230
|
+
if (this.options.authToken) {
|
|
231
|
+
url.searchParams.set("auth_token", this.options.authToken);
|
|
232
|
+
}
|
|
225
233
|
|
|
226
234
|
this.socket = new WebSocket(url);
|
|
227
235
|
|
|
@@ -401,6 +409,42 @@ export class BridgeSession {
|
|
|
401
409
|
this.sendTerminalList();
|
|
402
410
|
break;
|
|
403
411
|
}
|
|
412
|
+
case "terminal.history.request": {
|
|
413
|
+
const p = parseTypedPayload("terminal.history.request", envelope.payload);
|
|
414
|
+
const count = p.count ?? 100;
|
|
415
|
+
let entries: string[] = [];
|
|
416
|
+
let shell = "unknown";
|
|
417
|
+
try {
|
|
418
|
+
const home = homedir();
|
|
419
|
+
// Try zsh first, then bash
|
|
420
|
+
const histFiles = [
|
|
421
|
+
{ path: join(home, ".zsh_history"), shell: "zsh" },
|
|
422
|
+
{ path: join(home, ".bash_history"), shell: "bash" },
|
|
423
|
+
];
|
|
424
|
+
for (const hf of histFiles) {
|
|
425
|
+
if (existsSync(hf.path)) {
|
|
426
|
+
const raw = readFileSync(hf.path, "utf8");
|
|
427
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
428
|
+
// zsh history lines may start with ": <timestamp>:0;" — strip prefix
|
|
429
|
+
const parsed = lines.map((l) => {
|
|
430
|
+
const m = l.match(/^:\s*\d+:\d+;(.*)$/);
|
|
431
|
+
return m ? m[1]! : l;
|
|
432
|
+
});
|
|
433
|
+
// Deduplicate and take last N
|
|
434
|
+
const unique = [...new Set(parsed.reverse())].slice(0, count).reverse();
|
|
435
|
+
entries = unique;
|
|
436
|
+
shell = hf.shell;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch {}
|
|
441
|
+
this.send(createEnvelope({
|
|
442
|
+
type: "terminal.history.response",
|
|
443
|
+
sessionId: this.sessionId,
|
|
444
|
+
payload: { entries, shell },
|
|
445
|
+
}));
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
404
448
|
case "session.ack": {
|
|
405
449
|
const p = parseTypedPayload("session.ack", envelope.payload);
|
|
406
450
|
const term = this.terminals.get(tid);
|
|
@@ -609,13 +653,14 @@ export class BridgeSession {
|
|
|
609
653
|
|
|
610
654
|
localWs.on("close", (code, reason) => {
|
|
611
655
|
this.tunnelSockets.delete(requestId);
|
|
656
|
+
const safeCode = typeof code === "number" && code >= 1000 && code <= 4999 ? code : 1000;
|
|
612
657
|
this.send(
|
|
613
658
|
createEnvelope({
|
|
614
659
|
type: "tunnel.ws.close",
|
|
615
660
|
sessionId: this.sessionId,
|
|
616
661
|
payload: {
|
|
617
662
|
requestId,
|
|
618
|
-
code,
|
|
663
|
+
code: safeCode,
|
|
619
664
|
reason: reason?.toString() || "",
|
|
620
665
|
},
|
|
621
666
|
}),
|
|
@@ -270,7 +270,7 @@ export class ScreenShare {
|
|
|
270
270
|
|
|
271
271
|
// RTP state
|
|
272
272
|
let seqNum = 0;
|
|
273
|
-
let timestamp = 0
|
|
273
|
+
let timestamp = -Math.floor(90000 / fps); // so first bump yields 0
|
|
274
274
|
const clockRate = 90000;
|
|
275
275
|
const frameDuration = Math.floor(clockRate / fps);
|
|
276
276
|
const ssrc = (Math.random() * 0xffffffff) >>> 0;
|
|
@@ -350,7 +350,7 @@ export class ScreenShare {
|
|
|
350
350
|
}
|
|
351
351
|
};
|
|
352
352
|
|
|
353
|
-
const extractNalUnits = () => {
|
|
353
|
+
const extractNalUnits = (flush = false) => {
|
|
354
354
|
// Find NAL unit boundaries (0x00000001 or 0x000001)
|
|
355
355
|
const units: Buffer[] = [];
|
|
356
356
|
let start = -1;
|
|
@@ -373,11 +373,22 @@ export class ScreenShare {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
// On flush (ffmpeg exit), emit the trailing NAL unit too
|
|
377
|
+
if (flush && start >= 0) {
|
|
378
|
+
units.push(nalBuffer.subarray(start));
|
|
379
|
+
nalBuffer = Buffer.alloc(0);
|
|
380
|
+
return units;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (start >= 0) {
|
|
377
384
|
// Keep remaining data from last start code onward
|
|
378
385
|
nalBuffer = nalBuffer.subarray(start);
|
|
379
|
-
}
|
|
380
|
-
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Safety cap: if buffer grows beyond 4MB without producing NALs, discard
|
|
389
|
+
if (nalBuffer.length > 4 * 1024 * 1024 && units.length === 0) {
|
|
390
|
+
process.stderr.write(`[screen-share] NAL buffer exceeded 4MB, resetting\n`);
|
|
391
|
+
nalBuffer = Buffer.alloc(0);
|
|
381
392
|
}
|
|
382
393
|
|
|
383
394
|
return units;
|
|
@@ -401,6 +412,11 @@ export class ScreenShare {
|
|
|
401
412
|
});
|
|
402
413
|
|
|
403
414
|
this.ffmpeg.on("exit", (code) => {
|
|
415
|
+
// Flush any remaining NAL unit in the buffer
|
|
416
|
+
const remaining = extractNalUnits(true);
|
|
417
|
+
for (const unit of remaining) {
|
|
418
|
+
sendNalUnit(unit);
|
|
419
|
+
}
|
|
404
420
|
if (this.active) {
|
|
405
421
|
process.stderr.write(`[screen-share] ffmpeg exited with code ${code}\n`);
|
|
406
422
|
this.stop();
|