@sickr/cli 0.9.11 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/run.js +135 -3
  2. package/package.json +1 -1
package/dist/run.js CHANGED
@@ -137,6 +137,100 @@ export function normalizeRunEventForRunner(event, identity) {
137
137
  identity.sessions.add(event.session);
138
138
  return event;
139
139
  }
140
+ export function stripAnsi(input) {
141
+ return input.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, '');
142
+ }
143
+ export function cleanPtyResponse(input, lastPrompt = '') {
144
+ const prompt = lastPrompt.trim();
145
+ return stripAnsi(input)
146
+ .replace(/[\u2800-\u28ff]/g, '')
147
+ .replace(/\r/g, '\n')
148
+ .split('\n')
149
+ .map((line) => line.replace(/^>+\s?/, '').trimEnd())
150
+ .filter((line) => {
151
+ const trimmed = line.trim();
152
+ if (!trimmed)
153
+ return false;
154
+ if (prompt && trimmed === prompt)
155
+ return false;
156
+ if (trimmed.includes('Send a message (/? for help)'))
157
+ return false;
158
+ return true;
159
+ })
160
+ .join('\n')
161
+ .trim();
162
+ }
163
+ export class HooklessPtyEventSynth {
164
+ identity;
165
+ send;
166
+ responseDelayMs;
167
+ inputBuffer = '';
168
+ outputBuffer = '';
169
+ lastPrompt = '';
170
+ awaitingResponse = false;
171
+ responseTimer = null;
172
+ constructor(identity, send, responseDelayMs = 700) {
173
+ this.identity = identity;
174
+ this.send = send;
175
+ this.responseDelayMs = responseDelayMs;
176
+ this.identity.sessions.add(this.sessionId());
177
+ }
178
+ sessionId() {
179
+ return this.identity.runner.slice(0, 12);
180
+ }
181
+ start() {
182
+ this.sendEvent('start', this.identity.agent, `${this.identity.agent} PTY run started`);
183
+ }
184
+ observeInput(chunk) {
185
+ this.inputBuffer += chunk;
186
+ const submitIndex = Math.max(this.inputBuffer.lastIndexOf('\r'), this.inputBuffer.lastIndexOf('\n'));
187
+ if (submitIndex < 0)
188
+ return;
189
+ const submitted = this.inputBuffer.slice(0, submitIndex);
190
+ this.inputBuffer = this.inputBuffer.slice(submitIndex + 1);
191
+ this.recordPrompt(submitted);
192
+ }
193
+ recordPrompt(text) {
194
+ const prompt = stripAnsi(text).replace(/\r/g, '\n').trim();
195
+ if (!prompt)
196
+ return;
197
+ this.lastPrompt = prompt;
198
+ this.outputBuffer = '';
199
+ this.awaitingResponse = true;
200
+ this.sendEvent('prompt', 'Prompt', prompt);
201
+ }
202
+ observeOutput(chunk) {
203
+ if (!this.awaitingResponse)
204
+ return;
205
+ this.outputBuffer += chunk;
206
+ if (this.responseTimer)
207
+ clearTimeout(this.responseTimer);
208
+ this.responseTimer = setTimeout(() => this.flushResponse(), this.responseDelayMs);
209
+ }
210
+ flushResponse() {
211
+ if (this.responseTimer) {
212
+ clearTimeout(this.responseTimer);
213
+ this.responseTimer = null;
214
+ }
215
+ const detail = cleanPtyResponse(this.outputBuffer, this.lastPrompt);
216
+ this.outputBuffer = '';
217
+ if (!detail)
218
+ return;
219
+ this.awaitingResponse = false;
220
+ this.sendEvent('response', this.identity.agent, detail);
221
+ }
222
+ sendEvent(kind, label, detail) {
223
+ this.send({
224
+ kind,
225
+ label,
226
+ detail,
227
+ at: new Date().toISOString(),
228
+ agent: this.identity.agent,
229
+ session: this.sessionId(),
230
+ runner: this.identity.runner,
231
+ });
232
+ }
233
+ }
140
234
  export function decideSteer(msg, defaultMode = 'pty') {
141
235
  const text = String(msg.text ?? '');
142
236
  if (!text)
@@ -333,6 +427,22 @@ export async function startRun(opts) {
333
427
  const agentLabel = provider ? PROVIDERS[provider].recordLabel : opts.agent;
334
428
  const runnerId = randomUUID();
335
429
  const runnerIdentity = { agent: agentLabel, runner: runnerId, sessions: new Set() };
430
+ let ws = null;
431
+ const pendingEvents = [];
432
+ const publishEvent = (event) => {
433
+ if (ws?.readyState === 1) {
434
+ try {
435
+ ws.send(JSON.stringify({ kind: 'event', event }));
436
+ return;
437
+ }
438
+ catch { /* queue below */ }
439
+ }
440
+ pendingEvents.push(event);
441
+ };
442
+ const hooklessSynth = provider && !PROVIDERS[provider].supportsHooks
443
+ ? new HooklessPtyEventSynth(runnerIdentity, publishEvent)
444
+ : null;
445
+ let hooklessStarted = false;
336
446
  const cols = process.stdout.columns ?? 80;
337
447
  const rows = process.stdout.rows ?? 24;
338
448
  // mode=auto (default) injects the agent's "no prompt / full perms" flag
@@ -364,8 +474,15 @@ export async function startRun(opts) {
364
474
  if (process.stdin.isTTY)
365
475
  process.stdin.setRawMode(true);
366
476
  process.stdin.resume();
367
- process.stdin.on('data', (chunk) => pty.write(chunk.toString('utf8')));
368
- pty.on('data', (data) => process.stdout.write(data));
477
+ process.stdin.on('data', (chunk) => {
478
+ const text = chunk.toString('utf8');
479
+ hooklessSynth?.observeInput(text);
480
+ pty.write(text);
481
+ });
482
+ pty.on('data', (data) => {
483
+ hooklessSynth?.observeOutput(data);
484
+ process.stdout.write(data);
485
+ });
369
486
  // Forward terminal resize. SIGWINCH fires when the terminal window changes.
370
487
  const resize = () => pty.resize(process.stdout.columns ?? cols, process.stdout.rows ?? rows);
371
488
  process.stdout.on('resize', resize);
@@ -385,6 +502,7 @@ export async function startRun(opts) {
385
502
  process.stdin.setRawMode(false);
386
503
  }
387
504
  catch { /* ignore */ }
505
+ hooklessSynth?.flushResponse();
388
506
  try {
389
507
  ws?.close();
390
508
  }
@@ -404,7 +522,6 @@ export async function startRun(opts) {
404
522
  pty.on('exit', (exitCode) => cleanup(exitCode ?? 0));
405
523
  // Open WS to live-service as a pusher. Same auth + url shape as live.ts.
406
524
  const offsets = readOffsets();
407
- let ws = null;
408
525
  let backoff = 1000;
409
526
  // Recursive reconnect loop. Runs forever.
410
527
  const runWs = async () => {
@@ -434,6 +551,20 @@ export async function startRun(opts) {
434
551
  ws.send(JSON.stringify({ kind: 'hello', agent: runnerIdentity.agent, runner: runnerIdentity.runner }));
435
552
  }
436
553
  catch { /* reconnect loop handles failures */ }
554
+ if (hooklessSynth && !hooklessStarted) {
555
+ hooklessStarted = true;
556
+ hooklessSynth.start();
557
+ }
558
+ while (pendingEvents.length > 0) {
559
+ const event = pendingEvents.shift();
560
+ try {
561
+ ws.send(JSON.stringify({ kind: 'event', event }));
562
+ }
563
+ catch {
564
+ pendingEvents.unshift(event);
565
+ break;
566
+ }
567
+ }
437
568
  tailTimer = setInterval(() => pumpNewLines(ws, offsets, runnerIdentity), 500);
438
569
  });
439
570
  ws.addEventListener('message', (ev) => {
@@ -473,6 +604,7 @@ export async function startRun(opts) {
473
604
  pty.write('\r');
474
605
  }
475
606
  catch { /* pty exited */ } }, 80);
607
+ hooklessSynth?.recordPrompt(m.text);
476
608
  }
477
609
  catch { /* PTY may have exited */ }
478
610
  if (opts.verbose)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sickr/cli",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
4
4
  "type": "module",
5
5
  "description": "npx @sickr/cli - replay, live look, and workflow orchestration for AI coding agents.",
6
6
  "bin": {