@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,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