@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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand colors and ANSI formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Uses raw ANSI escape codes for zero external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
const ESC = "\x1b[";
|
|
7
|
+
const RESET = `${ESC}0m`;
|
|
8
|
+
// ── Brand colors (24-bit / true-color) ───────────────
|
|
9
|
+
/** Claude brand orange (#F07623) */
|
|
10
|
+
export const claude = (s) => `${ESC}38;2;240;118;35m${s}${RESET}`;
|
|
11
|
+
/** Gemini brand blue (#4285F4) */
|
|
12
|
+
export const gemini = (s) => `${ESC}38;2;66;133;244m${s}${RESET}`;
|
|
13
|
+
/** Codex brand green (#10A37F) */
|
|
14
|
+
export const codex = (s) => `${ESC}38;2;16;163;127m${s}${RESET}`;
|
|
15
|
+
/** Loop brand cyan (#00D4FF) */
|
|
16
|
+
export const loop = (s) => `${ESC}38;2;0;212;255m${s}${RESET}`;
|
|
17
|
+
// ── Semantic colors ──────────────────────────────────
|
|
18
|
+
/** Green for success messages */
|
|
19
|
+
export const success = (s) => `${ESC}32m${s}${RESET}`;
|
|
20
|
+
/** Red for error messages */
|
|
21
|
+
export const error = (s) => `${ESC}31m${s}${RESET}`;
|
|
22
|
+
/** Yellow for warnings */
|
|
23
|
+
export const warn = (s) => `${ESC}33m${s}${RESET}`;
|
|
24
|
+
/** Gray / dim text */
|
|
25
|
+
export const dim = (s) => `${ESC}2m${s}${RESET}`;
|
|
26
|
+
/** Bold text */
|
|
27
|
+
export const bold = (s) => `${ESC}1m${s}${RESET}`;
|
|
28
|
+
// ── Aliases for convenience ─────────────────────────
|
|
29
|
+
export const orange = claude;
|
|
30
|
+
export const gBlue = gemini;
|
|
31
|
+
export const gGreen = codex;
|
|
32
|
+
export const red = error;
|
|
33
|
+
export const green = success;
|
|
34
|
+
export const yellow = warn;
|
|
35
|
+
export const cyan = loop;
|
|
36
|
+
export const blue = (s) => `${ESC}34m${s}${RESET}`;
|
|
37
|
+
// ── Lookup helper ────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Return the brand-color function for an engine name.
|
|
40
|
+
* Falls back to `loop` cyan for unknown engines.
|
|
41
|
+
*/
|
|
42
|
+
export function brandColor(engine) {
|
|
43
|
+
switch (engine) {
|
|
44
|
+
case "claude":
|
|
45
|
+
return claude;
|
|
46
|
+
case "gemini":
|
|
47
|
+
return gemini;
|
|
48
|
+
case "codex":
|
|
49
|
+
return codex;
|
|
50
|
+
default:
|
|
51
|
+
return loop;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Format a byte count for human-readable display.
|
|
56
|
+
*/
|
|
57
|
+
export function formatBytes(bytes) {
|
|
58
|
+
if (bytes < 1024)
|
|
59
|
+
return `${bytes}B`;
|
|
60
|
+
const kb = bytes / 1024;
|
|
61
|
+
if (kb < 1024)
|
|
62
|
+
return `${kb.toFixed(1)}KB`;
|
|
63
|
+
return `${(kb / 1024).toFixed(1)}MB`;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=colors.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface AgentInfo {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
state: "working" | "waiting_input" | "idle" | "blocked";
|
|
5
|
+
}
|
|
6
|
+
interface Message {
|
|
7
|
+
from: string;
|
|
8
|
+
text: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Simple blessed dashboard for monitoring loop agents.
|
|
13
|
+
* Layout: agent list panel (left) + message panel (right) + status bar (bottom).
|
|
14
|
+
*/
|
|
15
|
+
export declare class Dashboard {
|
|
16
|
+
private screen;
|
|
17
|
+
private agentList;
|
|
18
|
+
private messageBox;
|
|
19
|
+
private statusBar;
|
|
20
|
+
private agents;
|
|
21
|
+
private messages;
|
|
22
|
+
start(): void;
|
|
23
|
+
stop(): void;
|
|
24
|
+
/** Update the agent list display. */
|
|
25
|
+
updateAgents(agents: AgentInfo[]): void;
|
|
26
|
+
/** Append a message to the message panel. */
|
|
27
|
+
addMessage(msg: Message): void;
|
|
28
|
+
/** Update the status bar text. */
|
|
29
|
+
setStatus(text: string): void;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=dashboard.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import blessed from "blessed";
|
|
2
|
+
/**
|
|
3
|
+
* Simple blessed dashboard for monitoring loop agents.
|
|
4
|
+
* Layout: agent list panel (left) + message panel (right) + status bar (bottom).
|
|
5
|
+
*/
|
|
6
|
+
export class Dashboard {
|
|
7
|
+
screen = null;
|
|
8
|
+
agentList = null;
|
|
9
|
+
messageBox = null;
|
|
10
|
+
statusBar = null;
|
|
11
|
+
agents = [];
|
|
12
|
+
messages = [];
|
|
13
|
+
start() {
|
|
14
|
+
this.screen = blessed.screen({
|
|
15
|
+
smartCSR: true,
|
|
16
|
+
title: "loop dashboard",
|
|
17
|
+
});
|
|
18
|
+
// Agent list panel (left, 30% width)
|
|
19
|
+
this.agentList = blessed.list({
|
|
20
|
+
parent: this.screen,
|
|
21
|
+
label: " Agents ",
|
|
22
|
+
left: 0,
|
|
23
|
+
top: 0,
|
|
24
|
+
width: "30%",
|
|
25
|
+
height: "100%-1",
|
|
26
|
+
border: { type: "line" },
|
|
27
|
+
style: {
|
|
28
|
+
border: { fg: "cyan" },
|
|
29
|
+
selected: { bg: "blue" },
|
|
30
|
+
item: { fg: "white" },
|
|
31
|
+
},
|
|
32
|
+
keys: true,
|
|
33
|
+
vi: true,
|
|
34
|
+
scrollable: true,
|
|
35
|
+
items: [],
|
|
36
|
+
});
|
|
37
|
+
// Message panel (right, 70% width)
|
|
38
|
+
this.messageBox = blessed.box({
|
|
39
|
+
parent: this.screen,
|
|
40
|
+
label: " Messages ",
|
|
41
|
+
left: "30%",
|
|
42
|
+
top: 0,
|
|
43
|
+
width: "70%",
|
|
44
|
+
height: "100%-1",
|
|
45
|
+
border: { type: "line" },
|
|
46
|
+
style: {
|
|
47
|
+
border: { fg: "cyan" },
|
|
48
|
+
label: { fg: "cyan" },
|
|
49
|
+
},
|
|
50
|
+
scrollable: true,
|
|
51
|
+
alwaysScroll: true,
|
|
52
|
+
tags: true,
|
|
53
|
+
content: "{gray-fg}No messages yet{/gray-fg}",
|
|
54
|
+
});
|
|
55
|
+
// Status bar (bottom)
|
|
56
|
+
this.statusBar = blessed.box({
|
|
57
|
+
parent: this.screen,
|
|
58
|
+
bottom: 0,
|
|
59
|
+
left: 0,
|
|
60
|
+
width: "100%",
|
|
61
|
+
height: 1,
|
|
62
|
+
style: {
|
|
63
|
+
bg: "blue",
|
|
64
|
+
fg: "white",
|
|
65
|
+
},
|
|
66
|
+
tags: true,
|
|
67
|
+
content: " loop dashboard | q: quit | tab: switch focus",
|
|
68
|
+
});
|
|
69
|
+
// Keybindings
|
|
70
|
+
this.screen.key(["q", "C-c"], () => {
|
|
71
|
+
this.stop();
|
|
72
|
+
});
|
|
73
|
+
this.screen.key(["tab"], () => {
|
|
74
|
+
if (this.agentList && this.messageBox) {
|
|
75
|
+
if (this.screen.focused === this.agentList) {
|
|
76
|
+
this.messageBox.focus();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.agentList.focus();
|
|
80
|
+
}
|
|
81
|
+
this.screen?.render();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
this.agentList.focus();
|
|
85
|
+
this.screen.render();
|
|
86
|
+
}
|
|
87
|
+
stop() {
|
|
88
|
+
if (this.screen) {
|
|
89
|
+
this.screen.destroy();
|
|
90
|
+
this.screen = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Update the agent list display. */
|
|
94
|
+
updateAgents(agents) {
|
|
95
|
+
this.agents = agents;
|
|
96
|
+
if (!this.agentList)
|
|
97
|
+
return;
|
|
98
|
+
const items = this.agents.map((a) => {
|
|
99
|
+
const marker = stateMarker(a.state);
|
|
100
|
+
return `${marker} ${a.label}`;
|
|
101
|
+
});
|
|
102
|
+
this.agentList.setItems(items);
|
|
103
|
+
this.screen?.render();
|
|
104
|
+
}
|
|
105
|
+
/** Append a message to the message panel. */
|
|
106
|
+
addMessage(msg) {
|
|
107
|
+
this.messages.push(msg);
|
|
108
|
+
if (!this.messageBox)
|
|
109
|
+
return;
|
|
110
|
+
const lines = this.messages
|
|
111
|
+
.slice(-100) // Keep last 100 messages
|
|
112
|
+
.map((m) => `{gray-fg}${m.timestamp}{/gray-fg} {cyan-fg}${m.from}{/cyan-fg}: ${m.text}`);
|
|
113
|
+
this.messageBox.setContent(lines.join("\n"));
|
|
114
|
+
this.messageBox.setScrollPerc(100);
|
|
115
|
+
this.screen?.render();
|
|
116
|
+
}
|
|
117
|
+
/** Update the status bar text. */
|
|
118
|
+
setStatus(text) {
|
|
119
|
+
if (!this.statusBar)
|
|
120
|
+
return;
|
|
121
|
+
this.statusBar.setContent(` ${text}`);
|
|
122
|
+
this.screen?.render();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function stateMarker(state) {
|
|
126
|
+
switch (state) {
|
|
127
|
+
case "working":
|
|
128
|
+
return "{green-fg}*{/green-fg}";
|
|
129
|
+
case "waiting_input":
|
|
130
|
+
return "{yellow-fg}?{/yellow-fg}";
|
|
131
|
+
case "blocked":
|
|
132
|
+
return "{red-fg}!{/red-fg}";
|
|
133
|
+
case "idle":
|
|
134
|
+
default:
|
|
135
|
+
return "{gray-fg}-{/gray-fg}";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ExecutionMode = "auto" | "manual";
|
|
2
|
+
export interface PromptResult {
|
|
3
|
+
value: string;
|
|
4
|
+
action: "submit" | "done" | "cancel";
|
|
5
|
+
}
|
|
6
|
+
export declare function promptUser(opts?: {
|
|
7
|
+
hint?: string;
|
|
8
|
+
mode?: string;
|
|
9
|
+
}): Promise<PromptResult>;
|
|
10
|
+
//# sourceMappingURL=input.d.ts.map
|
package/dist/ui/input.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { dim, cyan, bold } from "./colors.js";
|
|
2
|
+
function modeIndicator(mode) {
|
|
3
|
+
const a = mode === "auto" ? bold("\u23F5\u23F5 Auto") : dim("\u23F5\u23F5 Auto");
|
|
4
|
+
const m = mode === "manual" ? bold("\u23F5\u23F5 Manual") : dim("\u23F5\u23F5 Manual");
|
|
5
|
+
return ` ${a} ${m} ${dim("Shift+Tab \u21C4")}`;
|
|
6
|
+
}
|
|
7
|
+
export function promptUser(opts) {
|
|
8
|
+
const cols = process.stdout.columns || 80;
|
|
9
|
+
let mode = (opts?.mode === "auto" ? "auto" : "manual");
|
|
10
|
+
// Top bar
|
|
11
|
+
const tag = opts?.hint ? ` ${opts.hint} ` : " \u25AA\u25AA\u25AA ";
|
|
12
|
+
const barLen = Math.max(0, cols - tag.length - 4);
|
|
13
|
+
const leftBar = "\u2500".repeat(Math.floor(barLen * 0.85));
|
|
14
|
+
const rightBar = "\u2500".repeat(barLen - leftBar.length);
|
|
15
|
+
process.stdout.write(dim(` ${leftBar}${tag}${rightBar}`) + "\n");
|
|
16
|
+
// Pre-print: input line (empty), bottom bar, mode indicator
|
|
17
|
+
process.stdout.write("\n");
|
|
18
|
+
process.stdout.write(dim(` ${"\u2500".repeat(Math.max(0, cols - 4))}`) + "\n");
|
|
19
|
+
process.stdout.write(modeIndicator(mode));
|
|
20
|
+
// Move cursor up to input line, write prompt
|
|
21
|
+
process.stdout.write("\x1b[2A\r");
|
|
22
|
+
process.stdout.write(cyan(" \u276F "));
|
|
23
|
+
// Raw-mode input loop
|
|
24
|
+
let buffer = "";
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const isTTY = !!process.stdin.isTTY;
|
|
27
|
+
if (isTTY)
|
|
28
|
+
process.stdin.setRawMode(true);
|
|
29
|
+
process.stdin.resume();
|
|
30
|
+
function cleanup() {
|
|
31
|
+
process.stdin.removeListener("data", onData);
|
|
32
|
+
if (isTTY)
|
|
33
|
+
process.stdin.setRawMode(false);
|
|
34
|
+
process.stdin.pause();
|
|
35
|
+
}
|
|
36
|
+
function finish(action) {
|
|
37
|
+
// Move cursor past bottom bar + mode indicator, then newline
|
|
38
|
+
process.stdout.write("\x1b[2B\n");
|
|
39
|
+
cleanup();
|
|
40
|
+
resolve({ value: buffer.trim(), action });
|
|
41
|
+
}
|
|
42
|
+
function onData(data) {
|
|
43
|
+
const str = typeof data === "string" ? data : data.toString("utf8");
|
|
44
|
+
// Shift+Tab: toggle mode
|
|
45
|
+
if (str === "\x1b[Z") {
|
|
46
|
+
mode = mode === "auto" ? "manual" : "auto";
|
|
47
|
+
process.stdout.write("\x1b7"); // save cursor
|
|
48
|
+
process.stdout.write("\x1b[2B\r\x1b[2K"); // down 2, beginning, clear line
|
|
49
|
+
process.stdout.write(modeIndicator(mode));
|
|
50
|
+
process.stdout.write("\x1b8"); // restore cursor
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Ctrl+C: cancel
|
|
54
|
+
if (str === "\x03") {
|
|
55
|
+
finish("cancel");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Ctrl+D: done (exit multi-turn)
|
|
59
|
+
if (str === "\x04") {
|
|
60
|
+
finish("done");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Enter: submit
|
|
64
|
+
if (str === "\r" || str === "\n") {
|
|
65
|
+
finish("submit");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Backspace
|
|
69
|
+
if (str === "\x7f" || str === "\x08") {
|
|
70
|
+
if (buffer.length > 0) {
|
|
71
|
+
const chars = [...buffer];
|
|
72
|
+
const removed = chars.pop();
|
|
73
|
+
buffer = chars.join("");
|
|
74
|
+
// Move back by the display width of the removed character
|
|
75
|
+
const width = removed.length > 1 || (removed.codePointAt(0) ?? 0) > 0xFFFF ? 2 : 1;
|
|
76
|
+
for (let i = 0; i < width; i++) {
|
|
77
|
+
process.stdout.write("\x1b[1D \x1b[1D");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Ignore other escape sequences
|
|
83
|
+
if (str.startsWith("\x1b"))
|
|
84
|
+
return;
|
|
85
|
+
// Printable characters (supports paste)
|
|
86
|
+
for (const ch of str) {
|
|
87
|
+
if (ch >= " ") {
|
|
88
|
+
buffer += ch;
|
|
89
|
+
process.stdout.write(ch);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
process.stdin.on("data", onData);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface InteractiveConfig {
|
|
2
|
+
executor: string;
|
|
3
|
+
reviewer: string;
|
|
4
|
+
task: string;
|
|
5
|
+
iterations: number;
|
|
6
|
+
threshold: number;
|
|
7
|
+
dir: string;
|
|
8
|
+
verbose: boolean;
|
|
9
|
+
mode: string;
|
|
10
|
+
passthroughArgs: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function interactive(): Promise<InteractiveConfig | null>;
|
|
13
|
+
//# sourceMappingURL=interactive.d.ts.map
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { renderBanner } from "./banner.js";
|
|
5
|
+
import { orange, gBlue, gGreen, bold, dim } from "./colors.js";
|
|
6
|
+
export async function interactive() {
|
|
7
|
+
// Banner
|
|
8
|
+
console.log(renderBanner());
|
|
9
|
+
p.intro("Configure your loop session");
|
|
10
|
+
// Working directory
|
|
11
|
+
const dirChoice = await p.select({
|
|
12
|
+
message: "Working directory",
|
|
13
|
+
options: [
|
|
14
|
+
{
|
|
15
|
+
value: "cwd",
|
|
16
|
+
label: `Current directory (${process.cwd()})`,
|
|
17
|
+
hint: "recommended",
|
|
18
|
+
},
|
|
19
|
+
{ value: "custom", label: "Custom path" },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
if (p.isCancel(dirChoice)) {
|
|
23
|
+
p.cancel("Cancelled.");
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
let dir = ".";
|
|
27
|
+
if (dirChoice === "custom") {
|
|
28
|
+
while (true) {
|
|
29
|
+
const dirInput = await p.text({
|
|
30
|
+
message: "Enter path",
|
|
31
|
+
placeholder: "/path/to/your/project",
|
|
32
|
+
});
|
|
33
|
+
if (p.isCancel(dirInput)) {
|
|
34
|
+
p.cancel("Cancelled.");
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (existsSync(resolve(dirInput))) {
|
|
38
|
+
dir = dirInput;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
p.log.error(`Directory not found: ${resolve(dirInput)}. Please try again.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Executor
|
|
45
|
+
const executor = await p.select({
|
|
46
|
+
message: "Select executor",
|
|
47
|
+
options: [
|
|
48
|
+
{
|
|
49
|
+
value: "claude",
|
|
50
|
+
label: orange("\u25CF") + " Claude",
|
|
51
|
+
hint: "Anthropic Claude Code CLI",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
value: "gemini",
|
|
55
|
+
label: gBlue("\u25CF") + " Gemini",
|
|
56
|
+
hint: "Google Gemini CLI",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
value: "codex",
|
|
60
|
+
label: gGreen("\u25CF") + " Codex",
|
|
61
|
+
hint: "OpenAI Codex CLI",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
if (p.isCancel(executor)) {
|
|
66
|
+
p.cancel("Cancelled.");
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// Reviewer
|
|
70
|
+
const reviewer = await p.select({
|
|
71
|
+
message: "Select reviewer",
|
|
72
|
+
options: [
|
|
73
|
+
{
|
|
74
|
+
value: "claude",
|
|
75
|
+
label: orange("\u25CF") + " Claude",
|
|
76
|
+
hint: "Anthropic Claude Code CLI",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
value: "gemini",
|
|
80
|
+
label: gBlue("\u25CF") + " Gemini",
|
|
81
|
+
hint: "Google Gemini CLI",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
value: "codex",
|
|
85
|
+
label: gGreen("\u25CF") + " Codex",
|
|
86
|
+
hint: "OpenAI Codex CLI",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
initialValue: (executor === "claude" ? "gemini" : "claude"),
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(reviewer)) {
|
|
92
|
+
p.cancel("Cancelled.");
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Native CLI flags (optional)
|
|
96
|
+
const passArgsInput = await p.text({
|
|
97
|
+
message: "Native CLI flags for executor (optional)",
|
|
98
|
+
placeholder: "e.g., --model claude-sonnet-4-20250514",
|
|
99
|
+
defaultValue: "",
|
|
100
|
+
});
|
|
101
|
+
if (p.isCancel(passArgsInput)) {
|
|
102
|
+
p.cancel("Cancelled.");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const passthroughArgs = passArgsInput.trim()
|
|
106
|
+
? passArgsInput.split(/\s+/).filter(Boolean)
|
|
107
|
+
: [];
|
|
108
|
+
// Task
|
|
109
|
+
const task = await p.text({
|
|
110
|
+
message: "Enter your task",
|
|
111
|
+
placeholder: "e.g. Write a quicksort implementation in Python",
|
|
112
|
+
validate(value) {
|
|
113
|
+
if (!value.trim())
|
|
114
|
+
return "Task cannot be empty";
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if (p.isCancel(task)) {
|
|
118
|
+
p.cancel("Cancelled.");
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
// Execution mode
|
|
122
|
+
const mode = await p.select({
|
|
123
|
+
message: "Execution mode",
|
|
124
|
+
options: [
|
|
125
|
+
{
|
|
126
|
+
value: "manual",
|
|
127
|
+
label: bold("\u23F5\u23F5 Manual"),
|
|
128
|
+
hint: "review each step, multi-turn conversation",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
value: "auto",
|
|
132
|
+
label: bold("\u23F5\u23F5 Auto"),
|
|
133
|
+
hint: "fully automatic executor \u2192 reviewer",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
if (p.isCancel(mode)) {
|
|
138
|
+
p.cancel("Cancelled.");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
// Iterations
|
|
142
|
+
const iterations = await p.text({
|
|
143
|
+
message: "Max iterations",
|
|
144
|
+
placeholder: "3",
|
|
145
|
+
defaultValue: "3",
|
|
146
|
+
validate(value) {
|
|
147
|
+
const n = parseInt(value, 10);
|
|
148
|
+
if (isNaN(n) || n < 1 || n > 20)
|
|
149
|
+
return "Enter a number between 1 and 20";
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
if (p.isCancel(iterations)) {
|
|
153
|
+
p.cancel("Cancelled.");
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// Threshold
|
|
157
|
+
const threshold = await p.text({
|
|
158
|
+
message: "Approval threshold (1-10)",
|
|
159
|
+
placeholder: "9",
|
|
160
|
+
defaultValue: "9",
|
|
161
|
+
validate(value) {
|
|
162
|
+
const n = parseInt(value, 10);
|
|
163
|
+
if (isNaN(n) || n < 1 || n > 10)
|
|
164
|
+
return "Enter a number between 1 and 10";
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
if (p.isCancel(threshold)) {
|
|
168
|
+
p.cancel("Cancelled.");
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// Verbose
|
|
172
|
+
const verbose = await p.confirm({
|
|
173
|
+
message: "Stream verbose output?",
|
|
174
|
+
initialValue: false,
|
|
175
|
+
});
|
|
176
|
+
if (p.isCancel(verbose)) {
|
|
177
|
+
p.cancel("Cancelled.");
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
// Summary
|
|
181
|
+
const resolvedDir = resolve(dir || ".");
|
|
182
|
+
const engineLabel = (name) => {
|
|
183
|
+
switch (name) {
|
|
184
|
+
case "claude":
|
|
185
|
+
return orange("\u25CF") + " Claude";
|
|
186
|
+
case "gemini":
|
|
187
|
+
return gBlue("\u25CF") + " Gemini";
|
|
188
|
+
case "codex":
|
|
189
|
+
return gGreen("\u25CF") + " Codex";
|
|
190
|
+
default:
|
|
191
|
+
return name;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const modeLabel = mode === "auto" ? bold("\u23F5\u23F5 Auto") : bold("\u23F5\u23F5 Manual");
|
|
195
|
+
const summary = [
|
|
196
|
+
` Executor: ${engineLabel(executor)}`,
|
|
197
|
+
` Reviewer: ${engineLabel(reviewer)}`,
|
|
198
|
+
"",
|
|
199
|
+
` Task: ${task.length > 40 ? task.slice(0, 40) + "..." : task}`,
|
|
200
|
+
` Iterations: ${iterations}`,
|
|
201
|
+
` Threshold: ${threshold}`,
|
|
202
|
+
` Directory: ${resolvedDir}`,
|
|
203
|
+
` Verbose: ${verbose ? "on" : "off"}`,
|
|
204
|
+
` Mode: ${modeLabel}`,
|
|
205
|
+
` CLI flags: ${passthroughArgs.length > 0 ? passthroughArgs.join(" ") : dim("none")}`,
|
|
206
|
+
].join("\n");
|
|
207
|
+
p.note(summary, "Configuration");
|
|
208
|
+
// Confirm
|
|
209
|
+
const confirmed = await p.confirm({
|
|
210
|
+
message: "Launch?",
|
|
211
|
+
initialValue: true,
|
|
212
|
+
});
|
|
213
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
214
|
+
p.cancel("Cancelled.");
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
p.outro("Launching loop...");
|
|
218
|
+
return {
|
|
219
|
+
executor,
|
|
220
|
+
reviewer,
|
|
221
|
+
task,
|
|
222
|
+
iterations: parseInt(iterations, 10),
|
|
223
|
+
threshold: parseInt(threshold, 10),
|
|
224
|
+
dir: resolvedDir,
|
|
225
|
+
verbose,
|
|
226
|
+
mode,
|
|
227
|
+
passthroughArgs,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=interactive.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface RendererStats {
|
|
2
|
+
elapsed_ms: number;
|
|
3
|
+
bytes: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class PtyRenderer {
|
|
6
|
+
private color;
|
|
7
|
+
private engineLabel;
|
|
8
|
+
private role;
|
|
9
|
+
private receivedBytes;
|
|
10
|
+
private started;
|
|
11
|
+
private endedWithLineBreak;
|
|
12
|
+
start(engineLabel: string, role: string, color: (s: string) => string): void;
|
|
13
|
+
write(data: string): void;
|
|
14
|
+
stop(stats: RendererStats): void;
|
|
15
|
+
get totalBytes(): number;
|
|
16
|
+
}
|
|
17
|
+
export interface KeystrokeHandlerOpts {
|
|
18
|
+
writeToPty: (data: string) => void;
|
|
19
|
+
onDone: () => void;
|
|
20
|
+
onCancel: () => void;
|
|
21
|
+
onModeToggle: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Forward user input to the PTY while reserving a small set of
|
|
25
|
+
* loop control shortcuts.
|
|
26
|
+
* Returns a cleanup function.
|
|
27
|
+
*/
|
|
28
|
+
export declare function startKeystrokeHandler(writeToPty: (data: string) => void, opts: {
|
|
29
|
+
onDone: () => void;
|
|
30
|
+
onCancel: () => void;
|
|
31
|
+
onModeToggle: () => void;
|
|
32
|
+
}): () => void;
|
|
33
|
+
//# sourceMappingURL=renderer.d.ts.map
|