@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -0
  4. package/dist/agent/activity.d.ts +64 -0
  5. package/dist/agent/activity.js +265 -0
  6. package/dist/agent/launcher.d.ts +42 -0
  7. package/dist/agent/launcher.js +243 -0
  8. package/dist/agent/pty-session.d.ts +113 -0
  9. package/dist/agent/pty-session.js +490 -0
  10. package/dist/agent/ready-detector.d.ts +46 -0
  11. package/dist/agent/ready-detector.js +86 -0
  12. package/dist/agent/wrapper.d.ts +18 -0
  13. package/dist/agent/wrapper.js +110 -0
  14. package/dist/bin/lclaude.d.ts +3 -0
  15. package/dist/bin/lclaude.js +7 -0
  16. package/dist/bin/lcodex.d.ts +3 -0
  17. package/dist/bin/lcodex.js +7 -0
  18. package/dist/bin/lgemini.d.ts +3 -0
  19. package/dist/bin/lgemini.js +7 -0
  20. package/dist/bus/daemon.d.ts +56 -0
  21. package/dist/bus/daemon.js +135 -0
  22. package/dist/bus/event-bus.d.ts +105 -0
  23. package/dist/bus/event-bus.js +157 -0
  24. package/dist/bus/message.d.ts +48 -0
  25. package/dist/bus/message.js +129 -0
  26. package/dist/bus/queue.d.ts +50 -0
  27. package/dist/bus/queue.js +100 -0
  28. package/dist/bus/store.d.ts +88 -0
  29. package/dist/bus/store.js +212 -0
  30. package/dist/bus/subscriber.d.ts +76 -0
  31. package/dist/bus/subscriber.js +187 -0
  32. package/dist/config/index.d.ts +8 -0
  33. package/dist/config/index.js +72 -0
  34. package/dist/config/schema.d.ts +18 -0
  35. package/dist/config/schema.js +58 -0
  36. package/dist/core/conversation.d.ts +34 -0
  37. package/dist/core/conversation.js +289 -0
  38. package/dist/core/engine.d.ts +40 -0
  39. package/dist/core/engine.js +288 -0
  40. package/dist/core/loop.d.ts +33 -0
  41. package/dist/core/loop.js +209 -0
  42. package/dist/core/protocol.d.ts +60 -0
  43. package/dist/core/protocol.js +162 -0
  44. package/dist/core/scoring.d.ts +34 -0
  45. package/dist/core/scoring.js +69 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.js +408 -0
  48. package/dist/orchestrator/daemon.d.ts +74 -0
  49. package/dist/orchestrator/daemon.js +294 -0
  50. package/dist/orchestrator/group.d.ts +73 -0
  51. package/dist/orchestrator/group.js +166 -0
  52. package/dist/orchestrator/ipc-server.d.ts +60 -0
  53. package/dist/orchestrator/ipc-server.js +166 -0
  54. package/dist/orchestrator/scheduler.d.ts +32 -0
  55. package/dist/orchestrator/scheduler.js +95 -0
  56. package/dist/plan/context.d.ts +8 -0
  57. package/dist/plan/context.js +42 -0
  58. package/dist/plan/decisions.d.ts +18 -0
  59. package/dist/plan/decisions.js +143 -0
  60. package/dist/plan/shared-plan.d.ts +33 -0
  61. package/dist/plan/shared-plan.js +211 -0
  62. package/dist/skills/executor.d.ts +7 -0
  63. package/dist/skills/executor.js +11 -0
  64. package/dist/skills/loader.d.ts +16 -0
  65. package/dist/skills/loader.js +80 -0
  66. package/dist/skills/registry.d.ts +13 -0
  67. package/dist/skills/registry.js +54 -0
  68. package/dist/terminal/adapter.d.ts +61 -0
  69. package/dist/terminal/adapter.js +42 -0
  70. package/dist/terminal/detect.d.ts +30 -0
  71. package/dist/terminal/detect.js +77 -0
  72. package/dist/terminal/iterm2-adapter.d.ts +19 -0
  73. package/dist/terminal/iterm2-adapter.js +120 -0
  74. package/dist/terminal/pty-adapter.d.ts +18 -0
  75. package/dist/terminal/pty-adapter.js +84 -0
  76. package/dist/terminal/terminal-adapter.d.ts +17 -0
  77. package/dist/terminal/terminal-adapter.js +94 -0
  78. package/dist/terminal/tmux-adapter.d.ts +18 -0
  79. package/dist/terminal/tmux-adapter.js +127 -0
  80. package/dist/ui/banner.d.ts +3 -0
  81. package/dist/ui/banner.js +145 -0
  82. package/dist/ui/colors.d.ts +41 -0
  83. package/dist/ui/colors.js +65 -0
  84. package/dist/ui/dashboard.d.ts +32 -0
  85. package/dist/ui/dashboard.js +138 -0
  86. package/dist/ui/input.d.ts +10 -0
  87. package/dist/ui/input.js +96 -0
  88. package/dist/ui/interactive.d.ts +13 -0
  89. package/dist/ui/interactive.js +230 -0
  90. package/dist/ui/renderer.d.ts +33 -0
  91. package/dist/ui/renderer.js +106 -0
  92. package/dist/utils/ansi.d.ts +11 -0
  93. package/dist/utils/ansi.js +16 -0
  94. package/dist/utils/fs.d.ts +34 -0
  95. package/dist/utils/fs.js +115 -0
  96. package/dist/utils/lock.d.ts +12 -0
  97. package/dist/utils/lock.js +116 -0
  98. package/dist/utils/process.d.ts +31 -0
  99. package/dist/utils/process.js +111 -0
  100. package/dist/utils/pty-filter.d.ts +31 -0
  101. package/dist/utils/pty-filter.js +187 -0
  102. package/package.json +71 -0
  103. package/skills/loop/SKILL.md +19 -0
  104. package/skills/plan/SKILL.md +9 -0
  105. 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