@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.
- package/dist/cli.js +61 -11
- 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 });
|
|
@@ -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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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