@suzuke/agend 1.0.2 → 1.1.1

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.
@@ -0,0 +1,184 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import { createInterface } from "node:readline";
4
+ function execTmux(args) {
5
+ return new Promise((resolve, reject) => {
6
+ execFile("tmux", args, (err, stdout) => {
7
+ if (err)
8
+ reject(err);
9
+ else
10
+ resolve(stdout.trim());
11
+ });
12
+ });
13
+ }
14
+ /**
15
+ * Persistent tmux control mode client that monitors %output events
16
+ * to detect per-pane idle state. One instance per tmux session.
17
+ *
18
+ * Usage:
19
+ * const ctrl = new TmuxControlClient("agend", 2000, logger);
20
+ * ctrl.start();
21
+ * await ctrl.waitForIdle("@5"); // wait until window @5 is idle
22
+ * tmux.pasteText(msg);
23
+ */
24
+ export class TmuxControlClient extends EventEmitter {
25
+ sessionName;
26
+ silenceMs;
27
+ logger;
28
+ proc = null;
29
+ rl = null;
30
+ lastOutputAt = new Map(); // paneId → timestamp
31
+ paneToWindow = new Map(); // paneId → windowId
32
+ stopped = false;
33
+ reconnectTimer = null;
34
+ constructor(sessionName, silenceMs = 2000, logger) {
35
+ super();
36
+ this.sessionName = sessionName;
37
+ this.silenceMs = silenceMs;
38
+ this.logger = logger;
39
+ }
40
+ start() {
41
+ this.stopped = false;
42
+ this.connect();
43
+ }
44
+ // PLACEHOLDER_REST
45
+ stop() {
46
+ this.stopped = true;
47
+ if (this.reconnectTimer) {
48
+ clearTimeout(this.reconnectTimer);
49
+ this.reconnectTimer = null;
50
+ }
51
+ this.cleanup();
52
+ }
53
+ /**
54
+ * Register a window so we can track its pane's output.
55
+ * Call this after createWindow().
56
+ */
57
+ async registerWindow(windowId) {
58
+ try {
59
+ const paneId = await execTmux([
60
+ "list-panes", "-t", `${this.sessionName}:${windowId}`,
61
+ "-F", "#{pane_id}",
62
+ ]);
63
+ if (paneId) {
64
+ this.paneToWindow.set(paneId, windowId);
65
+ this.logger?.debug({ windowId, paneId }, "Registered window→pane mapping");
66
+ }
67
+ }
68
+ catch {
69
+ this.logger?.debug({ windowId }, "Failed to resolve pane ID for window");
70
+ }
71
+ }
72
+ /** Unregister a window (call on killWindow) */
73
+ unregisterWindow(windowId) {
74
+ for (const [pane, win] of this.paneToWindow) {
75
+ if (win === windowId) {
76
+ this.paneToWindow.delete(pane);
77
+ this.lastOutputAt.delete(pane);
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ /** Check if a window's pane has been silent for at least silenceMs */
83
+ isIdle(windowId) {
84
+ const paneId = this.windowToPaneId(windowId);
85
+ if (!paneId)
86
+ return true; // unknown window = assume idle
87
+ const last = this.lastOutputAt.get(paneId);
88
+ if (last == null)
89
+ return true;
90
+ return Date.now() - last >= this.silenceMs;
91
+ }
92
+ // PLACEHOLDER_WAIT
93
+ /**
94
+ * Wait until a window's pane is idle (no output for silenceMs).
95
+ * Returns true if idle detected, false if timeout reached.
96
+ */
97
+ waitForIdle(windowId, timeoutMs = 30_000) {
98
+ if (this.isIdle(windowId))
99
+ return Promise.resolve(true);
100
+ return new Promise((resolve) => {
101
+ const check = setInterval(() => {
102
+ if (this.isIdle(windowId)) {
103
+ clearInterval(check);
104
+ clearTimeout(timer);
105
+ resolve(true);
106
+ }
107
+ }, 200);
108
+ const timer = setTimeout(() => {
109
+ clearInterval(check);
110
+ this.logger?.warn({ windowId, timeoutMs }, "waitForIdle timed out — forcing delivery");
111
+ resolve(false);
112
+ }, timeoutMs);
113
+ });
114
+ }
115
+ /**
116
+ * Wait until a window's pane produces any output.
117
+ * Used to verify CLI startup — if no output within timeout, CLI likely failed.
118
+ */
119
+ waitForOutput(windowId, timeoutMs = 15_000) {
120
+ const paneId = this.windowToPaneId(windowId);
121
+ // If already has output recorded, it's alive
122
+ if (paneId && this.lastOutputAt.has(paneId))
123
+ return Promise.resolve(true);
124
+ return new Promise((resolve) => {
125
+ const check = setInterval(() => {
126
+ const pid = this.windowToPaneId(windowId);
127
+ if (pid && this.lastOutputAt.has(pid)) {
128
+ clearInterval(check);
129
+ clearTimeout(timer);
130
+ resolve(true);
131
+ }
132
+ }, 300);
133
+ const timer = setTimeout(() => {
134
+ clearInterval(check);
135
+ resolve(false);
136
+ }, timeoutMs);
137
+ });
138
+ }
139
+ windowToPaneId(windowId) {
140
+ for (const [pane, win] of this.paneToWindow) {
141
+ if (win === windowId)
142
+ return pane;
143
+ }
144
+ return undefined;
145
+ }
146
+ connect() {
147
+ if (this.stopped)
148
+ return;
149
+ this.proc = spawn("tmux", ["-C", "attach", "-t", this.sessionName, "-r"], {
150
+ stdio: ["pipe", "pipe", "pipe"],
151
+ });
152
+ this.rl = createInterface({ input: this.proc.stdout });
153
+ this.rl.on("line", (line) => this.parseLine(line));
154
+ this.proc.on("close", () => {
155
+ this.cleanup();
156
+ if (!this.stopped) {
157
+ this.logger?.debug("Control mode disconnected — reconnecting in 2s");
158
+ this.reconnectTimer = setTimeout(() => this.connect(), 2000);
159
+ }
160
+ });
161
+ this.proc.on("error", (err) => {
162
+ this.logger?.warn({ err: err.message }, "Control mode spawn error");
163
+ });
164
+ this.logger?.debug("tmux control mode connected");
165
+ }
166
+ parseLine(line) {
167
+ if (line.startsWith("%output ")) {
168
+ const match = line.match(/^%output (%\d+) /);
169
+ if (match) {
170
+ this.lastOutputAt.set(match[1], Date.now());
171
+ }
172
+ }
173
+ }
174
+ cleanup() {
175
+ this.rl?.close();
176
+ this.rl = null;
177
+ if (this.proc && !this.proc.killed) {
178
+ this.proc.stdin?.write("detach\n");
179
+ this.proc.kill();
180
+ }
181
+ this.proc = null;
182
+ }
183
+ }
184
+ //# sourceMappingURL=tmux-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux-control.js","sourceRoot":"","sources":["../src/tmux-control.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suzuke/agend",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {