@lawrence369/loop-cli 0.1.0
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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/agent/activity.d.ts +64 -0
- package/dist/agent/activity.js +265 -0
- package/dist/agent/launcher.d.ts +42 -0
- package/dist/agent/launcher.js +243 -0
- package/dist/agent/pty-session.d.ts +113 -0
- package/dist/agent/pty-session.js +490 -0
- package/dist/agent/ready-detector.d.ts +46 -0
- package/dist/agent/ready-detector.js +86 -0
- package/dist/agent/wrapper.d.ts +18 -0
- package/dist/agent/wrapper.js +110 -0
- package/dist/bin/lclaude.d.ts +3 -0
- package/dist/bin/lclaude.js +7 -0
- package/dist/bin/lcodex.d.ts +3 -0
- package/dist/bin/lcodex.js +7 -0
- package/dist/bin/lgemini.d.ts +3 -0
- package/dist/bin/lgemini.js +7 -0
- package/dist/bus/daemon.d.ts +56 -0
- package/dist/bus/daemon.js +135 -0
- package/dist/bus/event-bus.d.ts +105 -0
- package/dist/bus/event-bus.js +157 -0
- package/dist/bus/message.d.ts +48 -0
- package/dist/bus/message.js +129 -0
- package/dist/bus/queue.d.ts +50 -0
- package/dist/bus/queue.js +100 -0
- package/dist/bus/store.d.ts +88 -0
- package/dist/bus/store.js +212 -0
- package/dist/bus/subscriber.d.ts +76 -0
- package/dist/bus/subscriber.js +187 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +72 -0
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.js +58 -0
- package/dist/core/conversation.d.ts +34 -0
- package/dist/core/conversation.js +289 -0
- package/dist/core/engine.d.ts +40 -0
- package/dist/core/engine.js +288 -0
- package/dist/core/loop.d.ts +33 -0
- package/dist/core/loop.js +209 -0
- package/dist/core/protocol.d.ts +60 -0
- package/dist/core/protocol.js +162 -0
- package/dist/core/scoring.d.ts +34 -0
- package/dist/core/scoring.js +69 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +408 -0
- package/dist/orchestrator/daemon.d.ts +74 -0
- package/dist/orchestrator/daemon.js +294 -0
- package/dist/orchestrator/group.d.ts +73 -0
- package/dist/orchestrator/group.js +166 -0
- package/dist/orchestrator/ipc-server.d.ts +60 -0
- package/dist/orchestrator/ipc-server.js +166 -0
- package/dist/orchestrator/scheduler.d.ts +32 -0
- package/dist/orchestrator/scheduler.js +95 -0
- package/dist/plan/context.d.ts +8 -0
- package/dist/plan/context.js +42 -0
- package/dist/plan/decisions.d.ts +18 -0
- package/dist/plan/decisions.js +143 -0
- package/dist/plan/shared-plan.d.ts +33 -0
- package/dist/plan/shared-plan.js +211 -0
- package/dist/skills/executor.d.ts +7 -0
- package/dist/skills/executor.js +11 -0
- package/dist/skills/loader.d.ts +16 -0
- package/dist/skills/loader.js +80 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +54 -0
- package/dist/terminal/adapter.d.ts +61 -0
- package/dist/terminal/adapter.js +42 -0
- package/dist/terminal/detect.d.ts +30 -0
- package/dist/terminal/detect.js +77 -0
- package/dist/terminal/iterm2-adapter.d.ts +19 -0
- package/dist/terminal/iterm2-adapter.js +120 -0
- package/dist/terminal/pty-adapter.d.ts +18 -0
- package/dist/terminal/pty-adapter.js +84 -0
- package/dist/terminal/terminal-adapter.d.ts +17 -0
- package/dist/terminal/terminal-adapter.js +94 -0
- package/dist/terminal/tmux-adapter.d.ts +18 -0
- package/dist/terminal/tmux-adapter.js +127 -0
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.js +145 -0
- package/dist/ui/colors.d.ts +41 -0
- package/dist/ui/colors.js +65 -0
- package/dist/ui/dashboard.d.ts +32 -0
- package/dist/ui/dashboard.js +138 -0
- package/dist/ui/input.d.ts +10 -0
- package/dist/ui/input.js +96 -0
- package/dist/ui/interactive.d.ts +13 -0
- package/dist/ui/interactive.js +230 -0
- package/dist/ui/renderer.d.ts +33 -0
- package/dist/ui/renderer.js +106 -0
- package/dist/utils/ansi.d.ts +11 -0
- package/dist/utils/ansi.js +16 -0
- package/dist/utils/fs.d.ts +34 -0
- package/dist/utils/fs.js +115 -0
- package/dist/utils/lock.d.ts +12 -0
- package/dist/utils/lock.js +116 -0
- package/dist/utils/process.d.ts +31 -0
- package/dist/utils/process.js +111 -0
- package/dist/utils/pty-filter.d.ts +31 -0
- package/dist/utils/pty-filter.js +187 -0
- package/package.json +71 -0
- package/skills/loop/SKILL.md +19 -0
- package/skills/plan/SKILL.md +9 -0
- package/skills/review/SKILL.md +14 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PtySession — merged PTY session manager.
|
|
3
|
+
*
|
|
4
|
+
* Combines iterloop's EventEmitter-based PtySession (content/status/idle
|
|
5
|
+
* classification, ring-buffer prompt detection, transcript management) with
|
|
6
|
+
* ufoo's PtyWrapper features (JSONL I/O logging, inject socket, monitoring).
|
|
7
|
+
*
|
|
8
|
+
* Events emitted:
|
|
9
|
+
* "pty-data" (data: string) — raw PTY output
|
|
10
|
+
* "content" (line: string) — meaningful text after classification
|
|
11
|
+
* "status" (text: string) — status update (thinking, etc.)
|
|
12
|
+
* "idle" () — prompt detected, CLI ready for input
|
|
13
|
+
* "exit" (code: number) — PTY process exited
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from "node:events";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as net from "node:net";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import pty from "node-pty";
|
|
20
|
+
import { stripAnsi } from "../utils/ansi.js";
|
|
21
|
+
import { classifyLine } from "../utils/pty-filter.js";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Ring-buffer size for prompt detection (last N chars of clean output). */
|
|
26
|
+
const RING_BUFFER_SIZE = 256;
|
|
27
|
+
/** Maximum transcript size — roughly 50 KB. */
|
|
28
|
+
const MAX_TRANSCRIPT_BYTES = 50 * 1024;
|
|
29
|
+
/** Default prompt pattern shared by all supported engines. */
|
|
30
|
+
const DEFAULT_PROMPT_PATTERN = /(?:^|\n)[>❯]\s*$/;
|
|
31
|
+
// Inject-socket message types (mirrors ufoo's ptySocketContract)
|
|
32
|
+
const SOCKET_MSG = {
|
|
33
|
+
OUTPUT: "output",
|
|
34
|
+
REPLAY: "replay",
|
|
35
|
+
SUBSCRIBED: "subscribed",
|
|
36
|
+
SUBSCRIBE: "subscribe",
|
|
37
|
+
RAW: "raw",
|
|
38
|
+
RESIZE: "resize",
|
|
39
|
+
};
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// PtySession
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export class PtySession extends EventEmitter {
|
|
44
|
+
// -- PTY process ----------------------------------------------------------
|
|
45
|
+
_pty;
|
|
46
|
+
_alive = true;
|
|
47
|
+
_lastExitCode = 0;
|
|
48
|
+
// -- Output classification ------------------------------------------------
|
|
49
|
+
_engine;
|
|
50
|
+
_promptPattern;
|
|
51
|
+
// Ring buffer for prompt detection (last RING_BUFFER_SIZE chars of clean text)
|
|
52
|
+
_ringBuffer = "";
|
|
53
|
+
// Line-level parsing state
|
|
54
|
+
_currentLine = "";
|
|
55
|
+
_lastEmittedStatus = "";
|
|
56
|
+
_lastEmittedContent = "";
|
|
57
|
+
// Transcript (accumulated meaningful content lines)
|
|
58
|
+
_contentLines = [];
|
|
59
|
+
_contentBytes = 0;
|
|
60
|
+
// -- JSONL logger (optional, enabled via enableLogging) -------------------
|
|
61
|
+
_logger = null;
|
|
62
|
+
_loggerBroken = false;
|
|
63
|
+
// -- Inject socket (optional, enabled via enableInjectSocket) -------------
|
|
64
|
+
_injectServer = null;
|
|
65
|
+
_injectSocketPath = null;
|
|
66
|
+
_outputSubscribers = new Set();
|
|
67
|
+
_outputRingBuffer = "";
|
|
68
|
+
OUTPUT_RING_MAX = 256 * 1024; // 256 KB
|
|
69
|
+
// ========================================================================
|
|
70
|
+
// Constructor
|
|
71
|
+
// ========================================================================
|
|
72
|
+
constructor(command, args, opts) {
|
|
73
|
+
super();
|
|
74
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
75
|
+
const cols = opts?.cols ?? process.stdout.columns ?? 80;
|
|
76
|
+
const rows = opts?.rows ?? process.stdout.rows ?? 24;
|
|
77
|
+
this._engine = opts?.engine;
|
|
78
|
+
this._promptPattern = DEFAULT_PROMPT_PATTERN;
|
|
79
|
+
this._pty = pty.spawn(command, args, {
|
|
80
|
+
name: "xterm-256color",
|
|
81
|
+
cols,
|
|
82
|
+
rows,
|
|
83
|
+
cwd,
|
|
84
|
+
env: { ...process.env, ...(opts?.env ?? {}) },
|
|
85
|
+
});
|
|
86
|
+
// ── PTY data handler ──────────────────────────────────────────────
|
|
87
|
+
this._pty.onData((data) => {
|
|
88
|
+
if (!this._alive)
|
|
89
|
+
return;
|
|
90
|
+
const cleaned = stripAnsi(data);
|
|
91
|
+
// Update ring buffer for prompt detection
|
|
92
|
+
this._ringBuffer += cleaned;
|
|
93
|
+
if (this._ringBuffer.length > RING_BUFFER_SIZE) {
|
|
94
|
+
this._ringBuffer = this._ringBuffer.slice(-RING_BUFFER_SIZE);
|
|
95
|
+
}
|
|
96
|
+
// Emit raw PTY data
|
|
97
|
+
this.emit("pty-data", data);
|
|
98
|
+
// Forward to output subscribers (inject socket)
|
|
99
|
+
this._forwardToSubscribers(data);
|
|
100
|
+
// Log output
|
|
101
|
+
this._logEntry("out", data);
|
|
102
|
+
// Classify lines
|
|
103
|
+
this._processCleanChunk(cleaned);
|
|
104
|
+
// Prompt detection → emit idle
|
|
105
|
+
if (this._promptPattern.test(this._ringBuffer)) {
|
|
106
|
+
this.emit("idle");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// ── PTY exit handler ──────────────────────────────────────────────
|
|
110
|
+
this._pty.onExit(({ exitCode }) => {
|
|
111
|
+
this._alive = false;
|
|
112
|
+
this._lastExitCode = exitCode;
|
|
113
|
+
this.emit("exit", exitCode);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// ========================================================================
|
|
117
|
+
// Core operations
|
|
118
|
+
// ========================================================================
|
|
119
|
+
write(data) {
|
|
120
|
+
if (!this._alive)
|
|
121
|
+
return;
|
|
122
|
+
try {
|
|
123
|
+
this._pty.write(data);
|
|
124
|
+
this._logEntry("in", data, "terminal");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Process may have exited between alive-check and write
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
resize(cols, rows) {
|
|
131
|
+
if (!this._alive)
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
this._pty.resize(cols, rows);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Process may have exited
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
kill() {
|
|
141
|
+
if (!this._alive)
|
|
142
|
+
return;
|
|
143
|
+
this._alive = false;
|
|
144
|
+
try {
|
|
145
|
+
this._pty.kill();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Already dead
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Full cleanup — kills the process, closes logger, tears down inject socket.
|
|
153
|
+
* Safe to call multiple times.
|
|
154
|
+
*/
|
|
155
|
+
destroy() {
|
|
156
|
+
this.kill();
|
|
157
|
+
this._closeLogger();
|
|
158
|
+
this._closeInjectSocket();
|
|
159
|
+
this.removeAllListeners();
|
|
160
|
+
}
|
|
161
|
+
// ========================================================================
|
|
162
|
+
// State accessors
|
|
163
|
+
// ========================================================================
|
|
164
|
+
get pid() {
|
|
165
|
+
return this._pty.pid;
|
|
166
|
+
}
|
|
167
|
+
get isAlive() {
|
|
168
|
+
return this._alive;
|
|
169
|
+
}
|
|
170
|
+
/** The exit code of the PTY process (0 if still alive). */
|
|
171
|
+
get exitCode() {
|
|
172
|
+
return this._lastExitCode;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Write data followed by a carriage return (convenience for sending commands).
|
|
176
|
+
*/
|
|
177
|
+
sendLine(line) {
|
|
178
|
+
this.write(line + "\r");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Alias for getTranscript() — returns filtered meaningful content.
|
|
182
|
+
*/
|
|
183
|
+
getCleanOutput() {
|
|
184
|
+
return this.getTranscript();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Return accumulated meaningful content, capped at ~50 KB.
|
|
188
|
+
*/
|
|
189
|
+
getTranscript() {
|
|
190
|
+
let result = this._contentLines.join("\n").trim();
|
|
191
|
+
if (Buffer.byteLength(result) > MAX_TRANSCRIPT_BYTES) {
|
|
192
|
+
const buf = Buffer.from(result);
|
|
193
|
+
result = buf.subarray(buf.length - MAX_TRANSCRIPT_BYTES).toString("utf-8");
|
|
194
|
+
const firstNewline = result.indexOf("\n");
|
|
195
|
+
if (firstNewline > 0) {
|
|
196
|
+
result = result.slice(firstNewline + 1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
// ========================================================================
|
|
202
|
+
// Optional features — JSONL logging (from ufoo PtyWrapper)
|
|
203
|
+
// ========================================================================
|
|
204
|
+
/**
|
|
205
|
+
* Enable JSONL I/O logging to the given directory.
|
|
206
|
+
* Creates a timestamped `.jsonl` log file.
|
|
207
|
+
*/
|
|
208
|
+
enableLogging(logDir) {
|
|
209
|
+
if (this._logger)
|
|
210
|
+
return;
|
|
211
|
+
try {
|
|
212
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Directory may already exist
|
|
216
|
+
}
|
|
217
|
+
const logFile = path.join(logDir, `pty-${this.pid}-${Date.now()}.jsonl`);
|
|
218
|
+
this._logger = fs.createWriteStream(logFile, { flags: "a" });
|
|
219
|
+
this._loggerBroken = false;
|
|
220
|
+
this._logger.on("error", () => {
|
|
221
|
+
this._loggerBroken = true;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// ========================================================================
|
|
225
|
+
// Optional features — inject socket (from ufoo launcher)
|
|
226
|
+
// ========================================================================
|
|
227
|
+
/**
|
|
228
|
+
* Start a Unix-domain socket server at `socketPath` that accepts
|
|
229
|
+
* JSON-line commands: inject, raw write, resize, subscribe.
|
|
230
|
+
*/
|
|
231
|
+
enableInjectSocket(socketPath) {
|
|
232
|
+
if (this._injectServer)
|
|
233
|
+
return;
|
|
234
|
+
this._injectSocketPath = socketPath;
|
|
235
|
+
// Ensure parent directory exists
|
|
236
|
+
const dir = path.dirname(socketPath);
|
|
237
|
+
try {
|
|
238
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// May already exist
|
|
242
|
+
}
|
|
243
|
+
// Remove stale socket file
|
|
244
|
+
try {
|
|
245
|
+
if (fs.existsSync(socketPath))
|
|
246
|
+
fs.unlinkSync(socketPath);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Ignore
|
|
250
|
+
}
|
|
251
|
+
this._injectServer = net.createServer((client) => {
|
|
252
|
+
let buffer = "";
|
|
253
|
+
client.on("data", (chunk) => {
|
|
254
|
+
buffer += chunk.toString("utf8");
|
|
255
|
+
const lines = buffer.split("\n");
|
|
256
|
+
buffer = lines.pop() ?? "";
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
if (!line.trim())
|
|
259
|
+
continue;
|
|
260
|
+
try {
|
|
261
|
+
const req = JSON.parse(line);
|
|
262
|
+
this._handleInjectRequest(req, client);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const msg = err instanceof Error ? err.message : "parse error";
|
|
266
|
+
client.write(JSON.stringify({ ok: false, error: msg }) + "\n");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
client.on("error", () => {
|
|
271
|
+
this._outputSubscribers.delete(client);
|
|
272
|
+
});
|
|
273
|
+
client.on("close", () => {
|
|
274
|
+
this._outputSubscribers.delete(client);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
this._injectServer.listen(socketPath);
|
|
278
|
+
this._injectServer.on("error", () => {
|
|
279
|
+
// Non-fatal — inject socket is optional
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// ========================================================================
|
|
283
|
+
// Private — output classification (from iterloop PtySession)
|
|
284
|
+
// ========================================================================
|
|
285
|
+
/**
|
|
286
|
+
* Process a chunk of ANSI-stripped text, splitting into lines and
|
|
287
|
+
* classifying each as content / status / ignore.
|
|
288
|
+
*/
|
|
289
|
+
_processCleanChunk(cleaned) {
|
|
290
|
+
for (const ch of cleaned) {
|
|
291
|
+
if (ch === "\n" || ch === "\r") {
|
|
292
|
+
if (this._currentLine.trim()) {
|
|
293
|
+
const kind = classifyLine(this._currentLine, this._engine);
|
|
294
|
+
if (kind === "content") {
|
|
295
|
+
const contentText = this._cleanContentLine(this._currentLine);
|
|
296
|
+
if (contentText) {
|
|
297
|
+
// Deduplicate (TUI re-renders can duplicate content)
|
|
298
|
+
const norm = contentText.replace(/\s+/g, "");
|
|
299
|
+
const lastNorm = this._lastEmittedContent.replace(/\s+/g, "");
|
|
300
|
+
if (norm !== lastNorm) {
|
|
301
|
+
this._lastEmittedContent = contentText;
|
|
302
|
+
this._contentLines.push(contentText);
|
|
303
|
+
this._contentBytes += Buffer.byteLength(contentText);
|
|
304
|
+
while (this._contentBytes > MAX_TRANSCRIPT_BYTES && this._contentLines.length > 1) {
|
|
305
|
+
const dropped = this._contentLines.shift();
|
|
306
|
+
this._contentBytes -= Buffer.byteLength(dropped);
|
|
307
|
+
}
|
|
308
|
+
this.emit("content", contentText);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (kind === "status") {
|
|
313
|
+
const statusText = this._extractStatusText(this._currentLine);
|
|
314
|
+
if (statusText && statusText !== this._lastEmittedStatus) {
|
|
315
|
+
this._lastEmittedStatus = statusText;
|
|
316
|
+
this.emit("status", statusText);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// "ignore" → silently discard
|
|
320
|
+
}
|
|
321
|
+
this._currentLine = "";
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
this._currentLine += ch;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Strip the ⏺ content marker prefix used by Claude CLI. */
|
|
329
|
+
_cleanContentLine(line) {
|
|
330
|
+
let text = line.trim();
|
|
331
|
+
if (text.startsWith("⏺")) {
|
|
332
|
+
text = text.slice(1).trimStart();
|
|
333
|
+
}
|
|
334
|
+
return text;
|
|
335
|
+
}
|
|
336
|
+
/** Strip spinner / status prefixes to extract the status message. */
|
|
337
|
+
_extractStatusText(line) {
|
|
338
|
+
return line
|
|
339
|
+
.trim()
|
|
340
|
+
.replace(/^[\s✳✶✻✽✢·⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏●]+\s*/, "")
|
|
341
|
+
.trim();
|
|
342
|
+
}
|
|
343
|
+
// ========================================================================
|
|
344
|
+
// Private — JSONL logging
|
|
345
|
+
// ========================================================================
|
|
346
|
+
_logEntry(dir, data, source) {
|
|
347
|
+
if (!this._logger || this._loggerBroken)
|
|
348
|
+
return;
|
|
349
|
+
const entry = {
|
|
350
|
+
ts: Date.now(),
|
|
351
|
+
dir,
|
|
352
|
+
data: { text: data, encoding: "utf8", size: data.length },
|
|
353
|
+
};
|
|
354
|
+
if (source)
|
|
355
|
+
entry.source = source;
|
|
356
|
+
try {
|
|
357
|
+
this._logger.write(JSON.stringify(entry) + "\n");
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
this._loggerBroken = true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
_closeLogger() {
|
|
364
|
+
if (!this._logger)
|
|
365
|
+
return;
|
|
366
|
+
try {
|
|
367
|
+
this._logger.end();
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Ignore cleanup errors
|
|
371
|
+
}
|
|
372
|
+
this._logger = null;
|
|
373
|
+
this._loggerBroken = false;
|
|
374
|
+
}
|
|
375
|
+
// ========================================================================
|
|
376
|
+
// Private — inject socket helpers
|
|
377
|
+
// ========================================================================
|
|
378
|
+
_handleInjectRequest(req, client) {
|
|
379
|
+
const type = req.type;
|
|
380
|
+
if (type === "inject" && typeof req.command === "string") {
|
|
381
|
+
this.write(req.command);
|
|
382
|
+
// Send CR after a short delay to allow TUI to process
|
|
383
|
+
setTimeout(() => {
|
|
384
|
+
this.write("\r");
|
|
385
|
+
}, 200);
|
|
386
|
+
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
387
|
+
this._logEntry("in", req.command, "inject");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (type === SOCKET_MSG.RAW && typeof req.data === "string") {
|
|
391
|
+
this.write(req.data);
|
|
392
|
+
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (type === SOCKET_MSG.RESIZE &&
|
|
396
|
+
typeof req.cols === "number" &&
|
|
397
|
+
typeof req.rows === "number") {
|
|
398
|
+
this.resize(req.cols, req.rows);
|
|
399
|
+
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (type === SOCKET_MSG.SUBSCRIBE) {
|
|
403
|
+
this._outputSubscribers.add(client);
|
|
404
|
+
client.write(JSON.stringify({ type: SOCKET_MSG.SUBSCRIBED, ok: true }) + "\n");
|
|
405
|
+
// Replay buffered output
|
|
406
|
+
if (this._outputRingBuffer.length > 0) {
|
|
407
|
+
client.write(JSON.stringify({
|
|
408
|
+
type: SOCKET_MSG.REPLAY,
|
|
409
|
+
data: this._outputRingBuffer,
|
|
410
|
+
encoding: "utf8",
|
|
411
|
+
}) + "\n");
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
client.write(JSON.stringify({ ok: false, error: "unknown request type" }) + "\n");
|
|
416
|
+
}
|
|
417
|
+
_forwardToSubscribers(data) {
|
|
418
|
+
// Accumulate in ring buffer
|
|
419
|
+
this._outputRingBuffer += data;
|
|
420
|
+
if (this._outputRingBuffer.length > this.OUTPUT_RING_MAX) {
|
|
421
|
+
this._outputRingBuffer = this._outputRingBuffer.slice(-this.OUTPUT_RING_MAX);
|
|
422
|
+
}
|
|
423
|
+
if (this._outputSubscribers.size === 0)
|
|
424
|
+
return;
|
|
425
|
+
const msg = JSON.stringify({
|
|
426
|
+
type: SOCKET_MSG.OUTPUT,
|
|
427
|
+
data,
|
|
428
|
+
encoding: "utf8",
|
|
429
|
+
}) + "\n";
|
|
430
|
+
for (const sub of this._outputSubscribers) {
|
|
431
|
+
try {
|
|
432
|
+
sub.write(msg);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
this._outputSubscribers.delete(sub);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
_closeInjectSocket() {
|
|
440
|
+
// Destroy all subscriber connections
|
|
441
|
+
for (const sub of this._outputSubscribers) {
|
|
442
|
+
try {
|
|
443
|
+
sub.destroy();
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Ignore
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
this._outputSubscribers.clear();
|
|
450
|
+
if (this._injectServer) {
|
|
451
|
+
try {
|
|
452
|
+
this._injectServer.close();
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// Ignore
|
|
456
|
+
}
|
|
457
|
+
this._injectServer = null;
|
|
458
|
+
}
|
|
459
|
+
if (this._injectSocketPath) {
|
|
460
|
+
try {
|
|
461
|
+
if (fs.existsSync(this._injectSocketPath)) {
|
|
462
|
+
fs.unlinkSync(this._injectSocketPath);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// Ignore
|
|
467
|
+
}
|
|
468
|
+
this._injectSocketPath = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
export function createPtySession(commandOrOpts, args, opts) {
|
|
473
|
+
if (typeof commandOrOpts === "string") {
|
|
474
|
+
return new PtySession(commandOrOpts, args ?? [], opts);
|
|
475
|
+
}
|
|
476
|
+
const o = commandOrOpts;
|
|
477
|
+
const session = new PtySession(o.cmd, o.args ?? [], {
|
|
478
|
+
cwd: o.cwd,
|
|
479
|
+
env: o.env,
|
|
480
|
+
cols: o.cols,
|
|
481
|
+
rows: o.rows,
|
|
482
|
+
engine: o.engineName,
|
|
483
|
+
});
|
|
484
|
+
if (o.onData)
|
|
485
|
+
session.on("pty-data", o.onData);
|
|
486
|
+
if (o.onExit)
|
|
487
|
+
session.on("exit", o.onExit);
|
|
488
|
+
return session;
|
|
489
|
+
}
|
|
490
|
+
//# sourceMappingURL=pty-session.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReadyDetector — detects when an agent CLI has finished initializing
|
|
3
|
+
* and is ready to accept input.
|
|
4
|
+
*
|
|
5
|
+
* Listens to PtySession "idle" events. The first idle event after
|
|
6
|
+
* construction is interpreted as "the agent prompt is visible".
|
|
7
|
+
*
|
|
8
|
+
* Also supports a force-ready fallback so callers can guarantee
|
|
9
|
+
* the ready callback fires even if prompt detection fails.
|
|
10
|
+
*
|
|
11
|
+
* Ported from ufoo's readyDetector.js, adapted to the PtySession
|
|
12
|
+
* EventEmitter interface.
|
|
13
|
+
*/
|
|
14
|
+
import type { PtySession } from "./pty-session.js";
|
|
15
|
+
export declare class ReadyDetector {
|
|
16
|
+
private _ready;
|
|
17
|
+
private _callbacks;
|
|
18
|
+
private readonly _createdAt;
|
|
19
|
+
private _readyAt;
|
|
20
|
+
private readonly _onIdle;
|
|
21
|
+
private readonly _session;
|
|
22
|
+
constructor(ptySession: PtySession);
|
|
23
|
+
/**
|
|
24
|
+
* Register a callback that fires once when the agent is ready.
|
|
25
|
+
* If already ready, the callback is invoked synchronously.
|
|
26
|
+
*/
|
|
27
|
+
onReady(callback: () => void): void;
|
|
28
|
+
/**
|
|
29
|
+
* Force the detector into the ready state.
|
|
30
|
+
* Useful as a timeout-based fallback.
|
|
31
|
+
*/
|
|
32
|
+
forceReady(): void;
|
|
33
|
+
/** Whether the ready event has already fired. */
|
|
34
|
+
get isReady(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Time in milliseconds from construction to ready detection.
|
|
37
|
+
* Returns `null` if not yet ready.
|
|
38
|
+
*/
|
|
39
|
+
get detectionTimeMs(): number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Tear down the detector and remove the PtySession listener.
|
|
42
|
+
*/
|
|
43
|
+
destroy(): void;
|
|
44
|
+
private _triggerReady;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=ready-detector.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReadyDetector — detects when an agent CLI has finished initializing
|
|
3
|
+
* and is ready to accept input.
|
|
4
|
+
*
|
|
5
|
+
* Listens to PtySession "idle" events. The first idle event after
|
|
6
|
+
* construction is interpreted as "the agent prompt is visible".
|
|
7
|
+
*
|
|
8
|
+
* Also supports a force-ready fallback so callers can guarantee
|
|
9
|
+
* the ready callback fires even if prompt detection fails.
|
|
10
|
+
*
|
|
11
|
+
* Ported from ufoo's readyDetector.js, adapted to the PtySession
|
|
12
|
+
* EventEmitter interface.
|
|
13
|
+
*/
|
|
14
|
+
export class ReadyDetector {
|
|
15
|
+
_ready = false;
|
|
16
|
+
_callbacks = [];
|
|
17
|
+
_createdAt = Date.now();
|
|
18
|
+
_readyAt = null;
|
|
19
|
+
_onIdle;
|
|
20
|
+
_session;
|
|
21
|
+
constructor(ptySession) {
|
|
22
|
+
this._session = ptySession;
|
|
23
|
+
this._onIdle = () => {
|
|
24
|
+
this._triggerReady();
|
|
25
|
+
};
|
|
26
|
+
// Listen to the first "idle" event
|
|
27
|
+
this._session.on("idle", this._onIdle);
|
|
28
|
+
}
|
|
29
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Register a callback that fires once when the agent is ready.
|
|
32
|
+
* If already ready, the callback is invoked synchronously.
|
|
33
|
+
*/
|
|
34
|
+
onReady(callback) {
|
|
35
|
+
if (this._ready) {
|
|
36
|
+
callback();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this._callbacks.push(callback);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Force the detector into the ready state.
|
|
44
|
+
* Useful as a timeout-based fallback.
|
|
45
|
+
*/
|
|
46
|
+
forceReady() {
|
|
47
|
+
this._triggerReady();
|
|
48
|
+
}
|
|
49
|
+
/** Whether the ready event has already fired. */
|
|
50
|
+
get isReady() {
|
|
51
|
+
return this._ready;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Time in milliseconds from construction to ready detection.
|
|
55
|
+
* Returns `null` if not yet ready.
|
|
56
|
+
*/
|
|
57
|
+
get detectionTimeMs() {
|
|
58
|
+
return this._readyAt !== null ? this._readyAt - this._createdAt : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Tear down the detector and remove the PtySession listener.
|
|
62
|
+
*/
|
|
63
|
+
destroy() {
|
|
64
|
+
this._session.removeListener("idle", this._onIdle);
|
|
65
|
+
this._callbacks = [];
|
|
66
|
+
}
|
|
67
|
+
// ── Private ─────────────────────────────────────────────────────────────
|
|
68
|
+
_triggerReady() {
|
|
69
|
+
if (this._ready)
|
|
70
|
+
return;
|
|
71
|
+
this._ready = true;
|
|
72
|
+
this._readyAt = Date.now();
|
|
73
|
+
// Remove the listener — we only need the first idle event
|
|
74
|
+
this._session.removeListener("idle", this._onIdle);
|
|
75
|
+
for (const cb of this._callbacks) {
|
|
76
|
+
try {
|
|
77
|
+
cb();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Ignore callback errors
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this._callbacks = [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=ready-detector.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent wrapper — entry point for lclaude / lgemini / lcodex binaries.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the engine-specific CLI command, launches it through
|
|
5
|
+
* AgentLauncher, and forwards stdin/stdout for interactive use.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Launch a wrapped agent CLI.
|
|
9
|
+
*
|
|
10
|
+
* This function does not return until the agent exits (or is killed).
|
|
11
|
+
* It sets up the full lifecycle: PtySession, detectors, signal handlers,
|
|
12
|
+
* and interactive stdin/stdout forwarding.
|
|
13
|
+
*
|
|
14
|
+
* @param engineName - One of "claude", "gemini", "codex"
|
|
15
|
+
* @param extraArgs - Additional CLI arguments appended after defaults
|
|
16
|
+
*/
|
|
17
|
+
export declare function launchWrappedAgent(engineName: string, extraArgs?: string[]): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=wrapper.d.ts.map
|