@sickr/replay 0.5.0 → 0.5.2

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) {
@@ -30,6 +30,9 @@ Commands:
30
30
  runs to ~/.sickr/runs (secrets redacted).
31
31
  --codex install for Codex (.codex/hooks.json) instead of
32
32
  Claude Code (.claude/settings.json)
33
+ --both install for BOTH Claude Code and Codex (so the
34
+ combined `, open;
35
+ --today ` view sees everything)
33
36
  --no-name label your prompts "Human" instead of your
34
37
  login/git name (default is your login name)
35
38
  open [run] Render a run to a local HTML timeline and open it. 100% local.
@@ -39,8 +42,10 @@ Commands:
39
42
  filterable by agent, sortable by prompt/response time).
40
43
  share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
41
44
  (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
45
+ --open also open the published link in your browser
46
+ --yes skip the confirmation prompt
47
+ Or publish a COMBINED multi-agent view with a window:
48
+ --today / --since <2h|30m|1d> / --all (+ --claude/--codex).
44
49
  list List recorded runs, newest first.
45
50
  stop Stop recording — removes SICKR's hooks from this project.
46
51
  Your recorded runs are kept; run \`init\` to start again.
@@ -323,56 +328,80 @@ function handleList(provider) {
323
328
  process.stdout.write(`${id} ${s.agent.padEnd(7)} ${String(s.events).padStart(4)} ev ${when}${snippet}\n`);
324
329
  });
325
330
  }
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');
331
+ async function confirmPublish(yes, what) {
332
+ if (yes)
333
+ return true;
334
+ if (!process.stdin.isTTY) {
335
+ process.stderr.write('sickr: re-run with --yes to publish non-interactively.\n');
330
336
  process.exit(1);
331
- return;
337
+ return false;
332
338
  }
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
- }
339
+ process.stdout.write(`Publish ${what} publicly? [y/N] `);
340
+ const a = await promptLine();
341
+ if (a !== 'y' && a !== 'yes') {
342
+ process.stdout.write('sickr: cancelled.\n');
343
+ return false;
349
344
  }
350
- let url;
345
+ return true;
346
+ }
347
+ /** Publish with a single friendly retry on 429 (the WAF allows ~1/10s). Returns the URL. */
348
+ async function publishWithRetry(payload) {
351
349
  try {
352
- ({ url } = await publish(payload, REPLAY_ENDPOINT));
350
+ return (await publish(payload, REPLAY_ENDPOINT)).url;
353
351
  }
354
352
  catch (err) {
355
353
  if (err instanceof PublishError && err.status === 429) {
356
354
  process.stdout.write('sickr: rate-limited — you can publish about once every 10s. Waiting to retry once...\n');
357
355
  await new Promise((r) => setTimeout(r, 11_000));
358
356
  try {
359
- ({ url } = await publish(payload, REPLAY_ENDPOINT));
357
+ return (await publish(payload, REPLAY_ENDPOINT)).url;
360
358
  }
361
359
  catch (retryErr) {
362
360
  if (retryErr instanceof PublishError && retryErr.status === 429) {
363
361
  process.stderr.write('sickr: still rate-limited. Give it a minute and run `share` again.\n');
364
362
  process.exit(1);
365
- return;
366
363
  }
367
364
  throw retryErr;
368
365
  }
369
366
  }
370
- else {
371
- throw err;
372
- }
367
+ throw err;
368
+ }
369
+ }
370
+ async function handleShare(runId, yes, open) {
371
+ const id = runId ?? latestRunId();
372
+ if (!id) {
373
+ process.stderr.write('sickr: no runs to share. Use Claude Code or Codex first.\n');
374
+ process.exit(1);
375
+ return;
376
+ }
377
+ const payload = buildSharePayload(loadRun(id));
378
+ process.stdout.write(`sickr: about to publish run "${id}" (${payload.run.events.length} events, secrets already redacted) to ${REPLAY_ENDPOINT}\n` +
379
+ `sickr: tip — run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
380
+ if (!(await confirmPublish(yes, 'this run')))
381
+ return;
382
+ const url = await publishWithRetry(payload);
383
+ process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\n`);
384
+ if (open)
385
+ openInBrowser(url);
386
+ }
387
+ async function handleShareCombined(sel, yes, open) {
388
+ const runs = sel.ids
389
+ .map((id) => ({ id, events: loadRun(id).events }))
390
+ .filter((r) => r.events.length)
391
+ .map((r) => ({ agent: runSummary(r.id).agent, events: r.events }));
392
+ if (runs.length === 0) {
393
+ process.stdout.write(`sickr: no runs in ${sel.label} to share.\n`);
394
+ return;
373
395
  }
374
- process.stdout.write(`sickr: published ${url}\n`);
375
- process.stdout.write('sickr: this link expires in 24h.\n');
396
+ const turns = runs.reduce((n, r) => n + r.events.filter((e) => e.kind === 'prompt').length, 0);
397
+ const agents = Array.from(new Set(runs.map((r) => r.agent))).join(', ');
398
+ const payload = buildCombinedPayload(runs, sel.label);
399
+ 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` +
400
+ `sickr: tip — run the matching \`open\` window to review locally before sharing.\n`);
401
+ if (!(await confirmPublish(yes, 'this combined replay')))
402
+ return;
403
+ const url = await publishWithRetry(payload);
404
+ process.stdout.write(`sickr: published → ${url}\nsickr: this link expires in 24h.\n`);
376
405
  if (open)
377
406
  openInBrowser(url);
378
407
  }
@@ -404,9 +433,16 @@ async function main() {
404
433
  case 'record':
405
434
  handleRecord(await readStdin(), provider);
406
435
  return;
407
- case 'init':
408
- handleInit(provider, rest.includes('--no-name'));
436
+ case 'init': {
437
+ const noName = rest.includes('--no-name');
438
+ if (rest.includes('--both')) {
439
+ handleInit('claude', noName);
440
+ handleInit('codex', noName);
441
+ return;
442
+ }
443
+ handleInit(provider, noName);
409
444
  return;
445
+ }
410
446
  case 'open': {
411
447
  const sel = selectWindow(rest);
412
448
  if (sel) {
@@ -428,9 +464,17 @@ async function main() {
428
464
  case 'clear':
429
465
  await handleClear(rest.includes('--yes') || rest.includes('-y'));
430
466
  return;
431
- case 'share':
432
- await handleShare(rest.find((a) => !a.startsWith('-')), rest.includes('--yes') || rest.includes('-y'), rest.includes('--open'));
467
+ case 'share': {
468
+ const yes = rest.includes('--yes') || rest.includes('-y');
469
+ const openAfter = rest.includes('--open');
470
+ const sel = selectWindow(rest);
471
+ if (sel) {
472
+ await handleShareCombined(sel, yes, openAfter);
473
+ return;
474
+ }
475
+ await handleShare(rest.find((a) => !a.startsWith('-')), yes, openAfter);
433
476
  return;
477
+ }
434
478
  default:
435
479
  process.stderr.write('sickr: unknown command. Run `npx @sickr/replay help`.\n');
436
480
  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.2",
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" },