@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,211 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const SHARED_PLAN_FILENAME = ".loop-plan.md";
4
+ // ── Helpers ─────────────────────────────────────────
5
+ function getPlanPath(cwd) {
6
+ return join(cwd, SHARED_PLAN_FILENAME);
7
+ }
8
+ function readPlan(cwd) {
9
+ const path = getPlanPath(cwd);
10
+ if (!existsSync(path))
11
+ return null;
12
+ try {
13
+ const content = readFileSync(path, "utf-8");
14
+ return parsePlan(content);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ function writePlanSafe(cwd, content) {
21
+ try {
22
+ writeFileSync(getPlanPath(cwd), content, "utf-8");
23
+ }
24
+ catch {
25
+ // Fail-silent: never crash on write errors
26
+ }
27
+ }
28
+ // ── Public API ──────────────────────────────────────
29
+ /** Initialize a new shared plan file. */
30
+ export async function initSharedPlan(cwd, task) {
31
+ const content = renderPlan({
32
+ task,
33
+ iterations: [],
34
+ fileChangeLog: [],
35
+ });
36
+ writePlanSafe(cwd, content);
37
+ }
38
+ /** Update the shared plan with a new iteration record. */
39
+ export async function updateSharedPlan(cwd, record, filesChanged) {
40
+ const plan = readPlan(cwd) ?? {
41
+ task: "",
42
+ iterations: [],
43
+ fileChangeLog: [],
44
+ };
45
+ plan.iterations.push(record);
46
+ for (const file of filesChanged) {
47
+ plan.fileChangeLog.push({
48
+ iteration: record.iteration,
49
+ file,
50
+ action: "modified",
51
+ });
52
+ }
53
+ writePlanSafe(cwd, renderPlan(plan));
54
+ }
55
+ /** Generate a context snippet from the shared plan for the executor prompt. */
56
+ export async function getExecutorContext(cwd) {
57
+ const plan = readPlan(cwd);
58
+ if (!plan || plan.iterations.length === 0)
59
+ return "";
60
+ const lastIter = plan.iterations[plan.iterations.length - 1];
61
+ const lines = [
62
+ "## Previous Iteration Context (from shared plan)",
63
+ "",
64
+ `Last iteration: ${lastIter.iteration}`,
65
+ `Reviewer score: ${lastIter.score}/10`,
66
+ `Approved: ${lastIter.approved ? "Yes" : "No"}`,
67
+ "",
68
+ "Recent feedback:",
69
+ lastIter.reviewerFeedback,
70
+ ];
71
+ if (plan.fileChangeLog.length > 0) {
72
+ lines.push("");
73
+ lines.push("Files changed so far:");
74
+ for (const change of plan.fileChangeLog) {
75
+ lines.push(`- ${change.file} (${change.action}, iteration ${change.iteration})`);
76
+ }
77
+ }
78
+ return lines.join("\n");
79
+ }
80
+ /** Generate a context snippet for the reviewer prompt. */
81
+ export async function getReviewerContext(cwd) {
82
+ const plan = readPlan(cwd);
83
+ if (!plan || plan.iterations.length === 0)
84
+ return "";
85
+ const lines = [
86
+ "## Iteration History (from shared plan)",
87
+ "",
88
+ ];
89
+ for (const iter of plan.iterations) {
90
+ lines.push(`### Iteration ${iter.iteration}: Score ${iter.score}/10 (${iter.approved ? "Approved" : "Not approved"})`);
91
+ lines.push(`Key feedback: ${iter.reviewerFeedback.split("\n")[0]}`);
92
+ lines.push("");
93
+ }
94
+ return lines.join("\n");
95
+ }
96
+ /** Clear the shared plan file. */
97
+ export async function clearPlan(cwd) {
98
+ try {
99
+ const path = getPlanPath(cwd);
100
+ if (existsSync(path)) {
101
+ const { unlinkSync } = await import("node:fs");
102
+ unlinkSync(path);
103
+ }
104
+ }
105
+ catch {
106
+ // Fail-silent
107
+ }
108
+ }
109
+ /** Show the raw plan content. */
110
+ export async function showPlan(cwd) {
111
+ const path = getPlanPath(cwd);
112
+ if (!existsSync(path))
113
+ return "No plan file found.";
114
+ try {
115
+ return readFileSync(path, "utf-8");
116
+ }
117
+ catch {
118
+ return "Error reading plan file.";
119
+ }
120
+ }
121
+ // ── Rendering ───────────────────────────────────────
122
+ function renderPlan(plan) {
123
+ const sections = [];
124
+ sections.push("# Loop Shared Plan");
125
+ sections.push("");
126
+ sections.push("## Task");
127
+ sections.push(plan.task);
128
+ sections.push("");
129
+ sections.push("## Iteration History");
130
+ sections.push("");
131
+ if (plan.iterations.length === 0) {
132
+ sections.push("_No iterations yet._");
133
+ }
134
+ else {
135
+ for (const iter of plan.iterations) {
136
+ sections.push(`### Iteration ${iter.iteration}`);
137
+ sections.push(`- **Timestamp:** ${iter.timestamp}`);
138
+ sections.push(`- **Executor:** ${iter.executor}`);
139
+ sections.push(`- **Reviewer:** ${iter.reviewer}`);
140
+ sections.push(`- **Score:** ${iter.score}/10`);
141
+ sections.push(`- **Approved:** ${iter.approved ? "Yes" : "No"}`);
142
+ sections.push("");
143
+ sections.push("**Executor Summary:**");
144
+ sections.push(iter.executorSummary);
145
+ sections.push("");
146
+ sections.push("**Reviewer Feedback:**");
147
+ sections.push(iter.reviewerFeedback);
148
+ sections.push("");
149
+ }
150
+ }
151
+ sections.push("## File Change Log");
152
+ sections.push("");
153
+ if (plan.fileChangeLog.length === 0) {
154
+ sections.push("_No file changes recorded._");
155
+ }
156
+ else {
157
+ for (const change of plan.fileChangeLog) {
158
+ sections.push(`- [Iteration ${change.iteration}] ${change.action}: ${change.file}`);
159
+ }
160
+ }
161
+ sections.push("");
162
+ return sections.join("\n");
163
+ }
164
+ // ── Parsing ─────────────────────────────────────────
165
+ function parsePlan(content) {
166
+ const plan = {
167
+ task: "",
168
+ iterations: [],
169
+ fileChangeLog: [],
170
+ };
171
+ // Extract task
172
+ const taskMatch = content.match(/## Task\n([\s\S]*?)(?=\n## )/);
173
+ if (taskMatch)
174
+ plan.task = taskMatch[1].trim();
175
+ // Extract iteration records
176
+ const iterPattern = /### Iteration (\d+)\n([\s\S]*?)(?=\n### Iteration |\n## |$)/g;
177
+ let iterMatch;
178
+ while ((iterMatch = iterPattern.exec(content)) !== null) {
179
+ const num = parseInt(iterMatch[1], 10);
180
+ const body = iterMatch[2];
181
+ const scoreMatch = body.match(/\*\*Score:\*\*\s*(\d+)/);
182
+ const approvedMatch = body.match(/\*\*Approved:\*\*\s*(Yes|No)/i);
183
+ const executorMatch = body.match(/\*\*Executor:\*\*\s*(\w+)/);
184
+ const reviewerMatch = body.match(/\*\*Reviewer:\*\*\s*(\w+)/);
185
+ const timestampMatch = body.match(/\*\*Timestamp:\*\*\s*(.+)/);
186
+ const summaryMatch = body.match(/\*\*Executor Summary:\*\*\n([\s\S]*?)(?=\n\*\*Reviewer Feedback:\*\*|$)/);
187
+ const feedbackMatch = body.match(/\*\*Reviewer Feedback:\*\*\n([\s\S]*?)$/);
188
+ plan.iterations.push({
189
+ iteration: num,
190
+ timestamp: timestampMatch?.[1]?.trim() ?? "",
191
+ executor: executorMatch?.[1] ?? "",
192
+ reviewer: reviewerMatch?.[1] ?? "",
193
+ executorSummary: summaryMatch?.[1]?.trim() ?? "",
194
+ score: scoreMatch ? parseInt(scoreMatch[1], 10) : 0,
195
+ approved: approvedMatch?.[1]?.toLowerCase() === "yes",
196
+ reviewerFeedback: feedbackMatch?.[1]?.trim() ?? "",
197
+ });
198
+ }
199
+ // Extract file change log
200
+ const changePattern = /- \[Iteration (\d+)\] (created|modified|deleted): (.+)/g;
201
+ let changeMatch;
202
+ while ((changeMatch = changePattern.exec(content)) !== null) {
203
+ plan.fileChangeLog.push({
204
+ iteration: parseInt(changeMatch[1], 10),
205
+ file: changeMatch[3].trim(),
206
+ action: changeMatch[2],
207
+ });
208
+ }
209
+ return plan;
210
+ }
211
+ //# sourceMappingURL=shared-plan.js.map
@@ -0,0 +1,7 @@
1
+ import { type Skill } from "./loader.js";
2
+ /**
3
+ * Inject skill content into a prompt.
4
+ * Each skill is wrapped with delimiters for clarity.
5
+ */
6
+ export declare function injectSkills(prompt: string, skills: Skill[]): string;
7
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Inject skill content into a prompt.
3
+ * Each skill is wrapped with delimiters for clarity.
4
+ */
5
+ export function injectSkills(prompt, skills) {
6
+ if (skills.length === 0)
7
+ return prompt;
8
+ const skillBlocks = skills.map((skill) => `--- SKILL: ${skill.name} ---\n${skill.content}\n--- END SKILL ---`);
9
+ return skillBlocks.join("\n\n") + "\n\n" + prompt;
10
+ }
11
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1,16 @@
1
+ export interface Skill {
2
+ name: string;
3
+ description: string;
4
+ content: string;
5
+ path: string;
6
+ scope: "global" | "project" | "builtin";
7
+ }
8
+ /**
9
+ * Discover skills from multiple locations.
10
+ * Search order (later scopes override earlier on name collision):
11
+ * 1. Built-in: <packageRoot>/skills/<name>/SKILL.md
12
+ * 2. Global: ~/.loop/skills/<name>/SKILL.md
13
+ * 3. Project: <cwd>/SKILLS/<name>/SKILL.md
14
+ */
15
+ export declare function discoverSkills(cwd: string): Promise<Skill[]>;
16
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ import matter from "gray-matter";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const PACKAGE_ROOT = join(__dirname, "..", "..");
8
+ function readSkillFile(skillDir, scope) {
9
+ const skillPath = join(skillDir, "SKILL.md");
10
+ if (!existsSync(skillPath))
11
+ return null;
12
+ try {
13
+ const raw = readFileSync(skillPath, "utf-8");
14
+ const parsed = matter(raw);
15
+ const data = parsed.data;
16
+ const name = typeof data.name === "string" && data.name
17
+ ? data.name
18
+ : skillDir.split("/").pop() ?? "unknown";
19
+ const description = typeof data.description === "string"
20
+ ? data.description.trim()
21
+ : "";
22
+ return {
23
+ name,
24
+ description,
25
+ content: parsed.content.trim(),
26
+ path: skillPath,
27
+ scope,
28
+ };
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function discoverFromDir(baseDir, scope) {
35
+ if (!existsSync(baseDir))
36
+ return [];
37
+ const skills = [];
38
+ try {
39
+ const entries = readdirSync(baseDir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (entry.isDirectory()) {
42
+ const skill = readSkillFile(join(baseDir, entry.name), scope);
43
+ if (skill) {
44
+ skills.push(skill);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ catch {
50
+ // Directory not readable — skip
51
+ }
52
+ return skills;
53
+ }
54
+ /**
55
+ * Discover skills from multiple locations.
56
+ * Search order (later scopes override earlier on name collision):
57
+ * 1. Built-in: <packageRoot>/skills/<name>/SKILL.md
58
+ * 2. Global: ~/.loop/skills/<name>/SKILL.md
59
+ * 3. Project: <cwd>/SKILLS/<name>/SKILL.md
60
+ */
61
+ export async function discoverSkills(cwd) {
62
+ const seen = new Map();
63
+ // 1. Built-in skills
64
+ const builtinDir = join(PACKAGE_ROOT, "skills");
65
+ for (const skill of discoverFromDir(builtinDir, "builtin")) {
66
+ seen.set(skill.name, skill);
67
+ }
68
+ // 2. Global skills (~/.loop/skills/)
69
+ const globalDir = join(homedir(), ".loop", "skills");
70
+ for (const skill of discoverFromDir(globalDir, "global")) {
71
+ seen.set(skill.name, skill);
72
+ }
73
+ // 3. Project skills (<cwd>/SKILLS/)
74
+ const projectDir = join(cwd, "SKILLS");
75
+ for (const skill of discoverFromDir(projectDir, "project")) {
76
+ seen.set(skill.name, skill);
77
+ }
78
+ return Array.from(seen.values());
79
+ }
80
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1,13 @@
1
+ import { type Skill } from "./loader.js";
2
+ export declare class SkillRegistry {
3
+ private skills;
4
+ /** Load/reload all discoverable skills. */
5
+ load(cwd: string): Promise<void>;
6
+ /** Get a single skill by name. */
7
+ get(name: string): Skill | undefined;
8
+ /** List all loaded skills. */
9
+ list(): Skill[];
10
+ /** Add a new skill (write SKILL.md to disk). */
11
+ add(name: string, content: string, scope: "global" | "project", cwd?: string): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { discoverSkills } from "./loader.js";
5
+ export class SkillRegistry {
6
+ skills = new Map();
7
+ /** Load/reload all discoverable skills. */
8
+ async load(cwd) {
9
+ this.skills.clear();
10
+ const discovered = await discoverSkills(cwd);
11
+ for (const skill of discovered) {
12
+ this.skills.set(skill.name, skill);
13
+ }
14
+ }
15
+ /** Get a single skill by name. */
16
+ get(name) {
17
+ return this.skills.get(name);
18
+ }
19
+ /** List all loaded skills. */
20
+ list() {
21
+ return Array.from(this.skills.values());
22
+ }
23
+ /** Add a new skill (write SKILL.md to disk). */
24
+ async add(name, content, scope, cwd) {
25
+ // Prevent path traversal
26
+ if (/[/\\]|^\.\.?$/.test(name) || name.includes("..")) {
27
+ throw new Error(`Invalid skill name: ${name}`);
28
+ }
29
+ let baseDir;
30
+ if (scope === "global") {
31
+ baseDir = join(homedir(), ".loop", "skills");
32
+ }
33
+ else {
34
+ if (!cwd) {
35
+ throw new Error("cwd is required when adding a project-scoped skill");
36
+ }
37
+ baseDir = join(cwd, "SKILLS");
38
+ }
39
+ const skillDir = join(baseDir, name);
40
+ mkdirSync(skillDir, { recursive: true });
41
+ const skillPath = join(skillDir, "SKILL.md");
42
+ const fileContent = `---\nname: ${name}\ndescription: ""\n---\n\n${content}\n`;
43
+ writeFileSync(skillPath, fileContent, "utf-8");
44
+ // Register in memory
45
+ this.skills.set(name, {
46
+ name,
47
+ description: "",
48
+ content,
49
+ path: skillPath,
50
+ scope,
51
+ });
52
+ }
53
+ }
54
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Terminal adapter interface and factory.
3
+ *
4
+ * Each adapter wraps a specific terminal environment (Terminal.app, tmux,
5
+ * iTerm2, internal PTY) behind a common interface so the agent launcher
6
+ * can spawn and manage processes uniformly.
7
+ *
8
+ * Ported from ufoo's adapterRouter.js / adapterContract.js with simplified
9
+ * TypeScript interfaces.
10
+ */
11
+ import type { LaunchMode } from "./detect.js";
12
+ export interface TerminalCapabilities {
13
+ /** Can bring the terminal window / pane to the foreground. */
14
+ supportsActivate: boolean;
15
+ /** Can inject text into a running process via send-keys or socket. */
16
+ supportsInjection: boolean;
17
+ /** Supports reusing a previous session in the same terminal / pane. */
18
+ supportsSessionReuse: boolean;
19
+ /** Can programmatically resize the PTY. */
20
+ supportsResize: boolean;
21
+ }
22
+ export interface AdapterLaunchOptions {
23
+ cwd: string;
24
+ env?: Record<string, string>;
25
+ cols?: number;
26
+ rows?: number;
27
+ }
28
+ export interface LaunchedProcess {
29
+ pid: number;
30
+ write(data: string): void;
31
+ resize(cols: number, rows: number): void;
32
+ kill(): void;
33
+ onData(handler: (data: string) => void): void;
34
+ onExit(handler: (code: number) => void): void;
35
+ }
36
+ export interface TerminalAdapter {
37
+ mode: LaunchMode;
38
+ capabilities: TerminalCapabilities;
39
+ /**
40
+ * Spawn a new process inside this terminal environment.
41
+ */
42
+ launch(command: string, args: string[], opts: AdapterLaunchOptions): Promise<LaunchedProcess>;
43
+ /**
44
+ * Inject text into a running process (e.g. tmux send-keys).
45
+ * Only available when `capabilities.supportsInjection` is true.
46
+ */
47
+ inject?(processOrId: number | string, command: string): Promise<void>;
48
+ /**
49
+ * Bring the terminal window / pane to the foreground.
50
+ * Only available when `capabilities.supportsActivate` is true.
51
+ */
52
+ activate?(processOrId: number | string): Promise<void>;
53
+ }
54
+ /**
55
+ * Create the appropriate terminal adapter for the given launch mode.
56
+ *
57
+ * Adapters are lazily imported to avoid loading unnecessary platform-specific
58
+ * code (e.g. osascript helpers on Linux).
59
+ */
60
+ export declare function createAdapter(mode: LaunchMode): Promise<TerminalAdapter>;
61
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Terminal adapter interface and factory.
3
+ *
4
+ * Each adapter wraps a specific terminal environment (Terminal.app, tmux,
5
+ * iTerm2, internal PTY) behind a common interface so the agent launcher
6
+ * can spawn and manage processes uniformly.
7
+ *
8
+ * Ported from ufoo's adapterRouter.js / adapterContract.js with simplified
9
+ * TypeScript interfaces.
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Factory
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Create the appropriate terminal adapter for the given launch mode.
16
+ *
17
+ * Adapters are lazily imported to avoid loading unnecessary platform-specific
18
+ * code (e.g. osascript helpers on Linux).
19
+ */
20
+ export async function createAdapter(mode) {
21
+ switch (mode) {
22
+ case "terminal": {
23
+ const { NativeTerminalAdapter } = await import("./terminal-adapter.js");
24
+ return new NativeTerminalAdapter();
25
+ }
26
+ case "tmux": {
27
+ const { TmuxAdapter } = await import("./tmux-adapter.js");
28
+ return new TmuxAdapter();
29
+ }
30
+ case "iterm2": {
31
+ const { ITerm2Adapter } = await import("./iterm2-adapter.js");
32
+ return new ITerm2Adapter();
33
+ }
34
+ case "pty":
35
+ case "auto":
36
+ default: {
37
+ const { PtyAdapter } = await import("./pty-adapter.js");
38
+ return new PtyAdapter();
39
+ }
40
+ }
41
+ }
42
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Terminal environment detection.
3
+ *
4
+ * Identifies the terminal emulator (iTerm2, tmux, Terminal.app, etc.)
5
+ * and resolves a launch mode for agent spawning.
6
+ *
7
+ * Ported from ufoo's terminal/detect.js and launcher.js `resolveLaunchMode`.
8
+ */
9
+ export type LaunchMode = "terminal" | "tmux" | "iterm2" | "pty" | "auto";
10
+ /**
11
+ * Detect the appropriate launch mode based on environment variables.
12
+ *
13
+ * Priority:
14
+ * 1. LOOP_LAUNCH_MODE env override (explicit user choice)
15
+ * 2. TMUX_PANE present → "tmux"
16
+ * 3. ITERM_SESSION_ID present → "iterm2"
17
+ * 4. Fallback → "pty" (headless internal PTY)
18
+ */
19
+ export declare function detectTerminal(): LaunchMode;
20
+ /**
21
+ * Detect the TTY device path for the current process.
22
+ * Returns `undefined` when running without a controlling terminal.
23
+ */
24
+ export declare function detectTTY(): string | undefined;
25
+ /**
26
+ * Return the current tmux pane identifier (e.g. `%0`, `%1`).
27
+ * Returns `undefined` when not inside a tmux session.
28
+ */
29
+ export declare function detectTmuxPane(): string | undefined;
30
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Terminal environment detection.
3
+ *
4
+ * Identifies the terminal emulator (iTerm2, tmux, Terminal.app, etc.)
5
+ * and resolves a launch mode for agent spawning.
6
+ *
7
+ * Ported from ufoo's terminal/detect.js and launcher.js `resolveLaunchMode`.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ // ---------------------------------------------------------------------------
11
+ // Terminal detection
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Detect the appropriate launch mode based on environment variables.
15
+ *
16
+ * Priority:
17
+ * 1. LOOP_LAUNCH_MODE env override (explicit user choice)
18
+ * 2. TMUX_PANE present → "tmux"
19
+ * 3. ITERM_SESSION_ID present → "iterm2"
20
+ * 4. Fallback → "pty" (headless internal PTY)
21
+ */
22
+ export function detectTerminal() {
23
+ const explicit = (process.env.LOOP_LAUNCH_MODE ?? "").trim().toLowerCase();
24
+ if (explicit === "terminal" || explicit === "tmux" || explicit === "iterm2" || explicit === "pty") {
25
+ return explicit;
26
+ }
27
+ if (process.env.TMUX_PANE)
28
+ return "tmux";
29
+ if (process.env.ITERM_SESSION_ID)
30
+ return "iterm2";
31
+ return "pty";
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // TTY detection
35
+ // ---------------------------------------------------------------------------
36
+ function normalizeTty(raw) {
37
+ const trimmed = raw.trim();
38
+ if (!trimmed || trimmed === "not a tty" || trimmed === "/dev/tty") {
39
+ return undefined;
40
+ }
41
+ return trimmed;
42
+ }
43
+ /**
44
+ * Detect the TTY device path for the current process.
45
+ * Returns `undefined` when running without a controlling terminal.
46
+ */
47
+ export function detectTTY() {
48
+ // Allow explicit override for test / sandbox environments
49
+ const override = normalizeTty(process.env.LOOP_TTY_OVERRIDE ?? "");
50
+ if (override)
51
+ return override;
52
+ try {
53
+ const result = spawnSync("tty", {
54
+ stdio: [0, "pipe", "ignore"],
55
+ encoding: "utf8",
56
+ });
57
+ if (result.status === 0 && result.stdout) {
58
+ return normalizeTty(result.stdout);
59
+ }
60
+ }
61
+ catch {
62
+ // tty command unavailable or failed — not fatal
63
+ }
64
+ return undefined;
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // tmux pane detection
68
+ // ---------------------------------------------------------------------------
69
+ /**
70
+ * Return the current tmux pane identifier (e.g. `%0`, `%1`).
71
+ * Returns `undefined` when not inside a tmux session.
72
+ */
73
+ export function detectTmuxPane() {
74
+ const pane = process.env.TMUX_PANE;
75
+ return pane ? pane.trim() || undefined : undefined;
76
+ }
77
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1,19 @@
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 type { AdapterLaunchOptions, LaunchedProcess, TerminalAdapter, TerminalCapabilities } from "./adapter.js";
11
+ import type { LaunchMode } from "./detect.js";
12
+ export declare class ITerm2Adapter 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
+ activate(_processOrId: number | string): Promise<void>;
18
+ }
19
+ //# sourceMappingURL=iterm2-adapter.d.ts.map