@sickr/replay 0.4.4 → 0.4.6
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 +49 -34
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
4
|
+
import { homedir, userInfo } from 'node:os';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
6
|
+
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 } from './render.js';
|
|
@@ -30,8 +30,8 @@ Commands:
|
|
|
30
30
|
runs to ~/.sickr/runs (secrets redacted).
|
|
31
31
|
--codex install for Codex (.codex/hooks.json) instead of
|
|
32
32
|
Claude Code (.claude/settings.json)
|
|
33
|
-
--
|
|
34
|
-
(default
|
|
33
|
+
--no-name label your prompts "Human" instead of your
|
|
34
|
+
login/git name (default is your login name)
|
|
35
35
|
open [run] Render a run to a local HTML timeline and open it in your
|
|
36
36
|
browser. 100% local — nothing is uploaded. Defaults to the
|
|
37
37
|
newest run; pass a run id, or --codex / --claude to open the
|
|
@@ -71,14 +71,28 @@ const PROVIDERS = {
|
|
|
71
71
|
function configPath() {
|
|
72
72
|
return join(homedir(), '.sickr', 'config.json');
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
/** The machine's own identity — git user.name, else OS username, else "Human". */
|
|
75
|
+
function loginName() {
|
|
76
|
+
try {
|
|
77
|
+
const n = execFileSync('git', ['config', 'user.name'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
78
|
+
if (n)
|
|
79
|
+
return n;
|
|
80
|
+
}
|
|
81
|
+
catch { /* git not configured */ }
|
|
82
|
+
try {
|
|
83
|
+
const u = userInfo().username;
|
|
84
|
+
if (u)
|
|
85
|
+
return u;
|
|
86
|
+
}
|
|
87
|
+
catch { /* no os user */ }
|
|
88
|
+
return 'Human';
|
|
89
|
+
}
|
|
90
|
+
/** Human label for prompts: the name stored at init (login name, or "Human" if anonymized). */
|
|
91
|
+
function resolveName() {
|
|
78
92
|
try {
|
|
79
93
|
const c = JSON.parse(readFileSync(configPath(), 'utf8'));
|
|
80
|
-
if (c.
|
|
81
|
-
return c.
|
|
94
|
+
if (c.name)
|
|
95
|
+
return c.name;
|
|
82
96
|
}
|
|
83
97
|
catch { /* no config */ }
|
|
84
98
|
return 'Human';
|
|
@@ -87,13 +101,13 @@ function resolveHandle() {
|
|
|
87
101
|
export function handleRecord(input, provider = 'claude') {
|
|
88
102
|
try {
|
|
89
103
|
const cc = JSON.parse(input);
|
|
90
|
-
appendEvent(currentRunId(cc), cc, { human:
|
|
104
|
+
appendEvent(currentRunId(cc), cc, { human: resolveName(), agent: PROVIDERS[provider].label });
|
|
91
105
|
}
|
|
92
106
|
catch {
|
|
93
107
|
/* swallow: recording is best-effort and must not disrupt the session */
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
|
-
export function handleInit(provider,
|
|
110
|
+
export function handleInit(provider, noName = false) {
|
|
97
111
|
const p = PROVIDERS[provider];
|
|
98
112
|
const settingsPath = p.settingsPath();
|
|
99
113
|
const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, 'utf8')) : {};
|
|
@@ -105,17 +119,11 @@ export function handleInit(provider, handle) {
|
|
|
105
119
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
106
120
|
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
|
|
107
121
|
mkdirSync(runsDir(), { recursive: true });
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
catch { /* none */ }
|
|
114
|
-
writeFileSync(configPath(), JSON.stringify({ ...existing, handle }, null, 2) + '\n');
|
|
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';
|
|
122
|
+
// Always (re)write the name so re-running init resets it — no arbitrary names
|
|
123
|
+
// (avoids impersonation on public shares); just the login name, or "Human".
|
|
124
|
+
const name = noName ? 'Human' : loginName();
|
|
125
|
+
writeFileSync(configPath(), JSON.stringify({ name }, null, 2) + '\n');
|
|
126
|
+
const labelLine = `Your prompts will be labelled "${name}"${noName ? '' : ' — run `init --no-name` to anonymize'}.\n`;
|
|
119
127
|
const nextSteps = provider === 'codex'
|
|
120
128
|
? '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
129
|
: 'Use Claude Code as normal, then: npx @sickr/replay open\n';
|
|
@@ -184,7 +192,11 @@ function openInBrowser(file) {
|
|
|
184
192
|
/** A short, human-readable summary of a run: agent + first prompt + event count. */
|
|
185
193
|
function runSummary(id) {
|
|
186
194
|
const run = loadRun(id);
|
|
187
|
-
|
|
195
|
+
// Use the LATEST response label — long-lived runs may carry pre-provider
|
|
196
|
+
// ('Response') labels from older CLI versions in their early events.
|
|
197
|
+
const responses = run.events.filter((e) => e.kind === 'response');
|
|
198
|
+
const last = responses.length ? responses[responses.length - 1].label : '';
|
|
199
|
+
const agent = last && last !== 'Response' ? last : '—';
|
|
188
200
|
const prompt = (run.events.find((e) => e.kind === 'prompt')?.detail || '').replace(/\s+/g, ' ').trim();
|
|
189
201
|
return { agent, prompt, events: run.events.length };
|
|
190
202
|
}
|
|
@@ -226,11 +238,15 @@ function handleOpen(runId, provider) {
|
|
|
226
238
|
`→ ${out} (newest run; use \`list\` to see others, \`open <id>\` to pick one)\n`);
|
|
227
239
|
openInBrowser(out);
|
|
228
240
|
}
|
|
229
|
-
function handleList() {
|
|
241
|
+
function handleList(provider) {
|
|
230
242
|
const dir = runsDir();
|
|
231
|
-
|
|
243
|
+
let files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.ndjson')) : [];
|
|
244
|
+
if (provider) {
|
|
245
|
+
const want = PROVIDERS[provider].label;
|
|
246
|
+
files = files.filter((f) => loadRun(f.replace(/\.ndjson$/, '')).events.some((e) => e.kind === 'response' && e.label === want));
|
|
247
|
+
}
|
|
232
248
|
if (files.length === 0) {
|
|
233
|
-
process.stdout.write('sickr: no runs yet.\n');
|
|
249
|
+
process.stdout.write(provider ? `sickr: no ${PROVIDERS[provider].label} runs yet.\n` : 'sickr: no runs yet.\n');
|
|
234
250
|
return;
|
|
235
251
|
}
|
|
236
252
|
files
|
|
@@ -324,20 +340,19 @@ async function main() {
|
|
|
324
340
|
case 'record':
|
|
325
341
|
handleRecord(await readStdin(), provider);
|
|
326
342
|
return;
|
|
327
|
-
case 'init':
|
|
328
|
-
|
|
329
|
-
const handle = asIdx >= 0 ? rest[asIdx + 1] : undefined;
|
|
330
|
-
handleInit(provider, handle);
|
|
343
|
+
case 'init':
|
|
344
|
+
handleInit(provider, rest.includes('--no-name'));
|
|
331
345
|
return;
|
|
332
|
-
}
|
|
333
346
|
case 'open': {
|
|
334
347
|
const openProvider = rest.includes('--codex') ? 'codex' : rest.includes('--claude') ? 'claude' : undefined;
|
|
335
348
|
handleOpen(rest.find((a) => !a.startsWith('-')), openProvider);
|
|
336
349
|
return;
|
|
337
350
|
}
|
|
338
|
-
case 'list':
|
|
339
|
-
|
|
351
|
+
case 'list': {
|
|
352
|
+
const listProvider = rest.includes('--codex') ? 'codex' : rest.includes('--claude') ? 'claude' : undefined;
|
|
353
|
+
handleList(listProvider);
|
|
340
354
|
return;
|
|
355
|
+
}
|
|
341
356
|
case 'stop':
|
|
342
357
|
handleStop();
|
|
343
358
|
return;
|
package/package.json
CHANGED