@sickr/cli 0.9.9 → 0.9.11
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 +146 -20
- package/dist/hookConfig.js +12 -6
- package/dist/providers.js +16 -13
- package/dist/recorder.js +10 -1
- package/dist/run.js +39 -9
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir, userInfo } from 'node:os';
|
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { spawn, spawnSync, execFileSync } from 'node:child_process';
|
|
7
7
|
import { appendEvent, loadRun, runsDir, latestRunId } from './recorder.js';
|
|
8
|
-
import {
|
|
8
|
+
import { removeHooks, mergeHooksForEvents, removeHooksForEvents } from './hookConfig.js';
|
|
9
9
|
import { renderRunHtml, renderCombinedHtml } from './render.js';
|
|
10
10
|
import { buildSharePayload, buildCombinedPayload, publish, PublishError } from './share.js';
|
|
11
11
|
import { AUTH_ENDPOINT, readCredentials, writeCredentials, clearCredentials, startDevice, pollDevice, sleep } from './auth.js';
|
|
@@ -69,8 +69,8 @@ function providerFromFlags(args) {
|
|
|
69
69
|
return 'codex';
|
|
70
70
|
if (args.includes('--gemini'))
|
|
71
71
|
return 'gemini';
|
|
72
|
-
if (args.includes('--
|
|
73
|
-
return '
|
|
72
|
+
if (args.includes('--ollama'))
|
|
73
|
+
return 'ollama';
|
|
74
74
|
if (args.includes('--local'))
|
|
75
75
|
return 'local';
|
|
76
76
|
return 'claude';
|
|
@@ -113,7 +113,7 @@ RUN ($12) - remote control from the browser.
|
|
|
113
113
|
sickr run claude
|
|
114
114
|
sickr run codex
|
|
115
115
|
sickr run gemini
|
|
116
|
-
sickr run
|
|
116
|
+
sickr run ollama --model llama3.2
|
|
117
117
|
sickr run <bin>
|
|
118
118
|
Flags: --mode auto (default) | --mode interactive
|
|
119
119
|
|
|
@@ -129,9 +129,9 @@ Product modes are ordered: Prime Workflow > Run > Live > Replay.
|
|
|
129
129
|
Prime Workflow runs PRIME for agentic or hybrid teams: plan, review,
|
|
130
130
|
implement, merge, evaluate, and ship under control.
|
|
131
131
|
|
|
132
|
-
Requires Node 20+. Codex capture needs
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
Requires Node 20+. Codex capture needs one manual /hooks trust approval
|
|
133
|
+
per stable hook command. Gemini uses its native hook events. Ollama is
|
|
134
|
+
the supported local LLM runtime; configure the model on workflow.sickr.ai.
|
|
135
135
|
|
|
136
136
|
https://sickr.ai
|
|
137
137
|
`;
|
|
@@ -172,9 +172,13 @@ export function handleRecord(input, provider = 'claude') {
|
|
|
172
172
|
try {
|
|
173
173
|
const cc = JSON.parse(input);
|
|
174
174
|
appendEvent(currentRunId(cc), cc, { human: resolveName(), agent: PROVIDERS[provider].recordLabel });
|
|
175
|
+
if (provider === 'gemini')
|
|
176
|
+
process.stdout.write('{}\n');
|
|
175
177
|
}
|
|
176
178
|
catch {
|
|
177
179
|
/* swallow: recording is best-effort and must not disrupt the session */
|
|
180
|
+
if (provider === 'gemini')
|
|
181
|
+
process.stdout.write('{}\n');
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
export function handleInit(provider, noName = false) {
|
|
@@ -189,7 +193,8 @@ export function handleInit(provider, noName = false) {
|
|
|
189
193
|
// Remove any prior SICKR hook first, then install the current command — so
|
|
190
194
|
// re-running init (or a CLI upgrade that changes the command) self-heals
|
|
191
195
|
// instead of leaving a stale hook. Scoped to this provider's file.
|
|
192
|
-
const
|
|
196
|
+
const events = p.hookEvents ?? ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'];
|
|
197
|
+
const merged = mergeHooksForEvents(removeHooksForEvents(settings, events), command, events);
|
|
193
198
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
194
199
|
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
|
|
195
200
|
mkdirSync(runsDir(), { recursive: true });
|
|
@@ -199,7 +204,7 @@ export function handleInit(provider, noName = false) {
|
|
|
199
204
|
writeFileSync(configPath(), JSON.stringify({ name }, null, 2) + '\n');
|
|
200
205
|
const labelLine = `Your prompts will be labelled "${name}"${noName ? '' : ' — run `init --no-name` to anonymize'}.\n`;
|
|
201
206
|
const nextSteps = p.requiresManualHookTrust
|
|
202
|
-
? `Next: in ${p.displayName}, trust the SICKR recorder when prompted (Codex
|
|
207
|
+
? `Next: in ${p.displayName}, trust the SICKR recorder once when prompted${provider === 'codex' ? ' (type `/hooks` in Codex)' : ''},\nthen use the agent as normal and: npx @sickr/cli replay open\n`
|
|
203
208
|
: `Use ${p.displayName} as normal, then: npx @sickr/cli replay open\n`;
|
|
204
209
|
process.stdout.write(`sickr: installed ${p.displayName} recording hooks in ${settingsPath}\n` +
|
|
205
210
|
`Runs are recorded locally to ${runsDir()} (secrets redacted).\n` +
|
|
@@ -674,12 +679,61 @@ export function handleWhoami() {
|
|
|
674
679
|
process.stdout.write(`sickr: ${c.login}${c.name ? ` (${c.name})` : ''} · since ${c.login_at}\n`);
|
|
675
680
|
}
|
|
676
681
|
function valueAt(rest, flag) {
|
|
682
|
+
const eq = rest.find((item) => item.startsWith(`${flag}=`));
|
|
683
|
+
if (eq)
|
|
684
|
+
return eq.slice(flag.length + 1) || null;
|
|
677
685
|
const i = rest.indexOf(flag);
|
|
678
686
|
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('-') ? rest[i + 1] : null;
|
|
679
687
|
}
|
|
680
688
|
function isRecord(value) {
|
|
681
689
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
682
690
|
}
|
|
691
|
+
function stringArray(value) {
|
|
692
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : null;
|
|
693
|
+
}
|
|
694
|
+
function providerFromValue(value) {
|
|
695
|
+
if (value === 'claude' || value === 'codex' || value === 'gemini' || value === 'ollama' || value === 'local')
|
|
696
|
+
return value;
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
function productModeFromValue(value) {
|
|
700
|
+
if (value === 'prime_workflow' || value === 'run' || value === 'live' || value === 'replay')
|
|
701
|
+
return value;
|
|
702
|
+
if (value === 'workflow' || value === 'prime')
|
|
703
|
+
return 'prime_workflow';
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
export function configuredRunInvocationFromStatus(status) {
|
|
707
|
+
const root = isRecord(status) ? status : {};
|
|
708
|
+
const agent = isRecord(root.agent) ? root.agent : {};
|
|
709
|
+
const runtime = isRecord(agent.runtime_config) ? agent.runtime_config
|
|
710
|
+
: isRecord(agent.run_config) ? agent.run_config
|
|
711
|
+
: isRecord(root.runtime_config) ? root.runtime_config
|
|
712
|
+
: {};
|
|
713
|
+
const provider = providerFromValue(runtime.provider) ?? providerFromValue(agent.provider) ?? 'claude';
|
|
714
|
+
const productMode = productModeFromValue(runtime.product_mode) ?? productModeFromValue(runtime.mode) ?? productModeFromValue(agent.product_mode) ?? productModeFromValue(agent.mode) ?? 'run';
|
|
715
|
+
const command = typeof runtime.command === 'string' ? runtime.command
|
|
716
|
+
: typeof agent.command === 'string' ? agent.command
|
|
717
|
+
: PROVIDERS[provider].defaultCommand;
|
|
718
|
+
const configuredArgs = stringArray(runtime.args) ?? stringArray(agent.args) ?? [];
|
|
719
|
+
const model = typeof runtime.model === 'string' ? runtime.model
|
|
720
|
+
: typeof agent.model === 'string' ? agent.model
|
|
721
|
+
: null;
|
|
722
|
+
const agentArgs = (provider === 'ollama' || provider === 'local') && model && configuredArgs.length === 0
|
|
723
|
+
? ['--model', model]
|
|
724
|
+
: configuredArgs;
|
|
725
|
+
const mode = runtime.pty_mode === 'interactive' || runtime.run_mode === 'interactive' || runtime.mode === 'interactive' || agent.pty_mode === 'interactive' ? 'interactive' : 'auto';
|
|
726
|
+
return { agent: command, agentArgs, mode, productMode };
|
|
727
|
+
}
|
|
728
|
+
export async function resolveConfiguredRun(agentId, credentials = readAgentCredentials()) {
|
|
729
|
+
if (!credentials) {
|
|
730
|
+
throw new Error('agent is not connected. Run `sickr prime connect --agent-id <id>` first.');
|
|
731
|
+
}
|
|
732
|
+
if (credentials.agent_id !== agentId) {
|
|
733
|
+
throw new Error(`local machine is connected as ${credentials.agent_id}, not ${agentId}. Run \`sickr prime connect --agent-id ${agentId}\` first.`);
|
|
734
|
+
}
|
|
735
|
+
return configuredRunInvocationFromStatus(await fetchAgentStatus(credentials));
|
|
736
|
+
}
|
|
683
737
|
function agentContextLabel(context) {
|
|
684
738
|
const root = isRecord(context) ? context : {};
|
|
685
739
|
const agent = isRecord(root.agent) ? root.agent : {};
|
|
@@ -870,7 +924,7 @@ async function handleReplay(rest) {
|
|
|
870
924
|
handleInit('codex', noName);
|
|
871
925
|
return;
|
|
872
926
|
}
|
|
873
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === '
|
|
927
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'ollama' || agent === 'local') {
|
|
874
928
|
handleInit(agent, noName);
|
|
875
929
|
return;
|
|
876
930
|
}
|
|
@@ -895,12 +949,12 @@ async function handleReplay(rest) {
|
|
|
895
949
|
handleOpenCombined(sel);
|
|
896
950
|
return;
|
|
897
951
|
}
|
|
898
|
-
const openProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--
|
|
952
|
+
const openProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--ollama', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
|
|
899
953
|
handleOpen(replayRest.find((a) => !a.startsWith('-')), openProvider);
|
|
900
954
|
return;
|
|
901
955
|
}
|
|
902
956
|
if (sub === 'list') {
|
|
903
|
-
const listProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--
|
|
957
|
+
const listProvider = replayRest.some((a) => ['--codex', '--claude', '--gemini', '--ollama', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
|
|
904
958
|
handleList(listProvider);
|
|
905
959
|
return;
|
|
906
960
|
}
|
|
@@ -996,6 +1050,56 @@ async function handlePrime(rest) {
|
|
|
996
1050
|
}
|
|
997
1051
|
runPrime(rest.length ? rest : ['status']);
|
|
998
1052
|
}
|
|
1053
|
+
async function handleStart(rest) {
|
|
1054
|
+
const agentId = valueAt(rest, '--agent-id') ?? rest.find((arg) => !arg.startsWith('-')) ?? null;
|
|
1055
|
+
if (!agentId) {
|
|
1056
|
+
await handlePrime(['start', ...rest]);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
let configured;
|
|
1060
|
+
try {
|
|
1061
|
+
configured = await resolveConfiguredRun(agentId);
|
|
1062
|
+
}
|
|
1063
|
+
catch (e) {
|
|
1064
|
+
process.stderr.write(`sickr: ${e.message}\n`);
|
|
1065
|
+
process.exit(2);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (configured.productMode === 'prime_workflow') {
|
|
1069
|
+
await handlePrime(['start', ...rest]);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (configured.productMode === 'live') {
|
|
1073
|
+
const { startLive } = await import('./live.js');
|
|
1074
|
+
if (!(await requireReplayPro('sickr start'))) {
|
|
1075
|
+
process.exit(3);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
await startLive({ verbose: rest.includes('--verbose') || rest.includes('-v'), background: rest.includes('--background') });
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (configured.productMode === 'replay') {
|
|
1082
|
+
await handleReplay([]);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (!(await requireReplayPro('sickr start'))) {
|
|
1086
|
+
process.exit(3);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const { startRun } = await import('./run.js');
|
|
1090
|
+
try {
|
|
1091
|
+
await startRun({
|
|
1092
|
+
agent: configured.agent,
|
|
1093
|
+
agentArgs: configured.agentArgs,
|
|
1094
|
+
verbose: rest.includes('--verbose') || rest.includes('-v'),
|
|
1095
|
+
mode: configured.mode,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
catch (e) {
|
|
1099
|
+
process.stderr.write(`sickr: ${e.message}\n`);
|
|
1100
|
+
process.exit(5);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
999
1103
|
export async function readStreamWithIdle(input, idleMs = 250, emptyMs = 1500) {
|
|
1000
1104
|
const chunks = [];
|
|
1001
1105
|
const iterator = input[Symbol.asyncIterator]();
|
|
@@ -1052,7 +1156,7 @@ async function main() {
|
|
|
1052
1156
|
handleInit('codex', noName);
|
|
1053
1157
|
return;
|
|
1054
1158
|
}
|
|
1055
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === '
|
|
1159
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'ollama' || agent === 'local') {
|
|
1056
1160
|
handleInit(agent, noName);
|
|
1057
1161
|
return;
|
|
1058
1162
|
}
|
|
@@ -1066,12 +1170,12 @@ async function main() {
|
|
|
1066
1170
|
handleOpenCombined(sel);
|
|
1067
1171
|
return;
|
|
1068
1172
|
}
|
|
1069
|
-
const openProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--
|
|
1173
|
+
const openProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--ollama', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
|
|
1070
1174
|
handleOpen(rest.find((a) => !a.startsWith('-')), openProvider);
|
|
1071
1175
|
return;
|
|
1072
1176
|
}
|
|
1073
1177
|
case 'list': {
|
|
1074
|
-
const listProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--
|
|
1178
|
+
const listProvider = rest.some((a) => ['--codex', '--claude', '--gemini', '--ollama', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
|
|
1075
1179
|
handleList(listProvider);
|
|
1076
1180
|
return;
|
|
1077
1181
|
}
|
|
@@ -1100,7 +1204,7 @@ async function main() {
|
|
|
1100
1204
|
await handlePrime(rest);
|
|
1101
1205
|
return;
|
|
1102
1206
|
case 'start':
|
|
1103
|
-
await
|
|
1207
|
+
await handleStart(rest);
|
|
1104
1208
|
return;
|
|
1105
1209
|
case 'status':
|
|
1106
1210
|
await handlePrime(['status', ...rest]);
|
|
@@ -1156,6 +1260,13 @@ async function main() {
|
|
|
1156
1260
|
const filtered = [];
|
|
1157
1261
|
let mode = 'auto';
|
|
1158
1262
|
for (let i = 0; i < rest.length; i++) {
|
|
1263
|
+
if (rest[i] === '--agent-id' && rest[i + 1]) {
|
|
1264
|
+
i++;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (rest[i].startsWith('--agent-id=')) {
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1159
1270
|
if (rest[i] === '--mode' && rest[i + 1]) {
|
|
1160
1271
|
const v = rest[i + 1];
|
|
1161
1272
|
if (v !== 'auto' && v !== 'interactive') {
|
|
@@ -1181,15 +1292,30 @@ async function main() {
|
|
|
1181
1292
|
}
|
|
1182
1293
|
const positional = filtered.filter((a) => !a.startsWith('-'));
|
|
1183
1294
|
const flags = filtered.filter((a) => a.startsWith('-'));
|
|
1184
|
-
const
|
|
1295
|
+
const configuredAgentId = valueAt(rest, '--agent-id');
|
|
1296
|
+
let agent = positional[0];
|
|
1297
|
+
let configuredArgs = null;
|
|
1298
|
+
if (!agent && configuredAgentId) {
|
|
1299
|
+
try {
|
|
1300
|
+
const configured = await resolveConfiguredRun(configuredAgentId);
|
|
1301
|
+
agent = configured.agent;
|
|
1302
|
+
configuredArgs = configured.agentArgs;
|
|
1303
|
+
mode = configured.mode;
|
|
1304
|
+
}
|
|
1305
|
+
catch (e) {
|
|
1306
|
+
process.stderr.write(`sickr: ${e.message}\n`);
|
|
1307
|
+
process.exit(2);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1185
1311
|
if (!agent) {
|
|
1186
|
-
process.stderr.write('sickr: usage — `sickr run <agent> [--mode auto|interactive] [args...]`
|
|
1312
|
+
process.stderr.write('sickr: usage — `sickr run <agent> [--mode auto|interactive] [args...]` or `sickr run --agent-id <id>`\n');
|
|
1187
1313
|
process.exit(1);
|
|
1188
1314
|
return;
|
|
1189
1315
|
}
|
|
1190
1316
|
// Pass through every non-sickr flag plus positional tail to the agent.
|
|
1191
|
-
const passthroughFlags = flags.filter((f) => f !== '--verbose' && f !== '-v');
|
|
1192
|
-
const agentArgs = [...passthroughFlags, ...positional.slice(1)];
|
|
1317
|
+
const passthroughFlags = flags.filter((f) => f !== '--verbose' && f !== '-v' && f !== '--agent-id');
|
|
1318
|
+
const agentArgs = configuredArgs ?? [...passthroughFlags, ...positional.slice(1)];
|
|
1193
1319
|
const verbose = flags.includes('--verbose') || flags.includes('-v');
|
|
1194
1320
|
if (!(await requireReplayPro('sickr run'))) {
|
|
1195
1321
|
process.exit(3);
|
package/dist/hookConfig.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// PreToolUse (not PostToolUse) captures each tool action once — hooking both
|
|
2
2
|
// would record every tool twice. Stop carries the assistant's final response.
|
|
3
|
-
const
|
|
3
|
+
const DEFAULT_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'];
|
|
4
4
|
const TAG = 'npx @sickr/cli record';
|
|
5
5
|
const LEGACY_TAGS = ['@sickr/replay record', 'npx sickr record'];
|
|
6
6
|
/**
|
|
@@ -8,31 +8,34 @@ const LEGACY_TAGS = ['@sickr/replay record', 'npx sickr record'];
|
|
|
8
8
|
* Idempotent — re-running never duplicates the SICKR hook. Preserves any
|
|
9
9
|
* existing unrelated hooks.
|
|
10
10
|
*/
|
|
11
|
-
export function
|
|
11
|
+
export function mergeHooksForEvents(settings, recordCommand, events = DEFAULT_EVENTS) {
|
|
12
12
|
const next = { ...(settings ?? {}) };
|
|
13
13
|
next.hooks = { ...(next.hooks ?? {}) };
|
|
14
14
|
const command = /\brecord\b/.test(recordCommand) ? recordCommand : `${recordCommand} record`;
|
|
15
|
-
for (const ev of
|
|
15
|
+
for (const ev of events) {
|
|
16
16
|
const groups = Array.isArray(next.hooks[ev]) ? [...next.hooks[ev]] : [];
|
|
17
17
|
const serialized = JSON.stringify(groups);
|
|
18
18
|
const present = serialized.includes(TAG) || LEGACY_TAGS.some((tag) => serialized.includes(tag));
|
|
19
19
|
if (!present)
|
|
20
|
-
groups.push({ hooks: [{ type: 'command', command }] });
|
|
20
|
+
groups.push({ hooks: [{ type: 'command', command, name: 'SICKR recorder', timeout: 10000 }] });
|
|
21
21
|
next.hooks[ev] = groups;
|
|
22
22
|
}
|
|
23
23
|
return next;
|
|
24
24
|
}
|
|
25
|
+
export function mergeHooks(settings, recordCommand) {
|
|
26
|
+
return mergeHooksForEvents(settings, recordCommand, DEFAULT_EVENTS);
|
|
27
|
+
}
|
|
25
28
|
/**
|
|
26
29
|
* Remove the SICKR recording hooks from a Claude Code settings object — the
|
|
27
30
|
* inverse of mergeHooks. Unrelated hooks are preserved; an event left with no
|
|
28
31
|
* hooks is dropped so settings.json stays clean.
|
|
29
32
|
*/
|
|
30
|
-
export function
|
|
33
|
+
export function removeHooksForEvents(settings, events = DEFAULT_EVENTS) {
|
|
31
34
|
const next = { ...(settings ?? {}) };
|
|
32
35
|
if (!next.hooks)
|
|
33
36
|
return next;
|
|
34
37
|
const hooks = { ...next.hooks };
|
|
35
|
-
for (const ev of
|
|
38
|
+
for (const ev of events) {
|
|
36
39
|
const groups = Array.isArray(hooks[ev]) ? hooks[ev] : undefined;
|
|
37
40
|
if (!groups)
|
|
38
41
|
continue;
|
|
@@ -48,3 +51,6 @@ export function removeHooks(settings) {
|
|
|
48
51
|
next.hooks = hooks;
|
|
49
52
|
return next;
|
|
50
53
|
}
|
|
54
|
+
export function removeHooks(settings) {
|
|
55
|
+
return removeHooksForEvents(settings, DEFAULT_EVENTS);
|
|
56
|
+
}
|
package/dist/providers.js
CHANGED
|
@@ -10,6 +10,7 @@ export const PROVIDERS = {
|
|
|
10
10
|
supportsPtyControl: true,
|
|
11
11
|
supportsStructuredStream: false,
|
|
12
12
|
settingsPath: () => join(process.cwd(), '.claude', 'settings.json'),
|
|
13
|
+
hookEvents: ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'],
|
|
13
14
|
notes: 'Hooks are installed into .claude/settings.json and Claude reads them without an extra trust command.',
|
|
14
15
|
},
|
|
15
16
|
codex: {
|
|
@@ -23,6 +24,7 @@ export const PROVIDERS = {
|
|
|
23
24
|
supportsStructuredStream: false,
|
|
24
25
|
settingsPath: () => join(process.cwd(), '.codex', 'hooks.json'),
|
|
25
26
|
recordFlag: '--codex',
|
|
27
|
+
hookEvents: ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'],
|
|
26
28
|
notes: 'Codex requires the user to type /hooks once and trust the SICKR recorder.',
|
|
27
29
|
},
|
|
28
30
|
gemini: {
|
|
@@ -36,29 +38,30 @@ export const PROVIDERS = {
|
|
|
36
38
|
supportsStructuredStream: false,
|
|
37
39
|
settingsPath: () => join(process.cwd(), '.gemini', 'settings.json'),
|
|
38
40
|
recordFlag: '--gemini',
|
|
39
|
-
|
|
41
|
+
hookEvents: ['SessionStart', 'BeforeAgent', 'BeforeTool', 'AfterAgent', 'SessionEnd'],
|
|
42
|
+
notes: 'Gemini CLI hooks use BeforeAgent/BeforeTool/AfterAgent events and require JSON-safe hook output.',
|
|
40
43
|
},
|
|
41
|
-
|
|
42
|
-
provider: '
|
|
43
|
-
displayName: '
|
|
44
|
-
recordLabel: '
|
|
45
|
-
defaultCommand: '
|
|
44
|
+
ollama: {
|
|
45
|
+
provider: 'ollama',
|
|
46
|
+
displayName: 'Ollama',
|
|
47
|
+
recordLabel: 'Ollama',
|
|
48
|
+
defaultCommand: 'ollama',
|
|
46
49
|
supportsHooks: false,
|
|
47
50
|
requiresManualHookTrust: false,
|
|
48
51
|
supportsPtyControl: true,
|
|
49
|
-
supportsStructuredStream:
|
|
50
|
-
notes: '
|
|
52
|
+
supportsStructuredStream: false,
|
|
53
|
+
notes: 'Ollama is the first supported local runtime. Configure the model in workflow.sickr.ai and run through the PTY wrapper.',
|
|
51
54
|
},
|
|
52
55
|
local: {
|
|
53
56
|
provider: 'local',
|
|
54
57
|
displayName: 'Local LLM',
|
|
55
58
|
recordLabel: 'Local',
|
|
56
|
-
defaultCommand: '
|
|
59
|
+
defaultCommand: 'ollama',
|
|
57
60
|
supportsHooks: false,
|
|
58
61
|
requiresManualHookTrust: false,
|
|
59
62
|
supportsPtyControl: true,
|
|
60
63
|
supportsStructuredStream: true,
|
|
61
|
-
notes: '
|
|
64
|
+
notes: 'Custom local runtimes use the configured command from workflow.sickr.ai. Ollama is the supported default.',
|
|
62
65
|
},
|
|
63
66
|
};
|
|
64
67
|
export function providerForAgent(agent) {
|
|
@@ -70,9 +73,9 @@ export function providerForAgent(agent) {
|
|
|
70
73
|
return 'codex';
|
|
71
74
|
if (base === 'gemini')
|
|
72
75
|
return 'gemini';
|
|
73
|
-
if (base === '
|
|
74
|
-
return '
|
|
75
|
-
if (base === '
|
|
76
|
+
if (base === 'ollama')
|
|
77
|
+
return 'ollama';
|
|
78
|
+
if (base === 'vllm' || base === 'llama.cpp' || base === 'llama-server' || base === 'sickr-local-agent')
|
|
76
79
|
return 'local';
|
|
77
80
|
return null;
|
|
78
81
|
}
|
package/dist/recorder.js
CHANGED
|
@@ -99,14 +99,23 @@ export function mapEvent(cc, now = new Date(), ctx = {}) {
|
|
|
99
99
|
case 'SessionStart':
|
|
100
100
|
return { kind: 'start', label: 'Session', detail: redact(String(cc.cwd ?? '')), ...base };
|
|
101
101
|
case 'UserPromptSubmit':
|
|
102
|
+
case 'BeforeAgent':
|
|
102
103
|
return { kind: 'prompt', label: (ctx.human || 'Human').slice(0, 40), detail: redact(String(cc.prompt ?? '')).slice(0, 400), ...base };
|
|
103
104
|
case 'Stop': {
|
|
104
105
|
// Codex hands us the reply directly; Claude Code we read from the transcript.
|
|
105
106
|
const text = String(cc.last_assistant_message ?? '') || (cc.transcript_path ? extractLastAssistantText(String(cc.transcript_path)) : '');
|
|
106
107
|
return { kind: 'response', label: (ctx.agent || 'Agent').slice(0, 40), detail: redact(text).slice(0, 2000), ...base };
|
|
107
108
|
}
|
|
109
|
+
case 'AfterAgent': {
|
|
110
|
+
const text = String(cc.prompt_response ?? cc.last_assistant_message ?? '');
|
|
111
|
+
return { kind: 'response', label: (ctx.agent || 'Agent').slice(0, 40), detail: redact(text).slice(0, 2000), ...base };
|
|
112
|
+
}
|
|
113
|
+
case 'SessionEnd':
|
|
114
|
+
return { kind: 'stop', label: 'Session', detail: redact(String(cc.reason ?? 'ended')), ...base };
|
|
108
115
|
case 'PreToolUse':
|
|
109
|
-
case '
|
|
116
|
+
case 'BeforeTool':
|
|
117
|
+
case 'PostToolUse':
|
|
118
|
+
case 'AfterTool': {
|
|
110
119
|
const tool = String(cc.tool_name ?? 'tool');
|
|
111
120
|
const input = (cc.tool_input ?? {});
|
|
112
121
|
let raw;
|
package/dist/run.js
CHANGED
|
@@ -28,7 +28,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
28
28
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
29
29
|
import { readCredentials } from './auth.js';
|
|
30
30
|
import { runsDir } from './recorder.js';
|
|
31
|
-
import {
|
|
31
|
+
import { mergeHooksForEvents, removeHooksForEvents } from './hookConfig.js';
|
|
32
32
|
import { LIVE_BASE, decodeWsPayload, splitJsonObjects } from './live.js';
|
|
33
33
|
import { PROVIDERS, providerForAgent, recordCommandFor } from './providers.js';
|
|
34
34
|
/** Resolve the agent binary path. Precedence:
|
|
@@ -124,6 +124,19 @@ export function steerMatchesRunner(msg, identity) {
|
|
|
124
124
|
return normTarget(msg.targetAgent) === normTarget(identity.agent);
|
|
125
125
|
return true;
|
|
126
126
|
}
|
|
127
|
+
export function normalizeRunEventForRunner(event, identity) {
|
|
128
|
+
if (event.runner && event.runner !== identity.runner)
|
|
129
|
+
return null;
|
|
130
|
+
if (!event.runner && event.agent && normTarget(event.agent) !== normTarget(identity.agent))
|
|
131
|
+
return null;
|
|
132
|
+
if (!event.agent)
|
|
133
|
+
event.agent = identity.agent;
|
|
134
|
+
if (!event.runner)
|
|
135
|
+
event.runner = identity.runner;
|
|
136
|
+
if (event.session)
|
|
137
|
+
identity.sessions.add(event.session);
|
|
138
|
+
return event;
|
|
139
|
+
}
|
|
127
140
|
export function decideSteer(msg, defaultMode = 'pty') {
|
|
128
141
|
const text = String(msg.text ?? '');
|
|
129
142
|
if (!text)
|
|
@@ -224,7 +237,8 @@ export function ensureRecordingHooks(agent) {
|
|
|
224
237
|
// load (tiny boot perf + lets tests run without the module).
|
|
225
238
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
226
239
|
const command = recordCommandFor(provider);
|
|
227
|
-
const
|
|
240
|
+
const events = adapter.hookEvents ?? ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'];
|
|
241
|
+
const merged = mergeHooksForEvents(removeHooksForEvents(existing, events), command, events);
|
|
228
242
|
mkdirSync(join(settingsPath, '..'), { recursive: true });
|
|
229
243
|
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
|
|
230
244
|
mkdirSync(runsDir(), { recursive: true });
|
|
@@ -275,6 +289,26 @@ export function autoModeArgsFor(agent, existingArgs) {
|
|
|
275
289
|
// Unknown agent — no opinion. Operator should pass their own flags.
|
|
276
290
|
return [];
|
|
277
291
|
}
|
|
292
|
+
export function providerArgsFor(agent, existingArgs) {
|
|
293
|
+
const provider = providerForAgent(agent);
|
|
294
|
+
if (provider !== 'ollama')
|
|
295
|
+
return existingArgs;
|
|
296
|
+
if (existingArgs[0] === 'run' || existingArgs[0] === 'serve' || existingArgs[0] === 'pull')
|
|
297
|
+
return existingArgs;
|
|
298
|
+
const modelFlag = existingArgs.findIndex((arg) => arg === '--model');
|
|
299
|
+
if (modelFlag >= 0 && existingArgs[modelFlag + 1]) {
|
|
300
|
+
return ['run', existingArgs[modelFlag + 1], ...existingArgs.filter((_, index) => index !== modelFlag && index !== modelFlag + 1)];
|
|
301
|
+
}
|
|
302
|
+
const modelEq = existingArgs.find((arg) => arg.startsWith('--model='));
|
|
303
|
+
if (modelEq) {
|
|
304
|
+
const model = modelEq.slice('--model='.length);
|
|
305
|
+
return ['run', model, ...existingArgs.filter((arg) => arg !== modelEq)];
|
|
306
|
+
}
|
|
307
|
+
const model = process.env.SICKR_OLLAMA_MODEL;
|
|
308
|
+
if (model)
|
|
309
|
+
return ['run', model, ...existingArgs];
|
|
310
|
+
return existingArgs;
|
|
311
|
+
}
|
|
278
312
|
export async function startRun(opts) {
|
|
279
313
|
const creds = readCredentials();
|
|
280
314
|
if (!creds) {
|
|
@@ -306,7 +340,7 @@ export async function startRun(opts) {
|
|
|
306
340
|
// operator can't see. --mode interactive disables the injection.
|
|
307
341
|
const mode = opts.mode ?? 'auto';
|
|
308
342
|
const injected = mode === 'auto' ? autoModeArgsFor(opts.agent, opts.agentArgs) : [];
|
|
309
|
-
const effectiveArgs = [...injected, ...opts.agentArgs];
|
|
343
|
+
const effectiveArgs = providerArgsFor(opts.agent, [...injected, ...opts.agentArgs]);
|
|
310
344
|
process.stdout.write(`sickr: wrapping ${agentBin} in PTY. Watch + steer at: https://sickr.ai/r/${urlid}\n`);
|
|
311
345
|
if (mode === 'auto' && injected.length > 0) {
|
|
312
346
|
process.stdout.write(` mode=auto — injected ${injected.join(' ')} (use --mode interactive to disable)\n`);
|
|
@@ -509,13 +543,9 @@ function pumpNewLines(ws, offsets, identity) {
|
|
|
509
543
|
for (const line of result.lines) {
|
|
510
544
|
for (const fragment of splitJsonObjects(line)) {
|
|
511
545
|
try {
|
|
512
|
-
const event = JSON.parse(fragment);
|
|
513
|
-
if (event
|
|
514
|
-
continue;
|
|
515
|
-
if (!event.runner && event.agent && normTarget(event.agent) !== normTarget(identity.agent))
|
|
546
|
+
const event = normalizeRunEventForRunner(JSON.parse(fragment), identity);
|
|
547
|
+
if (!event)
|
|
516
548
|
continue;
|
|
517
|
-
if (event.session)
|
|
518
|
-
identity.sessions.add(event.session);
|
|
519
549
|
ws.send(JSON.stringify({ kind: 'event', event }));
|
|
520
550
|
}
|
|
521
551
|
catch { /* skip malformed */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sickr/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "npx @sickr/cli - replay, live look, and workflow orchestration for AI coding agents.",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsc",
|
|
17
|
+
"build": "node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc",
|
|
18
18
|
"test": "vitest run",
|
|
19
19
|
"dev": "tsc -w",
|
|
20
20
|
"prepublishOnly": "npm run build && npm test && node scripts/pre-publish-check.mjs"
|