@sickr/cli 0.9.5 → 0.9.7

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 (3) hide show
  1. package/dist/cli.js +122 -94
  2. package/dist/run.js +174 -11
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -51,103 +51,72 @@ export function buildWorkflowInvocation(rest, command = 'uvx') {
51
51
  }
52
52
  return { command, args: rest };
53
53
  }
54
- export const HELP = `SICKR Replay audit & replay what your AI coding agent did.
54
+ export const HELP = `sickrrecord, replay, and remote-control your AI coding agents.
55
55
 
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.
56
+ Usage: npx @sickr/cli <command> [options]
58
57
 
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.
58
+ REPLAY (free) local recording of every prompt, edit and command.
59
+ replay Install Claude + Codex recording hooks. Use the agents
60
+ as normala redacted timeline is captured to
61
+ ~/.sickr/runs.
62
+ replay open [id] Render the newest run (or a specific id) as a local
63
+ HTML timeline. Combine across agents with --today,
64
+ --since <2h|30m|1d>, or --all (+ --claude / --codex).
65
+ replay share [id] Publish a redacted run to sickr.ai/r/<id> (asks first).
66
+ Add --yes to skip the prompt; --open to open after.
67
+ Or combine a window: --today / --since <dur> / --all.
68
+ Anon links live 24h; signed-in links live 7 days.
69
+ replay list List recorded runs, newest first (+ --claude/--codex).
70
+ replay stop Remove sickr hooks from this project (keeps runs).
71
+ replay clear Delete all local runs in ~/.sickr/runs (asks first).
62
72
 
63
- Usage: npx @sickr/cli <command> [options]
73
+ LIVE LOOK (Replay Pro) — passive streaming to a watching browser.
74
+ live Stream the current session to sickr.ai/r/<your-link> in
75
+ real time. Browser steer messages land in
76
+ ~/.sickr/inbox/<urlid>.md and your terminal prints them.
77
+ Requires \`sickr login\` + Replay Pro entitlement.
78
+ live status Show pid + connection state.
79
+ live stop Stop the sidecar.
64
80
 
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.
81
+ REMOTE CONTROL (Replay Pro) — browser keystrokes drive the agent.
82
+ run <agent> Wrap an agent in a PTY sickr owns; browser steer
83
+ messages are written directly into the agent's stdin.
84
+ Real remote control, not pasteboard.
85
+ sickr run claude Claude Code under sickr
86
+ sickr run codex Codex under sickr
87
+ sickr run <bin> arbitrary binary
88
+ Flags:
89
+ --mode auto (default) inject the agent's
90
+ "no prompt / full perms" flag so it
91
+ doesn't stall on tool confirms the
92
+ operator can't see.
93
+ --mode interactive pass agent flags through
94
+ verbatim agent prompts for tool use.
95
+ Requires \`sickr login\` + Replay Pro entitlement.
96
+ Codex 0.133+ needs \`/hooks\` typed once to trust the
97
+ recorder; Claude is silent (hooks auto-installed).
80
98
 
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
119
- sickr run codex same for Codex
120
- sickr run --cmd <bin> arbitrary command
121
- \`node-pty\` ships as an optional dep + auto-installs with the
122
- CLI on supported platforms (mac, Linux, Windows 10+).
123
- Browsers can override per-message: {mode:'queue'|'steer',
124
- submit:true|false}; default is auto-steer with submit.
125
- agent connect --agent-id <id>
126
- Connect this machine to a configured SICKR agent using GitHub
127
- browser approval. Stores the agent key in ~/.sickr/agent.json.
128
- agent status Show the connected agent, org and team.
129
- agent rotate Rotate this machine's agent key.
130
- agent disconnect
131
- Revoke this machine's agent key and remove it locally.
132
- help Show this help.
99
+ ACCOUNT
100
+ login Sign in with GitHub. Unlocks persistent shares
101
+ (24h 7d) and Replay Pro entitlement.
102
+ logout Forget the local login.
103
+ whoami Show who you're signed in as.
133
104
 
134
- Requires Node 18+. Codex capture needs Codex CLI 0.133+ (run /hooks to trust);
135
- Claude Code: any hooks-capable build.
105
+ WORKFLOW (sickr-managed agent + ticketing)
106
+ workflow connect --agent-id <id> Approve this machine for a configured
107
+ workflow agent (GitHub browser flow).
108
+ workflow start --agent-id <id> Start the orchestrator for an agent.
109
+ workflow status Show running daemon status.
110
+ workflow rotate | disconnect Rotate or revoke this machine's key.
136
111
 
137
- ────────────────────────────────────────────────────────────────────
138
- This replays your AI coding agents on ONE machine. SICKR governs your whole team.
139
- Issue tracking + your team + automation + agents — one governed workflow for
140
- audit, accountability, productivity and confidence.
112
+ help Show this help.
141
113
 
142
- · Gates & approvals work holds at plan sign-off, review, merge and
143
- validation checks until each one passes.
144
- · Humans + agents on one board — agents are first-class teammates with
145
- roles, capacity and accountability, not a side channel.
146
- · A full, signed-off audit trail across every actor and every change.
147
- · Runs 24/7 — produce as much work as you like; the team handles it.
114
+ Requires Node 20+. Codex capture needs Codex CLI 0.133+.
148
115
 
149
- Free tier available · bring your own Claude or Codex subscription.
150
- https://sickr.ai
116
+ ────────────────────────────────────────────────────────────────────
117
+ sickr also governs your whole team — issue tracking + humans + agents on one
118
+ board, gates & approvals, signed audit trail, runs 24/7. Free tier available;
119
+ bring your own Claude or Codex subscription. → https://sickr.ai
151
120
  `;
152
121
  export function currentRunId(cc) {
153
122
  return String(cc.session_id ?? 'session');
@@ -850,6 +819,24 @@ async function fetchReplayProEntitlement() {
850
819
  return false;
851
820
  }
852
821
  }
822
+ /** Gate a Pro-only command. Returns true if the user is allowed; otherwise
823
+ * prints a friendly explanation and returns false. Distinguishes the three
824
+ * failure modes (not-logged-in / logged-in-no-pro / network-fail) so the
825
+ * operator knows exactly what to do next. */
826
+ async function requireReplayPro(commandLabel) {
827
+ const creds = readCredentials();
828
+ if (!creds) {
829
+ process.stderr.write(`sickr: \`${commandLabel}\` is a Replay Pro feature.\n` +
830
+ ` run \`sickr login\` first — your Pro entitlement is attached to your GitHub account.\n`);
831
+ return false;
832
+ }
833
+ if (await fetchReplayProEntitlement())
834
+ return true;
835
+ process.stderr.write(`sickr: \`${commandLabel}\` is a Replay Pro feature.\n` +
836
+ ` you are signed in as ${creds.login}, but your account isn't on Replay Pro.\n` +
837
+ ` join the rolling-cohort waitlist → https://sickr.ai/#waitlist\n`);
838
+ return false;
839
+ }
853
840
  async function handleReplay(rest) {
854
841
  const sub = replaySubcommand(rest);
855
842
  if (!sub) {
@@ -923,6 +910,10 @@ async function handleReplay(rest) {
923
910
  liveStatus();
924
911
  return;
925
912
  }
913
+ if (!(await requireReplayPro('sickr replay live'))) {
914
+ process.exit(3);
915
+ return;
916
+ }
926
917
  await startLive({ verbose: replayRest.includes('--verbose') || replayRest.includes('-v'), background: replayRest.includes('--background') });
927
918
  return;
928
919
  }
@@ -1122,28 +1113,65 @@ async function main() {
1122
1113
  liveStatus();
1123
1114
  return;
1124
1115
  }
1116
+ if (!(await requireReplayPro('sickr live'))) {
1117
+ process.exit(3);
1118
+ return;
1119
+ }
1125
1120
  // default: start (foreground)
1126
1121
  const opts = { verbose: rest.includes('--verbose') || rest.includes('-v'), background: rest.includes('--background') };
1127
1122
  await startLive(opts);
1128
1123
  return;
1129
1124
  }
1130
1125
  case 'run': {
1131
- // sickr run <agent> [...agent-args] [--verbose]
1126
+ // sickr run <agent> [--mode auto|interactive] [...agent-args] [--verbose]
1132
1127
  // Wraps the agent in a PTY the CLI owns. Browser steers go
1133
1128
  // straight to stdin. node-pty is loaded on first use.
1134
- const positional = rest.filter((a) => !a.startsWith('-'));
1135
- const flags = rest.filter((a) => a.startsWith('-'));
1129
+ // --mode is stripped before forwarding; the rest passes through verbatim.
1130
+ const filtered = [];
1131
+ let mode = 'auto';
1132
+ for (let i = 0; i < rest.length; i++) {
1133
+ if (rest[i] === '--mode' && rest[i + 1]) {
1134
+ const v = rest[i + 1];
1135
+ if (v !== 'auto' && v !== 'interactive') {
1136
+ process.stderr.write(`sickr: --mode must be 'auto' or 'interactive' (got '${v}')\n`);
1137
+ process.exit(1);
1138
+ return;
1139
+ }
1140
+ mode = v;
1141
+ i++;
1142
+ continue;
1143
+ }
1144
+ if (rest[i].startsWith('--mode=')) {
1145
+ const v = rest[i].slice('--mode='.length);
1146
+ if (v !== 'auto' && v !== 'interactive') {
1147
+ process.stderr.write(`sickr: --mode must be 'auto' or 'interactive' (got '${v}')\n`);
1148
+ process.exit(1);
1149
+ return;
1150
+ }
1151
+ mode = v;
1152
+ continue;
1153
+ }
1154
+ filtered.push(rest[i]);
1155
+ }
1156
+ const positional = filtered.filter((a) => !a.startsWith('-'));
1157
+ const flags = filtered.filter((a) => a.startsWith('-'));
1136
1158
  const agent = positional[0];
1137
1159
  if (!agent) {
1138
- process.stderr.write('sickr: usage — `sickr run <agent> [args...]` (agent: claude | codex | <bin name>)\n');
1160
+ process.stderr.write('sickr: usage — `sickr run <agent> [--mode auto|interactive] [args...]` (agent: claude | codex | <bin name>)\n');
1139
1161
  process.exit(1);
1140
1162
  return;
1141
1163
  }
1142
- const agentArgs = positional.slice(1);
1164
+ // Pass through every non-sickr flag plus positional tail to the agent.
1165
+ const passthroughFlags = flags.filter((f) => f !== '--verbose' && f !== '-v');
1166
+ const agentArgs = [...passthroughFlags, ...positional.slice(1)];
1143
1167
  const verbose = flags.includes('--verbose') || flags.includes('-v');
1168
+ if (!(await requireReplayPro('sickr run'))) {
1169
+ process.exit(3);
1170
+ return;
1171
+ }
1144
1172
  const { startRun } = await import('./run.js');
1145
1173
  try {
1146
- await startRun({ agent, agentArgs, verbose });
1174
+ await startRun({ agent, agentArgs, verbose, mode });
1147
1175
  }
1148
1176
  catch (e) {
1149
1177
  process.stderr.write(`sickr: ${e.message}\n`);
package/dist/run.js CHANGED
@@ -23,16 +23,62 @@
23
23
  import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync, statSync, openSync, readSync, closeSync, unlinkSync } from 'node:fs';
24
24
  import { homedir } from 'node:os';
25
25
  import { join } from 'node:path';
26
+ import { execFileSync } from 'node:child_process';
26
27
  import { setTimeout as sleep } from 'node:timers/promises';
27
28
  import { readCredentials } from './auth.js';
28
29
  import { runsDir } from './recorder.js';
30
+ import { mergeHooks, removeHooks } from './hookConfig.js';
29
31
  import { LIVE_BASE, decodeWsPayload, splitJsonObjects } from './live.js';
30
- /** Resolve the agent binary path. `claude` / `codex` look up <NAME>_BIN
31
- * env, else fall through to the bare name on PATH. `--cmd <path>` is
32
- * the escape hatch for arbitrary processes. */
33
- function resolveAgent(agent) {
32
+ /** Resolve the agent binary path. Precedence:
33
+ * 1. SICKR_AGENT_BIN_<NAME> env (e.g. SICKR_AGENT_BIN_CLAUDE)
34
+ * 2. <NAME>_BIN env (e.g. CLAUDE_BIN)
35
+ * 3. OS shell lookup (`where` on Windows, `which` on Unix) so PATHEXT
36
+ * resolution finds `claude.cmd`, `claude.exe`, etc.
37
+ * 4. Bare name (node-pty handles it; rarely works on Windows).
38
+ *
39
+ * Windows-specific: node-pty's CreateProcess does NOT respect PATHEXT,
40
+ * so a bare `claude` will fail with 'Cannot create process, error code: 2'
41
+ * even when `claude.cmd` is on PATH. The shell lookup at step 3 closes
42
+ * the gap. Surfaced 2026-06-01 on the first Windows E2E test. */
43
+ export function resolveAgent(agent) {
44
+ if (/[\/\\]/.test(agent))
45
+ return agent; // absolute / relative path used as-is
34
46
  const envOverride = process.env[`SICKR_AGENT_BIN_${agent.toUpperCase()}`] ?? process.env[`${agent.toUpperCase()}_BIN`];
35
- return envOverride || agent;
47
+ if (envOverride)
48
+ return envOverride;
49
+ // Shell lookup. Cheap (~20ms) and only fires once per `sickr run`.
50
+ // Wrapped in try/catch so a missing `where`/`which` doesn't crash —
51
+ // we fall through to the bare name, which gives the original error
52
+ // and lets the user set SICKR_AGENT_BIN_<NAME> as escape hatch.
53
+ try {
54
+ const isWin = process.platform === 'win32';
55
+ const cmd = isWin ? 'where' : 'which';
56
+ const out = execFileSync(cmd, [agent], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
57
+ const candidates = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
58
+ if (candidates.length === 0)
59
+ return agent;
60
+ // On Windows, `where` returns multiple candidates when both the npm
61
+ // shell-script wrapper (no extension) and the .cmd/.bat shim exist.
62
+ // CreateProcess can't run the unextensioned wrapper, so prefer
63
+ // executable extensions in this order: .exe, .cmd, .bat, .com,
64
+ // then anything else. Bug surfaced 2026-06-01 — `where claude` led
65
+ // with `C:\...\nvm\v22\claude` (no ext) before `claude.cmd`.
66
+ if (isWin) {
67
+ const EXEC_RANK = ['.exe', '.cmd', '.bat', '.com'];
68
+ const sorted = candidates.slice().sort((a, b) => {
69
+ const ar = EXEC_RANK.findIndex((ext) => a.toLowerCase().endsWith(ext));
70
+ const br = EXEC_RANK.findIndex((ext) => b.toLowerCase().endsWith(ext));
71
+ // Items with a recognised exec ext sort first; ties keep original order.
72
+ const ai = ar === -1 ? EXEC_RANK.length : ar;
73
+ const bi = br === -1 ? EXEC_RANK.length : br;
74
+ return ai - bi;
75
+ });
76
+ return sorted[0];
77
+ }
78
+ return candidates[0];
79
+ }
80
+ catch { /* not found; fall through */ }
81
+ return agent;
36
82
  }
37
83
  /** UTC date YYYY-MM-DD — matches sickr-ui's /api/replay slot id formula. */
38
84
  function utcDate(now = new Date()) { return now.toISOString().slice(0, 10); }
@@ -72,9 +118,17 @@ export function decideSteer(msg, defaultMode = 'pty') {
72
118
  const mode = msg.mode === 'queue' ? 'inbox' : msg.mode === 'steer' ? 'pty' : defaultMode;
73
119
  if (mode === 'inbox')
74
120
  return { target: 'inbox' };
75
- // For PTY: append \r unless submit was explicitly false. Default true
76
- // because the whole point of `sickr run` is auto-submit; opt out
77
- // requires saying so.
121
+ // For PTY: append a line terminator unless submit was explicitly
122
+ // false. Default true because the whole point of `sickr run` is
123
+ // auto-submit; opt out requires saying so.
124
+ //
125
+ // We send \r (CR only). That's what the Enter key produces in most
126
+ // TUIs running under a PTY in raw mode (the terminal driver doesn't
127
+ // translate CR -> LF for raw input). Surfaced 2026-06-01: bare \r
128
+ // worked for echo / cat tests but Claude Code's React Ink input
129
+ // handler appeared to ignore single \r on Windows ConPTY. Sending
130
+ // \r\n is widely compatible: bare-\r TUIs treat the trailing \n as
131
+ // an empty no-op line; line-buffered programs see a single line.
78
132
  const submit = msg.submit !== false;
79
133
  return { target: 'pty', bytes: submit ? text + '\r' : text };
80
134
  }
@@ -113,6 +167,54 @@ function tailFrom(path, from) {
113
167
  closeSync(fd);
114
168
  }
115
169
  }
170
+ /** Silently ensure recording hooks are installed for the wrapped agent
171
+ * in the CURRENT working directory. Without this, Claude / Codex won't
172
+ * emit events to ~/.sickr/runs/, so the WS push has nothing to forward
173
+ * to the DO and the browser /r/<urlid> view stays empty.
174
+ *
175
+ * Surfaced 2026-06-01: first E2E spawned Claude under PTY but refresh
176
+ * on the browser showed only an unrelated event from a prior session.
177
+ * Operator hit the "have to remember `sickr init claude` first" trap.
178
+ *
179
+ * This function is idempotent — if the file already has the right
180
+ * hooks the merge is a no-op. We don't print anything on success
181
+ * (the spawn is about to take over the terminal). Returns true if
182
+ * hooks were ensured / installed; false if the agent name isn't
183
+ * one we know how to init for. */
184
+ export function ensureRecordingHooks(agent) {
185
+ const provider = agent === 'claude' || agent === 'codex' ? agent : null;
186
+ if (!provider)
187
+ 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');
191
+ let existing = {};
192
+ if (existsSync(settingsPath)) {
193
+ try {
194
+ existing = JSON.parse(readFileSync(settingsPath, 'utf8'));
195
+ }
196
+ catch {
197
+ existing = {};
198
+ }
199
+ }
200
+ // Cheap detection — if the file already mentions our record command,
201
+ // skip the write. Avoids touching a file every run when the user
202
+ // already init'd here.
203
+ const serialized = JSON.stringify(existing);
204
+ if (serialized.includes('@sickr/cli record') || serialized.includes('@sickr/replay record')) {
205
+ return true;
206
+ }
207
+ // Merge using the same logic as cli.ts::handleInit but silent.
208
+ // Dynamic import keeps the file from pulling hookConfig at module
209
+ // load (tiny boot perf + lets tests run without the module).
210
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
211
+ const command = `npx @sickr/cli record${provider === 'codex' ? ' --codex' : ''}`;
212
+ const merged = mergeHooks(removeHooks(existing), command);
213
+ mkdirSync(join(settingsPath, '..'), { recursive: true });
214
+ writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
215
+ mkdirSync(runsDir(), { recursive: true });
216
+ return true;
217
+ }
116
218
  /** Best-effort load of node-pty. Returns the spawn function or throws
117
219
  * with a copy-pasteable install hint. */
118
220
  async function loadNodePtySpawn() {
@@ -134,6 +236,30 @@ async function loadNodePtySpawn() {
134
236
  `Underlying error: ${e.message ?? 'unknown'}`);
135
237
  }
136
238
  }
239
+ /** Per-agent flag that disables prompting / sandboxing so the wrapped
240
+ * agent doesn't stall waiting for unattended confirmation. Pure function
241
+ * — exported for unit tests. Honors operator overrides: if the operator
242
+ * already passed the relevant flag (or any opposite), we don't double-add.
243
+ *
244
+ * History (2026-06-01): `sickr run claude` defaulted to interactive and
245
+ * surfaced as a "hung" terminal — Claude was waiting on a tool-use
246
+ * permission prompt the operator couldn't see in the PTY mirror. Moving
247
+ * the default to "auto / full permissions" closes the gap; `--mode
248
+ * interactive` opts back in for sensitive sessions. */
249
+ export function autoModeArgsFor(agent, existingArgs) {
250
+ const a = agent.toLowerCase();
251
+ const has = (needle) => existingArgs.some((x) => x === needle || x.startsWith(needle + '='));
252
+ if (a === 'claude' || a.endsWith('/claude') || a.endsWith('\\claude') || a.endsWith('claude.cmd') || a.endsWith('claude.exe')) {
253
+ return has('--dangerously-skip-permissions') ? [] : ['--dangerously-skip-permissions'];
254
+ }
255
+ if (a === 'codex' || a.endsWith('/codex') || a.endsWith('\\codex') || a.endsWith('codex.cmd') || a.endsWith('codex.exe')) {
256
+ if (has('--dangerously-bypass-approvals-and-sandbox') || has('--full-auto') || has('--ask-for-approval'))
257
+ return [];
258
+ return ['--dangerously-bypass-approvals-and-sandbox'];
259
+ }
260
+ // Unknown agent — no opinion. Operator should pass their own flags.
261
+ return [];
262
+ }
137
263
  export async function startRun(opts) {
138
264
  const creds = readCredentials();
139
265
  if (!creds) {
@@ -149,12 +275,33 @@ export async function startRun(opts) {
149
275
  process.stderr.write(`sickr: couldn't resolve live url (${e.message}). Replay Pro required.\n`);
150
276
  process.exit(4);
151
277
  }
278
+ // Ensure recording hooks are installed in the cwd for known agents.
279
+ // Without this, Claude / Codex run normally but emit no events — the
280
+ // browser /r/<urlid> stays empty until events flow.
281
+ ensureRecordingHooks(opts.agent);
152
282
  const agentBin = resolveAgent(opts.agent);
153
283
  const cols = process.stdout.columns ?? 80;
154
284
  const rows = process.stdout.rows ?? 24;
285
+ // mode=auto (default) injects the agent's "no prompt / full perms" flag
286
+ // so the wrapped CLI doesn't stall on tool-use confirmations the
287
+ // operator can't see. --mode interactive disables the injection.
288
+ const mode = opts.mode ?? 'auto';
289
+ const injected = mode === 'auto' ? autoModeArgsFor(opts.agent, opts.agentArgs) : [];
290
+ const effectiveArgs = [...injected, ...opts.agentArgs];
155
291
  process.stdout.write(`sickr: wrapping ${agentBin} in PTY. Watch + steer at: https://sickr.ai/r/${urlid}\n`);
292
+ if (mode === 'auto' && injected.length > 0) {
293
+ process.stdout.write(` mode=auto — injected ${injected.join(' ')} (use --mode interactive to disable)\n`);
294
+ }
295
+ else if (mode === 'interactive') {
296
+ process.stdout.write(` mode=interactive — agent will prompt for tool use; you confirm in the PTY\n`);
297
+ }
298
+ if (opts.agent === 'codex') {
299
+ // Codex 0.133+ gates new hooks until trusted; without trust the recorder
300
+ // is dormant and /r/<urlid> stays empty even though the PTY is live.
301
+ process.stdout.write(` codex hooks: type \`/hooks\` once inside codex to trust the recorder.\n`);
302
+ }
156
303
  process.stdout.write(` browser steer messages land directly in your agent. ^C exits.\n\n`);
157
- const pty = ptySpawn(agentBin, opts.agentArgs, {
304
+ const pty = ptySpawn(agentBin, effectiveArgs, {
158
305
  name: 'xterm-256color',
159
306
  cols, rows,
160
307
  cwd: process.cwd(),
@@ -246,12 +393,28 @@ export async function startRun(opts) {
246
393
  if (m.kind === 'steer' && m.text) {
247
394
  const decision = decideSteer(m);
248
395
  if (decision.target === 'pty' && decision.bytes != null) {
396
+ // Submit-fix (2026-06-01): Claude Code's TUI treats a burst of
397
+ // characters arriving with no inter-keystroke gap as a paste
398
+ // — and within a paste, \r is an inline newline, NOT a submit.
399
+ // Text appeared in the input box but never got sent until the
400
+ // operator manually hit Enter. Split the write: body first,
401
+ // then a short delay, then a standalone \r so it lands as a
402
+ // discrete Enter keypress instead of "tail of a paste".
403
+ const bytes = decision.bytes;
404
+ const bodyOnly = bytes.endsWith('\r') ? bytes.slice(0, -1) : bytes;
405
+ const submit = bytes.endsWith('\r');
249
406
  try {
250
- pty.write(decision.bytes);
407
+ if (bodyOnly.length > 0)
408
+ pty.write(bodyOnly);
409
+ if (submit)
410
+ setTimeout(() => { try {
411
+ pty.write('\r');
412
+ }
413
+ catch { /* pty exited */ } }, 80);
251
414
  }
252
415
  catch { /* PTY may have exited */ }
253
416
  if (opts.verbose)
254
- process.stderr.write(`sickr: pty steer ${decision.bytes.length}b\n`);
417
+ process.stderr.write(`sickr: pty steer ${bytes.length}b${submit ? ' (split for submit)' : ''}\n`);
255
418
  }
256
419
  else {
257
420
  appendInbox(urlid, m.text, m.at ?? new Date().toISOString());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/cli",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "type": "module",
5
5
  "description": "npx @sickr/cli - replay, live look, and workflow orchestration for AI coding agents.",
6
6
  "bin": { "replay": "dist/cli.js", "sickr": "dist/cli.js" },