@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.
Files changed (2) hide show
  1. package/dist/cli.js +49 -34
  2. 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
- --as "<name>" label your prompts with <name> on replays
34
- (default "Human"; or set SICKR_HANDLE)
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
- /** Display name for the human: SICKR_HANDLE env, else ~/.sickr/config.json handle, else "Human". */
75
- function resolveHandle() {
76
- if (process.env.SICKR_HANDLE)
77
- return process.env.SICKR_HANDLE;
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.handle)
81
- return c.handle;
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: resolveHandle(), agent: PROVIDERS[provider].label });
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, handle) {
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
- if (handle) {
109
- let existing = {};
110
- try {
111
- existing = JSON.parse(readFileSync(configPath(), 'utf8'));
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
- const agent = run.events.find((e) => e.kind === 'response')?.label || '—';
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
- const files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.ndjson')) : [];
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
- const asIdx = rest.indexOf('--as');
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
- handleList();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/replay",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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" },