@sickr/cli 0.9.6 → 0.9.8

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 CHANGED
@@ -11,8 +11,9 @@ import { buildSharePayload, buildCombinedPayload, publish, PublishError } from '
11
11
  import { AUTH_ENDPOINT, readCredentials, writeCredentials, clearCredentials, startDevice, pollDevice, sleep } from './auth.js';
12
12
  import { ui, card, kv } from './ui.js';
13
13
  import { AGENT_API_URL, clearAgentCredentials, disconnectAgent, fetchAgentStatus, pollAgentConnect, readAgentCredentials, rotateAgentKey, startAgentConnect, writeAgentCredentials, } from './agentAuth.js';
14
+ import { PROVIDERS, recordCommandFor } from './providers.js';
14
15
  const REPLAY_ENDPOINT = process.env.SICKR_REPLAY_ENDPOINT ?? 'https://sickr.ai/api/replay';
15
- const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'login', 'logout', 'whoami', 'agent', 'live', 'run', 'replay', 'workflow', 'help'];
16
+ const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'login', 'logout', 'whoami', 'agent', 'live', 'run', 'replay', 'prime', 'workflow', 'start', 'status', 'help'];
16
17
  export function parseCommand(argv) {
17
18
  const c = argv[0];
18
19
  return c && COMMANDS.includes(c) ? c : null;
@@ -26,7 +27,7 @@ export function replaySubcommand(rest) {
26
27
  }
27
28
  return null;
28
29
  }
29
- export function workflowAgentAlias(rest) {
30
+ export function primeAgentAlias(rest) {
30
31
  const sub = rest[0];
31
32
  if (sub === 'connect' || sub === 'rotate' || sub === 'disconnect')
32
33
  return [...rest];
@@ -35,7 +36,7 @@ export function workflowAgentAlias(rest) {
35
36
  function withoutFirst(rest) {
36
37
  return rest.slice(1);
37
38
  }
38
- export function buildWorkflowInvocation(rest, command = 'uvx') {
39
+ export function buildPrimeInvocation(rest, command = 'uvx') {
39
40
  const sub = rest[0] ?? 'status';
40
41
  const tail = withoutFirst(rest);
41
42
  if (sub === 'start') {
@@ -51,121 +52,92 @@ export function buildWorkflowInvocation(rest, command = 'uvx') {
51
52
  }
52
53
  return { command, args: rest };
53
54
  }
54
- export const HELP = `SICKR Replay — audit & replay what your AI coding agent did.
55
+ export const workflowAgentAlias = primeAgentAlias;
56
+ export const buildWorkflowInvocation = buildPrimeInvocation;
57
+ export function runtimeConfigSummary(config) {
58
+ const modeLabel = {
59
+ replay: 'Replay',
60
+ live: 'Live',
61
+ run: 'Run',
62
+ prime_workflow: 'Prime Workflow',
63
+ };
64
+ const provider = PROVIDERS[config.provider];
65
+ return `${config.agent_id}: ${modeLabel[config.mode]} on ${provider.displayName}`;
66
+ }
67
+ function providerFromFlags(args) {
68
+ if (args.includes('--codex'))
69
+ return 'codex';
70
+ if (args.includes('--gemini'))
71
+ return 'gemini';
72
+ if (args.includes('--cursor'))
73
+ return 'cursor';
74
+ if (args.includes('--local'))
75
+ return 'local';
76
+ return 'claude';
77
+ }
78
+ export const HELP = `sickr - replay, live view, remote control, and Prime Workflow for AI coding agents.
55
79
 
56
- Records your Claude Code or Codex session (prompts, edits, commands) to a local,
57
- redacted timeline you can replay — and optionally share as a public link.
80
+ Usage: npx @sickr/cli <command> [options]
58
81
 
59
- Why: a durable record of every agent action — a dashcam for your coding agent.
60
- If your agent (Claude or Codex) loses context or can't reload a past chat, the
61
- replay log helps you — and it — recall exactly what was just done.
82
+ ACCOUNT
83
+ login Sign in with GitHub.
84
+ logout Forget the local login.
85
+ whoami Show who you're signed in as.
62
86
 
63
- Usage: npx @sickr/cli <command> [options]
87
+ START HERE
88
+ start --agent-id <id>
89
+ Fetch the configured runtime from workflow.sickr.ai
90
+ and start the right mode for that agent: Prime Workflow,
91
+ Run, Live, or Replay.
92
+
93
+ REPLAY (free) - local recording of every prompt, edit and command.
94
+ replay Install Claude + Codex recording hooks. Use the agents
95
+ as normal - a redacted timeline is captured to
96
+ ~/.sickr/runs.
97
+ replay open [id] Render the newest run as a local HTML timeline.
98
+ replay share [id] Publish a redacted run to sickr.ai/r/<id>.
99
+ replay list List recorded runs, newest first.
100
+ replay stop Remove sickr hooks from this project.
101
+ replay clear Delete all local runs in ~/.sickr/runs.
64
102
 
65
- Commands:
66
- replay Auto mode: installs Claude + Codex recording hooks. If the
67
- signed-in user has Replay Pro, starts Live Look as well.
68
- replay init all|claude|codex
69
- Install recording hooks under the unified command name.
70
- replay share Publish a redacted replay to sickr.ai/r/<id>.
71
- replay status Show Live Look sidecar status.
72
- workflow connect --agent-id <id>
73
- Connect this machine to a configured SICKR workflow agent.
74
- workflow start --agent-id <id>
75
- Start the workflow orchestrator for that configured agent.
76
- workflow status
77
- Show running workflow daemon status.
78
- workflow rotate | workflow disconnect
79
- Rotate or revoke this machine's workflow agent key.
103
+ LIVE ($9) - passive streaming to a watching browser.
104
+ live Stream events to sickr.ai/r/<your-link>. Browser text is
105
+ queued to ~/.sickr/inbox/<urlid>.md; it does not control
106
+ an already-running agent.
107
+ live status Show pid + connection state.
108
+ live stop Stop the sidecar.
80
109
 
81
- Legacy replay commands remain supported:
82
- init <agent> Install recording hooks for an agent (REQUIRED no default)
83
- and start capturing to ~/.sickr/runs (secrets redacted):
84
- claude Claude Code (.claude/settings.json)
85
- codex Codex (.codex/hooks.json needs Codex 0.133+)
86
- all both of the above (feeds the combined view)
87
- Flag: --no-name (label prompts "Human", not your login name)
88
- open [run] Render a run to a local HTML timeline and open it. 100% local.
89
- Defaults to the newest run; pass a run id, or --codex/--claude
90
- for the newest run of that agent. Combine across agents with a
91
- window: --today, --since <2h|30m|1d>, or --all (interleaved,
92
- filterable by agent, sortable by prompt/response time).
93
- share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
94
- (shows a preview and asks first). Links expire after 24h.
95
- --open also open the published link in your browser
96
- --yes skip the confirmation prompt
97
- Or publish a COMBINED multi-agent view with a window:
98
- --today / --since <2h|30m|1d> / --all (+ --claude/--codex).
99
- list List recorded runs, newest first.
100
- stop Stop recording — removes SICKR's hooks from this project.
101
- Your recorded runs are kept; run \`init\` to start again.
102
- clear Delete all local runs in ~/.sickr/runs (asks first).
103
- login Sign in with GitHub (optional — unlocks persistent shares and
104
- Replay Pro cohort eligibility). Zero-account use still works.
105
- logout Forget the local login. Server-side session stays valid until
106
- it expires; revoke from your account page if needed.
107
- whoami Show who you're logged in as.
108
- live Replay Pro: stream the current session to sickr.ai/r/<your-link>
109
- in real time. Requires \`login\` and Replay Pro entitlement.
110
- replay live start the sidecar (foreground; ^C exits)
111
- replay live status show pid + connection state
112
- replay live stop stop the sidecar
113
- While running, steer messages from the watching browser are
114
- saved to ~/.sickr/inbox/<urlid>.md and printed in your terminal.
115
- run <agent> Replay Pro (Phase 2): wrap an agent in a PTY this CLI owns.
116
- Browser steer messages land directly in the agent's stdin —
117
- real remote control, no copy-paste from inbox.
118
- sickr run claude spawn Claude Code under sickr (auto-perms)
119
- sickr run codex same for Codex (auto-perms)
120
- sickr run <bin> arbitrary command
121
- Modes:
122
- --mode auto (default) inject the agent's "no prompt /
123
- full permissions" flag so it
124
- doesn't stall on tool confirms
125
- the operator can't see (claude:
126
- --dangerously-skip-permissions;
127
- codex: --dangerously-bypass-
128
- approvals-and-sandbox)
129
- --mode interactive pass agent flags through verbatim;
130
- agent will prompt for tool use
131
- \`node-pty\` ships as an optional dep + auto-installs with the
132
- CLI on supported platforms (mac, Linux, Windows 10+).
133
- Browsers can override per-message: {mode:'queue'|'steer',
134
- submit:true|false}; default is auto-steer with submit.
135
- agent connect --agent-id <id>
136
- Connect this machine to a configured SICKR agent using GitHub
137
- browser approval. Stores the agent key in ~/.sickr/agent.json.
138
- agent status Show the connected agent, org and team.
139
- agent rotate Rotate this machine's agent key.
140
- agent disconnect
141
- Revoke this machine's agent key and remove it locally.
142
- help Show this help.
110
+ RUN ($12) - remote control from the browser.
111
+ run <agent> Wrap an agent in a PTY sickr owns; browser steer messages
112
+ are written directly into the agent's stdin.
113
+ sickr run claude
114
+ sickr run codex
115
+ sickr run gemini
116
+ sickr run cursor-agent
117
+ sickr run <bin>
118
+ Flags: --mode auto (default) | --mode interactive
143
119
 
144
- Requires Node 18+. Codex capture needs Codex CLI 0.133+ (run /hooks to trust);
145
- Claude Code: any hooks-capable build.
120
+ PRIME WORKFLOW - governed ticket execution.
121
+ prime connect --agent-id <id> Approve this machine for a configured
122
+ Prime Workflow agent.
123
+ prime start --agent-id <id> Start PRIME for an agent.
124
+ prime status Show running daemon status.
125
+ prime stop Stop the local PRIME daemon.
126
+ prime rotate | disconnect Rotate or revoke this machine's key.
146
127
 
147
- ────────────────────────────────────────────────────────────────────
148
- This replays your AI coding agents on ONE machine. SICKR governs your whole team.
149
- Issue tracking + your team + automation + agents — one governed workflow for
150
- audit, accountability, productivity and confidence.
128
+ Product modes are ordered: Prime Workflow > Run > Live > Replay.
129
+ Prime Workflow runs PRIME for agentic or hybrid teams: plan, review,
130
+ implement, merge, evaluate, and ship under control.
151
131
 
152
- · Gates & approvals work holds at plan sign-off, review, merge and
153
- validation checks until each one passes.
154
- · Humans + agents on one board — agents are first-class teammates with
155
- roles, capacity and accountability, not a side channel.
156
- · A full, signed-off audit trail across every actor and every change.
157
- · Runs 24/7 — produce as much work as you like; the team handles it.
132
+ Requires Node 20+. Codex capture needs Codex CLI 0.133+ and one manual
133
+ /hooks trust approval. Gemini/Cursor/local runners are adapter-backed and
134
+ may have provider-specific setup prompts.
158
135
 
159
- Free tier available · bring your own Claude or Codex subscription.
160
- → https://sickr.ai
136
+ https://sickr.ai
161
137
  `;
162
138
  export function currentRunId(cc) {
163
139
  return String(cc.session_id ?? 'session');
164
140
  }
165
- const PROVIDERS = {
166
- claude: { name: 'Claude Code', label: 'Claude', settingsPath: () => join(process.cwd(), '.claude', 'settings.json') },
167
- codex: { name: 'Codex', label: 'Codex', settingsPath: () => join(process.cwd(), '.codex', 'hooks.json') },
168
- };
169
141
  function configPath() {
170
142
  return join(homedir(), '.sickr', 'config.json');
171
143
  }
@@ -199,7 +171,7 @@ function resolveName() {
199
171
  export function handleRecord(input, provider = 'claude') {
200
172
  try {
201
173
  const cc = JSON.parse(input);
202
- appendEvent(currentRunId(cc), cc, { human: resolveName(), agent: PROVIDERS[provider].label });
174
+ appendEvent(currentRunId(cc), cc, { human: resolveName(), agent: PROVIDERS[provider].recordLabel });
203
175
  }
204
176
  catch {
205
177
  /* swallow: recording is best-effort and must not disrupt the session */
@@ -207,9 +179,13 @@ export function handleRecord(input, provider = 'claude') {
207
179
  }
208
180
  export function handleInit(provider, noName = false) {
209
181
  const p = PROVIDERS[provider];
182
+ if (!p.supportsHooks || !p.settingsPath) {
183
+ process.stdout.write(`sickr: ${p.displayName} does not use SICKR hooks. Use \`sickr run ${p.defaultCommand}\` or \`sickr start --agent-id <id>\`.\n`);
184
+ return;
185
+ }
210
186
  const settingsPath = p.settingsPath();
211
187
  const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, 'utf8')) : {};
212
- const command = `npx @sickr/cli record${provider === 'codex' ? ' --codex' : ''}`;
188
+ const command = recordCommandFor(provider);
213
189
  // Remove any prior SICKR hook first, then install the current command — so
214
190
  // re-running init (or a CLI upgrade that changes the command) self-heals
215
191
  // instead of leaving a stale hook. Scoped to this provider's file.
@@ -222,16 +198,16 @@ export function handleInit(provider, noName = false) {
222
198
  const name = noName ? 'Human' : loginName();
223
199
  writeFileSync(configPath(), JSON.stringify({ name }, null, 2) + '\n');
224
200
  const labelLine = `Your prompts will be labelled "${name}"${noName ? '' : ' — run `init --no-name` to anonymize'}.\n`;
225
- const nextSteps = provider === 'codex'
226
- ? 'Next: in Codex, run `/hooks` to review & trust these hooks (Codex gates new hooks),\nthen use Codex as normal and: npx @sickr/cli replay open --codex\n'
227
- : 'Use Claude Code as normal, then: npx @sickr/cli replay open\n';
228
- process.stdout.write(`sickr: installed ${p.name} recording hooks in ${settingsPath}\n` +
201
+ const nextSteps = p.requiresManualHookTrust
202
+ ? `Next: in ${p.displayName}, trust the SICKR recorder when prompted (Codex uses \`/hooks\`),\nthen use the agent as normal and: npx @sickr/cli replay open\n`
203
+ : `Use ${p.displayName} as normal, then: npx @sickr/cli replay open\n`;
204
+ process.stdout.write(`sickr: installed ${p.displayName} recording hooks in ${settingsPath}\n` +
229
205
  `Runs are recorded locally to ${runsDir()} (secrets redacted).\n` +
230
206
  labelLine + nextSteps);
231
207
  }
232
208
  /** Stop recording: remove SICKR's hooks from this project (both providers), keep runs. */
233
209
  export function handleStop() {
234
- const targets = [PROVIDERS.claude.settingsPath(), PROVIDERS.codex.settingsPath()];
210
+ const targets = Object.values(PROVIDERS).filter((p) => p.supportsHooks && p.settingsPath).map((p) => p.settingsPath());
235
211
  const cleaned = [];
236
212
  for (const settingsPath of targets) {
237
213
  if (!existsSync(settingsPath))
@@ -316,9 +292,9 @@ export function latestRunIdFor(agent) {
316
292
  function handleOpen(runId, provider) {
317
293
  let id = runId;
318
294
  if (!id && provider) {
319
- id = latestRunIdFor(PROVIDERS[provider].label) ?? undefined;
295
+ id = latestRunIdFor(PROVIDERS[provider].recordLabel) ?? undefined;
320
296
  if (!id) {
321
- process.stdout.write(`sickr: no ${PROVIDERS[provider].label} runs yet use ${PROVIDERS[provider].name} with the hooks installed, then try again.\n`);
297
+ process.stdout.write(`sickr: no ${PROVIDERS[provider].recordLabel} runs yet - use ${PROVIDERS[provider].displayName} with the hooks installed, then try again.\n`);
322
298
  return;
323
299
  }
324
300
  }
@@ -403,11 +379,11 @@ function handleList(provider) {
403
379
  const dir = runsDir();
404
380
  let files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.ndjson')) : [];
405
381
  if (provider) {
406
- const want = PROVIDERS[provider].label;
382
+ const want = PROVIDERS[provider].recordLabel;
407
383
  files = files.filter((f) => loadRun(f.replace(/\.ndjson$/, '')).events.some((e) => e.kind === 'response' && e.label === want));
408
384
  }
409
385
  if (files.length === 0) {
410
- process.stdout.write(provider ? `sickr: no ${PROVIDERS[provider].label} runs yet.\n` : 'sickr: no runs yet.\n');
386
+ process.stdout.write(provider ? `sickr: no ${PROVIDERS[provider].recordLabel} runs yet.\n` : 'sickr: no runs yet.\n');
411
387
  return;
412
388
  }
413
389
  files
@@ -471,7 +447,7 @@ function expiryCopy(ttl_days) {
471
447
  return {
472
448
  kind: 'pro',
473
449
  value: `${ttl_days} days`,
474
- tag: 'Replay Pro',
450
+ tag: 'paid retention',
475
451
  footer: [],
476
452
  };
477
453
  if (ttl_days >= 2)
@@ -487,7 +463,7 @@ function expiryCopy(ttl_days) {
487
463
  tag: 'anon link',
488
464
  footer: [
489
465
  'run `npx @sickr/cli login` to extend new links to 7 days.',
490
- 'Replay Pro (live + remote) early access, rolling cohorts:',
466
+ 'Live ($9) and Run ($12) - early access, rolling cohorts:',
491
467
  ' https://sickr.ai/#waitlist',
492
468
  ],
493
469
  };
@@ -504,7 +480,7 @@ function tipLine(text) {
504
480
  }
505
481
  function legacyExpiryLine(ttl_days) {
506
482
  if (ttl_days >= 30)
507
- return `sickr: this link is live for ${ttl_days} days (Replay Pro retention).\n`;
483
+ return `sickr: this link is live for ${ttl_days} days (paid retention).\n`;
508
484
  if (ttl_days >= 2)
509
485
  return `sickr: this link is live for ${ttl_days} days — re-share before it expires to extend.\n`;
510
486
  return `sickr: this link expires in 24h. Run \`npx @sickr/cli login\` to extend to 7 days.\n`;
@@ -552,7 +528,7 @@ async function handleShare(runId, yes, open) {
552
528
  else {
553
529
  process.stdout.write(`sickr: published → ${url}\n` +
554
530
  legacyExpiryLine(ttl_days) +
555
- (exp.kind === 'anon' ? `sickr: Replay Pro (live + remote) early access, rolling out in cohorts https://sickr.ai/#waitlist\n` : ''));
531
+ (exp.kind === 'anon' ? `sickr: Live ($9) and Run ($12) - early access, rolling out in cohorts -> https://sickr.ai/#waitlist\n` : ''));
556
532
  }
557
533
  if (open)
558
534
  openInBrowser(url);
@@ -605,7 +581,7 @@ async function handleShareCombined(sel, yes, open) {
605
581
  else {
606
582
  process.stdout.write(`sickr: published → ${url}\n` +
607
583
  legacyExpiryLine(ttl_days) +
608
- (exp.kind === 'anon' ? `sickr: Replay Pro (live + remote) early access, rolling out in cohorts https://sickr.ai/#waitlist\n` : ''));
584
+ (exp.kind === 'anon' ? `sickr: Live ($9) and Run ($12) - early access, rolling out in cohorts -> https://sickr.ai/#waitlist\n` : ''));
609
585
  }
610
586
  if (open)
611
587
  openInBrowser(url);
@@ -860,6 +836,24 @@ async function fetchReplayProEntitlement() {
860
836
  return false;
861
837
  }
862
838
  }
839
+ /** Gate a Pro-only command. Returns true if the user is allowed; otherwise
840
+ * prints a friendly explanation and returns false. Distinguishes the three
841
+ * failure modes (not-logged-in / logged-in-no-pro / network-fail) so the
842
+ * operator knows exactly what to do next. */
843
+ async function requireReplayPro(commandLabel) {
844
+ const creds = readCredentials();
845
+ if (!creds) {
846
+ process.stderr.write(`sickr: \`${commandLabel}\` requires a paid Live, Run, or Prime Workflow entitlement.\n` +
847
+ ` run \`sickr login\` first — your Pro entitlement is attached to your GitHub account.\n`);
848
+ return false;
849
+ }
850
+ if (await fetchReplayProEntitlement())
851
+ return true;
852
+ process.stderr.write(`sickr: \`${commandLabel}\` requires a paid Live, Run, or Prime Workflow entitlement.\n` +
853
+ ` you are signed in as ${creds.login}, but this account does not have access yet.\n` +
854
+ ` join the rolling-cohort waitlist -> https://sickr.ai/#waitlist\n`);
855
+ return false;
856
+ }
863
857
  async function handleReplay(rest) {
864
858
  const sub = replaySubcommand(rest);
865
859
  if (!sub) {
@@ -876,11 +870,11 @@ async function handleReplay(rest) {
876
870
  handleInit('codex', noName);
877
871
  return;
878
872
  }
879
- if (agent === 'claude' || agent === 'codex') {
873
+ if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'cursor' || agent === 'local') {
880
874
  handleInit(agent, noName);
881
875
  return;
882
876
  }
883
- process.stderr.write('sickr: choose an agent - `sickr replay init claude`, `codex`, or `all`.\n');
877
+ process.stderr.write('sickr: choose an agent - `sickr replay init claude`, `codex`, `gemini`, or `all`.\n');
884
878
  process.exit(1);
885
879
  return;
886
880
  }
@@ -901,12 +895,12 @@ async function handleReplay(rest) {
901
895
  handleOpenCombined(sel);
902
896
  return;
903
897
  }
904
- const openProvider = replayRest.includes('--codex') ? 'codex' : replayRest.includes('--claude') ? 'claude' : undefined;
898
+ const openProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
905
899
  handleOpen(replayRest.find((a) => !a.startsWith('-')), openProvider);
906
900
  return;
907
901
  }
908
902
  if (sub === 'list') {
909
- const listProvider = replayRest.includes('--codex') ? 'codex' : replayRest.includes('--claude') ? 'claude' : undefined;
903
+ const listProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
910
904
  handleList(listProvider);
911
905
  return;
912
906
  }
@@ -933,6 +927,10 @@ async function handleReplay(rest) {
933
927
  liveStatus();
934
928
  return;
935
929
  }
930
+ if (!(await requireReplayPro('sickr replay live'))) {
931
+ process.exit(3);
932
+ return;
933
+ }
936
934
  await startLive({ verbose: replayRest.includes('--verbose') || replayRest.includes('-v'), background: replayRest.includes('--background') });
937
935
  return;
938
936
  }
@@ -943,7 +941,7 @@ async function handleReplay(rest) {
943
941
  return;
944
942
  }
945
943
  process.stdout.write('sickr: replay recording is ready for Claude and Codex.\n' +
946
- ' Replay Pro unlocks live viewing with `sickr replay live`.\n');
944
+ ' Live unlocks browser viewing; Run unlocks browser control.\n');
947
945
  }
948
946
  function commandCandidates(mappedArgs) {
949
947
  const override = process.env.SICKR_ORCHESTRATOR_CMD?.trim();
@@ -957,8 +955,8 @@ function commandCandidates(mappedArgs) {
957
955
  { command: 'python', args: ['-m', 'labudi_orchestrator.cli', ...mappedArgs] },
958
956
  ];
959
957
  }
960
- function runWorkflow(rest) {
961
- const mapped = buildWorkflowInvocation(rest).args;
958
+ function runPrime(rest) {
959
+ const mapped = buildPrimeInvocation(rest).args;
962
960
  const candidates = commandCandidates(mapped);
963
961
  let lastStatus = 1;
964
962
  for (const candidate of candidates) {
@@ -968,18 +966,18 @@ function runWorkflow(rest) {
968
966
  continue;
969
967
  }
970
968
  if (result.error) {
971
- process.stderr.write(`sickr: workflow command failed to start: ${result.error.message}\n`);
969
+ process.stderr.write(`sickr: prime command failed to start: ${result.error.message}\n`);
972
970
  process.exit(1);
973
971
  return;
974
972
  }
975
973
  process.exit(result.status ?? 0);
976
974
  return;
977
975
  }
978
- process.stderr.write('sickr: workflow orchestrator is not available. Install Python package `sickr` with `uv tool install sickr`, or set SICKR_ORCHESTRATOR_CMD.\n');
976
+ process.stderr.write('sickr: Prime Workflow orchestrator is not available. Install Python package `sickr` with `uv tool install sickr`, or set SICKR_ORCHESTRATOR_CMD.\n');
979
977
  process.exit(lastStatus);
980
978
  }
981
- async function handleWorkflow(rest) {
982
- const alias = workflowAgentAlias(rest);
979
+ async function handlePrime(rest) {
980
+ const alias = primeAgentAlias(rest);
983
981
  if (alias) {
984
982
  const sub = alias[0];
985
983
  const agentRest = alias.slice(1);
@@ -996,7 +994,7 @@ async function handleWorkflow(rest) {
996
994
  return;
997
995
  }
998
996
  }
999
- runWorkflow(rest.length ? rest : ['status']);
997
+ runPrime(rest.length ? rest : ['status']);
1000
998
  }
1001
999
  export async function readStreamWithIdle(input, idleMs = 250, emptyMs = 1500) {
1002
1000
  const chunks = [];
@@ -1041,7 +1039,7 @@ async function main() {
1041
1039
  }
1042
1040
  const cmd = parseCommand(argv);
1043
1041
  const rest = argv.slice(1);
1044
- const provider = rest.includes('--codex') ? 'codex' : 'claude';
1042
+ const provider = providerFromFlags(rest);
1045
1043
  switch (cmd) {
1046
1044
  case 'record':
1047
1045
  handleRecord(await readStdin(), provider);
@@ -1054,11 +1052,11 @@ async function main() {
1054
1052
  handleInit('codex', noName);
1055
1053
  return;
1056
1054
  }
1057
- if (agent === 'claude' || agent === 'codex') {
1055
+ if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'cursor' || agent === 'local') {
1058
1056
  handleInit(agent, noName);
1059
1057
  return;
1060
1058
  }
1061
- process.stderr.write('sickr: choose an agent `init claude`, `init codex`, or `init all`.\n');
1059
+ process.stderr.write('sickr: choose an agent - `init claude`, `init codex`, `init gemini`, or `init all`.\n');
1062
1060
  process.exit(1);
1063
1061
  return;
1064
1062
  }
@@ -1068,12 +1066,12 @@ async function main() {
1068
1066
  handleOpenCombined(sel);
1069
1067
  return;
1070
1068
  }
1071
- const openProvider = rest.includes('--codex') ? 'codex' : rest.includes('--claude') ? 'claude' : undefined;
1069
+ const openProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
1072
1070
  handleOpen(rest.find((a) => !a.startsWith('-')), openProvider);
1073
1071
  return;
1074
1072
  }
1075
1073
  case 'list': {
1076
- const listProvider = rest.includes('--codex') ? 'codex' : rest.includes('--claude') ? 'claude' : undefined;
1074
+ const listProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
1077
1075
  handleList(listProvider);
1078
1076
  return;
1079
1077
  }
@@ -1095,8 +1093,17 @@ async function main() {
1095
1093
  case 'replay':
1096
1094
  await handleReplay(rest);
1097
1095
  return;
1096
+ case 'prime':
1097
+ await handlePrime(rest);
1098
+ return;
1098
1099
  case 'workflow':
1099
- await handleWorkflow(rest);
1100
+ await handlePrime(rest);
1101
+ return;
1102
+ case 'start':
1103
+ await handlePrime(['start', ...rest]);
1104
+ return;
1105
+ case 'status':
1106
+ await handlePrime(['status', ...rest]);
1100
1107
  return;
1101
1108
  case 'agent': {
1102
1109
  const sub = rest[0];
@@ -1132,6 +1139,10 @@ async function main() {
1132
1139
  liveStatus();
1133
1140
  return;
1134
1141
  }
1142
+ if (!(await requireReplayPro('sickr live'))) {
1143
+ process.exit(3);
1144
+ return;
1145
+ }
1135
1146
  // default: start (foreground)
1136
1147
  const opts = { verbose: rest.includes('--verbose') || rest.includes('-v'), background: rest.includes('--background') };
1137
1148
  await startLive(opts);
@@ -1180,6 +1191,10 @@ async function main() {
1180
1191
  const passthroughFlags = flags.filter((f) => f !== '--verbose' && f !== '-v');
1181
1192
  const agentArgs = [...passthroughFlags, ...positional.slice(1)];
1182
1193
  const verbose = flags.includes('--verbose') || flags.includes('-v');
1194
+ if (!(await requireReplayPro('sickr run'))) {
1195
+ process.exit(3);
1196
+ return;
1197
+ }
1183
1198
  const { startRun } = await import('./run.js');
1184
1199
  try {
1185
1200
  await startRun({ agent, agentArgs, verbose, mode });
package/dist/live.js CHANGED
@@ -136,7 +136,7 @@ export async function startLive(opts = {}) {
136
136
  urlid = await computeUrlid(creds);
137
137
  }
138
138
  catch (e) {
139
- process.stderr.write(`sickr: couldn't resolve live url (${e.message}). Replay Pro required.\n`);
139
+ process.stderr.write(`sickr: couldn't resolve live url (${e.message}). Live entitlement required.\n`);
140
140
  try {
141
141
  unlinkSync(pidPath());
142
142
  }
@@ -0,0 +1,85 @@
1
+ import { join } from 'node:path';
2
+ export const PROVIDERS = {
3
+ claude: {
4
+ provider: 'claude',
5
+ displayName: 'Claude Code',
6
+ recordLabel: 'Claude',
7
+ defaultCommand: 'claude',
8
+ supportsHooks: true,
9
+ requiresManualHookTrust: false,
10
+ supportsPtyControl: true,
11
+ supportsStructuredStream: false,
12
+ settingsPath: () => join(process.cwd(), '.claude', 'settings.json'),
13
+ notes: 'Hooks are installed into .claude/settings.json and Claude reads them without an extra trust command.',
14
+ },
15
+ codex: {
16
+ provider: 'codex',
17
+ displayName: 'Codex',
18
+ recordLabel: 'Codex',
19
+ defaultCommand: 'codex',
20
+ supportsHooks: true,
21
+ requiresManualHookTrust: true,
22
+ supportsPtyControl: true,
23
+ supportsStructuredStream: false,
24
+ settingsPath: () => join(process.cwd(), '.codex', 'hooks.json'),
25
+ recordFlag: '--codex',
26
+ notes: 'Codex requires the user to type /hooks once and trust the SICKR recorder.',
27
+ },
28
+ gemini: {
29
+ provider: 'gemini',
30
+ displayName: 'Gemini CLI',
31
+ recordLabel: 'Gemini',
32
+ defaultCommand: 'gemini',
33
+ supportsHooks: true,
34
+ requiresManualHookTrust: true,
35
+ supportsPtyControl: true,
36
+ supportsStructuredStream: false,
37
+ settingsPath: () => join(process.cwd(), '.gemini', 'settings.json'),
38
+ recordFlag: '--gemini',
39
+ notes: 'Gemini CLI has a hooks surface; SICKR keeps it behind an adapter so trust behavior can be handled per release.',
40
+ },
41
+ cursor: {
42
+ provider: 'cursor',
43
+ displayName: 'Cursor CLI',
44
+ recordLabel: 'Cursor',
45
+ defaultCommand: 'cursor-agent',
46
+ supportsHooks: false,
47
+ requiresManualHookTrust: false,
48
+ supportsPtyControl: true,
49
+ supportsStructuredStream: true,
50
+ notes: 'Cursor is best captured through headless stream-json for workflow jobs; interactive control uses the PTY wrapper.',
51
+ },
52
+ local: {
53
+ provider: 'local',
54
+ displayName: 'Local LLM',
55
+ recordLabel: 'Local',
56
+ defaultCommand: 'sickr-local-agent',
57
+ supportsHooks: false,
58
+ requiresManualHookTrust: false,
59
+ supportsPtyControl: true,
60
+ supportsStructuredStream: true,
61
+ notes: 'Local models should run through a SICKR-owned harness against Ollama, vLLM, llama.cpp, or an OpenAI-compatible endpoint.',
62
+ },
63
+ };
64
+ export function providerForAgent(agent) {
65
+ const a = agent.toLowerCase();
66
+ const base = a.replace(/^.*[\\/]/, '').replace(/\.(cmd|exe|bat|com)$/i, '');
67
+ if (base === 'claude')
68
+ return 'claude';
69
+ if (base === 'codex')
70
+ return 'codex';
71
+ if (base === 'gemini')
72
+ return 'gemini';
73
+ if (base === 'cursor' || base === 'cursor-agent')
74
+ return 'cursor';
75
+ if (base === 'ollama' || base === 'vllm' || base === 'llama.cpp' || base === 'llama-server' || base === 'sickr-local-agent')
76
+ return 'local';
77
+ return null;
78
+ }
79
+ export function recordCommandFor(provider) {
80
+ const p = PROVIDERS[provider];
81
+ return `npx @sickr/cli record${p.recordFlag ? ` ${p.recordFlag}` : ''}`;
82
+ }
83
+ export function modeRank(mode) {
84
+ return { replay: 0, live: 1, run: 2, prime_workflow: 3 }[mode];
85
+ }
package/dist/run.js CHANGED
@@ -29,6 +29,7 @@ import { readCredentials } from './auth.js';
29
29
  import { runsDir } from './recorder.js';
30
30
  import { mergeHooks, removeHooks } from './hookConfig.js';
31
31
  import { LIVE_BASE, decodeWsPayload, splitJsonObjects } from './live.js';
32
+ import { PROVIDERS, providerForAgent, recordCommandFor } from './providers.js';
32
33
  /** Resolve the agent binary path. Precedence:
33
34
  * 1. SICKR_AGENT_BIN_<NAME> env (e.g. SICKR_AGENT_BIN_CLAUDE)
34
35
  * 2. <NAME>_BIN env (e.g. CLAUDE_BIN)
@@ -182,12 +183,13 @@ function tailFrom(path, from) {
182
183
  * hooks were ensured / installed; false if the agent name isn't
183
184
  * one we know how to init for. */
184
185
  export function ensureRecordingHooks(agent) {
185
- const provider = agent === 'claude' || agent === 'codex' ? agent : null;
186
+ const provider = providerForAgent(agent);
186
187
  if (!provider)
187
188
  return false; // Custom binaries — caller decides whether to install hooks
188
- const settingsPath = provider === 'codex'
189
- ? join(process.cwd(), '.codex', 'hooks.json')
190
- : join(process.cwd(), '.claude', 'settings.json');
189
+ const adapter = PROVIDERS[provider];
190
+ if (!adapter.supportsHooks || !adapter.settingsPath)
191
+ return false;
192
+ const settingsPath = adapter.settingsPath();
191
193
  let existing = {};
192
194
  if (existsSync(settingsPath)) {
193
195
  try {
@@ -208,7 +210,7 @@ export function ensureRecordingHooks(agent) {
208
210
  // Dynamic import keeps the file from pulling hookConfig at module
209
211
  // load (tiny boot perf + lets tests run without the module).
210
212
  // eslint-disable-next-line @typescript-eslint/no-require-imports
211
- const command = `npx @sickr/cli record${provider === 'codex' ? ' --codex' : ''}`;
213
+ const command = recordCommandFor(provider);
212
214
  const merged = mergeHooks(removeHooks(existing), command);
213
215
  mkdirSync(join(settingsPath, '..'), { recursive: true });
214
216
  writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
@@ -272,7 +274,7 @@ export async function startRun(opts) {
272
274
  urlid = await computeUrlid(creds);
273
275
  }
274
276
  catch (e) {
275
- process.stderr.write(`sickr: couldn't resolve live url (${e.message}). Replay Pro required.\n`);
277
+ process.stderr.write(`sickr: couldn't resolve live url (${e.message}). Run entitlement required.\n`);
276
278
  process.exit(4);
277
279
  }
278
280
  // Ensure recording hooks are installed in the cwd for known agents.
@@ -295,6 +297,11 @@ export async function startRun(opts) {
295
297
  else if (mode === 'interactive') {
296
298
  process.stdout.write(` mode=interactive — agent will prompt for tool use; you confirm in the PTY\n`);
297
299
  }
300
+ if (opts.agent === 'codex') {
301
+ // Codex 0.133+ gates new hooks until trusted; without trust the recorder
302
+ // is dormant and /r/<urlid> stays empty even though the PTY is live.
303
+ process.stdout.write(` codex hooks: type \`/hooks\` once inside codex to trust the recorder.\n`);
304
+ }
298
305
  process.stdout.write(` browser steer messages land directly in your agent. ^C exits.\n\n`);
299
306
  const pty = ptySpawn(agentBin, effectiveArgs, {
300
307
  name: 'xterm-256color',
@@ -388,12 +395,28 @@ export async function startRun(opts) {
388
395
  if (m.kind === 'steer' && m.text) {
389
396
  const decision = decideSteer(m);
390
397
  if (decision.target === 'pty' && decision.bytes != null) {
398
+ // Submit-fix (2026-06-01): Claude Code's TUI treats a burst of
399
+ // characters arriving with no inter-keystroke gap as a paste
400
+ // — and within a paste, \r is an inline newline, NOT a submit.
401
+ // Text appeared in the input box but never got sent until the
402
+ // operator manually hit Enter. Split the write: body first,
403
+ // then a short delay, then a standalone \r so it lands as a
404
+ // discrete Enter keypress instead of "tail of a paste".
405
+ const bytes = decision.bytes;
406
+ const bodyOnly = bytes.endsWith('\r') ? bytes.slice(0, -1) : bytes;
407
+ const submit = bytes.endsWith('\r');
391
408
  try {
392
- pty.write(decision.bytes);
409
+ if (bodyOnly.length > 0)
410
+ pty.write(bodyOnly);
411
+ if (submit)
412
+ setTimeout(() => { try {
413
+ pty.write('\r');
414
+ }
415
+ catch { /* pty exited */ } }, 80);
393
416
  }
394
417
  catch { /* PTY may have exited */ }
395
418
  if (opts.verbose)
396
- process.stderr.write(`sickr: pty steer ${decision.bytes.length}b\n`);
419
+ process.stderr.write(`sickr: pty steer ${bytes.length}b${submit ? ' (split for submit)' : ''}\n`);
397
420
  }
398
421
  else {
399
422
  appendInbox(urlid, m.text, m.at ?? new Date().toISOString());
package/package.json CHANGED
@@ -1,18 +1,27 @@
1
1
  {
2
2
  "name": "@sickr/cli",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "type": "module",
5
5
  "description": "npx @sickr/cli - replay, live look, and workflow orchestration for AI coding agents.",
6
- "bin": { "replay": "dist/cli.js", "sickr": "dist/cli.js" },
7
- "files": ["dist"],
8
- "publishConfig": { "access": "public" },
6
+ "bin": {
7
+ "replay": "dist/cli.js",
8
+ "sickr": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
9
16
  "scripts": {
10
17
  "build": "tsc",
11
18
  "test": "vitest run",
12
19
  "dev": "tsc -w",
13
20
  "prepublishOnly": "npm run build && npm test && node scripts/pre-publish-check.mjs"
14
21
  },
15
- "engines": { "node": ">=20" },
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
16
25
  "license": "UNLICENSED",
17
26
  "optionalDependencies": {
18
27
  "node-pty": "^1.0.0",