@sickr/cli 0.9.11 → 0.9.12
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 +128 -3
- package/package.json +1 -1
package/dist/run.js
CHANGED
|
@@ -137,6 +137,93 @@ 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(/\r/g, '\n')
|
|
147
|
+
.split('\n')
|
|
148
|
+
.map((line) => line.replace(/^>+\s?/, '').trimEnd())
|
|
149
|
+
.filter((line) => {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed)
|
|
152
|
+
return false;
|
|
153
|
+
if (prompt && trimmed === prompt)
|
|
154
|
+
return false;
|
|
155
|
+
if (trimmed === 'Send a message (/? for help)')
|
|
156
|
+
return false;
|
|
157
|
+
return true;
|
|
158
|
+
})
|
|
159
|
+
.join('\n')
|
|
160
|
+
.trim();
|
|
161
|
+
}
|
|
162
|
+
export class HooklessPtyEventSynth {
|
|
163
|
+
identity;
|
|
164
|
+
send;
|
|
165
|
+
responseDelayMs;
|
|
166
|
+
inputBuffer = '';
|
|
167
|
+
outputBuffer = '';
|
|
168
|
+
lastPrompt = '';
|
|
169
|
+
responseTimer = null;
|
|
170
|
+
constructor(identity, send, responseDelayMs = 700) {
|
|
171
|
+
this.identity = identity;
|
|
172
|
+
this.send = send;
|
|
173
|
+
this.responseDelayMs = responseDelayMs;
|
|
174
|
+
this.identity.sessions.add(this.sessionId());
|
|
175
|
+
}
|
|
176
|
+
sessionId() {
|
|
177
|
+
return this.identity.runner.slice(0, 12);
|
|
178
|
+
}
|
|
179
|
+
start() {
|
|
180
|
+
this.sendEvent('start', this.identity.agent, `${this.identity.agent} PTY run started`);
|
|
181
|
+
}
|
|
182
|
+
observeInput(chunk) {
|
|
183
|
+
this.inputBuffer += chunk;
|
|
184
|
+
const submitIndex = Math.max(this.inputBuffer.lastIndexOf('\r'), this.inputBuffer.lastIndexOf('\n'));
|
|
185
|
+
if (submitIndex < 0)
|
|
186
|
+
return;
|
|
187
|
+
const submitted = this.inputBuffer.slice(0, submitIndex);
|
|
188
|
+
this.inputBuffer = this.inputBuffer.slice(submitIndex + 1);
|
|
189
|
+
this.recordPrompt(submitted);
|
|
190
|
+
}
|
|
191
|
+
recordPrompt(text) {
|
|
192
|
+
const prompt = stripAnsi(text).replace(/\r/g, '\n').trim();
|
|
193
|
+
if (!prompt)
|
|
194
|
+
return;
|
|
195
|
+
this.lastPrompt = prompt;
|
|
196
|
+
this.sendEvent('prompt', 'Prompt', prompt);
|
|
197
|
+
}
|
|
198
|
+
observeOutput(chunk) {
|
|
199
|
+
this.outputBuffer += chunk;
|
|
200
|
+
if (this.responseTimer)
|
|
201
|
+
clearTimeout(this.responseTimer);
|
|
202
|
+
this.responseTimer = setTimeout(() => this.flushResponse(), this.responseDelayMs);
|
|
203
|
+
}
|
|
204
|
+
flushResponse() {
|
|
205
|
+
if (this.responseTimer) {
|
|
206
|
+
clearTimeout(this.responseTimer);
|
|
207
|
+
this.responseTimer = null;
|
|
208
|
+
}
|
|
209
|
+
const detail = cleanPtyResponse(this.outputBuffer, this.lastPrompt);
|
|
210
|
+
this.outputBuffer = '';
|
|
211
|
+
if (!detail)
|
|
212
|
+
return;
|
|
213
|
+
this.sendEvent('response', this.identity.agent, detail);
|
|
214
|
+
}
|
|
215
|
+
sendEvent(kind, label, detail) {
|
|
216
|
+
this.send({
|
|
217
|
+
kind,
|
|
218
|
+
label,
|
|
219
|
+
detail,
|
|
220
|
+
at: new Date().toISOString(),
|
|
221
|
+
agent: this.identity.agent,
|
|
222
|
+
session: this.sessionId(),
|
|
223
|
+
runner: this.identity.runner,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
140
227
|
export function decideSteer(msg, defaultMode = 'pty') {
|
|
141
228
|
const text = String(msg.text ?? '');
|
|
142
229
|
if (!text)
|
|
@@ -333,6 +420,22 @@ export async function startRun(opts) {
|
|
|
333
420
|
const agentLabel = provider ? PROVIDERS[provider].recordLabel : opts.agent;
|
|
334
421
|
const runnerId = randomUUID();
|
|
335
422
|
const runnerIdentity = { agent: agentLabel, runner: runnerId, sessions: new Set() };
|
|
423
|
+
let ws = null;
|
|
424
|
+
const pendingEvents = [];
|
|
425
|
+
const publishEvent = (event) => {
|
|
426
|
+
if (ws?.readyState === 1) {
|
|
427
|
+
try {
|
|
428
|
+
ws.send(JSON.stringify({ kind: 'event', event }));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
catch { /* queue below */ }
|
|
432
|
+
}
|
|
433
|
+
pendingEvents.push(event);
|
|
434
|
+
};
|
|
435
|
+
const hooklessSynth = provider && !PROVIDERS[provider].supportsHooks
|
|
436
|
+
? new HooklessPtyEventSynth(runnerIdentity, publishEvent)
|
|
437
|
+
: null;
|
|
438
|
+
let hooklessStarted = false;
|
|
336
439
|
const cols = process.stdout.columns ?? 80;
|
|
337
440
|
const rows = process.stdout.rows ?? 24;
|
|
338
441
|
// mode=auto (default) injects the agent's "no prompt / full perms" flag
|
|
@@ -364,8 +467,15 @@ export async function startRun(opts) {
|
|
|
364
467
|
if (process.stdin.isTTY)
|
|
365
468
|
process.stdin.setRawMode(true);
|
|
366
469
|
process.stdin.resume();
|
|
367
|
-
process.stdin.on('data', (chunk) =>
|
|
368
|
-
|
|
470
|
+
process.stdin.on('data', (chunk) => {
|
|
471
|
+
const text = chunk.toString('utf8');
|
|
472
|
+
hooklessSynth?.observeInput(text);
|
|
473
|
+
pty.write(text);
|
|
474
|
+
});
|
|
475
|
+
pty.on('data', (data) => {
|
|
476
|
+
hooklessSynth?.observeOutput(data);
|
|
477
|
+
process.stdout.write(data);
|
|
478
|
+
});
|
|
369
479
|
// Forward terminal resize. SIGWINCH fires when the terminal window changes.
|
|
370
480
|
const resize = () => pty.resize(process.stdout.columns ?? cols, process.stdout.rows ?? rows);
|
|
371
481
|
process.stdout.on('resize', resize);
|
|
@@ -385,6 +495,7 @@ export async function startRun(opts) {
|
|
|
385
495
|
process.stdin.setRawMode(false);
|
|
386
496
|
}
|
|
387
497
|
catch { /* ignore */ }
|
|
498
|
+
hooklessSynth?.flushResponse();
|
|
388
499
|
try {
|
|
389
500
|
ws?.close();
|
|
390
501
|
}
|
|
@@ -404,7 +515,6 @@ export async function startRun(opts) {
|
|
|
404
515
|
pty.on('exit', (exitCode) => cleanup(exitCode ?? 0));
|
|
405
516
|
// Open WS to live-service as a pusher. Same auth + url shape as live.ts.
|
|
406
517
|
const offsets = readOffsets();
|
|
407
|
-
let ws = null;
|
|
408
518
|
let backoff = 1000;
|
|
409
519
|
// Recursive reconnect loop. Runs forever.
|
|
410
520
|
const runWs = async () => {
|
|
@@ -434,6 +544,20 @@ export async function startRun(opts) {
|
|
|
434
544
|
ws.send(JSON.stringify({ kind: 'hello', agent: runnerIdentity.agent, runner: runnerIdentity.runner }));
|
|
435
545
|
}
|
|
436
546
|
catch { /* reconnect loop handles failures */ }
|
|
547
|
+
if (hooklessSynth && !hooklessStarted) {
|
|
548
|
+
hooklessStarted = true;
|
|
549
|
+
hooklessSynth.start();
|
|
550
|
+
}
|
|
551
|
+
while (pendingEvents.length > 0) {
|
|
552
|
+
const event = pendingEvents.shift();
|
|
553
|
+
try {
|
|
554
|
+
ws.send(JSON.stringify({ kind: 'event', event }));
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
pendingEvents.unshift(event);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
437
561
|
tailTimer = setInterval(() => pumpNewLines(ws, offsets, runnerIdentity), 500);
|
|
438
562
|
});
|
|
439
563
|
ws.addEventListener('message', (ev) => {
|
|
@@ -473,6 +597,7 @@ export async function startRun(opts) {
|
|
|
473
597
|
pty.write('\r');
|
|
474
598
|
}
|
|
475
599
|
catch { /* pty exited */ } }, 80);
|
|
600
|
+
hooklessSynth?.recordPrompt(m.text);
|
|
476
601
|
}
|
|
477
602
|
catch { /* PTY may have exited */ }
|
|
478
603
|
if (opts.verbose)
|