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