@sickr/replay 0.5.5 → 0.6.0

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/auth.js ADDED
@@ -0,0 +1,52 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ function credsPath() {
5
+ return join(homedir(), '.sickr', 'credentials.json');
6
+ }
7
+ export function readCredentials() {
8
+ try {
9
+ return JSON.parse(readFileSync(credsPath(), 'utf8'));
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ export function writeCredentials(c) {
16
+ const dir = join(homedir(), '.sickr');
17
+ mkdirSync(dir, { recursive: true });
18
+ const p = credsPath();
19
+ writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
20
+ try {
21
+ chmodSync(p, 0o600);
22
+ }
23
+ catch { /* Windows: skip */ }
24
+ }
25
+ export function clearCredentials() {
26
+ try {
27
+ unlinkSync(credsPath());
28
+ }
29
+ catch { /* none */ }
30
+ }
31
+ /** Default to the production wedge worker; override with env for local dev. */
32
+ export const AUTH_ENDPOINT = process.env.SICKR_AUTH_ENDPOINT ?? 'https://sickr.ai/api';
33
+ export async function startDevice() {
34
+ const r = await fetch(`${AUTH_ENDPOINT}/auth/device/start`, { method: 'POST' });
35
+ if (!r.ok)
36
+ throw new Error(`device/start failed: ${r.status}`);
37
+ return await r.json();
38
+ }
39
+ export async function pollDevice(device_code) {
40
+ const r = await fetch(`${AUTH_ENDPOINT}/auth/device/poll`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ device_code }),
44
+ });
45
+ if (!r.ok)
46
+ throw new Error(`device/poll failed: ${r.status}`);
47
+ return await r.json();
48
+ }
49
+ /** Sleep that respects an AbortSignal if you ever need to cancel. */
50
+ export function sleep(ms) {
51
+ return new Promise((resolve) => setTimeout(resolve, ms));
52
+ }
package/dist/cli.js CHANGED
@@ -8,8 +8,9 @@ import { appendEvent, loadRun, runsDir, latestRunId } from './recorder.js';
8
8
  import { mergeHooks, removeHooks } from './hookConfig.js';
9
9
  import { renderRunHtml, renderCombinedHtml } from './render.js';
10
10
  import { buildSharePayload, buildCombinedPayload, publish, PublishError } from './share.js';
11
+ import { readCredentials, writeCredentials, clearCredentials, startDevice, pollDevice, sleep } from './auth.js';
11
12
  const REPLAY_ENDPOINT = process.env.SICKR_REPLAY_ENDPOINT ?? 'https://sickr.ai/api/replay';
12
- const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'help'];
13
+ const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'login', 'logout', 'whoami', 'help'];
13
14
  export function parseCommand(argv) {
14
15
  const c = argv[0];
15
16
  return c && COMMANDS.includes(c) ? c : null;
@@ -47,6 +48,11 @@ Commands:
47
48
  stop Stop recording — removes SICKR's hooks from this project.
48
49
  Your recorded runs are kept; run \`init\` to start again.
49
50
  clear Delete all local runs in ~/.sickr/runs (asks first).
51
+ login Sign in with GitHub (optional — unlocks persistent shares and
52
+ Replay Pro cohort eligibility). Zero-account use still works.
53
+ logout Forget the local login. Server-side session stays valid until
54
+ it expires; revoke from your account page if needed.
55
+ whoami Show who you're logged in as.
50
56
  help Show this help.
51
57
 
52
58
  Requires Node 18+. Codex capture needs Codex CLI 0.133+ (run /hooks to trust);
@@ -380,7 +386,7 @@ async function handleShare(runId, yes, open) {
380
386
  if (!(await confirmPublish(yes, 'this run')))
381
387
  return;
382
388
  const url = await publishWithRetry(payload);
383
- process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\nsickr: Replay Pro (live view + remote steer) — coming soon → https://sickr.ai/#waitlist\n`);
389
+ process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\nsickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n`);
384
390
  if (open)
385
391
  openInBrowser(url);
386
392
  }
@@ -401,7 +407,7 @@ async function handleShareCombined(sel, yes, open) {
401
407
  if (!(await confirmPublish(yes, 'this combined replay')))
402
408
  return;
403
409
  const url = await publishWithRetry(payload);
404
- process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\nsickr: Replay Pro (live view + remote steer) — coming soon → https://sickr.ai/#waitlist\n`);
410
+ process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\nsickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n`);
405
411
  if (open)
406
412
  openInBrowser(url);
407
413
  }
@@ -414,6 +420,84 @@ async function promptLine() {
414
420
  });
415
421
  });
416
422
  }
423
+ /** GitHub Device Flow login — optional, but unlocks persistent shares + Pro eligibility. */
424
+ export async function handleLogin() {
425
+ const existing = readCredentials();
426
+ if (existing) {
427
+ process.stdout.write(`sickr: already logged in as ${existing.login}. Run \`logout\` to switch accounts.\n`);
428
+ return;
429
+ }
430
+ let device;
431
+ try {
432
+ device = await startDevice();
433
+ }
434
+ catch (e) {
435
+ process.stderr.write(`sickr: could not start GitHub login (${e.message}).\n`);
436
+ process.exit(1);
437
+ return;
438
+ }
439
+ const verifyUrl = device.verification_uri_complete ?? device.verification_uri;
440
+ process.stdout.write(`\nsickr: open ${device.verification_uri} in your browser and enter this code:\n\n` +
441
+ ` ${device.user_code}\n\n` +
442
+ `(opening ${verifyUrl} for you …)\n`);
443
+ openInBrowser(verifyUrl);
444
+ const deadline = Date.now() + device.expires_in * 1000;
445
+ let intervalMs = Math.max(1, device.interval) * 1000;
446
+ while (Date.now() < deadline) {
447
+ await sleep(intervalMs);
448
+ let result;
449
+ try {
450
+ result = await pollDevice(device.device_code);
451
+ }
452
+ catch (e) {
453
+ process.stderr.write(`sickr: login poll failed (${e.message}).\n`);
454
+ process.exit(1);
455
+ return;
456
+ }
457
+ if (result.status === 'success') {
458
+ writeCredentials({
459
+ token: result.token, login: result.login, github_user_id: result.github_user_id,
460
+ name: result.name ?? null, login_at: new Date().toISOString(),
461
+ });
462
+ process.stdout.write(`\nsickr: logged in as ${result.login}. Your shares are now persistent and claimable.\n`);
463
+ return;
464
+ }
465
+ if (result.status === 'pending')
466
+ continue;
467
+ if (result.status === 'slow_down') {
468
+ intervalMs += 5000;
469
+ continue;
470
+ }
471
+ if (result.status === 'expired') {
472
+ process.stderr.write('sickr: login code expired. Run `login` again.\n');
473
+ process.exit(1);
474
+ return;
475
+ }
476
+ if (result.status === 'denied') {
477
+ process.stderr.write('sickr: authorization denied.\n');
478
+ process.exit(1);
479
+ return;
480
+ }
481
+ process.stderr.write(`sickr: login error: ${result.error ?? 'unknown'}.\n`);
482
+ process.exit(1);
483
+ return;
484
+ }
485
+ process.stderr.write('sickr: login timed out. Run `login` again.\n');
486
+ process.exit(1);
487
+ }
488
+ export function handleLogout() {
489
+ const c = readCredentials();
490
+ clearCredentials();
491
+ process.stdout.write(c ? `sickr: logged out (was ${c.login}).\n` : 'sickr: not logged in.\n');
492
+ }
493
+ export function handleWhoami() {
494
+ const c = readCredentials();
495
+ if (!c) {
496
+ process.stdout.write('sickr: not logged in. Run `npx @sickr/replay login`.\n');
497
+ return;
498
+ }
499
+ process.stdout.write(`sickr: ${c.login}${c.name ? ` (${c.name})` : ''} · since ${c.login_at}\n`);
500
+ }
417
501
  async function readStdin() {
418
502
  const chunks = [];
419
503
  for await (const chunk of process.stdin)
@@ -470,6 +554,15 @@ async function main() {
470
554
  case 'clear':
471
555
  await handleClear(rest.includes('--yes') || rest.includes('-y'));
472
556
  return;
557
+ case 'login':
558
+ await handleLogin();
559
+ return;
560
+ case 'logout':
561
+ handleLogout();
562
+ return;
563
+ case 'whoami':
564
+ handleWhoami();
565
+ return;
473
566
  case 'share': {
474
567
  const yes = rest.includes('--yes') || rest.includes('-y');
475
568
  const openAfter = rest.includes('--open');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/replay",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "npx @sickr/replay — local Claude Code audit + one-click share. The free wedge into SICKR.",
6
6
  "bin": { "replay": "dist/cli.js" },