@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,106 @@
1
+ import { dim, formatBytes } from "./colors.js";
2
+ const TERMINAL_RESET = "\x1b[0m\x1b[?25h\x1b[?2026l\x1b[?1049l\x1b[?1047l\x1b[?47l";
3
+ export class PtyRenderer {
4
+ color = (s) => s;
5
+ engineLabel = "";
6
+ role = "";
7
+ receivedBytes = 0;
8
+ started = false;
9
+ endedWithLineBreak = true;
10
+ start(engineLabel, role, color) {
11
+ if (this.started)
12
+ return;
13
+ this.started = true;
14
+ this.color = color;
15
+ this.engineLabel = engineLabel;
16
+ this.role = role;
17
+ this.receivedBytes = 0;
18
+ const header = this.color(` \u250C\u2500 \u25A0 ${this.engineLabel} (${this.role}) ${"\u2500".repeat(Math.max(0, 44 - this.engineLabel.length - this.role.length - 3))}\u2510`);
19
+ console.log(header);
20
+ console.log(this.color(" \u2502"));
21
+ this.endedWithLineBreak = true;
22
+ }
23
+ write(data) {
24
+ if (!this.started)
25
+ return;
26
+ this.receivedBytes += Buffer.byteLength(data);
27
+ process.stdout.write(data);
28
+ this.endedWithLineBreak = /(?:\r\n|\r|\n)$/.test(data);
29
+ }
30
+ stop(stats) {
31
+ if (!this.started)
32
+ return;
33
+ this.started = false;
34
+ // Restore common terminal modes in case the executor CLI was killed mid-TUI
35
+ process.stdout.write(TERMINAL_RESET);
36
+ if (!this.endedWithLineBreak) {
37
+ process.stdout.write("\r\n");
38
+ }
39
+ const elapsed = formatElapsed(stats.elapsed_ms);
40
+ const bytes = formatBytes(stats.bytes);
41
+ console.log(this.color(" \u2502"));
42
+ const statsText = `\u2713 done ${dim(`(${elapsed}, ${bytes})`)}`;
43
+ const footer = this.color(` \u2514${"\u2500".repeat(42)}`) +
44
+ ` ${statsText} ` +
45
+ this.color("\u2500\u2518");
46
+ console.log(footer);
47
+ }
48
+ get totalBytes() {
49
+ return this.receivedBytes;
50
+ }
51
+ }
52
+ function formatElapsed(ms) {
53
+ if (ms < 1000)
54
+ return `${ms}ms`;
55
+ const sec = ms / 1000;
56
+ if (sec < 60)
57
+ return `${sec.toFixed(1)}s`;
58
+ const min = Math.floor(sec / 60);
59
+ const remainSec = Math.floor(sec % 60);
60
+ return `${min}m${remainSec}s`;
61
+ }
62
+ /**
63
+ * Forward user input to the PTY while reserving a small set of
64
+ * loop control shortcuts.
65
+ * Returns a cleanup function.
66
+ */
67
+ export function startKeystrokeHandler(writeToPty, opts) {
68
+ const isTTY = !!process.stdin.isTTY;
69
+ if (isTTY)
70
+ process.stdin.setRawMode(true);
71
+ process.stdin.resume();
72
+ let lastCtrlC = 0;
73
+ function onData(data) {
74
+ const str = typeof data === "string" ? data : data.toString("utf8");
75
+ // Shift+Tab: toggle mode
76
+ if (str === "\x1b[Z") {
77
+ opts.onModeToggle();
78
+ return;
79
+ }
80
+ // Ctrl+D: done
81
+ if (str === "\x04") {
82
+ opts.onDone();
83
+ return;
84
+ }
85
+ // Ctrl+C: double-tap to cancel, single forwards to PTY
86
+ if (str === "\x03") {
87
+ const now = Date.now();
88
+ if (now - lastCtrlC < 500) {
89
+ opts.onCancel();
90
+ return;
91
+ }
92
+ lastCtrlC = now;
93
+ writeToPty(str);
94
+ return;
95
+ }
96
+ writeToPty(str);
97
+ }
98
+ process.stdin.on("data", onData);
99
+ return () => {
100
+ process.stdin.removeListener("data", onData);
101
+ if (isTTY)
102
+ process.stdin.setRawMode(false);
103
+ process.stdin.pause();
104
+ };
105
+ }
106
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Strip all ANSI escape sequences from a string.
3
+ * Delegates to the battle-tested `strip-ansi` package for robust PTY handling.
4
+ */
5
+ export declare function stripAnsi(s: string): string;
6
+ /**
7
+ * Returns true if the string consists entirely of ANSI escape codes
8
+ * (i.e., stripping them yields an empty string).
9
+ */
10
+ export declare function isAnsiOnly(s: string): boolean;
11
+ //# sourceMappingURL=ansi.d.ts.map
@@ -0,0 +1,16 @@
1
+ import stripAnsiModule from "strip-ansi";
2
+ /**
3
+ * Strip all ANSI escape sequences from a string.
4
+ * Delegates to the battle-tested `strip-ansi` package for robust PTY handling.
5
+ */
6
+ export function stripAnsi(s) {
7
+ return stripAnsiModule(s);
8
+ }
9
+ /**
10
+ * Returns true if the string consists entirely of ANSI escape codes
11
+ * (i.e., stripping them yields an empty string).
12
+ */
13
+ export function isAnsiOnly(s) {
14
+ return s.length > 0 && stripAnsiModule(s).length === 0;
15
+ }
16
+ //# sourceMappingURL=ansi.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Ensure a directory exists, creating it recursively if needed.
3
+ */
4
+ export declare function ensureDir(dir: string): Promise<void>;
5
+ /**
6
+ * Append a JSON object as a single line to a JSONL file.
7
+ */
8
+ export declare function appendJsonl(filePath: string, data: unknown): Promise<void>;
9
+ /**
10
+ * Read all lines from a JSONL file, parsing each as JSON.
11
+ * Returns an empty array if the file does not exist.
12
+ */
13
+ export declare function readJsonl<T>(filePath: string): Promise<T[]>;
14
+ /**
15
+ * Atomically write a file by writing to a temp file then renaming.
16
+ */
17
+ export declare function safeWriteFile(filePath: string, content: string): Promise<void>;
18
+ /**
19
+ * Read a file's contents, returning null if it does not exist.
20
+ */
21
+ export declare function safeReadFile(filePath: string): Promise<string | null>;
22
+ /**
23
+ * Check if a file exists.
24
+ */
25
+ export declare function fileExists(filePath: string): Promise<boolean>;
26
+ /**
27
+ * Truncate a file to empty (or create it if it doesn't exist).
28
+ */
29
+ export declare function truncateFile(filePath: string): Promise<void>;
30
+ /**
31
+ * Read the last non-empty line from a file.
32
+ */
33
+ export declare function readLastLine(filePath: string): Promise<string | null>;
34
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1,115 @@
1
+ import { mkdir, appendFile, readFile, writeFile, access, rename, unlink } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ /**
4
+ * Ensure a directory exists, creating it recursively if needed.
5
+ */
6
+ export async function ensureDir(dir) {
7
+ await mkdir(dir, { recursive: true });
8
+ }
9
+ /**
10
+ * Append a JSON object as a single line to a JSONL file.
11
+ */
12
+ export async function appendJsonl(filePath, data) {
13
+ await ensureDir(dirname(filePath));
14
+ const line = JSON.stringify(data) + "\n";
15
+ await appendFile(filePath, line, "utf8");
16
+ }
17
+ /**
18
+ * Read all lines from a JSONL file, parsing each as JSON.
19
+ * Returns an empty array if the file does not exist.
20
+ */
21
+ export async function readJsonl(filePath) {
22
+ let content;
23
+ try {
24
+ content = await readFile(filePath, "utf8");
25
+ }
26
+ catch (err) {
27
+ if (isNodeError(err) && err.code === "ENOENT") {
28
+ return [];
29
+ }
30
+ throw err;
31
+ }
32
+ const lines = content.trim().split("\n").filter(Boolean);
33
+ const results = [];
34
+ for (const line of lines) {
35
+ try {
36
+ results.push(JSON.parse(line));
37
+ }
38
+ catch {
39
+ // Skip malformed lines
40
+ }
41
+ }
42
+ return results;
43
+ }
44
+ /**
45
+ * Atomically write a file by writing to a temp file then renaming.
46
+ */
47
+ export async function safeWriteFile(filePath, content) {
48
+ await ensureDir(dirname(filePath));
49
+ const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
50
+ try {
51
+ await writeFile(tmpPath, content, "utf8");
52
+ await rename(tmpPath, filePath);
53
+ }
54
+ catch (err) {
55
+ // Clean up temp file on failure
56
+ try {
57
+ await unlink(tmpPath);
58
+ }
59
+ catch {
60
+ // Ignore cleanup errors
61
+ }
62
+ throw err;
63
+ }
64
+ }
65
+ /**
66
+ * Read a file's contents, returning null if it does not exist.
67
+ */
68
+ export async function safeReadFile(filePath) {
69
+ try {
70
+ return await readFile(filePath, "utf8");
71
+ }
72
+ catch (err) {
73
+ if (isNodeError(err) && err.code === "ENOENT") {
74
+ return null;
75
+ }
76
+ throw err;
77
+ }
78
+ }
79
+ /**
80
+ * Check if a file exists.
81
+ */
82
+ export async function fileExists(filePath) {
83
+ try {
84
+ await access(filePath);
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ /**
92
+ * Truncate a file to empty (or create it if it doesn't exist).
93
+ */
94
+ export async function truncateFile(filePath) {
95
+ await ensureDir(dirname(filePath));
96
+ await writeFile(filePath, "", "utf8");
97
+ }
98
+ /**
99
+ * Read the last non-empty line from a file.
100
+ */
101
+ export async function readLastLine(filePath) {
102
+ let content;
103
+ try {
104
+ content = await readFile(filePath, "utf8");
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ const lines = content.trim().split("\n").filter(Boolean);
110
+ return lines.length > 0 ? lines[lines.length - 1] : null;
111
+ }
112
+ function isNodeError(err) {
113
+ return err instanceof Error && "code" in err;
114
+ }
115
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Acquire a file-based lock, execute a function, then release.
3
+ * The lock file contains the PID of the holder.
4
+ * Stale locks (dead PID or older than 30s) are automatically cleaned.
5
+ */
6
+ export declare function withFileLock<T>(lockPath: string, fn: () => Promise<T>, timeout?: number): Promise<T>;
7
+ /**
8
+ * Atomically increment a counter file under a file lock.
9
+ * If the counter file is missing or corrupt, attempts to recover from 0.
10
+ */
11
+ export declare function nextSeq(counterPath: string, lockPath: string): Promise<number>;
12
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1,116 @@
1
+ import { openSync, writeSync, closeSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { isProcessAlive } from "./process.js";
4
+ const LOCK_TIMEOUT_MS = 5000;
5
+ const LOCK_POLL_MS = 25;
6
+ const LOCK_STALE_MS = 30000;
7
+ function isNodeError(err) {
8
+ return err instanceof Error && "code" in err;
9
+ }
10
+ function cleanupStaleLock(lockPath) {
11
+ let shouldRemove = false;
12
+ try {
13
+ const raw = readFileSync(lockPath, "utf8").trim();
14
+ const pid = parseInt(raw, 10);
15
+ if (!Number.isFinite(pid) || pid <= 0) {
16
+ shouldRemove = true;
17
+ }
18
+ else if (!isProcessAlive(pid)) {
19
+ shouldRemove = true;
20
+ }
21
+ }
22
+ catch {
23
+ shouldRemove = true;
24
+ }
25
+ if (!shouldRemove) {
26
+ try {
27
+ const stat = statSync(lockPath);
28
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
29
+ shouldRemove = true;
30
+ }
31
+ }
32
+ catch {
33
+ shouldRemove = true;
34
+ }
35
+ }
36
+ if (shouldRemove) {
37
+ try {
38
+ unlinkSync(lockPath);
39
+ }
40
+ catch {
41
+ // Ignore cleanup errors
42
+ }
43
+ }
44
+ }
45
+ function sleep(ms) {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+ /**
49
+ * Acquire a file-based lock, execute a function, then release.
50
+ * The lock file contains the PID of the holder.
51
+ * Stale locks (dead PID or older than 30s) are automatically cleaned.
52
+ */
53
+ export async function withFileLock(lockPath, fn, timeout = LOCK_TIMEOUT_MS) {
54
+ mkdirSync(dirname(lockPath), { recursive: true });
55
+ const deadline = Date.now() + timeout;
56
+ let lockFd = null;
57
+ while (Date.now() < deadline) {
58
+ try {
59
+ lockFd = openSync(lockPath, "wx");
60
+ writeSync(lockFd, `${process.pid}\n`);
61
+ break;
62
+ }
63
+ catch (err) {
64
+ if (isNodeError(err) && err.code === "EEXIST") {
65
+ cleanupStaleLock(lockPath);
66
+ await sleep(LOCK_POLL_MS);
67
+ continue;
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+ if (lockFd === null) {
73
+ throw new Error(`Failed to acquire file lock: ${lockPath} (timeout ${timeout}ms)`);
74
+ }
75
+ try {
76
+ return await fn();
77
+ }
78
+ finally {
79
+ try {
80
+ closeSync(lockFd);
81
+ }
82
+ catch {
83
+ // Ignore
84
+ }
85
+ try {
86
+ unlinkSync(lockPath);
87
+ }
88
+ catch {
89
+ // Ignore
90
+ }
91
+ }
92
+ }
93
+ /**
94
+ * Atomically increment a counter file under a file lock.
95
+ * If the counter file is missing or corrupt, attempts to recover from 0.
96
+ */
97
+ export async function nextSeq(counterPath, lockPath) {
98
+ return withFileLock(lockPath, async () => {
99
+ let current = 0;
100
+ try {
101
+ const raw = readFileSync(counterPath, "utf8").trim();
102
+ const parsed = parseInt(raw, 10);
103
+ if (Number.isFinite(parsed) && parsed > 0) {
104
+ current = parsed;
105
+ }
106
+ }
107
+ catch {
108
+ // Counter file missing or unreadable - start from 0
109
+ }
110
+ const next = current + 1;
111
+ mkdirSync(dirname(counterPath), { recursive: true });
112
+ writeFileSync(counterPath, `${next}\n`, "utf8");
113
+ return next;
114
+ });
115
+ }
116
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Check if a process with the given PID is alive.
3
+ */
4
+ export declare function isProcessAlive(pid: number): boolean;
5
+ /**
6
+ * Set up signal handlers for graceful shutdown.
7
+ * The cleanup function is called once on the first signal received.
8
+ */
9
+ export declare function setupSignalHandlers(cleanup: () => Promise<void>): void;
10
+ /**
11
+ * Daemonize a script by spawning it detached with stdio redirected to a log file.
12
+ * Returns the PID of the child process.
13
+ */
14
+ export declare function daemonize(script: string, args: string[], logPath: string): number;
15
+ /**
16
+ * Write the current process PID to a file.
17
+ */
18
+ export declare function writePidFile(pidPath: string): void;
19
+ /**
20
+ * Read a PID from a file, returning null if missing or invalid.
21
+ */
22
+ export declare function readPidFile(pidPath: string): number | null;
23
+ /**
24
+ * Remove a PID file.
25
+ */
26
+ export declare function removePidFile(pidPath: string): void;
27
+ /**
28
+ * Get the absolute path to a script within this package.
29
+ */
30
+ export declare function resolveScript(relativePath: string): string;
31
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1,111 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync, writeFileSync, unlinkSync, openSync, closeSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { mkdirSync } from "node:fs";
5
+ function isNodeError(err) {
6
+ return err instanceof Error && "code" in err;
7
+ }
8
+ /**
9
+ * Check if a process with the given PID is alive.
10
+ */
11
+ export function isProcessAlive(pid) {
12
+ if (!Number.isFinite(pid) || pid <= 0) {
13
+ return false;
14
+ }
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch (err) {
20
+ if (isNodeError(err) && err.code === "EPERM") {
21
+ // Process exists but we lack permission to signal it
22
+ return true;
23
+ }
24
+ return false;
25
+ }
26
+ }
27
+ /**
28
+ * Set up signal handlers for graceful shutdown.
29
+ * The cleanup function is called once on the first signal received.
30
+ */
31
+ export function setupSignalHandlers(cleanup) {
32
+ let cleaning = false;
33
+ const handler = () => {
34
+ if (cleaning)
35
+ return;
36
+ cleaning = true;
37
+ cleanup().finally(() => {
38
+ process.exit(0);
39
+ });
40
+ };
41
+ process.on("SIGTERM", handler);
42
+ process.on("SIGINT", handler);
43
+ process.on("exit", () => {
44
+ // Synchronous cleanup on exit - best effort
45
+ if (!cleaning) {
46
+ cleaning = true;
47
+ // Can't await here, but we try to be helpful
48
+ }
49
+ });
50
+ }
51
+ /**
52
+ * Daemonize a script by spawning it detached with stdio redirected to a log file.
53
+ * Returns the PID of the child process.
54
+ */
55
+ export function daemonize(script, args, logPath) {
56
+ mkdirSync(dirname(logPath), { recursive: true });
57
+ const logFd = openSync(logPath, "a");
58
+ const child = spawn(process.execPath, [script, ...args], {
59
+ detached: true,
60
+ stdio: ["ignore", logFd, logFd],
61
+ cwd: process.cwd(),
62
+ });
63
+ child.unref();
64
+ closeSync(logFd);
65
+ const pid = child.pid;
66
+ if (pid === undefined) {
67
+ throw new Error("Failed to spawn daemon process");
68
+ }
69
+ return pid;
70
+ }
71
+ /**
72
+ * Write the current process PID to a file.
73
+ */
74
+ export function writePidFile(pidPath) {
75
+ mkdirSync(dirname(pidPath), { recursive: true });
76
+ writeFileSync(pidPath, `${process.pid}\n`, "utf8");
77
+ }
78
+ /**
79
+ * Read a PID from a file, returning null if missing or invalid.
80
+ */
81
+ export function readPidFile(pidPath) {
82
+ try {
83
+ const raw = readFileSync(pidPath, "utf8").trim();
84
+ const pid = parseInt(raw, 10);
85
+ if (Number.isFinite(pid) && pid > 0) {
86
+ return pid;
87
+ }
88
+ return null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Remove a PID file.
96
+ */
97
+ export function removePidFile(pidPath) {
98
+ try {
99
+ unlinkSync(pidPath);
100
+ }
101
+ catch {
102
+ // Ignore - file may not exist
103
+ }
104
+ }
105
+ /**
106
+ * Get the absolute path to a script within this package.
107
+ */
108
+ export function resolveScript(relativePath) {
109
+ return resolve(relativePath);
110
+ }
111
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * PTY output classification and filtering.
3
+ *
4
+ * Ported from iterloop's pty-session.ts — classifies raw terminal output lines
5
+ * into content, status updates, or ignorable TUI noise so that only meaningful
6
+ * text reaches the reviewer and transcript.
7
+ */
8
+ export type LineClass = "content" | "status" | "ignore";
9
+ /**
10
+ * Classify a single (ANSI-stripped) line of PTY output.
11
+ *
12
+ * - `"content"` — meaningful AI output to keep
13
+ * - `"status"` — transient status / spinner text
14
+ * - `"ignore"` — TUI chrome, empty lines, noise
15
+ */
16
+ export declare function classifyLine(line: string, engine?: string): LineClass;
17
+ /**
18
+ * Filter raw PTY output to extract only meaningful content.
19
+ *
20
+ * Strips TUI chrome, spinners, box-drawing, update notices, and other noise.
21
+ * For Claude output, strips the ⏺ content marker prefix.
22
+ *
23
+ * Result is capped at ~50 KB (last 50 KB on overflow).
24
+ */
25
+ export declare function filterOutput(raw: string, _engine?: string): string;
26
+ /**
27
+ * Remove duplicate consecutive lines (TUI re-renders can emit the same
28
+ * content multiple times). Comparison ignores whitespace differences.
29
+ */
30
+ export declare function deduplicateLines(lines: string[]): string[];
31
+ //# sourceMappingURL=pty-filter.d.ts.map