@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.
- package/dist/cli.js +109 -9
- package/dist/ui.js +63 -0
- 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
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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