@calltelemetry/openclaw-linear 0.9.18 → 0.9.19
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/package.json +1 -1
- package/src/infra/tmux-runner.ts +50 -14
package/package.json
CHANGED
package/src/infra/tmux-runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync, spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import { mkdirSync,
|
|
3
|
+
import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import type { ActivityContent, LinearAgentApi } from "../api/linear-api.js";
|
|
6
6
|
import type { CliResult, OnProgressUpdate } from "../tools/cli-shared.js";
|
|
@@ -45,8 +45,12 @@ export function getActiveTmuxSession(issueId: string): TmuxSession | null {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Run a command inside a tmux session with
|
|
48
|
+
* Run a command inside a tmux session with output piped to a JSONL log.
|
|
49
49
|
* Monitors the log file for events and streams them to Linear.
|
|
50
|
+
*
|
|
51
|
+
* The command + tee are wrapped in a shell script so that tee runs INSIDE
|
|
52
|
+
* the tmux session (not in the outer shell). This ensures JSONL output
|
|
53
|
+
* from the CLI subprocess is captured to the log file.
|
|
50
54
|
*/
|
|
51
55
|
export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
52
56
|
const {
|
|
@@ -70,6 +74,9 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
70
74
|
// Ensure log directory exists
|
|
71
75
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
72
76
|
|
|
77
|
+
// Touch the log file so tail -f can start immediately
|
|
78
|
+
writeFileSync(logPath, "", { flag: "a" });
|
|
79
|
+
|
|
73
80
|
// Register active session
|
|
74
81
|
const session: TmuxSession = {
|
|
75
82
|
sessionName,
|
|
@@ -83,14 +90,25 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
83
90
|
const progress = createProgressEmitter({ header: progressHeader, onUpdate });
|
|
84
91
|
progress.emitHeader();
|
|
85
92
|
|
|
93
|
+
// Write a shell wrapper script so the entire pipeline (command | tee)
|
|
94
|
+
// runs inside the tmux session. This avoids quoting hell and ensures
|
|
95
|
+
// tee captures the subprocess output, not tmux's own stdout.
|
|
96
|
+
const scriptPath = `${logPath}.run.sh`;
|
|
97
|
+
|
|
86
98
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`
|
|
90
|
-
|
|
91
|
-
].join("
|
|
99
|
+
writeFileSync(scriptPath, [
|
|
100
|
+
"#!/bin/sh",
|
|
101
|
+
`exec ${command} 2>&1 | tee -a ${shellEscape(logPath)}`,
|
|
102
|
+
"",
|
|
103
|
+
].join("\n"), { mode: 0o755 });
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
// Start tmux session running the wrapper script
|
|
106
|
+
execSync(
|
|
107
|
+
`tmux new-session -d -s ${shellEscape(sessionName)} -c ${shellEscape(cwd)} ${shellEscape(scriptPath)}`,
|
|
108
|
+
{ stdio: "ignore", timeout: 10_000 },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
logger.info(`tmux session started: ${sessionName} (log: ${logPath})`);
|
|
94
112
|
|
|
95
113
|
// Tail the log file and process JSONL events
|
|
96
114
|
return await new Promise<CliResult>((resolve) => {
|
|
@@ -100,6 +118,7 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
100
118
|
|
|
101
119
|
let killed = false;
|
|
102
120
|
let killedByWatchdog = false;
|
|
121
|
+
let resolved = false;
|
|
103
122
|
const collectedMessages: string[] = [];
|
|
104
123
|
|
|
105
124
|
const timer = setTimeout(() => {
|
|
@@ -120,6 +139,9 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
120
139
|
watchdog.start();
|
|
121
140
|
|
|
122
141
|
function cleanup(reason: string) {
|
|
142
|
+
if (resolved) return;
|
|
143
|
+
resolved = true;
|
|
144
|
+
|
|
123
145
|
clearTimeout(timer);
|
|
124
146
|
watchdog.stop();
|
|
125
147
|
tail.kill();
|
|
@@ -132,6 +154,9 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
132
154
|
});
|
|
133
155
|
} catch { /* session may already be gone */ }
|
|
134
156
|
|
|
157
|
+
// Clean up wrapper script
|
|
158
|
+
try { unlinkSync(scriptPath); } catch { /* best effort */ }
|
|
159
|
+
|
|
135
160
|
activeSessions.delete(issueId);
|
|
136
161
|
|
|
137
162
|
const output = collectedMessages.join("\n\n") || "(no output)";
|
|
@@ -169,8 +194,9 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
169
194
|
return;
|
|
170
195
|
}
|
|
171
196
|
|
|
172
|
-
// Collect text for output
|
|
197
|
+
// Collect text for output — handle both Claude and Codex event shapes
|
|
173
198
|
if (event.type === "assistant") {
|
|
199
|
+
// Claude stream-json shape
|
|
174
200
|
const content = event.message?.content;
|
|
175
201
|
if (Array.isArray(content)) {
|
|
176
202
|
for (const block of content) {
|
|
@@ -180,6 +206,11 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
180
206
|
}
|
|
181
207
|
}
|
|
182
208
|
}
|
|
209
|
+
if (event.item?.type === "agent_message" || event.item?.type === "message") {
|
|
210
|
+
// Codex --json shape
|
|
211
|
+
const text = event.item.text ?? event.item.content ?? "";
|
|
212
|
+
if (text) collectedMessages.push(text);
|
|
213
|
+
}
|
|
183
214
|
|
|
184
215
|
// Stream to Linear
|
|
185
216
|
const activity = mapEvent(event);
|
|
@@ -192,16 +223,16 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
192
223
|
progress.push(formatActivityLogLine(activity));
|
|
193
224
|
}
|
|
194
225
|
|
|
195
|
-
// Detect completion
|
|
196
|
-
if (event.type === "result") {
|
|
226
|
+
// Detect completion — Claude uses "result", Codex uses "session.completed"
|
|
227
|
+
if (event.type === "result" || event.type === "session.completed") {
|
|
197
228
|
cleanup("done");
|
|
198
229
|
rl.close();
|
|
199
230
|
}
|
|
200
231
|
});
|
|
201
232
|
|
|
202
|
-
// Handle tail process ending (tmux session
|
|
233
|
+
// Handle tail process ending (tmux session exited)
|
|
203
234
|
tail.on("close", () => {
|
|
204
|
-
if (!
|
|
235
|
+
if (!resolved) {
|
|
205
236
|
cleanup("done");
|
|
206
237
|
}
|
|
207
238
|
rl.close();
|
|
@@ -209,11 +240,16 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
|
|
|
209
240
|
|
|
210
241
|
tail.on("error", (err) => {
|
|
211
242
|
logger.error(`tmux tail error: ${err}`);
|
|
212
|
-
|
|
243
|
+
if (!resolved) {
|
|
244
|
+
cleanup("error");
|
|
245
|
+
}
|
|
213
246
|
rl.close();
|
|
214
247
|
});
|
|
215
248
|
});
|
|
216
249
|
} catch (err) {
|
|
250
|
+
// Clean up wrapper script on failure
|
|
251
|
+
try { unlinkSync(scriptPath); } catch { /* best effort */ }
|
|
252
|
+
|
|
217
253
|
activeSessions.delete(issueId);
|
|
218
254
|
logger.error(`runInTmux failed: ${err}`);
|
|
219
255
|
return {
|