@sickr/replay 0.4.0 → 0.4.3

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 (2) hide show
  1. package/dist/cli.js +54 -9
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -33,7 +33,9 @@ Commands:
33
33
  --as "<name>" label your prompts with <name> on replays
34
34
  (default "Human"; or set SICKR_HANDLE)
35
35
  open [run] Render a run to a local HTML timeline and open it in your
36
- browser. 100% local — nothing is uploaded.
36
+ browser. 100% local — nothing is uploaded. Defaults to the
37
+ newest run; pass a run id, or --codex / --claude to open the
38
+ newest run for that agent.
37
39
  share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
38
40
  (shows a preview and asks first). Links expire after 24h.
39
41
  --open also open the published link in your browser
@@ -96,7 +98,10 @@ export function handleInit(provider, handle) {
96
98
  const settingsPath = p.settingsPath();
97
99
  const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, 'utf8')) : {};
98
100
  const command = `npx @sickr/replay record${provider === 'codex' ? ' --codex' : ''}`;
99
- const merged = mergeHooks(settings, command);
101
+ // Remove any prior SICKR hook first, then install the current command — so
102
+ // re-running init (or a CLI upgrade that changes the command) self-heals
103
+ // instead of leaving a stale hook. Scoped to this provider's file.
104
+ const merged = mergeHooks(removeHooks(settings), command);
100
105
  mkdirSync(dirname(settingsPath), { recursive: true });
101
106
  writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
102
107
  mkdirSync(runsDir(), { recursive: true });
@@ -171,17 +176,49 @@ function openInBrowser(file) {
171
176
  }
172
177
  catch { /* ignore */ }
173
178
  }
174
- function handleOpen(runId) {
175
- const id = runId ?? latestRunId();
179
+ /** A short, human-readable summary of a run: agent + first prompt + event count. */
180
+ function runSummary(id) {
181
+ const run = loadRun(id);
182
+ const agent = run.events.find((e) => e.kind === 'response')?.label || '—';
183
+ const prompt = (run.events.find((e) => e.kind === 'prompt')?.detail || '').replace(/\s+/g, ' ').trim();
184
+ return { agent, prompt, events: run.events.length };
185
+ }
186
+ /** Newest run whose agent (response label) matches `agent`, or null. */
187
+ export function latestRunIdFor(agent) {
188
+ const dir = runsDir();
189
+ if (!existsSync(dir))
190
+ return null;
191
+ const files = readdirSync(dir)
192
+ .filter((f) => f.endsWith('.ndjson'))
193
+ .sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs);
194
+ for (const f of files) {
195
+ const id = f.replace(/\.ndjson$/, '');
196
+ if (loadRun(id).events.some((e) => e.kind === 'response' && e.label === agent))
197
+ return id;
198
+ }
199
+ return null;
200
+ }
201
+ function handleOpen(runId, provider) {
202
+ let id = runId;
203
+ if (!id && provider) {
204
+ id = latestRunIdFor(PROVIDERS[provider].label) ?? undefined;
205
+ if (!id) {
206
+ process.stdout.write(`sickr: no ${PROVIDERS[provider].label} runs yet — use ${PROVIDERS[provider].name} with the hooks installed, then try again.\n`);
207
+ return;
208
+ }
209
+ }
210
+ id = id ?? latestRunId() ?? undefined;
176
211
  if (!id) {
177
- process.stdout.write('sickr: no runs recorded yet. Run `npx @sickr/replay init`, then use Claude Code.\n');
212
+ process.stdout.write('sickr: no runs recorded yet. Run `npx @sickr/replay init`, then use Claude Code or Codex.\n');
178
213
  return;
179
214
  }
180
215
  const html = renderRunHtml(loadRun(id));
181
216
  const out = join(homedir(), '.sickr', 'last.html');
182
217
  mkdirSync(join(homedir(), '.sickr'), { recursive: true });
183
218
  writeFileSync(out, html);
184
- process.stdout.write(`sickr: opened replay for ${id} → ${out}\n`);
219
+ const s = runSummary(id);
220
+ process.stdout.write(`sickr: opened ${s.agent} run ${id} · ${s.events} events${s.prompt ? ` · "${s.prompt.slice(0, 60)}"` : ''}\n` +
221
+ `→ ${out} (newest run; use \`list\` to see others, \`open <id>\` to pick one)\n`);
185
222
  openInBrowser(out);
186
223
  }
187
224
  function handleList() {
@@ -193,7 +230,13 @@ function handleList() {
193
230
  }
194
231
  files
195
232
  .sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs)
196
- .forEach((f) => process.stdout.write(`${f.replace(/\.ndjson$/, '')}\t${statSync(join(dir, f)).mtime.toISOString()}\n`));
233
+ .forEach((f) => {
234
+ const id = f.replace(/\.ndjson$/, '');
235
+ const s = runSummary(id);
236
+ const when = statSync(join(dir, f)).mtime.toISOString().replace('T', ' ').slice(0, 16);
237
+ const snippet = s.prompt ? ` "${s.prompt.slice(0, 48)}"` : '';
238
+ process.stdout.write(`${id} ${s.agent.padEnd(7)} ${String(s.events).padStart(4)} ev ${when}${snippet}\n`);
239
+ });
197
240
  }
198
241
  async function handleShare(runId, yes, open) {
199
242
  const id = runId ?? latestRunId();
@@ -282,9 +325,11 @@ async function main() {
282
325
  handleInit(provider, handle);
283
326
  return;
284
327
  }
285
- case 'open':
286
- handleOpen(argv.find((a, i) => i > 0 && !a.startsWith('-')));
328
+ case 'open': {
329
+ const openProvider = rest.includes('--codex') ? 'codex' : rest.includes('--claude') ? 'claude' : undefined;
330
+ handleOpen(rest.find((a) => !a.startsWith('-')), openProvider);
287
331
  return;
332
+ }
288
333
  case 'list':
289
334
  handleList();
290
335
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/replay",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
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" },