@sickr/cli 0.9.7 → 0.9.9
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 +124 -98
- package/dist/live.js +1 -1
- package/dist/providers.js +85 -0
- package/dist/recorder.js +2 -1
- package/dist/run.js +43 -9
- 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,80 +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
80
|
Usage: npx @sickr/cli <command> [options]
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
ACCOUNT
|
|
83
|
+
login Sign in with GitHub.
|
|
84
|
+
logout Forget the local login.
|
|
85
|
+
whoami Show who you're signed in as.
|
|
86
|
+
|
|
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.
|
|
59
94
|
replay Install Claude + Codex recording hooks. Use the agents
|
|
60
|
-
as normal
|
|
95
|
+
as normal - a redacted timeline is captured to
|
|
61
96
|
~/.sickr/runs.
|
|
62
|
-
replay open [id] Render the newest run
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
replay
|
|
66
|
-
|
|
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).
|
|
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.
|
|
72
102
|
|
|
73
|
-
LIVE
|
|
74
|
-
live Stream
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Requires \`sickr login\` + Replay Pro entitlement.
|
|
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.
|
|
78
107
|
live status Show pid + connection state.
|
|
79
108
|
live stop Stop the sidecar.
|
|
80
109
|
|
|
81
|
-
|
|
82
|
-
run <agent> Wrap an agent in a PTY sickr owns; browser steer
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
sickr run
|
|
86
|
-
sickr run
|
|
87
|
-
sickr run
|
|
88
|
-
|
|
89
|
-
|
|
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).
|
|
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
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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.
|
|
111
131
|
|
|
112
|
-
|
|
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.
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
136
|
+
https://sickr.ai
|
|
120
137
|
`;
|
|
121
138
|
export function currentRunId(cc) {
|
|
122
139
|
return String(cc.session_id ?? 'session');
|
|
123
140
|
}
|
|
124
|
-
const PROVIDERS = {
|
|
125
|
-
claude: { name: 'Claude Code', label: 'Claude', settingsPath: () => join(process.cwd(), '.claude', 'settings.json') },
|
|
126
|
-
codex: { name: 'Codex', label: 'Codex', settingsPath: () => join(process.cwd(), '.codex', 'hooks.json') },
|
|
127
|
-
};
|
|
128
141
|
function configPath() {
|
|
129
142
|
return join(homedir(), '.sickr', 'config.json');
|
|
130
143
|
}
|
|
@@ -158,7 +171,7 @@ function resolveName() {
|
|
|
158
171
|
export function handleRecord(input, provider = 'claude') {
|
|
159
172
|
try {
|
|
160
173
|
const cc = JSON.parse(input);
|
|
161
|
-
appendEvent(currentRunId(cc), cc, { human: resolveName(), agent: PROVIDERS[provider].
|
|
174
|
+
appendEvent(currentRunId(cc), cc, { human: resolveName(), agent: PROVIDERS[provider].recordLabel });
|
|
162
175
|
}
|
|
163
176
|
catch {
|
|
164
177
|
/* swallow: recording is best-effort and must not disrupt the session */
|
|
@@ -166,9 +179,13 @@ export function handleRecord(input, provider = 'claude') {
|
|
|
166
179
|
}
|
|
167
180
|
export function handleInit(provider, noName = false) {
|
|
168
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
|
+
}
|
|
169
186
|
const settingsPath = p.settingsPath();
|
|
170
187
|
const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, 'utf8')) : {};
|
|
171
|
-
const command =
|
|
188
|
+
const command = recordCommandFor(provider);
|
|
172
189
|
// Remove any prior SICKR hook first, then install the current command — so
|
|
173
190
|
// re-running init (or a CLI upgrade that changes the command) self-heals
|
|
174
191
|
// instead of leaving a stale hook. Scoped to this provider's file.
|
|
@@ -181,16 +198,16 @@ export function handleInit(provider, noName = false) {
|
|
|
181
198
|
const name = noName ? 'Human' : loginName();
|
|
182
199
|
writeFileSync(configPath(), JSON.stringify({ name }, null, 2) + '\n');
|
|
183
200
|
const labelLine = `Your prompts will be labelled "${name}"${noName ? '' : ' — run `init --no-name` to anonymize'}.\n`;
|
|
184
|
-
const nextSteps =
|
|
185
|
-
?
|
|
186
|
-
:
|
|
187
|
-
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` +
|
|
188
205
|
`Runs are recorded locally to ${runsDir()} (secrets redacted).\n` +
|
|
189
206
|
labelLine + nextSteps);
|
|
190
207
|
}
|
|
191
208
|
/** Stop recording: remove SICKR's hooks from this project (both providers), keep runs. */
|
|
192
209
|
export function handleStop() {
|
|
193
|
-
const targets =
|
|
210
|
+
const targets = Object.values(PROVIDERS).filter((p) => p.supportsHooks && p.settingsPath).map((p) => p.settingsPath());
|
|
194
211
|
const cleaned = [];
|
|
195
212
|
for (const settingsPath of targets) {
|
|
196
213
|
if (!existsSync(settingsPath))
|
|
@@ -275,9 +292,9 @@ export function latestRunIdFor(agent) {
|
|
|
275
292
|
function handleOpen(runId, provider) {
|
|
276
293
|
let id = runId;
|
|
277
294
|
if (!id && provider) {
|
|
278
|
-
id = latestRunIdFor(PROVIDERS[provider].
|
|
295
|
+
id = latestRunIdFor(PROVIDERS[provider].recordLabel) ?? undefined;
|
|
279
296
|
if (!id) {
|
|
280
|
-
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`);
|
|
281
298
|
return;
|
|
282
299
|
}
|
|
283
300
|
}
|
|
@@ -362,11 +379,11 @@ function handleList(provider) {
|
|
|
362
379
|
const dir = runsDir();
|
|
363
380
|
let files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.ndjson')) : [];
|
|
364
381
|
if (provider) {
|
|
365
|
-
const want = PROVIDERS[provider].
|
|
382
|
+
const want = PROVIDERS[provider].recordLabel;
|
|
366
383
|
files = files.filter((f) => loadRun(f.replace(/\.ndjson$/, '')).events.some((e) => e.kind === 'response' && e.label === want));
|
|
367
384
|
}
|
|
368
385
|
if (files.length === 0) {
|
|
369
|
-
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');
|
|
370
387
|
return;
|
|
371
388
|
}
|
|
372
389
|
files
|
|
@@ -430,7 +447,7 @@ function expiryCopy(ttl_days) {
|
|
|
430
447
|
return {
|
|
431
448
|
kind: 'pro',
|
|
432
449
|
value: `${ttl_days} days`,
|
|
433
|
-
tag: '
|
|
450
|
+
tag: 'paid retention',
|
|
434
451
|
footer: [],
|
|
435
452
|
};
|
|
436
453
|
if (ttl_days >= 2)
|
|
@@ -446,7 +463,7 @@ function expiryCopy(ttl_days) {
|
|
|
446
463
|
tag: 'anon link',
|
|
447
464
|
footer: [
|
|
448
465
|
'run `npx @sickr/cli login` to extend new links to 7 days.',
|
|
449
|
-
'
|
|
466
|
+
'Live ($9) and Run ($12) - early access, rolling cohorts:',
|
|
450
467
|
' https://sickr.ai/#waitlist',
|
|
451
468
|
],
|
|
452
469
|
};
|
|
@@ -463,7 +480,7 @@ function tipLine(text) {
|
|
|
463
480
|
}
|
|
464
481
|
function legacyExpiryLine(ttl_days) {
|
|
465
482
|
if (ttl_days >= 30)
|
|
466
|
-
return `sickr: this link is live for ${ttl_days} days (
|
|
483
|
+
return `sickr: this link is live for ${ttl_days} days (paid retention).\n`;
|
|
467
484
|
if (ttl_days >= 2)
|
|
468
485
|
return `sickr: this link is live for ${ttl_days} days — re-share before it expires to extend.\n`;
|
|
469
486
|
return `sickr: this link expires in 24h. Run \`npx @sickr/cli login\` to extend to 7 days.\n`;
|
|
@@ -511,7 +528,7 @@ async function handleShare(runId, yes, open) {
|
|
|
511
528
|
else {
|
|
512
529
|
process.stdout.write(`sickr: published → ${url}\n` +
|
|
513
530
|
legacyExpiryLine(ttl_days) +
|
|
514
|
-
(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` : ''));
|
|
515
532
|
}
|
|
516
533
|
if (open)
|
|
517
534
|
openInBrowser(url);
|
|
@@ -564,7 +581,7 @@ async function handleShareCombined(sel, yes, open) {
|
|
|
564
581
|
else {
|
|
565
582
|
process.stdout.write(`sickr: published → ${url}\n` +
|
|
566
583
|
legacyExpiryLine(ttl_days) +
|
|
567
|
-
(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` : ''));
|
|
568
585
|
}
|
|
569
586
|
if (open)
|
|
570
587
|
openInBrowser(url);
|
|
@@ -826,15 +843,15 @@ async function fetchReplayProEntitlement() {
|
|
|
826
843
|
async function requireReplayPro(commandLabel) {
|
|
827
844
|
const creds = readCredentials();
|
|
828
845
|
if (!creds) {
|
|
829
|
-
process.stderr.write(`sickr: \`${commandLabel}\`
|
|
846
|
+
process.stderr.write(`sickr: \`${commandLabel}\` requires a paid Live, Run, or Prime Workflow entitlement.\n` +
|
|
830
847
|
` run \`sickr login\` first — your Pro entitlement is attached to your GitHub account.\n`);
|
|
831
848
|
return false;
|
|
832
849
|
}
|
|
833
850
|
if (await fetchReplayProEntitlement())
|
|
834
851
|
return true;
|
|
835
|
-
process.stderr.write(`sickr: \`${commandLabel}\`
|
|
836
|
-
` you are signed in as ${creds.login}, but
|
|
837
|
-
` join the rolling-cohort waitlist
|
|
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`);
|
|
838
855
|
return false;
|
|
839
856
|
}
|
|
840
857
|
async function handleReplay(rest) {
|
|
@@ -853,11 +870,11 @@ async function handleReplay(rest) {
|
|
|
853
870
|
handleInit('codex', noName);
|
|
854
871
|
return;
|
|
855
872
|
}
|
|
856
|
-
if (agent === 'claude' || agent === 'codex') {
|
|
873
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'cursor' || agent === 'local') {
|
|
857
874
|
handleInit(agent, noName);
|
|
858
875
|
return;
|
|
859
876
|
}
|
|
860
|
-
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');
|
|
861
878
|
process.exit(1);
|
|
862
879
|
return;
|
|
863
880
|
}
|
|
@@ -878,12 +895,12 @@ async function handleReplay(rest) {
|
|
|
878
895
|
handleOpenCombined(sel);
|
|
879
896
|
return;
|
|
880
897
|
}
|
|
881
|
-
const openProvider = replayRest.
|
|
898
|
+
const openProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
|
|
882
899
|
handleOpen(replayRest.find((a) => !a.startsWith('-')), openProvider);
|
|
883
900
|
return;
|
|
884
901
|
}
|
|
885
902
|
if (sub === 'list') {
|
|
886
|
-
const listProvider = replayRest.
|
|
903
|
+
const listProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
|
|
887
904
|
handleList(listProvider);
|
|
888
905
|
return;
|
|
889
906
|
}
|
|
@@ -924,7 +941,7 @@ async function handleReplay(rest) {
|
|
|
924
941
|
return;
|
|
925
942
|
}
|
|
926
943
|
process.stdout.write('sickr: replay recording is ready for Claude and Codex.\n' +
|
|
927
|
-
'
|
|
944
|
+
' Live unlocks browser viewing; Run unlocks browser control.\n');
|
|
928
945
|
}
|
|
929
946
|
function commandCandidates(mappedArgs) {
|
|
930
947
|
const override = process.env.SICKR_ORCHESTRATOR_CMD?.trim();
|
|
@@ -938,8 +955,8 @@ function commandCandidates(mappedArgs) {
|
|
|
938
955
|
{ command: 'python', args: ['-m', 'labudi_orchestrator.cli', ...mappedArgs] },
|
|
939
956
|
];
|
|
940
957
|
}
|
|
941
|
-
function
|
|
942
|
-
const mapped =
|
|
958
|
+
function runPrime(rest) {
|
|
959
|
+
const mapped = buildPrimeInvocation(rest).args;
|
|
943
960
|
const candidates = commandCandidates(mapped);
|
|
944
961
|
let lastStatus = 1;
|
|
945
962
|
for (const candidate of candidates) {
|
|
@@ -949,18 +966,18 @@ function runWorkflow(rest) {
|
|
|
949
966
|
continue;
|
|
950
967
|
}
|
|
951
968
|
if (result.error) {
|
|
952
|
-
process.stderr.write(`sickr:
|
|
969
|
+
process.stderr.write(`sickr: prime command failed to start: ${result.error.message}\n`);
|
|
953
970
|
process.exit(1);
|
|
954
971
|
return;
|
|
955
972
|
}
|
|
956
973
|
process.exit(result.status ?? 0);
|
|
957
974
|
return;
|
|
958
975
|
}
|
|
959
|
-
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');
|
|
960
977
|
process.exit(lastStatus);
|
|
961
978
|
}
|
|
962
|
-
async function
|
|
963
|
-
const alias =
|
|
979
|
+
async function handlePrime(rest) {
|
|
980
|
+
const alias = primeAgentAlias(rest);
|
|
964
981
|
if (alias) {
|
|
965
982
|
const sub = alias[0];
|
|
966
983
|
const agentRest = alias.slice(1);
|
|
@@ -977,7 +994,7 @@ async function handleWorkflow(rest) {
|
|
|
977
994
|
return;
|
|
978
995
|
}
|
|
979
996
|
}
|
|
980
|
-
|
|
997
|
+
runPrime(rest.length ? rest : ['status']);
|
|
981
998
|
}
|
|
982
999
|
export async function readStreamWithIdle(input, idleMs = 250, emptyMs = 1500) {
|
|
983
1000
|
const chunks = [];
|
|
@@ -1022,7 +1039,7 @@ async function main() {
|
|
|
1022
1039
|
}
|
|
1023
1040
|
const cmd = parseCommand(argv);
|
|
1024
1041
|
const rest = argv.slice(1);
|
|
1025
|
-
const provider = rest
|
|
1042
|
+
const provider = providerFromFlags(rest);
|
|
1026
1043
|
switch (cmd) {
|
|
1027
1044
|
case 'record':
|
|
1028
1045
|
handleRecord(await readStdin(), provider);
|
|
@@ -1035,11 +1052,11 @@ async function main() {
|
|
|
1035
1052
|
handleInit('codex', noName);
|
|
1036
1053
|
return;
|
|
1037
1054
|
}
|
|
1038
|
-
if (agent === 'claude' || agent === 'codex') {
|
|
1055
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'cursor' || agent === 'local') {
|
|
1039
1056
|
handleInit(agent, noName);
|
|
1040
1057
|
return;
|
|
1041
1058
|
}
|
|
1042
|
-
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');
|
|
1043
1060
|
process.exit(1);
|
|
1044
1061
|
return;
|
|
1045
1062
|
}
|
|
@@ -1049,12 +1066,12 @@ async function main() {
|
|
|
1049
1066
|
handleOpenCombined(sel);
|
|
1050
1067
|
return;
|
|
1051
1068
|
}
|
|
1052
|
-
const openProvider = rest.
|
|
1069
|
+
const openProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
|
|
1053
1070
|
handleOpen(rest.find((a) => !a.startsWith('-')), openProvider);
|
|
1054
1071
|
return;
|
|
1055
1072
|
}
|
|
1056
1073
|
case 'list': {
|
|
1057
|
-
const listProvider = rest.
|
|
1074
|
+
const listProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--cursor', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
|
|
1058
1075
|
handleList(listProvider);
|
|
1059
1076
|
return;
|
|
1060
1077
|
}
|
|
@@ -1076,8 +1093,17 @@ async function main() {
|
|
|
1076
1093
|
case 'replay':
|
|
1077
1094
|
await handleReplay(rest);
|
|
1078
1095
|
return;
|
|
1096
|
+
case 'prime':
|
|
1097
|
+
await handlePrime(rest);
|
|
1098
|
+
return;
|
|
1079
1099
|
case 'workflow':
|
|
1080
|
-
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]);
|
|
1081
1107
|
return;
|
|
1082
1108
|
case 'agent': {
|
|
1083
1109
|
const sub = rest[0];
|
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/recorder.js
CHANGED
|
@@ -93,7 +93,8 @@ export function mapEvent(cc, now = new Date(), ctx = {}) {
|
|
|
93
93
|
const rawSession = String(cc.session_id ?? '');
|
|
94
94
|
const session = rawSession ? rawSession.slice(0, 12) : undefined;
|
|
95
95
|
const agent = ctx.agent;
|
|
96
|
-
const
|
|
96
|
+
const runner = process.env.SICKR_RUN_INSTANCE ? process.env.SICKR_RUN_INSTANCE.slice(0, 36) : undefined;
|
|
97
|
+
const base = { at, agent, session, runner };
|
|
97
98
|
switch (name) {
|
|
98
99
|
case 'SessionStart':
|
|
99
100
|
return { kind: 'start', label: 'Session', detail: redact(String(cc.cwd ?? '')), ...base };
|
package/dist/run.js
CHANGED
|
@@ -23,12 +23,14 @@
|
|
|
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 { randomUUID } from 'node:crypto';
|
|
26
27
|
import { execFileSync } from 'node:child_process';
|
|
27
28
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
28
29
|
import { readCredentials } from './auth.js';
|
|
29
30
|
import { runsDir } from './recorder.js';
|
|
30
31
|
import { mergeHooks, removeHooks } from './hookConfig.js';
|
|
31
32
|
import { LIVE_BASE, decodeWsPayload, splitJsonObjects } from './live.js';
|
|
33
|
+
import { PROVIDERS, providerForAgent, recordCommandFor } from './providers.js';
|
|
32
34
|
/** Resolve the agent binary path. Precedence:
|
|
33
35
|
* 1. SICKR_AGENT_BIN_<NAME> env (e.g. SICKR_AGENT_BIN_CLAUDE)
|
|
34
36
|
* 2. <NAME>_BIN env (e.g. CLAUDE_BIN)
|
|
@@ -110,6 +112,18 @@ function appendInbox(urlid, text, at) {
|
|
|
110
112
|
writeFileSync(file, `# steer inbox — ${urlid}\n\n`);
|
|
111
113
|
appendFileSync(file, `\n## ${at}\n\n${text}\n`);
|
|
112
114
|
}
|
|
115
|
+
function normTarget(s) {
|
|
116
|
+
return String(s ?? '').trim().toLowerCase();
|
|
117
|
+
}
|
|
118
|
+
export function steerMatchesRunner(msg, identity) {
|
|
119
|
+
if (msg.targetRunner)
|
|
120
|
+
return msg.targetRunner === identity.runner;
|
|
121
|
+
if (msg.targetSession)
|
|
122
|
+
return identity.sessions.has(msg.targetSession);
|
|
123
|
+
if (msg.targetAgent)
|
|
124
|
+
return normTarget(msg.targetAgent) === normTarget(identity.agent);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
113
127
|
export function decideSteer(msg, defaultMode = 'pty') {
|
|
114
128
|
const text = String(msg.text ?? '');
|
|
115
129
|
if (!text)
|
|
@@ -182,12 +196,13 @@ function tailFrom(path, from) {
|
|
|
182
196
|
* hooks were ensured / installed; false if the agent name isn't
|
|
183
197
|
* one we know how to init for. */
|
|
184
198
|
export function ensureRecordingHooks(agent) {
|
|
185
|
-
const provider = agent
|
|
199
|
+
const provider = providerForAgent(agent);
|
|
186
200
|
if (!provider)
|
|
187
201
|
return false; // Custom binaries — caller decides whether to install hooks
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
const adapter = PROVIDERS[provider];
|
|
203
|
+
if (!adapter.supportsHooks || !adapter.settingsPath)
|
|
204
|
+
return false;
|
|
205
|
+
const settingsPath = adapter.settingsPath();
|
|
191
206
|
let existing = {};
|
|
192
207
|
if (existsSync(settingsPath)) {
|
|
193
208
|
try {
|
|
@@ -208,7 +223,7 @@ export function ensureRecordingHooks(agent) {
|
|
|
208
223
|
// Dynamic import keeps the file from pulling hookConfig at module
|
|
209
224
|
// load (tiny boot perf + lets tests run without the module).
|
|
210
225
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
211
|
-
const command =
|
|
226
|
+
const command = recordCommandFor(provider);
|
|
212
227
|
const merged = mergeHooks(removeHooks(existing), command);
|
|
213
228
|
mkdirSync(join(settingsPath, '..'), { recursive: true });
|
|
214
229
|
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
|
|
@@ -272,7 +287,7 @@ export async function startRun(opts) {
|
|
|
272
287
|
urlid = await computeUrlid(creds);
|
|
273
288
|
}
|
|
274
289
|
catch (e) {
|
|
275
|
-
process.stderr.write(`sickr: couldn't resolve live url (${e.message}).
|
|
290
|
+
process.stderr.write(`sickr: couldn't resolve live url (${e.message}). Run entitlement required.\n`);
|
|
276
291
|
process.exit(4);
|
|
277
292
|
}
|
|
278
293
|
// Ensure recording hooks are installed in the cwd for known agents.
|
|
@@ -280,6 +295,10 @@ export async function startRun(opts) {
|
|
|
280
295
|
// browser /r/<urlid> stays empty until events flow.
|
|
281
296
|
ensureRecordingHooks(opts.agent);
|
|
282
297
|
const agentBin = resolveAgent(opts.agent);
|
|
298
|
+
const provider = providerForAgent(opts.agent);
|
|
299
|
+
const agentLabel = provider ? PROVIDERS[provider].recordLabel : opts.agent;
|
|
300
|
+
const runnerId = randomUUID();
|
|
301
|
+
const runnerIdentity = { agent: agentLabel, runner: runnerId, sessions: new Set() };
|
|
283
302
|
const cols = process.stdout.columns ?? 80;
|
|
284
303
|
const rows = process.stdout.rows ?? 24;
|
|
285
304
|
// mode=auto (default) injects the agent's "no prompt / full perms" flag
|
|
@@ -305,7 +324,7 @@ export async function startRun(opts) {
|
|
|
305
324
|
name: 'xterm-256color',
|
|
306
325
|
cols, rows,
|
|
307
326
|
cwd: process.cwd(),
|
|
308
|
-
env: process.env,
|
|
327
|
+
env: { ...process.env, SICKR_RUN_INSTANCE: runnerId, SICKR_RUN_AGENT: agentLabel },
|
|
309
328
|
});
|
|
310
329
|
// Wire stdio: parent stdin -> pty; pty -> parent stdout.
|
|
311
330
|
if (process.stdin.isTTY)
|
|
@@ -377,7 +396,11 @@ export async function startRun(opts) {
|
|
|
377
396
|
let opened = false;
|
|
378
397
|
ws.addEventListener('open', () => {
|
|
379
398
|
opened = true;
|
|
380
|
-
|
|
399
|
+
try {
|
|
400
|
+
ws.send(JSON.stringify({ kind: 'hello', agent: runnerIdentity.agent, runner: runnerIdentity.runner }));
|
|
401
|
+
}
|
|
402
|
+
catch { /* reconnect loop handles failures */ }
|
|
403
|
+
tailTimer = setInterval(() => pumpNewLines(ws, offsets, runnerIdentity), 500);
|
|
381
404
|
});
|
|
382
405
|
ws.addEventListener('message', (ev) => {
|
|
383
406
|
const raw = decodeWsPayload(ev.data);
|
|
@@ -391,6 +414,11 @@ export async function startRun(opts) {
|
|
|
391
414
|
return;
|
|
392
415
|
}
|
|
393
416
|
if (m.kind === 'steer' && m.text) {
|
|
417
|
+
if (!steerMatchesRunner(m, runnerIdentity)) {
|
|
418
|
+
if (opts.verbose)
|
|
419
|
+
process.stderr.write(`sickr: ignored steer for another agent/session\n`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
394
422
|
const decision = decideSteer(m);
|
|
395
423
|
if (decision.target === 'pty' && decision.bytes != null) {
|
|
396
424
|
// Submit-fix (2026-06-01): Claude Code's TUI treats a burst of
|
|
@@ -457,7 +485,7 @@ async function loadWsShim() {
|
|
|
457
485
|
throw new Error('`ws` is unavailable. It ships as an optional dependency of @sickr/cli; if it failed to install, try: `npm install -g ws`.');
|
|
458
486
|
}
|
|
459
487
|
}
|
|
460
|
-
function pumpNewLines(ws, offsets) {
|
|
488
|
+
function pumpNewLines(ws, offsets, identity) {
|
|
461
489
|
const dir = runsDir();
|
|
462
490
|
if (!existsSync(dir))
|
|
463
491
|
return;
|
|
@@ -482,6 +510,12 @@ function pumpNewLines(ws, offsets) {
|
|
|
482
510
|
for (const fragment of splitJsonObjects(line)) {
|
|
483
511
|
try {
|
|
484
512
|
const event = JSON.parse(fragment);
|
|
513
|
+
if (event.runner && event.runner !== identity.runner)
|
|
514
|
+
continue;
|
|
515
|
+
if (!event.runner && event.agent && normTarget(event.agent) !== normTarget(identity.agent))
|
|
516
|
+
continue;
|
|
517
|
+
if (event.session)
|
|
518
|
+
identity.sessions.add(event.session);
|
|
485
519
|
ws.send(JSON.stringify({ kind: 'event', event }));
|
|
486
520
|
}
|
|
487
521
|
catch { /* skip malformed */ }
|
package/package.json
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sickr/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
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",
|