@sickr/replay 0.4.0 → 0.4.4

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 +61 -11
  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 });
@@ -108,10 +113,15 @@ export function handleInit(provider, handle) {
108
113
  catch { /* none */ }
109
114
  writeFileSync(configPath(), JSON.stringify({ ...existing, handle }, null, 2) + '\n');
110
115
  }
116
+ const labelLine = handle
117
+ ? `Your prompts will be labelled "${handle}".\n`
118
+ : 'Tip: set SICKR_HANDLE or run `init --as "<name>"` to label your prompts.\n';
119
+ const nextSteps = provider === 'codex'
120
+ ? 'Next: in Codex, run `/hooks` to review & trust these hooks (Codex gates new hooks),\nthen use Codex as normal and: npx @sickr/replay open --codex\n'
121
+ : 'Use Claude Code as normal, then: npx @sickr/replay open\n';
111
122
  process.stdout.write(`sickr: installed ${p.name} recording hooks in ${settingsPath}\n` +
112
123
  `Runs are recorded locally to ${runsDir()} (secrets redacted).\n` +
113
- (handle ? `Your prompts will be labelled "${handle}".\n` : 'Tip: set SICKR_HANDLE or run `init --as "<name>"` to label your prompts.\n') +
114
- `Use ${p.name} as normal, then: npx @sickr/replay open\n`);
124
+ labelLine + nextSteps);
115
125
  }
116
126
  /** Stop recording: remove SICKR's hooks from this project (both providers), keep runs. */
117
127
  export function handleStop() {
@@ -171,17 +181,49 @@ function openInBrowser(file) {
171
181
  }
172
182
  catch { /* ignore */ }
173
183
  }
174
- function handleOpen(runId) {
175
- const id = runId ?? latestRunId();
184
+ /** A short, human-readable summary of a run: agent + first prompt + event count. */
185
+ function runSummary(id) {
186
+ const run = loadRun(id);
187
+ const agent = run.events.find((e) => e.kind === 'response')?.label || '—';
188
+ const prompt = (run.events.find((e) => e.kind === 'prompt')?.detail || '').replace(/\s+/g, ' ').trim();
189
+ return { agent, prompt, events: run.events.length };
190
+ }
191
+ /** Newest run whose agent (response label) matches `agent`, or null. */
192
+ export function latestRunIdFor(agent) {
193
+ const dir = runsDir();
194
+ if (!existsSync(dir))
195
+ return null;
196
+ const files = readdirSync(dir)
197
+ .filter((f) => f.endsWith('.ndjson'))
198
+ .sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs);
199
+ for (const f of files) {
200
+ const id = f.replace(/\.ndjson$/, '');
201
+ if (loadRun(id).events.some((e) => e.kind === 'response' && e.label === agent))
202
+ return id;
203
+ }
204
+ return null;
205
+ }
206
+ function handleOpen(runId, provider) {
207
+ let id = runId;
208
+ if (!id && provider) {
209
+ id = latestRunIdFor(PROVIDERS[provider].label) ?? undefined;
210
+ if (!id) {
211
+ process.stdout.write(`sickr: no ${PROVIDERS[provider].label} runs yet — use ${PROVIDERS[provider].name} with the hooks installed, then try again.\n`);
212
+ return;
213
+ }
214
+ }
215
+ id = id ?? latestRunId() ?? undefined;
176
216
  if (!id) {
177
- process.stdout.write('sickr: no runs recorded yet. Run `npx @sickr/replay init`, then use Claude Code.\n');
217
+ process.stdout.write('sickr: no runs recorded yet. Run `npx @sickr/replay init`, then use Claude Code or Codex.\n');
178
218
  return;
179
219
  }
180
220
  const html = renderRunHtml(loadRun(id));
181
221
  const out = join(homedir(), '.sickr', 'last.html');
182
222
  mkdirSync(join(homedir(), '.sickr'), { recursive: true });
183
223
  writeFileSync(out, html);
184
- process.stdout.write(`sickr: opened replay for ${id} → ${out}\n`);
224
+ const s = runSummary(id);
225
+ process.stdout.write(`sickr: opened ${s.agent} run ${id} · ${s.events} events${s.prompt ? ` · "${s.prompt.slice(0, 60)}"` : ''}\n` +
226
+ `→ ${out} (newest run; use \`list\` to see others, \`open <id>\` to pick one)\n`);
185
227
  openInBrowser(out);
186
228
  }
187
229
  function handleList() {
@@ -193,7 +235,13 @@ function handleList() {
193
235
  }
194
236
  files
195
237
  .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`));
238
+ .forEach((f) => {
239
+ const id = f.replace(/\.ndjson$/, '');
240
+ const s = runSummary(id);
241
+ const when = statSync(join(dir, f)).mtime.toISOString().replace('T', ' ').slice(0, 16);
242
+ const snippet = s.prompt ? ` "${s.prompt.slice(0, 48)}"` : '';
243
+ process.stdout.write(`${id} ${s.agent.padEnd(7)} ${String(s.events).padStart(4)} ev ${when}${snippet}\n`);
244
+ });
197
245
  }
198
246
  async function handleShare(runId, yes, open) {
199
247
  const id = runId ?? latestRunId();
@@ -282,9 +330,11 @@ async function main() {
282
330
  handleInit(provider, handle);
283
331
  return;
284
332
  }
285
- case 'open':
286
- handleOpen(argv.find((a, i) => i > 0 && !a.startsWith('-')));
333
+ case 'open': {
334
+ const openProvider = rest.includes('--codex') ? 'codex' : rest.includes('--claude') ? 'claude' : undefined;
335
+ handleOpen(rest.find((a) => !a.startsWith('-')), openProvider);
287
336
  return;
337
+ }
288
338
  case 'list':
289
339
  handleList();
290
340
  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.4",
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" },