@sickr/cli 0.9.10 → 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 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 { mergeHooks, removeHooks } from './hookConfig.js';
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('--cursor'))
73
- return 'cursor';
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 cursor-agent
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 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.
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 merged = mergeHooks(removeHooks(settings), command);
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 uses \`/hooks\`),\nthen use the agent as normal and: npx @sickr/cli replay open\n`
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 === 'cursor' || agent === 'local') {
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', '--cursor', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
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', '--cursor', '--local'].includes(a)) ? providerFromFlags(replayRest) : undefined;
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 === 'cursor' || agent === 'local') {
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', '--cursor', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
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', '--cursor', '--local'].includes(a)) ? providerFromFlags(rest) : undefined;
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 handlePrime(['start', ...rest]);
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 agent = positional[0];
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...]` (agent: claude | codex | <bin name>)\n');
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);
@@ -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 EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'];
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 mergeHooks(settings, recordCommand) {
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 EVENTS) {
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 removeHooks(settings) {
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 EVENTS) {
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
- notes: 'Gemini CLI has a hooks surface; SICKR keeps it behind an adapter so trust behavior can be handled per release.',
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
- cursor: {
42
- provider: 'cursor',
43
- displayName: 'Cursor CLI',
44
- recordLabel: 'Cursor',
45
- defaultCommand: 'cursor-agent',
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: true,
50
- notes: 'Cursor is best captured through headless stream-json for workflow jobs; interactive control uses the PTY wrapper.',
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: 'sickr-local-agent',
59
+ defaultCommand: 'ollama',
57
60
  supportsHooks: false,
58
61
  requiresManualHookTrust: false,
59
62
  supportsPtyControl: true,
60
63
  supportsStructuredStream: true,
61
- notes: 'Local models should run through a SICKR-owned harness against Ollama, vLLM, llama.cpp, or an OpenAI-compatible endpoint.',
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 === 'cursor' || base === 'cursor-agent')
74
- return 'cursor';
75
- if (base === 'ollama' || base === 'vllm' || base === 'llama.cpp' || base === 'llama-server' || base === 'sickr-local-agent')
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 'PostToolUse': {
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 { mergeHooks, removeHooks } from './hookConfig.js';
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:
@@ -237,7 +237,8 @@ export function ensureRecordingHooks(agent) {
237
237
  // load (tiny boot perf + lets tests run without the module).
238
238
  // eslint-disable-next-line @typescript-eslint/no-require-imports
239
239
  const command = recordCommandFor(provider);
240
- const merged = mergeHooks(removeHooks(existing), command);
240
+ const events = adapter.hookEvents ?? ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'];
241
+ const merged = mergeHooksForEvents(removeHooksForEvents(existing, events), command, events);
241
242
  mkdirSync(join(settingsPath, '..'), { recursive: true });
242
243
  writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
243
244
  mkdirSync(runsDir(), { recursive: true });
@@ -288,6 +289,26 @@ export function autoModeArgsFor(agent, existingArgs) {
288
289
  // Unknown agent — no opinion. Operator should pass their own flags.
289
290
  return [];
290
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
+ }
291
312
  export async function startRun(opts) {
292
313
  const creds = readCredentials();
293
314
  if (!creds) {
@@ -319,7 +340,7 @@ export async function startRun(opts) {
319
340
  // operator can't see. --mode interactive disables the injection.
320
341
  const mode = opts.mode ?? 'auto';
321
342
  const injected = mode === 'auto' ? autoModeArgsFor(opts.agent, opts.agentArgs) : [];
322
- const effectiveArgs = [...injected, ...opts.agentArgs];
343
+ const effectiveArgs = providerArgsFor(opts.agent, [...injected, ...opts.agentArgs]);
323
344
  process.stdout.write(`sickr: wrapping ${agentBin} in PTY. Watch + steer at: https://sickr.ai/r/${urlid}\n`);
324
345
  if (mode === 'auto' && injected.length > 0) {
325
346
  process.stdout.write(` mode=auto — injected ${injected.join(' ')} (use --mode interactive to disable)\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/cli",
3
- "version": "0.9.10",
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": {