@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 +69 -35
- package/dist/share.js +4 -0
- package/package.json +1 -1
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
|
|
43
|
-
--yes
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
371
|
-
throw err;
|
|
372
|
-
}
|
|
364
|
+
throw err;
|
|
373
365
|
}
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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