@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.
- package/dist/cli.js +54 -9
- 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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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