@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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iTerm2 adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses osascript with iTerm2's AppleScript API to create new sessions
|
|
5
|
+
* (tabs / splits) and run commands. Supports activation (bring to front)
|
|
6
|
+
* and text injection via `write text`.
|
|
7
|
+
*
|
|
8
|
+
* Reference: https://iterm2.com/documentation-scripting.html
|
|
9
|
+
*/
|
|
10
|
+
import { execFile, spawn } from "node:child_process";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function osascript(script) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
execFile("osascript", ["-e", script], { timeout: 10_000 }, (err, stdout) => {
|
|
17
|
+
if (err)
|
|
18
|
+
return reject(err);
|
|
19
|
+
resolve((stdout ?? "").trim());
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Implementation
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
export class ITerm2Adapter {
|
|
27
|
+
mode = "iterm2";
|
|
28
|
+
capabilities = {
|
|
29
|
+
supportsActivate: true,
|
|
30
|
+
supportsInjection: true,
|
|
31
|
+
supportsSessionReuse: true,
|
|
32
|
+
supportsResize: false,
|
|
33
|
+
};
|
|
34
|
+
async launch(command, args, opts) {
|
|
35
|
+
// Sanitize for AppleScript: escape backslashes, quotes, AND newlines
|
|
36
|
+
// to prevent AppleScript injection via crafted arguments.
|
|
37
|
+
const escapeAS = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "").replace(/\r/g, "");
|
|
38
|
+
const escaped = [command, ...args].map(escapeAS).join(" ");
|
|
39
|
+
const cwd = escapeAS(opts.cwd);
|
|
40
|
+
// Create a new iTerm2 tab and run the command there.
|
|
41
|
+
const script = `
|
|
42
|
+
tell application "iTerm2"
|
|
43
|
+
activate
|
|
44
|
+
tell current window
|
|
45
|
+
set newTab to (create tab with default profile)
|
|
46
|
+
tell current session of newTab
|
|
47
|
+
write text "cd \\"${cwd}\\" && ${escaped}"
|
|
48
|
+
end tell
|
|
49
|
+
end tell
|
|
50
|
+
end tell
|
|
51
|
+
`;
|
|
52
|
+
await osascript(script);
|
|
53
|
+
// Spawn a local child process for lifecycle tracking, same pattern as
|
|
54
|
+
// NativeTerminalAdapter.
|
|
55
|
+
const child = spawn(command, args, {
|
|
56
|
+
cwd: opts.cwd,
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
...(opts.env ?? {}),
|
|
60
|
+
// Preserve iTerm2 session id so child processes can detect the terminal
|
|
61
|
+
ITERM_SESSION_ID: process.env.ITERM_SESSION_ID ?? "",
|
|
62
|
+
TERM_PROGRAM: process.env.TERM_PROGRAM ?? "",
|
|
63
|
+
TERM_PROGRAM_VERSION: process.env.TERM_PROGRAM_VERSION ?? "",
|
|
64
|
+
},
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
});
|
|
67
|
+
const dataHandlers = [];
|
|
68
|
+
const exitHandlers = [];
|
|
69
|
+
child.stdout?.on("data", (chunk) => {
|
|
70
|
+
const text = chunk.toString("utf8");
|
|
71
|
+
for (const h of dataHandlers)
|
|
72
|
+
h(text);
|
|
73
|
+
});
|
|
74
|
+
child.stderr?.on("data", (chunk) => {
|
|
75
|
+
const text = chunk.toString("utf8");
|
|
76
|
+
for (const h of dataHandlers)
|
|
77
|
+
h(text);
|
|
78
|
+
});
|
|
79
|
+
child.on("exit", (code) => {
|
|
80
|
+
const exitCode = code ?? 1;
|
|
81
|
+
for (const h of exitHandlers)
|
|
82
|
+
h(exitCode);
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
pid: child.pid ?? 0,
|
|
86
|
+
write(data) {
|
|
87
|
+
child.stdin?.write(data);
|
|
88
|
+
},
|
|
89
|
+
resize(_cols, _rows) {
|
|
90
|
+
// iTerm2 resize via AppleScript is unreliable; no-op for now
|
|
91
|
+
},
|
|
92
|
+
kill() {
|
|
93
|
+
child.kill();
|
|
94
|
+
},
|
|
95
|
+
onData(handler) {
|
|
96
|
+
dataHandlers.push(handler);
|
|
97
|
+
},
|
|
98
|
+
onExit(handler) {
|
|
99
|
+
exitHandlers.push(handler);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async inject(processOrId, command) {
|
|
104
|
+
const escaped = command.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "").replace(/\r/g, "");
|
|
105
|
+
// Write text to the current iTerm2 session
|
|
106
|
+
const script = `
|
|
107
|
+
tell application "iTerm2"
|
|
108
|
+
tell current session of current window
|
|
109
|
+
write text "${escaped}"
|
|
110
|
+
end tell
|
|
111
|
+
end tell
|
|
112
|
+
`;
|
|
113
|
+
await osascript(script);
|
|
114
|
+
void processOrId;
|
|
115
|
+
}
|
|
116
|
+
async activate(_processOrId) {
|
|
117
|
+
await osascript('tell application "iTerm2" to activate');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=iterm2-adapter.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal PTY adapter (default fallback).
|
|
3
|
+
*
|
|
4
|
+
* Uses `node-pty` to spawn a pseudo-terminal without any visible terminal
|
|
5
|
+
* window — fully headless. This is the most commonly used adapter and serves
|
|
6
|
+
* as the default when no specific terminal environment is detected.
|
|
7
|
+
*
|
|
8
|
+
* Ported from ufoo's adapters/internalPtyAdapter.js.
|
|
9
|
+
*/
|
|
10
|
+
import type { AdapterLaunchOptions, LaunchedProcess, TerminalAdapter, TerminalCapabilities } from "./adapter.js";
|
|
11
|
+
import type { LaunchMode } from "./detect.js";
|
|
12
|
+
export declare class PtyAdapter implements TerminalAdapter {
|
|
13
|
+
readonly mode: LaunchMode;
|
|
14
|
+
readonly capabilities: TerminalCapabilities;
|
|
15
|
+
launch(command: string, args: string[], opts: AdapterLaunchOptions): Promise<LaunchedProcess>;
|
|
16
|
+
inject(_processOrId: number | string, command: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=pty-adapter.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal PTY adapter (default fallback).
|
|
3
|
+
*
|
|
4
|
+
* Uses `node-pty` to spawn a pseudo-terminal without any visible terminal
|
|
5
|
+
* window — fully headless. This is the most commonly used adapter and serves
|
|
6
|
+
* as the default when no specific terminal environment is detected.
|
|
7
|
+
*
|
|
8
|
+
* Ported from ufoo's adapters/internalPtyAdapter.js.
|
|
9
|
+
*/
|
|
10
|
+
import pty from "node-pty";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Implementation
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export class PtyAdapter {
|
|
15
|
+
mode = "pty";
|
|
16
|
+
capabilities = {
|
|
17
|
+
supportsActivate: false,
|
|
18
|
+
supportsInjection: true,
|
|
19
|
+
supportsSessionReuse: false,
|
|
20
|
+
supportsResize: true,
|
|
21
|
+
};
|
|
22
|
+
async launch(command, args, opts) {
|
|
23
|
+
const cols = opts.cols ?? process.stdout.columns ?? 80;
|
|
24
|
+
const rows = opts.rows ?? process.stdout.rows ?? 24;
|
|
25
|
+
const ptyProcess = pty.spawn(command, args, {
|
|
26
|
+
name: "xterm-256color",
|
|
27
|
+
cols,
|
|
28
|
+
rows,
|
|
29
|
+
cwd: opts.cwd,
|
|
30
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
31
|
+
});
|
|
32
|
+
const dataHandlers = [];
|
|
33
|
+
const exitHandlers = [];
|
|
34
|
+
ptyProcess.onData((data) => {
|
|
35
|
+
for (const h of dataHandlers)
|
|
36
|
+
h(data);
|
|
37
|
+
});
|
|
38
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
39
|
+
for (const h of exitHandlers)
|
|
40
|
+
h(exitCode);
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
pid: ptyProcess.pid,
|
|
44
|
+
write(data) {
|
|
45
|
+
try {
|
|
46
|
+
ptyProcess.write(data);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Process may have exited
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
resize(c, r) {
|
|
53
|
+
try {
|
|
54
|
+
ptyProcess.resize(c, r);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Process may have exited
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
kill() {
|
|
61
|
+
try {
|
|
62
|
+
ptyProcess.kill();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Already dead
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onData(handler) {
|
|
69
|
+
dataHandlers.push(handler);
|
|
70
|
+
},
|
|
71
|
+
onExit(handler) {
|
|
72
|
+
exitHandlers.push(handler);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async inject(_processOrId, command) {
|
|
77
|
+
// For the internal PTY, injection is just writing to the PTY stdin.
|
|
78
|
+
// The caller must hold a reference to the LaunchedProcess to use write().
|
|
79
|
+
// This method exists for symmetry but cannot reach an arbitrary process.
|
|
80
|
+
void command;
|
|
81
|
+
throw new Error("PtyAdapter.inject requires direct LaunchedProcess.write()");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=pty-adapter.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Terminal.app adapter (macOS).
|
|
3
|
+
*
|
|
4
|
+
* Opens a new Terminal.app window via `osascript` and runs the given command
|
|
5
|
+
* inside it. Supports bringing the terminal to the foreground.
|
|
6
|
+
*
|
|
7
|
+
* Ported from ufoo's adapters/terminalAdapter.js.
|
|
8
|
+
*/
|
|
9
|
+
import type { AdapterLaunchOptions, LaunchedProcess, TerminalAdapter, TerminalCapabilities } from "./adapter.js";
|
|
10
|
+
import type { LaunchMode } from "./detect.js";
|
|
11
|
+
export declare class NativeTerminalAdapter implements TerminalAdapter {
|
|
12
|
+
readonly mode: LaunchMode;
|
|
13
|
+
readonly capabilities: TerminalCapabilities;
|
|
14
|
+
launch(command: string, args: string[], opts: AdapterLaunchOptions): Promise<LaunchedProcess>;
|
|
15
|
+
activate(_processOrId: number | string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=terminal-adapter.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Terminal.app adapter (macOS).
|
|
3
|
+
*
|
|
4
|
+
* Opens a new Terminal.app window via `osascript` and runs the given command
|
|
5
|
+
* inside it. Supports bringing the terminal to the foreground.
|
|
6
|
+
*
|
|
7
|
+
* Ported from ufoo's adapters/terminalAdapter.js.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function osascript(script) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
execFile("osascript", ["-e", script], { timeout: 10_000 }, (err, stdout) => {
|
|
16
|
+
if (err)
|
|
17
|
+
return reject(err);
|
|
18
|
+
resolve((stdout ?? "").trim());
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Implementation
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export class NativeTerminalAdapter {
|
|
26
|
+
mode = "terminal";
|
|
27
|
+
capabilities = {
|
|
28
|
+
supportsActivate: true,
|
|
29
|
+
supportsInjection: false,
|
|
30
|
+
supportsSessionReuse: true,
|
|
31
|
+
supportsResize: false,
|
|
32
|
+
};
|
|
33
|
+
async launch(command, args, opts) {
|
|
34
|
+
// Build full shell command string (escaped for AppleScript)
|
|
35
|
+
const escaped = [command, ...args]
|
|
36
|
+
.map((a) => a.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "'\\''"))
|
|
37
|
+
.join(" ");
|
|
38
|
+
const cwd = opts.cwd.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "'\\''");
|
|
39
|
+
// Use osascript to open a new Terminal window and run the command
|
|
40
|
+
const script = `
|
|
41
|
+
tell application "Terminal"
|
|
42
|
+
activate
|
|
43
|
+
set newTab to do script "cd '${cwd}' && ${escaped}"
|
|
44
|
+
end tell
|
|
45
|
+
`;
|
|
46
|
+
await osascript(script);
|
|
47
|
+
// Terminal.app does not expose the child PID easily via AppleScript.
|
|
48
|
+
// We fall back to spawning the process ourselves so we can track I/O.
|
|
49
|
+
const child = spawn(command, args, {
|
|
50
|
+
cwd: opts.cwd,
|
|
51
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
});
|
|
54
|
+
const dataHandlers = [];
|
|
55
|
+
const exitHandlers = [];
|
|
56
|
+
child.stdout?.on("data", (chunk) => {
|
|
57
|
+
const text = chunk.toString("utf8");
|
|
58
|
+
for (const h of dataHandlers)
|
|
59
|
+
h(text);
|
|
60
|
+
});
|
|
61
|
+
child.stderr?.on("data", (chunk) => {
|
|
62
|
+
const text = chunk.toString("utf8");
|
|
63
|
+
for (const h of dataHandlers)
|
|
64
|
+
h(text);
|
|
65
|
+
});
|
|
66
|
+
child.on("exit", (code) => {
|
|
67
|
+
const exitCode = code ?? 1;
|
|
68
|
+
for (const h of exitHandlers)
|
|
69
|
+
h(exitCode);
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
pid: child.pid ?? 0,
|
|
73
|
+
write(data) {
|
|
74
|
+
child.stdin?.write(data);
|
|
75
|
+
},
|
|
76
|
+
resize(_cols, _rows) {
|
|
77
|
+
// Terminal.app does not support programmatic resize
|
|
78
|
+
},
|
|
79
|
+
kill() {
|
|
80
|
+
child.kill();
|
|
81
|
+
},
|
|
82
|
+
onData(handler) {
|
|
83
|
+
dataHandlers.push(handler);
|
|
84
|
+
},
|
|
85
|
+
onExit(handler) {
|
|
86
|
+
exitHandlers.push(handler);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async activate(_processOrId) {
|
|
91
|
+
await osascript('tell application "Terminal" to activate');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=terminal-adapter.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux pane adapter.
|
|
3
|
+
*
|
|
4
|
+
* Spawns commands inside tmux panes using `tmux split-window` and supports
|
|
5
|
+
* text injection via `tmux send-keys`.
|
|
6
|
+
*
|
|
7
|
+
* Ported from ufoo's adapters/tmuxAdapter.js.
|
|
8
|
+
*/
|
|
9
|
+
import type { AdapterLaunchOptions, LaunchedProcess, TerminalAdapter, TerminalCapabilities } from "./adapter.js";
|
|
10
|
+
import type { LaunchMode } from "./detect.js";
|
|
11
|
+
export declare class TmuxAdapter implements TerminalAdapter {
|
|
12
|
+
readonly mode: LaunchMode;
|
|
13
|
+
readonly capabilities: TerminalCapabilities;
|
|
14
|
+
launch(command: string, args: string[], opts: AdapterLaunchOptions): Promise<LaunchedProcess>;
|
|
15
|
+
inject(processOrId: number | string, command: string): Promise<void>;
|
|
16
|
+
activate(processOrId: number | string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=tmux-adapter.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux pane adapter.
|
|
3
|
+
*
|
|
4
|
+
* Spawns commands inside tmux panes using `tmux split-window` and supports
|
|
5
|
+
* text injection via `tmux send-keys`.
|
|
6
|
+
*
|
|
7
|
+
* Ported from ufoo's adapters/tmuxAdapter.js.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function tmuxCommand(args) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
execFile("tmux", args, { timeout: 10_000 }, (err, stdout) => {
|
|
16
|
+
if (err)
|
|
17
|
+
return reject(err);
|
|
18
|
+
resolve((stdout ?? "").trim());
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Implementation
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export class TmuxAdapter {
|
|
26
|
+
mode = "tmux";
|
|
27
|
+
capabilities = {
|
|
28
|
+
supportsActivate: true,
|
|
29
|
+
supportsInjection: true,
|
|
30
|
+
supportsSessionReuse: true,
|
|
31
|
+
supportsResize: true,
|
|
32
|
+
};
|
|
33
|
+
async launch(command, args, opts) {
|
|
34
|
+
// Shell-quote each argument to prevent injection when tmux interprets
|
|
35
|
+
// the command string via the shell.
|
|
36
|
+
const shellQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
37
|
+
const fullCommand = [command, ...args].map(shellQuote).join(" ");
|
|
38
|
+
// Split a new pane and run the command. tmux split-window returns the
|
|
39
|
+
// pane ID (e.g. %5) when using -P -F.
|
|
40
|
+
const paneId = await tmuxCommand([
|
|
41
|
+
"split-window",
|
|
42
|
+
"-h",
|
|
43
|
+
"-c",
|
|
44
|
+
opts.cwd,
|
|
45
|
+
"-P",
|
|
46
|
+
"-F",
|
|
47
|
+
"#{pane_id}",
|
|
48
|
+
fullCommand,
|
|
49
|
+
]);
|
|
50
|
+
// Resolve the PID of the initial command running inside the new pane.
|
|
51
|
+
let pid = 0;
|
|
52
|
+
try {
|
|
53
|
+
const pidStr = await tmuxCommand([
|
|
54
|
+
"display-message",
|
|
55
|
+
"-t",
|
|
56
|
+
paneId,
|
|
57
|
+
"-p",
|
|
58
|
+
"#{pane_pid}",
|
|
59
|
+
]);
|
|
60
|
+
pid = Number.parseInt(pidStr, 10) || 0;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Non-fatal — PID tracking is best-effort
|
|
64
|
+
}
|
|
65
|
+
// We also spawn the command locally so we can attach I/O handlers.
|
|
66
|
+
// This mirrors what the terminal-adapter does: osascript opens the
|
|
67
|
+
// window, but a local child_process tracks the lifecycle.
|
|
68
|
+
const child = spawn(command, args, {
|
|
69
|
+
cwd: opts.cwd,
|
|
70
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
72
|
+
});
|
|
73
|
+
const dataHandlers = [];
|
|
74
|
+
const exitHandlers = [];
|
|
75
|
+
child.stdout?.on("data", (chunk) => {
|
|
76
|
+
const text = chunk.toString("utf8");
|
|
77
|
+
for (const h of dataHandlers)
|
|
78
|
+
h(text);
|
|
79
|
+
});
|
|
80
|
+
child.stderr?.on("data", (chunk) => {
|
|
81
|
+
const text = chunk.toString("utf8");
|
|
82
|
+
for (const h of dataHandlers)
|
|
83
|
+
h(text);
|
|
84
|
+
});
|
|
85
|
+
child.on("exit", (code) => {
|
|
86
|
+
const exitCode = code ?? 1;
|
|
87
|
+
for (const h of exitHandlers)
|
|
88
|
+
h(exitCode);
|
|
89
|
+
});
|
|
90
|
+
const effectivePid = child.pid ?? pid;
|
|
91
|
+
return {
|
|
92
|
+
pid: effectivePid,
|
|
93
|
+
write(data) {
|
|
94
|
+
child.stdin?.write(data);
|
|
95
|
+
},
|
|
96
|
+
resize(cols, rows) {
|
|
97
|
+
// Resize the tmux pane
|
|
98
|
+
tmuxCommand(["resize-pane", "-t", paneId, "-x", String(cols), "-y", String(rows)]).catch(() => {
|
|
99
|
+
// Resize failure is non-fatal
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
kill() {
|
|
103
|
+
child.kill();
|
|
104
|
+
// Also kill the tmux pane to avoid orphans
|
|
105
|
+
tmuxCommand(["kill-pane", "-t", paneId]).catch(() => {
|
|
106
|
+
// Best-effort cleanup
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
onData(handler) {
|
|
110
|
+
dataHandlers.push(handler);
|
|
111
|
+
},
|
|
112
|
+
onExit(handler) {
|
|
113
|
+
exitHandlers.push(handler);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async inject(processOrId, command) {
|
|
118
|
+
const target = String(processOrId);
|
|
119
|
+
// Use tmux send-keys to inject text. We send the text first, then Enter.
|
|
120
|
+
await tmuxCommand(["send-keys", "-t", target, command, "Enter"]);
|
|
121
|
+
}
|
|
122
|
+
async activate(processOrId) {
|
|
123
|
+
const target = String(processOrId);
|
|
124
|
+
await tmuxCommand(["select-pane", "-t", target]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=tmux-adapter.js.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import gradient from "gradient-string";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { orange, gBlue, gGreen, dim, bold } from "./colors.js";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function getVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
11
|
+
return `v${pkg.version}`;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return "v0.0.0";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// 5-line block-letter logo for "loop"
|
|
18
|
+
const LARGE_LOGO = [
|
|
19
|
+
" ██╗ ██████╗ ██████╗ ██████╗ ",
|
|
20
|
+
" ██║ ██╔═══██╗██╔═══██╗██╔══██╗",
|
|
21
|
+
" ██║ ██║ ██║██║ ██║██████╔╝",
|
|
22
|
+
" ██║ ██║ ██║██║ ██║██╔═══╝ ",
|
|
23
|
+
" ███████╗╚██████╔╝╚██████╔╝██║ ",
|
|
24
|
+
" ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ",
|
|
25
|
+
];
|
|
26
|
+
const loopGradient = gradient(["#F07623", "#4285F4", "#10A37F"]);
|
|
27
|
+
function detectEngines() {
|
|
28
|
+
const engines = [
|
|
29
|
+
{ cmd: "claude", label: "Claude", colorFn: orange },
|
|
30
|
+
{ cmd: "gemini", label: "Gemini", colorFn: gBlue },
|
|
31
|
+
{ cmd: "codex", label: "Codex", colorFn: gGreen },
|
|
32
|
+
];
|
|
33
|
+
return engines.map(({ cmd, label, colorFn }) => {
|
|
34
|
+
let version = null;
|
|
35
|
+
try {
|
|
36
|
+
version = execFileSync(cmd, ["--version"], {
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
timeout: 5000,
|
|
39
|
+
}).trim();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// not installed
|
|
43
|
+
}
|
|
44
|
+
return { name: cmd, label, version, colorFn };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function renderBanner() {
|
|
48
|
+
const cols = process.stdout.columns || 80;
|
|
49
|
+
const engines = detectEngines();
|
|
50
|
+
if (cols >= 50) {
|
|
51
|
+
return renderLarge(cols, engines);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return renderCompact(cols, engines);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function renderEngineStatus() {
|
|
58
|
+
const engines = detectEngines();
|
|
59
|
+
for (const e of engines) {
|
|
60
|
+
const dot = e.version ? e.colorFn("\u25CF") : dim("\u25CB");
|
|
61
|
+
const label = e.version ? e.colorFn(e.label) : dim(e.label);
|
|
62
|
+
const ver = e.version ? dim(` (${e.version})`) : dim(" (not found)");
|
|
63
|
+
console.log(` ${dot} ${label}${ver}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function renderLarge(cols, engines) {
|
|
67
|
+
const VERSION = getVersion();
|
|
68
|
+
const maxLogoWidth = Math.max(...LARGE_LOGO.map((l) => l.length));
|
|
69
|
+
const boxWidth = maxLogoWidth + 6;
|
|
70
|
+
const hBar = "\u2550".repeat(boxWidth - 2);
|
|
71
|
+
const frameLine = (content, visibleLen) => {
|
|
72
|
+
const vLen = visibleLen ?? content.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
73
|
+
const padding = Math.max(0, boxWidth - 4 - vLen);
|
|
74
|
+
return ` \u2551 ${content}${" ".repeat(padding)}\u2551`;
|
|
75
|
+
};
|
|
76
|
+
const emptyLine = frameLine("", 0);
|
|
77
|
+
// Apply gradient to logo lines
|
|
78
|
+
const logoLines = LARGE_LOGO.map((line) => {
|
|
79
|
+
const padded = line + " ".repeat(maxLogoWidth - line.length);
|
|
80
|
+
const gradientLine = loopGradient(padded);
|
|
81
|
+
return frameLine(gradientLine, maxLogoWidth);
|
|
82
|
+
});
|
|
83
|
+
// Tagline + version on same line
|
|
84
|
+
const tagline = "Iterative multi-engine AI orchestration";
|
|
85
|
+
const version = VERSION;
|
|
86
|
+
const innerWidth = boxWidth - 4;
|
|
87
|
+
const tagVersionGap = Math.max(1, innerWidth - tagline.length - version.length);
|
|
88
|
+
const tagVersionLine = ` \u2551 ${dim(tagline)}${" ".repeat(tagVersionGap)}${bold(version)}\u2551`;
|
|
89
|
+
// Engine status lines
|
|
90
|
+
const engineLines = engines.map((e) => {
|
|
91
|
+
const dot = e.version ? e.colorFn("\u25CF") : dim("\u25CB");
|
|
92
|
+
const label = e.version ? e.colorFn(e.label) : dim(e.label);
|
|
93
|
+
const ver = e.version ? dim(` (${e.version})`) : dim(" (not found)");
|
|
94
|
+
const content = ` ${dot} ${label}${ver}`;
|
|
95
|
+
const visLen = 5 + e.label.length + (e.version ? ` (${e.version})`.length : " (not found)".length);
|
|
96
|
+
return frameLine(content, visLen);
|
|
97
|
+
});
|
|
98
|
+
// Build engine header
|
|
99
|
+
const engLabel = " Engines";
|
|
100
|
+
const engLabelLine = ` \u2551${dim(engLabel)}${" ".repeat(Math.max(0, boxWidth - 2 - engLabel.length))}\u2551`;
|
|
101
|
+
// Adapt to small columns by not rendering if too narrow
|
|
102
|
+
if (cols < boxWidth + 4) {
|
|
103
|
+
return renderCompact(cols, engines);
|
|
104
|
+
}
|
|
105
|
+
return [
|
|
106
|
+
"",
|
|
107
|
+
` \u2554${hBar}\u2557`,
|
|
108
|
+
emptyLine,
|
|
109
|
+
...logoLines,
|
|
110
|
+
emptyLine,
|
|
111
|
+
tagVersionLine,
|
|
112
|
+
emptyLine,
|
|
113
|
+
` \u2560${hBar}\u2563`,
|
|
114
|
+
engLabelLine,
|
|
115
|
+
emptyLine,
|
|
116
|
+
...engineLines,
|
|
117
|
+
emptyLine,
|
|
118
|
+
` \u255A${hBar}\u255D`,
|
|
119
|
+
"",
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
function renderCompact(cols, engines) {
|
|
123
|
+
const VERSION = getVersion();
|
|
124
|
+
const boxWidth = Math.min(cols - 2, 40);
|
|
125
|
+
const hBar = "\u2550".repeat(Math.max(0, boxWidth - 2));
|
|
126
|
+
const engineDots = engines
|
|
127
|
+
.map((e) => {
|
|
128
|
+
const dot = e.version ? e.colorFn("\u25CF") : dim("\u25CB");
|
|
129
|
+
return `${dot} ${e.version ? e.colorFn(e.label) : dim(e.label)}`;
|
|
130
|
+
})
|
|
131
|
+
.join(" ");
|
|
132
|
+
const title = loopGradient("\u25C8 loop");
|
|
133
|
+
const version = bold(VERSION);
|
|
134
|
+
return [
|
|
135
|
+
"",
|
|
136
|
+
` \u2554${hBar}\u2557`,
|
|
137
|
+
` \u2551 ${title} ${version}${" ".repeat(Math.max(0, boxWidth - 20))}\u2551`,
|
|
138
|
+
` \u2551 ${dim("AI orchestration")}${" ".repeat(Math.max(0, boxWidth - 22))}\u2551`,
|
|
139
|
+
` \u2560${hBar}\u2563`,
|
|
140
|
+
` \u2551 ${engineDots}${" ".repeat(Math.max(0, boxWidth - 30))}\u2551`,
|
|
141
|
+
` \u255A${hBar}\u255D`,
|
|
142
|
+
"",
|
|
143
|
+
].join("\n");
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=banner.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand colors and ANSI formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Uses raw ANSI escape codes for zero external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
/** Claude brand orange (#F07623) */
|
|
7
|
+
export declare const claude: (s: string) => string;
|
|
8
|
+
/** Gemini brand blue (#4285F4) */
|
|
9
|
+
export declare const gemini: (s: string) => string;
|
|
10
|
+
/** Codex brand green (#10A37F) */
|
|
11
|
+
export declare const codex: (s: string) => string;
|
|
12
|
+
/** Loop brand cyan (#00D4FF) */
|
|
13
|
+
export declare const loop: (s: string) => string;
|
|
14
|
+
/** Green for success messages */
|
|
15
|
+
export declare const success: (s: string) => string;
|
|
16
|
+
/** Red for error messages */
|
|
17
|
+
export declare const error: (s: string) => string;
|
|
18
|
+
/** Yellow for warnings */
|
|
19
|
+
export declare const warn: (s: string) => string;
|
|
20
|
+
/** Gray / dim text */
|
|
21
|
+
export declare const dim: (s: string) => string;
|
|
22
|
+
/** Bold text */
|
|
23
|
+
export declare const bold: (s: string) => string;
|
|
24
|
+
export declare const orange: (s: string) => string;
|
|
25
|
+
export declare const gBlue: (s: string) => string;
|
|
26
|
+
export declare const gGreen: (s: string) => string;
|
|
27
|
+
export declare const red: (s: string) => string;
|
|
28
|
+
export declare const green: (s: string) => string;
|
|
29
|
+
export declare const yellow: (s: string) => string;
|
|
30
|
+
export declare const cyan: (s: string) => string;
|
|
31
|
+
export declare const blue: (s: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Return the brand-color function for an engine name.
|
|
34
|
+
* Falls back to `loop` cyan for unknown engines.
|
|
35
|
+
*/
|
|
36
|
+
export declare function brandColor(engine: string): (s: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* Format a byte count for human-readable display.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatBytes(bytes: number): string;
|
|
41
|
+
//# sourceMappingURL=colors.d.ts.map
|