@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 +154 -139
- package/dist/live.js +1 -1
- package/dist/providers.js +85 -0
- package/dist/run.js +31 -8
- package/package.json +14 -5
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
|
|
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
|
|
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
|
|
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
|
-
|
|
57
|
-
redacted timeline you can replay — and optionally share as a public link.
|
|
80
|
+
Usage: npx @sickr/cli <command> [options]
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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].
|
|
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 =
|
|
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 =
|
|
226
|
-
?
|
|
227
|
-
:
|
|
228
|
-
process.stdout.write(`sickr: installed ${p.
|
|
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 =
|
|
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].
|
|
295
|
+
id = latestRunIdFor(PROVIDERS[provider].recordLabel) ?? undefined;
|
|
320
296
|
if (!id) {
|
|
321
|
-
process.stdout.write(`sickr: no ${PROVIDERS[provider].
|
|
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].
|
|
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].
|
|
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: '
|
|
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
|
-
'
|
|
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 (
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
'
|
|
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
|
|
961
|
-
const mapped =
|
|
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:
|
|
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:
|
|
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
|
|
982
|
-
const alias =
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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}).
|
|
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
|
|
186
|
+
const provider = providerForAgent(agent);
|
|
186
187
|
if (!provider)
|
|
187
188
|
return false; // Custom binaries — caller decides whether to install hooks
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
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 =
|
|
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}).
|
|
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
|
-
|
|
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 ${
|
|
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.
|
|
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": {
|
|
7
|
-
|
|
8
|
-
|
|
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": {
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
16
25
|
"license": "UNLICENSED",
|
|
17
26
|
"optionalDependencies": {
|
|
18
27
|
"node-pty": "^1.0.0",
|