@sickr/replay 0.7.0 → 0.8.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.
Files changed (3) hide show
  1. package/dist/cli.js +109 -9
  2. package/dist/ui.js +63 -0
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ 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
11
  import { readCredentials, writeCredentials, clearCredentials, startDevice, pollDevice, sleep } from './auth.js';
12
+ import { ui, card, kv } from './ui.js';
12
13
  import { AGENT_API_URL, clearAgentCredentials, disconnectAgent, fetchAgentStatus, pollAgentConnect, readAgentCredentials, rotateAgentKey, startAgentConnect, writeAgentCredentials, } from './agentAuth.js';
13
14
  const REPLAY_ENDPOINT = process.env.SICKR_REPLAY_ENDPOINT ?? 'https://sickr.ai/api/replay';
14
15
  const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'login', 'logout', 'whoami', 'agent', 'help'];
@@ -388,7 +389,43 @@ async function publishWithRetry(payload) {
388
389
  throw err;
389
390
  }
390
391
  }
391
- function expiryLine(ttl_days) {
392
+ function expiryCopy(ttl_days) {
393
+ if (ttl_days >= 30)
394
+ return {
395
+ kind: 'pro',
396
+ value: `${ttl_days} days`,
397
+ tag: 'Replay Pro',
398
+ footer: [],
399
+ };
400
+ if (ttl_days >= 2)
401
+ return {
402
+ kind: 'authed',
403
+ value: `in ${ttl_days} days`,
404
+ tag: 'signed-in link',
405
+ footer: ['re-share before it expires to roll the window forward.'],
406
+ };
407
+ return {
408
+ kind: 'anon',
409
+ value: 'in 24h',
410
+ tag: 'anon link',
411
+ footer: [
412
+ 'run `npx @sickr/replay login` to extend new links to 7 days.',
413
+ 'Replay Pro (live + remote) — early access, rolling cohorts:',
414
+ ' https://sickr.ai/#waitlist',
415
+ ],
416
+ };
417
+ }
418
+ function expiryValueStyled(e) {
419
+ if (e.kind === 'anon')
420
+ return ui.warn(e.value) + ' ' + ui.dim(ui.glyph.tag + ' ' + e.tag);
421
+ if (e.kind === 'pro')
422
+ return ui.brand(e.value) + ' ' + ui.accent(ui.bold(ui.glyph.tag + ' ' + e.tag));
423
+ return ui.brand(e.value) + ' ' + ui.dim(ui.glyph.tag + ' ' + e.tag);
424
+ }
425
+ function tipLine(text) {
426
+ return ' ' + ui.accent(ui.glyph.tip) + ' ' + ui.dim(text) + '\n';
427
+ }
428
+ function legacyExpiryLine(ttl_days) {
392
429
  if (ttl_days >= 30)
393
430
  return `sickr: this link is live for ${ttl_days} days (Replay Pro retention).\n`;
394
431
  if (ttl_days >= 2)
@@ -402,13 +439,44 @@ async function handleShare(runId, yes, open) {
402
439
  process.exit(1);
403
440
  return;
404
441
  }
405
- const payload = buildSharePayload(loadRun(id));
406
- process.stdout.write(`sickr: about to publish run "${id}" (${payload.run.events.length} events, secrets already redacted) to ${REPLAY_ENDPOINT}\n` +
407
- `sickr: tip run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
442
+ const run = loadRun(id);
443
+ const payload = buildSharePayload(run);
444
+ const agentSet = Array.from(new Set(run.events.map((e) => e.label).filter((a) => a && a !== '—' && a !== 'Response')));
445
+ const agent = agentSet.length ? agentSet.join(', ') : 'Agent';
446
+ const target = REPLAY_ENDPOINT.replace(/^https?:\/\//, '');
447
+ if (ui.enabled()) {
448
+ process.stdout.write(card('publish preview', [
449
+ kv('run', ui.white(id)),
450
+ kv('agent', ui.white(agent)),
451
+ kv('events', ui.white(String(payload.run.events.length))),
452
+ kv('secrets', `redacted ${ui.ok(ui.glyph.check)}`),
453
+ kv('target', ui.white(target)),
454
+ ]));
455
+ process.stdout.write(tipLine(`run \`npx @sickr/replay open ${id}\` to review locally first.`));
456
+ process.stdout.write('\n');
457
+ }
458
+ else {
459
+ process.stdout.write(`sickr: about to publish run "${id}" (${payload.run.events.length} events, secrets already redacted) to ${REPLAY_ENDPOINT}\n` +
460
+ `sickr: tip — run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
461
+ }
408
462
  if (!(await confirmPublish(yes, 'this run')))
409
463
  return;
410
464
  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`);
465
+ const exp = expiryCopy(ttl_days);
466
+ if (ui.enabled()) {
467
+ process.stdout.write(card('published', [
468
+ kv('url', ui.underline(ui.white(url))),
469
+ kv('run', ui.white(`${id} · ${payload.run.events.length} events`)),
470
+ kv(exp.kind === 'pro' ? 'retention' : 'expires', expiryValueStyled(exp)),
471
+ ]));
472
+ for (const line of exp.footer)
473
+ process.stdout.write(tipLine(line));
474
+ }
475
+ else {
476
+ process.stdout.write(`sickr: published → ${url}\n` +
477
+ legacyExpiryLine(ttl_days) +
478
+ (exp.kind === 'anon' ? `sickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n` : ''));
479
+ }
412
480
  if (open)
413
481
  openInBrowser(url);
414
482
  }
@@ -422,14 +490,46 @@ async function handleShareCombined(sel, yes, open) {
422
490
  return;
423
491
  }
424
492
  const turns = runs.reduce((n, r) => n + r.events.filter((e) => e.kind === 'prompt').length, 0);
425
- const agents = Array.from(new Set(runs.map((r) => r.agent))).join(', ');
493
+ // Drop the placeholder dash that runSummary returns when an agent label is
494
+ // missing — otherwise the join produces "across —, Codex, Claude".
495
+ const agentSet = Array.from(new Set(runs.map((r) => r.agent).filter((a) => a && a !== '—')));
496
+ const agents = agentSet.length ? agentSet.join(', ') : 'Agent';
426
497
  const payload = buildCombinedPayload(runs, sel.label);
427
- process.stdout.write(`sickr: about to publish a combined replay (${sel.label}) — ${runs.length} runs, ~${turns} turns across ${agents}, secrets already redacted, to ${REPLAY_ENDPOINT}\n` +
428
- `sickr: tip — run the matching \`open\` window to review locally before sharing.\n`);
498
+ const target = REPLAY_ENDPOINT.replace(/^https?:\/\//, '');
499
+ if (ui.enabled()) {
500
+ process.stdout.write(card('publish preview', [
501
+ kv('scope', ui.white(sel.label)),
502
+ kv('runs', ui.white(String(runs.length))),
503
+ kv('turns', ui.white(`~${turns}`)),
504
+ kv('agents', ui.white(agents)),
505
+ kv('secrets', `redacted ${ui.ok(ui.glyph.check)}`),
506
+ kv('target', ui.white(target)),
507
+ ]));
508
+ process.stdout.write(tipLine('run the matching `open` window to review locally first.'));
509
+ process.stdout.write('\n');
510
+ }
511
+ else {
512
+ process.stdout.write(`sickr: about to publish a combined replay (${sel.label}) — ${runs.length} runs, ~${turns} turns across ${agents}, secrets already redacted, to ${REPLAY_ENDPOINT}\n` +
513
+ `sickr: tip — run the matching \`open\` window to review locally before sharing.\n`);
514
+ }
429
515
  if (!(await confirmPublish(yes, 'this combined replay')))
430
516
  return;
431
517
  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`);
518
+ const exp = expiryCopy(ttl_days);
519
+ if (ui.enabled()) {
520
+ process.stdout.write(card('published', [
521
+ kv('url', ui.underline(ui.white(url))),
522
+ kv('scope', ui.white(`${sel.label} · ${runs.length} runs · ~${turns} turns`)),
523
+ kv(exp.kind === 'pro' ? 'retention' : 'expires', expiryValueStyled(exp)),
524
+ ]));
525
+ for (const line of exp.footer)
526
+ process.stdout.write(tipLine(line));
527
+ }
528
+ else {
529
+ process.stdout.write(`sickr: published → ${url}\n` +
530
+ legacyExpiryLine(ttl_days) +
531
+ (exp.kind === 'anon' ? `sickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n` : ''));
532
+ }
433
533
  if (open)
434
534
  openInBrowser(url);
435
535
  }
package/dist/ui.js ADDED
@@ -0,0 +1,63 @@
1
+ // Tiny inline ANSI + box helper. No deps — chalk/ora would balloon the install
2
+ // and clash with the "npx and forget" wedge. One place that decides when to
3
+ // down-grade to plain `sickr: ...` lines: noColor() — NO_COLOR / CI / non-TTY /
4
+ // dumb terminals / legacy cmd.exe. Every other code path goes through ui.enabled().
5
+ //
6
+ // Brand: Sickr wordmark cyan (51), plasma accent magenta (201), dim chrome
7
+ // gray (240/245), values bright white (255), amber (214) for anon-link
8
+ // urgency, green (42) for the redacted check. Matches the existing /r/<id>
9
+ // dark/plasma palette without hard-coding hex (terminals translate 256-color).
10
+ const ESC = '\x1b[';
11
+ function isStdoutTTY() {
12
+ return process.stdout && typeof process.stdout.isTTY === 'boolean' && process.stdout.isTTY;
13
+ }
14
+ function noColor() {
15
+ if (process.env.NO_COLOR != null || process.env.SICKR_NO_COLOR != null)
16
+ return true;
17
+ if (process.env.CI === 'true')
18
+ return true;
19
+ if (process.env.TERM === 'dumb')
20
+ return true;
21
+ // Legacy cmd.exe: claims TTY but mangles 256-color SGR. Modern Windows
22
+ // Terminal sets WT_SESSION; VS Code sets TERM_PROGRAM; both safe.
23
+ if (process.platform === 'win32' && !process.env.WT_SESSION && !process.env.TERM_PROGRAM)
24
+ return true;
25
+ return !isStdoutTTY();
26
+ }
27
+ const c = (code) => (s) => noColor() ? s : `${ESC}${code}m${s}${ESC}0m`;
28
+ const c256 = (n) => (s) => noColor() ? s : `${ESC}38;5;${n}m${s}${ESC}0m`;
29
+ export const ui = {
30
+ enabled() { return !noColor(); },
31
+ bold: c(1),
32
+ dim: c256(245),
33
+ border: c256(240),
34
+ brand: c256(51), // cyan — Sickr wordmark
35
+ accent: c256(201), // magenta — plasma glyphs / Pro tag
36
+ ok: c256(42), // green — redacted check
37
+ warn: c256(214), // amber — anon-link urgency
38
+ white: c256(255),
39
+ underline(s) { return noColor() ? s : `${ESC}4m${s}${ESC}24m`; },
40
+ glyph: { tip: '▸', check: '✓', tag: '⌗' },
41
+ };
42
+ function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
43
+ /** "label value" with the label dim-gray padded to a fixed column. */
44
+ export function kv(label, value, labelWidth = 10) {
45
+ return ui.dim(label.padEnd(labelWidth)) + value;
46
+ }
47
+ /** Render a Sickr-titled box around pre-formatted rows. Caller should gate on
48
+ * ui.enabled() — in plain mode emit `sickr: ...` lines directly instead. */
49
+ export function card(title, rows) {
50
+ const widths = rows.map((r) => stripAnsi(r).length);
51
+ const inner = Math.max(56, ...widths) + 2; // +2 = one space pad each side inside the border
52
+ const dash = (n) => '─'.repeat(Math.max(1, n));
53
+ // Title bar: "╭─ sickr ─ <title> ─────────╮"
54
+ const titleTextLen = 'sickr '.length + 2 + title.length + 1; // "sickr " + "─ " + title + " "
55
+ const tailLen = Math.max(1, inner - titleTextLen - 1);
56
+ const top = ui.border('╭─ ') + ui.brand(ui.bold('sickr')) + ui.border(' ─ ') + ui.dim(title) + ' ' + ui.border(dash(tailLen) + '╮');
57
+ const bot = ui.border('╰' + dash(inner) + '╯');
58
+ const body = rows.map((r, i) => {
59
+ const pad = ' '.repeat(Math.max(0, inner - widths[i] - 2));
60
+ return ui.border('│ ') + r + pad + ui.border(' │');
61
+ }).join('\n');
62
+ return `${top}\n${body}\n${bot}\n`;
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/replay",
3
- "version": "0.7.0",
3
+ "version": "0.8.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", "sickr": "dist/cli.js" },