@sickr/replay 0.5.6 → 0.7.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.
@@ -0,0 +1,76 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ export const AGENT_API_URL = (process.env.SICKR_API_URL ?? 'https://support-backend.sickr.ai').replace(/\/+$/, '');
5
+ function agentCredsPath() {
6
+ return join(homedir(), '.sickr', 'agent.json');
7
+ }
8
+ export function readAgentCredentials() {
9
+ try {
10
+ return JSON.parse(readFileSync(agentCredsPath(), 'utf8'));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function writeAgentCredentials(c) {
17
+ const dir = join(homedir(), '.sickr');
18
+ mkdirSync(dir, { recursive: true });
19
+ const p = agentCredsPath();
20
+ writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
21
+ try {
22
+ chmodSync(p, 0o600);
23
+ }
24
+ catch { /* Windows: skip */ }
25
+ }
26
+ export function clearAgentCredentials() {
27
+ try {
28
+ unlinkSync(agentCredsPath());
29
+ }
30
+ catch { /* none */ }
31
+ }
32
+ export async function startAgentConnect(apiUrl, agentId) {
33
+ const r = await fetch(`${apiUrl.replace(/\/+$/, '')}/agent-connect/start`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ agent_id: agentId }),
37
+ });
38
+ if (!r.ok)
39
+ throw new Error(`agent-connect/start failed: ${r.status}`);
40
+ return await r.json();
41
+ }
42
+ export async function pollAgentConnect(apiUrl, deviceCode) {
43
+ const r = await fetch(`${apiUrl.replace(/\/+$/, '')}/agent-connect/poll`, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({ device_code: deviceCode }),
47
+ });
48
+ if (!r.ok)
49
+ throw new Error(`agent-connect/poll failed: ${r.status}`);
50
+ return await r.json();
51
+ }
52
+ export async function fetchAgentStatus(c) {
53
+ const r = await fetch(`${c.api_url.replace(/\/+$/, '')}/agent/self/status`, {
54
+ headers: { Authorization: `Bearer ${c.api_key}` },
55
+ });
56
+ if (!r.ok)
57
+ throw new Error(`agent/self/status failed: ${r.status}`);
58
+ return await r.json();
59
+ }
60
+ export async function rotateAgentKey(c) {
61
+ const r = await fetch(`${c.api_url.replace(/\/+$/, '')}/agent/self/rotate`, {
62
+ method: 'POST',
63
+ headers: { Authorization: `Bearer ${c.api_key}` },
64
+ });
65
+ if (!r.ok)
66
+ throw new Error(`agent/self/rotate failed: ${r.status}`);
67
+ return await r.json();
68
+ }
69
+ export async function disconnectAgent(c) {
70
+ const r = await fetch(`${c.api_url.replace(/\/+$/, '')}/agent/self/disconnect`, {
71
+ method: 'POST',
72
+ headers: { Authorization: `Bearer ${c.api_key}` },
73
+ });
74
+ if (!r.ok)
75
+ throw new Error(`agent/self/disconnect failed: ${r.status}`);
76
+ }
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,64 +8,78 @@ 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';
12
+ import { AGENT_API_URL, clearAgentCredentials, disconnectAgent, fetchAgentStatus, pollAgentConnect, readAgentCredentials, rotateAgentKey, startAgentConnect, writeAgentCredentials, } from './agentAuth.js';
11
13
  const REPLAY_ENDPOINT = process.env.SICKR_REPLAY_ENDPOINT ?? 'https://sickr.ai/api/replay';
12
- const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'help'];
14
+ const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'login', 'logout', 'whoami', 'agent', 'help'];
13
15
  export function parseCommand(argv) {
14
16
  const c = argv[0];
15
17
  return c && COMMANDS.includes(c) ? c : null;
16
18
  }
17
- export const HELP = `SICKR Replay — audit & replay what your AI coding agent did.
18
-
19
- Records your Claude Code or Codex session (prompts, edits, commands) to a local,
20
- redacted timeline you can replay — and optionally share as a public link.
21
-
22
- Why: a durable record of every agent action — a dashcam for your coding agent.
23
- If your agent (Claude or Codex) loses context or can't reload a past chat, the
24
- replay log helps you — and it — recall exactly what was just done.
25
-
26
- Usage: npx @sickr/replay <command> [options]
27
-
28
- Commands:
29
- init <agent> Install recording hooks for an agent (REQUIRED — no default)
30
- and start capturing to ~/.sickr/runs (secrets redacted):
31
- claude Claude Code (.claude/settings.json)
32
- codex Codex (.codex/hooks.json — needs Codex 0.133+)
33
- all both of the above (feeds the combined view)
34
- Flag: --no-name (label prompts "Human", not your login name)
35
- open [run] Render a run to a local HTML timeline and open it. 100% local.
36
- Defaults to the newest run; pass a run id, or --codex/--claude
37
- for the newest run of that agent. Combine across agents with a
38
- window: --today, --since <2h|30m|1d>, or --all (interleaved,
39
- filterable by agent, sortable by prompt/response time).
40
- share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
41
- (shows a preview and asks first). Links expire after 24h.
42
- --open also open the published link in your browser
43
- --yes skip the confirmation prompt
44
- Or publish a COMBINED multi-agent view with a window:
45
- --today / --since <2h|30m|1d> / --all (+ --claude/--codex).
46
- list List recorded runs, newest first.
47
- stop Stop recording — removes SICKR's hooks from this project.
48
- Your recorded runs are kept; run \`init\` to start again.
49
- clear Delete all local runs in ~/.sickr/runs (asks first).
50
- help Show this help.
51
-
52
- Requires Node 18+. Codex capture needs Codex CLI 0.133+ (run /hooks to trust);
53
- Claude Code: any hooks-capable build.
54
-
55
- ────────────────────────────────────────────────────────────────────
56
- This replays your AI coding agents on ONE machine. SICKR governs your whole team.
57
- Issue tracking + your team + automation + agents — one governed workflow for
58
- audit, accountability, productivity and confidence.
59
-
60
- · Gates & approvals — work holds at plan sign-off, review, merge and
61
- validation checks until each one passes.
62
- · Humans + agents on one board — agents are first-class teammates with
63
- roles, capacity and accountability, not a side channel.
64
- · A full, signed-off audit trail across every actor and every change.
65
- · Runs 24/7 produce as much work as you like; the team handles it.
66
-
67
- Free tier available · bring your own Claude or Codex subscription.
68
- https://sickr.ai
19
+ export const HELP = `SICKR Replay — audit & replay what your AI coding agent did.
20
+
21
+ Records your Claude Code or Codex session (prompts, edits, commands) to a local,
22
+ redacted timeline you can replay — and optionally share as a public link.
23
+
24
+ Why: a durable record of every agent action — a dashcam for your coding agent.
25
+ If your agent (Claude or Codex) loses context or can't reload a past chat, the
26
+ replay log helps you — and it — recall exactly what was just done.
27
+
28
+ Usage: npx @sickr/replay <command> [options]
29
+
30
+ Commands:
31
+ init <agent> Install recording hooks for an agent (REQUIRED — no default)
32
+ and start capturing to ~/.sickr/runs (secrets redacted):
33
+ claude Claude Code (.claude/settings.json)
34
+ codex Codex (.codex/hooks.json — needs Codex 0.133+)
35
+ all both of the above (feeds the combined view)
36
+ Flag: --no-name (label prompts "Human", not your login name)
37
+ open [run] Render a run to a local HTML timeline and open it. 100% local.
38
+ Defaults to the newest run; pass a run id, or --codex/--claude
39
+ for the newest run of that agent. Combine across agents with a
40
+ window: --today, --since <2h|30m|1d>, or --all (interleaved,
41
+ filterable by agent, sortable by prompt/response time).
42
+ share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
43
+ (shows a preview and asks first). Links expire after 24h.
44
+ --open also open the published link in your browser
45
+ --yes skip the confirmation prompt
46
+ Or publish a COMBINED multi-agent view with a window:
47
+ --today / --since <2h|30m|1d> / --all (+ --claude/--codex).
48
+ list List recorded runs, newest first.
49
+ stop Stop recording — removes SICKR's hooks from this project.
50
+ Your recorded runs are kept; run \`init\` to start again.
51
+ clear Delete all local runs in ~/.sickr/runs (asks first).
52
+ login Sign in with GitHub (optional — unlocks persistent shares and
53
+ Replay Pro cohort eligibility). Zero-account use still works.
54
+ logout Forget the local login. Server-side session stays valid until
55
+ it expires; revoke from your account page if needed.
56
+ whoami Show who you're logged in as.
57
+ agent connect --agent-id <id>
58
+ Connect this machine to a configured SICKR agent using GitHub
59
+ browser approval. Stores the agent key in ~/.sickr/agent.json.
60
+ agent status Show the connected agent, org and team.
61
+ agent rotate Rotate this machine's agent key.
62
+ agent disconnect
63
+ Revoke this machine's agent key and remove it locally.
64
+ help Show this help.
65
+
66
+ Requires Node 18+. Codex capture needs Codex CLI 0.133+ (run /hooks to trust);
67
+ Claude Code: any hooks-capable build.
68
+
69
+ ────────────────────────────────────────────────────────────────────
70
+ This replays your AI coding agents on ONE machine. SICKR governs your whole team.
71
+ Issue tracking + your team + automation + agents — one governed workflow for
72
+ audit, accountability, productivity and confidence.
73
+
74
+ · Gates & approvals — work holds at plan sign-off, review, merge and
75
+ validation checks until each one passes.
76
+ · Humans + agents on one board — agents are first-class teammates with
77
+ roles, capacity and accountability, not a side channel.
78
+ · A full, signed-off audit trail across every actor and every change.
79
+ · Runs 24/7 — produce as much work as you like; the team handles it.
80
+
81
+ Free tier available · bring your own Claude or Codex subscription.
82
+ → https://sickr.ai
69
83
  `;
70
84
  export function currentRunId(cc) {
71
85
  return String(cc.session_id ?? 'session');
@@ -344,17 +358,24 @@ async function confirmPublish(yes, what) {
344
358
  }
345
359
  return true;
346
360
  }
347
- /** Publish with a single friendly retry on 429 (the WAF allows ~1/10s). Returns the URL. */
361
+ /** Publish with a single friendly retry on 429 (the WAF allows ~1/10s).
362
+ * If the user is logged in, attach their token so the share lands in the
363
+ * 7-day daily slot (free_authed) instead of the 24h anon dedup pool. */
348
364
  async function publishWithRetry(payload) {
365
+ const token = readCredentials()?.token ?? null;
366
+ const post = async () => {
367
+ const r = await publish(payload, REPLAY_ENDPOINT, { token });
368
+ return { url: r.url, ttl_days: r.ttl_days ?? 1 };
369
+ };
349
370
  try {
350
- return (await publish(payload, REPLAY_ENDPOINT)).url;
371
+ return await post();
351
372
  }
352
373
  catch (err) {
353
374
  if (err instanceof PublishError && err.status === 429) {
354
375
  process.stdout.write('sickr: rate-limited — you can publish about once every 10s. Waiting to retry once...\n');
355
376
  await new Promise((r) => setTimeout(r, 11_000));
356
377
  try {
357
- return (await publish(payload, REPLAY_ENDPOINT)).url;
378
+ return await post();
358
379
  }
359
380
  catch (retryErr) {
360
381
  if (retryErr instanceof PublishError && retryErr.status === 429) {
@@ -367,6 +388,13 @@ async function publishWithRetry(payload) {
367
388
  throw err;
368
389
  }
369
390
  }
391
+ function expiryLine(ttl_days) {
392
+ if (ttl_days >= 30)
393
+ return `sickr: this link is live for ${ttl_days} days (Replay Pro retention).\n`;
394
+ if (ttl_days >= 2)
395
+ return `sickr: this link is live for ${ttl_days} days — re-share before it expires to extend.\n`;
396
+ return `sickr: this link expires in 24h. Run \`replay login\` to extend to 7 days.\n`;
397
+ }
370
398
  async function handleShare(runId, yes, open) {
371
399
  const id = runId ?? latestRunId();
372
400
  if (!id) {
@@ -379,8 +407,8 @@ async function handleShare(runId, yes, open) {
379
407
  `sickr: tip — run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
380
408
  if (!(await confirmPublish(yes, 'this run')))
381
409
  return;
382
- const url = await publishWithRetry(payload);
383
- 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`);
410
+ const { url, ttl_days } = await publishWithRetry(payload);
411
+ process.stdout.write(`sickr: published → ${url}\n${expiryLine(ttl_days)}sickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n`);
384
412
  if (open)
385
413
  openInBrowser(url);
386
414
  }
@@ -400,8 +428,8 @@ async function handleShareCombined(sel, yes, open) {
400
428
  `sickr: tip — run the matching \`open\` window to review locally before sharing.\n`);
401
429
  if (!(await confirmPublish(yes, 'this combined replay')))
402
430
  return;
403
- const url = await publishWithRetry(payload);
404
- 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`);
431
+ const { url, ttl_days } = await publishWithRetry(payload);
432
+ process.stdout.write(`sickr: published → ${url}\n${expiryLine(ttl_days)}sickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n`);
405
433
  if (open)
406
434
  openInBrowser(url);
407
435
  }
@@ -414,6 +442,230 @@ async function promptLine() {
414
442
  });
415
443
  });
416
444
  }
445
+ /** GitHub Device Flow login — optional, but unlocks persistent shares + Pro eligibility. */
446
+ export async function handleLogin() {
447
+ const existing = readCredentials();
448
+ if (existing) {
449
+ process.stdout.write(`sickr: already logged in as ${existing.login}. Run \`logout\` to switch accounts.\n`);
450
+ return;
451
+ }
452
+ let device;
453
+ try {
454
+ device = await startDevice();
455
+ }
456
+ catch (e) {
457
+ process.stderr.write(`sickr: could not start GitHub login (${e.message}).\n`);
458
+ process.exit(1);
459
+ return;
460
+ }
461
+ const verifyUrl = device.verification_uri_complete ?? device.verification_uri;
462
+ process.stdout.write(`\nsickr: open ${device.verification_uri} in your browser and enter this code:\n\n` +
463
+ ` ${device.user_code}\n\n` +
464
+ `(opening ${verifyUrl} for you …)\n`);
465
+ openInBrowser(verifyUrl);
466
+ const deadline = Date.now() + device.expires_in * 1000;
467
+ let intervalMs = Math.max(1, device.interval) * 1000;
468
+ while (Date.now() < deadline) {
469
+ await sleep(intervalMs);
470
+ let result;
471
+ try {
472
+ result = await pollDevice(device.device_code);
473
+ }
474
+ catch (e) {
475
+ process.stderr.write(`sickr: login poll failed (${e.message}).\n`);
476
+ process.exit(1);
477
+ return;
478
+ }
479
+ if (result.status === 'success') {
480
+ writeCredentials({
481
+ token: result.token, login: result.login, github_user_id: result.github_user_id,
482
+ name: result.name ?? null, login_at: new Date().toISOString(),
483
+ });
484
+ process.stdout.write(`\nsickr: logged in as ${result.login}. Your shares are now persistent and claimable.\n`);
485
+ return;
486
+ }
487
+ if (result.status === 'pending')
488
+ continue;
489
+ if (result.status === 'slow_down') {
490
+ intervalMs += 5000;
491
+ continue;
492
+ }
493
+ if (result.status === 'expired') {
494
+ process.stderr.write('sickr: login code expired. Run `login` again.\n');
495
+ process.exit(1);
496
+ return;
497
+ }
498
+ if (result.status === 'denied') {
499
+ process.stderr.write('sickr: authorization denied.\n');
500
+ process.exit(1);
501
+ return;
502
+ }
503
+ process.stderr.write(`sickr: login error: ${result.error ?? 'unknown'}.\n`);
504
+ process.exit(1);
505
+ return;
506
+ }
507
+ process.stderr.write('sickr: login timed out. Run `login` again.\n');
508
+ process.exit(1);
509
+ }
510
+ export function handleLogout() {
511
+ const c = readCredentials();
512
+ clearCredentials();
513
+ process.stdout.write(c ? `sickr: logged out (was ${c.login}).\n` : 'sickr: not logged in.\n');
514
+ }
515
+ export function handleWhoami() {
516
+ const c = readCredentials();
517
+ if (!c) {
518
+ process.stdout.write('sickr: not logged in. Run `npx @sickr/replay login`.\n');
519
+ return;
520
+ }
521
+ process.stdout.write(`sickr: ${c.login}${c.name ? ` (${c.name})` : ''} · since ${c.login_at}\n`);
522
+ }
523
+ function valueAt(rest, flag) {
524
+ const i = rest.indexOf(flag);
525
+ return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('-') ? rest[i + 1] : null;
526
+ }
527
+ function isRecord(value) {
528
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
529
+ }
530
+ function agentContextLabel(context) {
531
+ const root = isRecord(context) ? context : {};
532
+ const agent = isRecord(root.agent) ? root.agent : {};
533
+ const org = isRecord(root.org) ? root.org : {};
534
+ const team = isRecord(root.team) ? root.team : {};
535
+ return {
536
+ agentId: typeof agent.agent_id === 'string' ? agent.agent_id : 'unknown',
537
+ provider: typeof agent.provider === 'string' ? agent.provider : 'unknown',
538
+ status: typeof agent.status === 'string' ? agent.status : 'unknown',
539
+ org: typeof org.name === 'string' ? org.name : 'unknown org',
540
+ team: typeof team.name === 'string' ? team.name : 'unknown team',
541
+ };
542
+ }
543
+ export async function handleAgentStatus() {
544
+ const c = readAgentCredentials();
545
+ if (!c) {
546
+ process.stdout.write('sickr: agent is not connected. Run `sickr agent connect --agent-id <id>`.\n');
547
+ return;
548
+ }
549
+ try {
550
+ const label = agentContextLabel(await fetchAgentStatus(c));
551
+ process.stdout.write(`sickr: connected as ${label.agentId} (${label.provider}, ${label.status})\n` +
552
+ `org: ${label.org} team: ${label.team}\n` +
553
+ `api: ${c.api_url}\n`);
554
+ }
555
+ catch (e) {
556
+ process.stderr.write(`sickr: agent status failed (${e.message}).\n`);
557
+ process.exit(1);
558
+ }
559
+ }
560
+ export async function handleAgentRotate() {
561
+ const c = readAgentCredentials();
562
+ if (!c) {
563
+ process.stderr.write('sickr: agent is not connected. Run `sickr agent connect --agent-id <id>`.\n');
564
+ process.exit(1);
565
+ return;
566
+ }
567
+ try {
568
+ const rotated = await rotateAgentKey(c);
569
+ writeAgentCredentials({ ...c, api_key: rotated.api_key, key_id: rotated.key?.id ?? c.key_id });
570
+ process.stdout.write(`sickr: rotated key for ${c.agent_id}.\n`);
571
+ }
572
+ catch (e) {
573
+ process.stderr.write(`sickr: agent rotate failed (${e.message}).\n`);
574
+ process.exit(1);
575
+ }
576
+ }
577
+ export async function handleAgentDisconnect() {
578
+ const c = readAgentCredentials();
579
+ if (!c) {
580
+ process.stdout.write('sickr: agent is not connected.\n');
581
+ return;
582
+ }
583
+ try {
584
+ await disconnectAgent(c);
585
+ clearAgentCredentials();
586
+ process.stdout.write(`sickr: disconnected ${c.agent_id} and removed the local key.\n`);
587
+ }
588
+ catch (e) {
589
+ process.stderr.write(`sickr: agent disconnect failed (${e.message}).\n`);
590
+ process.exit(1);
591
+ }
592
+ }
593
+ function storeApprovedAgent(apiUrl, result) {
594
+ writeAgentCredentials({
595
+ api_url: apiUrl,
596
+ agent_id: result.agent_id,
597
+ api_key: result.api_key,
598
+ key_id: result.key_id ?? null,
599
+ org_id: result.org_id,
600
+ team_id: result.team_id,
601
+ connected_at: new Date().toISOString(),
602
+ });
603
+ }
604
+ export async function handleAgentConnect(rest) {
605
+ const agentId = valueAt(rest, '--agent-id') ?? rest.find((a) => !a.startsWith('-')) ?? null;
606
+ if (!agentId) {
607
+ process.stderr.write('sickr: `agent connect` requires --agent-id <id>.\n');
608
+ process.exit(1);
609
+ return;
610
+ }
611
+ const apiUrl = (valueAt(rest, '--api-url') ?? AGENT_API_URL).replace(/\/+$/, '');
612
+ let started;
613
+ try {
614
+ started = await startAgentConnect(apiUrl, agentId);
615
+ }
616
+ catch (e) {
617
+ process.stderr.write(`sickr: could not start agent connect (${e.message}).\n`);
618
+ process.exit(1);
619
+ return;
620
+ }
621
+ const verifyUrl = started.verification_uri_complete ?? started.verification_uri;
622
+ process.stdout.write(`\nsickr: approve agent ${agentId} in your browser:\n\n` +
623
+ ` ${verifyUrl}\n\n` +
624
+ `code: ${started.user_code}\n` +
625
+ `(opening browser...)\n`);
626
+ openInBrowser(verifyUrl);
627
+ const deadline = Date.now() + started.expires_in * 1000;
628
+ const intervalMs = Math.max(1, started.interval) * 1000;
629
+ while (Date.now() < deadline) {
630
+ await sleep(intervalMs);
631
+ let polled;
632
+ try {
633
+ polled = await pollAgentConnect(apiUrl, started.device_code);
634
+ }
635
+ catch (e) {
636
+ process.stderr.write(`sickr: agent connect poll failed (${e.message}).\n`);
637
+ process.exit(1);
638
+ return;
639
+ }
640
+ if (polled.status === 'pending')
641
+ continue;
642
+ if (polled.status === 'approved') {
643
+ storeApprovedAgent(apiUrl, polled);
644
+ process.stdout.write(`\nsickr: connected ${polled.agent_id}. Run \`sickr agent status\` to verify.\n`);
645
+ return;
646
+ }
647
+ if (polled.status === 'expired') {
648
+ process.stderr.write('sickr: agent connect code expired. Run `sickr agent connect` again.\n');
649
+ process.exit(1);
650
+ return;
651
+ }
652
+ if (polled.status === 'denied') {
653
+ process.stderr.write('sickr: agent connect was denied.\n');
654
+ process.exit(1);
655
+ return;
656
+ }
657
+ if (polled.status === 'consumed') {
658
+ process.stderr.write('sickr: agent connect code was already used. Run `sickr agent connect` again.\n');
659
+ process.exit(1);
660
+ return;
661
+ }
662
+ process.stderr.write(`sickr: agent connect error: ${polled.error ?? 'unknown'}.\n`);
663
+ process.exit(1);
664
+ return;
665
+ }
666
+ process.stderr.write('sickr: agent connect timed out. Run `sickr agent connect` again.\n');
667
+ process.exit(1);
668
+ }
417
669
  async function readStdin() {
418
670
  const chunks = [];
419
671
  for await (const chunk of process.stdin)
@@ -470,6 +722,38 @@ async function main() {
470
722
  case 'clear':
471
723
  await handleClear(rest.includes('--yes') || rest.includes('-y'));
472
724
  return;
725
+ case 'login':
726
+ await handleLogin();
727
+ return;
728
+ case 'logout':
729
+ handleLogout();
730
+ return;
731
+ case 'whoami':
732
+ handleWhoami();
733
+ return;
734
+ case 'agent': {
735
+ const sub = rest[0];
736
+ const agentRest = rest.slice(1);
737
+ if (sub === 'connect') {
738
+ await handleAgentConnect(agentRest);
739
+ return;
740
+ }
741
+ if (sub === 'status') {
742
+ await handleAgentStatus();
743
+ return;
744
+ }
745
+ if (sub === 'disconnect') {
746
+ await handleAgentDisconnect();
747
+ return;
748
+ }
749
+ if (sub === 'rotate') {
750
+ await handleAgentRotate();
751
+ return;
752
+ }
753
+ process.stderr.write('sickr: unknown agent command. Use `sickr agent connect|status|disconnect|rotate`.\n');
754
+ process.exit(1);
755
+ return;
756
+ }
473
757
  case 'share': {
474
758
  const yes = rest.includes('--yes') || rest.includes('-y');
475
759
  const openAfter = rest.includes('--open');
package/dist/share.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { gzipSync } from 'node:zlib';
1
2
  /** Strip the local id; events are already redacted at capture time. */
2
3
  export function buildSharePayload(run) {
3
4
  return { run: { cwd: run.cwd, startedAt: run.startedAt, events: run.events } };
@@ -14,12 +15,15 @@ export class PublishError extends Error {
14
15
  this.name = 'PublishError';
15
16
  }
16
17
  }
17
- export async function publish(payload, endpoint) {
18
- const res = await fetch(endpoint, {
19
- method: 'POST',
20
- headers: { 'Content-Type': 'application/json' },
21
- body: JSON.stringify(payload),
22
- });
18
+ export async function publish(payload, endpoint, opts = {}) {
19
+ // Gzip the JSON body: ~5-10x smaller for typical sessions, comfortably fits
20
+ // the worker's 4 MB cap even for combined multi-day windows.
21
+ const raw = JSON.stringify(payload);
22
+ const gz = gzipSync(raw);
23
+ const headers = { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' };
24
+ if (opts.token)
25
+ headers.Authorization = `Bearer ${opts.token}`;
26
+ const res = await fetch(endpoint, { method: 'POST', headers, body: gz });
23
27
  if (!res.ok)
24
28
  throw new PublishError(res.status);
25
29
  return (await res.json());
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
- {
2
- "name": "@sickr/replay",
3
- "version": "0.5.6",
4
- "type": "module",
5
- "description": "npx @sickr/replay — local Claude Code audit + one-click share. The free wedge into SICKR.",
6
- "bin": { "replay": "dist/cli.js" },
7
- "files": ["dist"],
8
- "publishConfig": { "access": "public" },
9
- "scripts": {
10
- "build": "tsc",
11
- "test": "vitest run",
12
- "dev": "tsc -w"
13
- },
14
- "engines": { "node": ">=20" },
15
- "license": "UNLICENSED",
16
- "devDependencies": {
17
- "@types/node": "^20.14.0",
18
- "typescript": "^5.6.2",
19
- "vitest": "^2.1.1"
20
- }
21
- }
1
+ {
2
+ "name": "@sickr/replay",
3
+ "version": "0.7.0",
4
+ "type": "module",
5
+ "description": "npx @sickr/replay — local Claude Code audit + one-click share. The free wedge into SICKR.",
6
+ "bin": { "replay": "dist/cli.js", "sickr": "dist/cli.js" },
7
+ "files": ["dist"],
8
+ "publishConfig": { "access": "public" },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "vitest run",
12
+ "dev": "tsc -w"
13
+ },
14
+ "engines": { "node": ">=20" },
15
+ "license": "UNLICENSED",
16
+ "devDependencies": {
17
+ "@types/node": "^20.14.0",
18
+ "typescript": "^5.6.2",
19
+ "vitest": "^2.1.1"
20
+ }
21
+ }