@sickr/replay 0.5.0 → 0.5.1

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 CHANGED
@@ -7,7 +7,7 @@ import { spawn, execFileSync } from 'node:child_process';
7
7
  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
- import { buildSharePayload, publish, PublishError } from './share.js';
10
+ import { buildSharePayload, buildCombinedPayload, publish, PublishError } from './share.js';
11
11
  const REPLAY_ENDPOINT = process.env.SICKR_REPLAY_ENDPOINT ?? 'https://sickr.ai/api/replay';
12
12
  const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'help'];
13
13
  export function parseCommand(argv) {
@@ -39,8 +39,10 @@ Commands:
39
39
  filterable by agent, sortable by prompt/response time).
40
40
  share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
41
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
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).
44
46
  list List recorded runs, newest first.
45
47
  stop Stop recording — removes SICKR's hooks from this project.
46
48
  Your recorded runs are kept; run \`init\` to start again.
@@ -323,56 +325,80 @@ function handleList(provider) {
323
325
  process.stdout.write(`${id} ${s.agent.padEnd(7)} ${String(s.events).padStart(4)} ev ${when}${snippet}\n`);
324
326
  });
325
327
  }
326
- async function handleShare(runId, yes, open) {
327
- const id = runId ?? latestRunId();
328
- if (!id) {
329
- process.stderr.write('sickr: no runs to share. Use Claude Code first.\n');
328
+ async function confirmPublish(yes, what) {
329
+ if (yes)
330
+ return true;
331
+ if (!process.stdin.isTTY) {
332
+ process.stderr.write('sickr: re-run with --yes to publish non-interactively.\n');
330
333
  process.exit(1);
331
- return;
334
+ return false;
332
335
  }
333
- const run = loadRun(id);
334
- const payload = buildSharePayload(run);
335
- process.stdout.write(`sickr: about to publish run "${id}" (${payload.run.events.length} events, secrets already redacted) to ${REPLAY_ENDPOINT}\n` +
336
- `sickr: tip — run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
337
- if (!yes) {
338
- if (!process.stdin.isTTY) {
339
- process.stderr.write('sickr: re-run with --yes to publish non-interactively.\n');
340
- process.exit(1);
341
- return;
342
- }
343
- process.stdout.write('Publish this run publicly? [y/N] ');
344
- const answer = await promptLine();
345
- if (answer !== 'y' && answer !== 'yes') {
346
- process.stdout.write('sickr: cancelled.\n');
347
- return;
348
- }
336
+ process.stdout.write(`Publish ${what} publicly? [y/N] `);
337
+ const a = await promptLine();
338
+ if (a !== 'y' && a !== 'yes') {
339
+ process.stdout.write('sickr: cancelled.\n');
340
+ return false;
349
341
  }
350
- let url;
342
+ return true;
343
+ }
344
+ /** Publish with a single friendly retry on 429 (the WAF allows ~1/10s). Returns the URL. */
345
+ async function publishWithRetry(payload) {
351
346
  try {
352
- ({ url } = await publish(payload, REPLAY_ENDPOINT));
347
+ return (await publish(payload, REPLAY_ENDPOINT)).url;
353
348
  }
354
349
  catch (err) {
355
350
  if (err instanceof PublishError && err.status === 429) {
356
351
  process.stdout.write('sickr: rate-limited — you can publish about once every 10s. Waiting to retry once...\n');
357
352
  await new Promise((r) => setTimeout(r, 11_000));
358
353
  try {
359
- ({ url } = await publish(payload, REPLAY_ENDPOINT));
354
+ return (await publish(payload, REPLAY_ENDPOINT)).url;
360
355
  }
361
356
  catch (retryErr) {
362
357
  if (retryErr instanceof PublishError && retryErr.status === 429) {
363
358
  process.stderr.write('sickr: still rate-limited. Give it a minute and run `share` again.\n');
364
359
  process.exit(1);
365
- return;
366
360
  }
367
361
  throw retryErr;
368
362
  }
369
363
  }
370
- else {
371
- throw err;
372
- }
364
+ throw err;
373
365
  }
374
- process.stdout.write(`sickr: published → ${url}\n`);
375
- process.stdout.write('sickr: this link expires in 24h.\n');
366
+ }
367
+ async function handleShare(runId, yes, open) {
368
+ const id = runId ?? latestRunId();
369
+ if (!id) {
370
+ process.stderr.write('sickr: no runs to share. Use Claude Code or Codex first.\n');
371
+ process.exit(1);
372
+ return;
373
+ }
374
+ const payload = buildSharePayload(loadRun(id));
375
+ process.stdout.write(`sickr: about to publish run "${id}" (${payload.run.events.length} events, secrets already redacted) to ${REPLAY_ENDPOINT}\n` +
376
+ `sickr: tip — run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
377
+ if (!(await confirmPublish(yes, 'this run')))
378
+ return;
379
+ const url = await publishWithRetry(payload);
380
+ process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\n`);
381
+ if (open)
382
+ openInBrowser(url);
383
+ }
384
+ async function handleShareCombined(sel, yes, open) {
385
+ const runs = sel.ids
386
+ .map((id) => ({ id, events: loadRun(id).events }))
387
+ .filter((r) => r.events.length)
388
+ .map((r) => ({ agent: runSummary(r.id).agent, events: r.events }));
389
+ if (runs.length === 0) {
390
+ process.stdout.write(`sickr: no runs in ${sel.label} to share.\n`);
391
+ return;
392
+ }
393
+ const turns = runs.reduce((n, r) => n + r.events.filter((e) => e.kind === 'prompt').length, 0);
394
+ const agents = Array.from(new Set(runs.map((r) => r.agent))).join(', ');
395
+ const payload = buildCombinedPayload(runs, sel.label);
396
+ 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` +
397
+ `sickr: tip — run the matching \`open\` window to review locally before sharing.\n`);
398
+ if (!(await confirmPublish(yes, 'this combined replay')))
399
+ return;
400
+ const url = await publishWithRetry(payload);
401
+ process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\n`);
376
402
  if (open)
377
403
  openInBrowser(url);
378
404
  }
@@ -428,9 +454,17 @@ async function main() {
428
454
  case 'clear':
429
455
  await handleClear(rest.includes('--yes') || rest.includes('-y'));
430
456
  return;
431
- case 'share':
432
- await handleShare(rest.find((a) => !a.startsWith('-')), rest.includes('--yes') || rest.includes('-y'), rest.includes('--open'));
457
+ case 'share': {
458
+ const yes = rest.includes('--yes') || rest.includes('-y');
459
+ const openAfter = rest.includes('--open');
460
+ const sel = selectWindow(rest);
461
+ if (sel) {
462
+ await handleShareCombined(sel, yes, openAfter);
463
+ return;
464
+ }
465
+ await handleShare(rest.find((a) => !a.startsWith('-')), yes, openAfter);
433
466
  return;
467
+ }
434
468
  default:
435
469
  process.stderr.write('sickr: unknown command. Run `npx @sickr/replay help`.\n');
436
470
  process.exit(1);
package/dist/share.js CHANGED
@@ -2,6 +2,10 @@
2
2
  export function buildSharePayload(run) {
3
3
  return { run: { cwd: run.cwd, startedAt: run.startedAt, events: run.events } };
4
4
  }
5
+ /** Combined multi-agent payload: tagged runs + the window label. */
6
+ export function buildCombinedPayload(runs, window) {
7
+ return { window, runs: runs.map((r) => ({ agent: r.agent, events: r.events })) };
8
+ }
5
9
  export class PublishError extends Error {
6
10
  status;
7
11
  constructor(status) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/replay",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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" },