@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.
- package/dist/run.js +135 -3
- 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) =>
|
|
368
|
-
|
|
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)
|