@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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentLauncher — unified agent lifecycle manager.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* 1. Ensuring the .loop/ project directory exists
|
|
6
|
+
* 2. Detecting (or using specified) launch mode
|
|
7
|
+
* 3. Spawning a PtySession via the appropriate terminal adapter
|
|
8
|
+
* 4. Setting up ReadyDetector + ActivityDetector
|
|
9
|
+
* 5. Registering with the daemon via IPC (fail-silently if no daemon)
|
|
10
|
+
* 6. SIGTERM / SIGINT cleanup
|
|
11
|
+
*
|
|
12
|
+
* Simplified port of ufoo's launcher.js.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as net from "node:net";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { PtySession } from "./pty-session.js";
|
|
19
|
+
import { ActivityDetector } from "./activity.js";
|
|
20
|
+
import { ReadyDetector } from "./ready-detector.js";
|
|
21
|
+
import { detectTerminal } from "../terminal/detect.js";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function loopDir(projectRoot) {
|
|
26
|
+
return path.join(projectRoot, ".loop");
|
|
27
|
+
}
|
|
28
|
+
function runDir(projectRoot) {
|
|
29
|
+
return path.join(loopDir(projectRoot), "run");
|
|
30
|
+
}
|
|
31
|
+
function daemonSocketPath(projectRoot) {
|
|
32
|
+
return path.join(runDir(projectRoot), "daemon.sock");
|
|
33
|
+
}
|
|
34
|
+
function connectSocket(sockPath) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const client = net.createConnection(sockPath, () => resolve(client));
|
|
37
|
+
client.on("error", reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function connectWithRetry(sockPath, retries, delayMs) {
|
|
41
|
+
for (let i = 0; i < retries; i += 1) {
|
|
42
|
+
try {
|
|
43
|
+
return await connectSocket(sockPath);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Best-effort daemon registration. Returns the subscriber ID from the
|
|
53
|
+
* daemon or a locally-generated one if the daemon is unreachable.
|
|
54
|
+
*/
|
|
55
|
+
async function registerWithDaemon(projectRoot, agentType, subscriberId, nickname) {
|
|
56
|
+
const sockPath = daemonSocketPath(projectRoot);
|
|
57
|
+
const client = await connectWithRetry(sockPath, 3, 200);
|
|
58
|
+
if (!client)
|
|
59
|
+
return subscriberId; // daemon not running — proceed anyway
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
let buffer = "";
|
|
62
|
+
let settled = false;
|
|
63
|
+
const timeout = setTimeout(() => {
|
|
64
|
+
if (settled)
|
|
65
|
+
return;
|
|
66
|
+
settled = true;
|
|
67
|
+
try {
|
|
68
|
+
client.destroy();
|
|
69
|
+
}
|
|
70
|
+
catch { /* ignore */ }
|
|
71
|
+
resolve(subscriberId); // fall back to local ID
|
|
72
|
+
}, 5_000);
|
|
73
|
+
const cleanup = () => {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
client.removeAllListeners();
|
|
76
|
+
try {
|
|
77
|
+
client.end();
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
80
|
+
};
|
|
81
|
+
client.on("error", () => {
|
|
82
|
+
if (settled)
|
|
83
|
+
return;
|
|
84
|
+
settled = true;
|
|
85
|
+
cleanup();
|
|
86
|
+
resolve(subscriberId);
|
|
87
|
+
});
|
|
88
|
+
client.on("data", (data) => {
|
|
89
|
+
buffer += data.toString("utf8");
|
|
90
|
+
const lines = buffer.split(/\r?\n/);
|
|
91
|
+
buffer = lines.pop() ?? "";
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
if (!line.trim())
|
|
94
|
+
continue;
|
|
95
|
+
let payload;
|
|
96
|
+
try {
|
|
97
|
+
payload = JSON.parse(line);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (payload.type === "register_ok" && typeof payload.subscriberId === "string") {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
cleanup();
|
|
107
|
+
resolve(payload.subscriberId);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (payload.type === "error") {
|
|
111
|
+
if (settled)
|
|
112
|
+
return;
|
|
113
|
+
settled = true;
|
|
114
|
+
cleanup();
|
|
115
|
+
resolve(subscriberId); // fall back
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const req = {
|
|
121
|
+
type: "register_agent",
|
|
122
|
+
agentType,
|
|
123
|
+
nickname,
|
|
124
|
+
parentPid: process.pid,
|
|
125
|
+
};
|
|
126
|
+
client.write(JSON.stringify(req) + "\n");
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// AgentLauncher
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
export class AgentLauncher {
|
|
133
|
+
_projectRoot;
|
|
134
|
+
constructor(projectRoot) {
|
|
135
|
+
this._projectRoot = projectRoot;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Launch an agent, returning a handle with the PtySession and detectors.
|
|
139
|
+
*/
|
|
140
|
+
async launch(opts) {
|
|
141
|
+
// 1. Ensure .loop/ directory structure
|
|
142
|
+
const loopRoot = loopDir(this._projectRoot);
|
|
143
|
+
const run = runDir(this._projectRoot);
|
|
144
|
+
for (const dir of [loopRoot, run]) {
|
|
145
|
+
try {
|
|
146
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// May already exist
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 2. Resolve launch mode
|
|
153
|
+
const mode = opts.launchMode ?? detectTerminal();
|
|
154
|
+
// 3. Generate subscriber ID
|
|
155
|
+
const sessionId = randomUUID().slice(0, 8);
|
|
156
|
+
const localSubscriberId = `${opts.agentType}:${sessionId}`;
|
|
157
|
+
// 4. Set environment variables for child process
|
|
158
|
+
const childEnv = {
|
|
159
|
+
...(opts.env ?? {}),
|
|
160
|
+
LOOP_SUBSCRIBER_ID: localSubscriberId,
|
|
161
|
+
LOOP_AGENT_TYPE: opts.agentType,
|
|
162
|
+
LOOP_LAUNCH_MODE: mode,
|
|
163
|
+
};
|
|
164
|
+
if (opts.nickname) {
|
|
165
|
+
childEnv.LOOP_NICKNAME = opts.nickname;
|
|
166
|
+
}
|
|
167
|
+
// 5. Create PtySession
|
|
168
|
+
const ptySession = new PtySession(opts.command, opts.args, {
|
|
169
|
+
cwd: opts.cwd,
|
|
170
|
+
env: childEnv,
|
|
171
|
+
engine: opts.agentType,
|
|
172
|
+
});
|
|
173
|
+
// 6. Enable I/O logging
|
|
174
|
+
ptySession.enableLogging(run);
|
|
175
|
+
// 7. Enable inject socket
|
|
176
|
+
const injectSocketDir = path.join(loopRoot, "sockets");
|
|
177
|
+
const sanitizedId = localSubscriberId.replace(/:/g, "_");
|
|
178
|
+
const injectSockPath = path.join(injectSocketDir, `${sanitizedId}.sock`);
|
|
179
|
+
ptySession.enableInjectSocket(injectSockPath);
|
|
180
|
+
// 8. Set up detectors
|
|
181
|
+
const readyDetector = new ReadyDetector(ptySession);
|
|
182
|
+
const activityDetector = new ActivityDetector(ptySession, opts.agentType);
|
|
183
|
+
// Force-ready fallback after 10 seconds
|
|
184
|
+
const forceReadyTimer = setTimeout(() => {
|
|
185
|
+
readyDetector.forceReady();
|
|
186
|
+
}, 10_000);
|
|
187
|
+
if (typeof forceReadyTimer.unref === "function") {
|
|
188
|
+
forceReadyTimer.unref();
|
|
189
|
+
}
|
|
190
|
+
// 9. Register with daemon (best-effort, non-blocking)
|
|
191
|
+
const subscriberId = await registerWithDaemon(this._projectRoot, opts.agentType, localSubscriberId, opts.nickname ?? "");
|
|
192
|
+
// 10. Notify daemon when ready
|
|
193
|
+
readyDetector.onReady(() => {
|
|
194
|
+
clearTimeout(forceReadyTimer);
|
|
195
|
+
const sockPath = daemonSocketPath(this._projectRoot);
|
|
196
|
+
connectWithRetry(sockPath, 2, 100).then((client) => {
|
|
197
|
+
if (!client)
|
|
198
|
+
return;
|
|
199
|
+
client.write(JSON.stringify({
|
|
200
|
+
type: "agent_ready",
|
|
201
|
+
subscriberId,
|
|
202
|
+
}) + "\n");
|
|
203
|
+
client.end();
|
|
204
|
+
}).catch(() => {
|
|
205
|
+
// Daemon notification failure is non-fatal
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// 11. Build cleanup function
|
|
209
|
+
let cleaned = false;
|
|
210
|
+
const cleanup = async () => {
|
|
211
|
+
if (cleaned)
|
|
212
|
+
return;
|
|
213
|
+
cleaned = true;
|
|
214
|
+
clearTimeout(forceReadyTimer);
|
|
215
|
+
activityDetector.destroy();
|
|
216
|
+
readyDetector.destroy();
|
|
217
|
+
ptySession.destroy();
|
|
218
|
+
};
|
|
219
|
+
// 12. Signal handlers (stored for cleanup to prevent accumulation)
|
|
220
|
+
let signalHandled = false;
|
|
221
|
+
const handleSignal = (signal) => {
|
|
222
|
+
if (signalHandled)
|
|
223
|
+
return;
|
|
224
|
+
signalHandled = true;
|
|
225
|
+
const code = signal === "SIGTERM" ? 143 : 130;
|
|
226
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
227
|
+
process.removeListener("SIGINT", onSigint);
|
|
228
|
+
cleanup().finally(() => process.exit(code));
|
|
229
|
+
};
|
|
230
|
+
const onSigterm = () => handleSignal("SIGTERM");
|
|
231
|
+
const onSigint = () => handleSignal("SIGINT");
|
|
232
|
+
process.on("SIGTERM", onSigterm);
|
|
233
|
+
process.on("SIGINT", onSigint);
|
|
234
|
+
return {
|
|
235
|
+
subscriberId,
|
|
236
|
+
ptySession,
|
|
237
|
+
activityDetector,
|
|
238
|
+
readyDetector,
|
|
239
|
+
cleanup,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=launcher.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
export interface PtySessionOptions {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
cols?: number;
|
|
20
|
+
rows?: number;
|
|
21
|
+
/** Engine name used by the output classifier (e.g. "claude", "gemini"). */
|
|
22
|
+
engine?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class PtySession extends EventEmitter {
|
|
25
|
+
private readonly _pty;
|
|
26
|
+
private _alive;
|
|
27
|
+
private _lastExitCode;
|
|
28
|
+
private readonly _engine;
|
|
29
|
+
private readonly _promptPattern;
|
|
30
|
+
private _ringBuffer;
|
|
31
|
+
private _currentLine;
|
|
32
|
+
private _lastEmittedStatus;
|
|
33
|
+
private _lastEmittedContent;
|
|
34
|
+
private _contentLines;
|
|
35
|
+
private _contentBytes;
|
|
36
|
+
private _logger;
|
|
37
|
+
private _loggerBroken;
|
|
38
|
+
private _injectServer;
|
|
39
|
+
private _injectSocketPath;
|
|
40
|
+
private _outputSubscribers;
|
|
41
|
+
private _outputRingBuffer;
|
|
42
|
+
private readonly OUTPUT_RING_MAX;
|
|
43
|
+
constructor(command: string, args: string[], opts?: PtySessionOptions);
|
|
44
|
+
write(data: string): void;
|
|
45
|
+
resize(cols: number, rows: number): void;
|
|
46
|
+
kill(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Full cleanup — kills the process, closes logger, tears down inject socket.
|
|
49
|
+
* Safe to call multiple times.
|
|
50
|
+
*/
|
|
51
|
+
destroy(): void;
|
|
52
|
+
get pid(): number;
|
|
53
|
+
get isAlive(): boolean;
|
|
54
|
+
/** The exit code of the PTY process (0 if still alive). */
|
|
55
|
+
get exitCode(): number;
|
|
56
|
+
/**
|
|
57
|
+
* Write data followed by a carriage return (convenience for sending commands).
|
|
58
|
+
*/
|
|
59
|
+
sendLine(line: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Alias for getTranscript() — returns filtered meaningful content.
|
|
62
|
+
*/
|
|
63
|
+
getCleanOutput(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Return accumulated meaningful content, capped at ~50 KB.
|
|
66
|
+
*/
|
|
67
|
+
getTranscript(): string;
|
|
68
|
+
/**
|
|
69
|
+
* Enable JSONL I/O logging to the given directory.
|
|
70
|
+
* Creates a timestamped `.jsonl` log file.
|
|
71
|
+
*/
|
|
72
|
+
enableLogging(logDir: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Start a Unix-domain socket server at `socketPath` that accepts
|
|
75
|
+
* JSON-line commands: inject, raw write, resize, subscribe.
|
|
76
|
+
*/
|
|
77
|
+
enableInjectSocket(socketPath: string): void;
|
|
78
|
+
/**
|
|
79
|
+
* Process a chunk of ANSI-stripped text, splitting into lines and
|
|
80
|
+
* classifying each as content / status / ignore.
|
|
81
|
+
*/
|
|
82
|
+
private _processCleanChunk;
|
|
83
|
+
/** Strip the ⏺ content marker prefix used by Claude CLI. */
|
|
84
|
+
private _cleanContentLine;
|
|
85
|
+
/** Strip spinner / status prefixes to extract the status message. */
|
|
86
|
+
private _extractStatusText;
|
|
87
|
+
private _logEntry;
|
|
88
|
+
private _closeLogger;
|
|
89
|
+
private _handleInjectRequest;
|
|
90
|
+
private _forwardToSubscribers;
|
|
91
|
+
private _closeInjectSocket;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Factory options — alternative object-based signature.
|
|
95
|
+
*/
|
|
96
|
+
export interface CreatePtySessionOptions {
|
|
97
|
+
cmd: string;
|
|
98
|
+
args?: string[];
|
|
99
|
+
cwd?: string;
|
|
100
|
+
env?: Record<string, string>;
|
|
101
|
+
engineName?: string;
|
|
102
|
+
cols?: number;
|
|
103
|
+
rows?: number;
|
|
104
|
+
onData?: (data: string) => void;
|
|
105
|
+
onExit?: (code: number) => void;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Factory function for creating PtySession instances.
|
|
109
|
+
* Accepts either positional args or an options object.
|
|
110
|
+
*/
|
|
111
|
+
export declare function createPtySession(opts: CreatePtySessionOptions): PtySession;
|
|
112
|
+
export declare function createPtySession(command: string, args: string[], opts?: PtySessionOptions): PtySession;
|
|
113
|
+
//# sourceMappingURL=pty-session.d.ts.map
|