u-foo 2.3.23 → 2.3.24

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/bus/inject.js +172 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.23",
3
+ "version": "2.3.24",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
package/src/bus/inject.js CHANGED
@@ -1,4 +1,4 @@
1
- const { spawn } = require("child_process");
1
+ const { spawn, spawnSync } = require("child_process");
2
2
  const fs = require("fs");
3
3
  const net = require("net");
4
4
  const path = require("path");
@@ -12,12 +12,46 @@ const logInject = (message) => {
12
12
  }
13
13
  };
14
14
 
15
+ function escapeAppleScriptString(value) {
16
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
17
+ }
18
+
19
+ function appleScriptStringLiteral(value) {
20
+ const lines = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
21
+ if (lines.length === 0) return '""';
22
+ return lines.map((line) => `"${escapeAppleScriptString(line)}"`).join(" & linefeed & ");
23
+ }
24
+
25
+ function runAppleScript(lines = []) {
26
+ return new Promise((resolve, reject) => {
27
+ const proc = spawn("osascript", lines.flatMap((line) => ["-e", line]));
28
+ let stderr = "";
29
+ let stdout = "";
30
+
31
+ proc.stdout.on("data", (data) => {
32
+ stdout += data.toString("utf8");
33
+ });
34
+ proc.stderr.on("data", (data) => {
35
+ stderr += data.toString("utf8");
36
+ });
37
+ proc.on("close", (code) => {
38
+ if (code === 0) {
39
+ resolve(stdout.trim());
40
+ } else {
41
+ reject(new Error(stderr.trim() || "AppleScript failed"));
42
+ }
43
+ });
44
+ proc.on("error", reject);
45
+ });
46
+ }
47
+
15
48
  /**
16
49
  * 命令注入器 - 将命令注入到终端
17
50
  *
18
51
  * 支持的方式:
19
52
  * 1. PTY socket(直接写入,无需macOS权限)
20
53
  * 2. tmux send-keys(无需权限)
54
+ * 3. Terminal.app/iTerm2 tty lookup(macOS terminal mode fallback)
21
55
  */
22
56
  class Injector {
23
57
  constructor(busDir, agentsFile) {
@@ -175,6 +209,128 @@ class Injector {
175
209
  textProc.on("error", reject);
176
210
  }
177
211
 
212
+ /**
213
+ * Use Terminal.app's tty metadata to locate the target tab and paste input.
214
+ */
215
+ async injectTerminal(tty, command) {
216
+ const ttyLiteral = appleScriptStringLiteral(tty);
217
+ const commandLiteral = appleScriptStringLiteral(command);
218
+ const lines = [
219
+ 'tell application "Terminal"',
220
+ " set targetWindow to missing value",
221
+ " set targetTab to missing value",
222
+ " repeat with w in windows",
223
+ " repeat with t in tabs of w",
224
+ " try",
225
+ ` if tty of t is ${ttyLiteral} then`,
226
+ " set targetWindow to w",
227
+ " set targetTab to t",
228
+ " exit repeat",
229
+ " end if",
230
+ " end try",
231
+ " end repeat",
232
+ " if targetTab is not missing value then exit repeat",
233
+ " end repeat",
234
+ " if targetTab is missing value then",
235
+ ` error "No Terminal tab found with tty: " & ${ttyLiteral}`,
236
+ " end if",
237
+ " activate",
238
+ " set selected tab of targetWindow to targetTab",
239
+ " set index of targetWindow to 1",
240
+ "end tell",
241
+ "set oldClipboard to the clipboard",
242
+ "try",
243
+ ` set the clipboard to ${commandLiteral}`,
244
+ " delay 0.1",
245
+ ' tell application "System Events"',
246
+ ' tell process "Terminal"',
247
+ " key code 53",
248
+ " delay 0.1",
249
+ ' keystroke "v" using command down',
250
+ " delay 0.2",
251
+ " keystroke return",
252
+ " end tell",
253
+ " end tell",
254
+ "on error errMsg number errNo",
255
+ " set the clipboard to oldClipboard",
256
+ " error errMsg number errNo",
257
+ "end try",
258
+ "delay 0.2",
259
+ "set the clipboard to oldClipboard",
260
+ ];
261
+
262
+ return runAppleScript(lines);
263
+ }
264
+
265
+ /**
266
+ * Check if iTerm2 is available before trying its direct write API.
267
+ */
268
+ isItermRunning() {
269
+ try {
270
+ const res = spawnSync("osascript", ["-e", 'application "iTerm2" is running'], {
271
+ encoding: "utf8",
272
+ stdio: ["ignore", "pipe", "ignore"],
273
+ });
274
+ return res.status === 0 && String(res.stdout || "").trim() === "true";
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Use iTerm2's tty metadata to locate the target session and write input.
282
+ */
283
+ async injectIterm(tty, command) {
284
+ const ttyLiteral = appleScriptStringLiteral(tty);
285
+ const commandLiteral = appleScriptStringLiteral(command);
286
+ const lines = [
287
+ 'tell application "iTerm2"',
288
+ " activate",
289
+ " repeat with w in windows",
290
+ " repeat with t in tabs of w",
291
+ " repeat with s in sessions of t",
292
+ " try",
293
+ ` if tty of s is ${ttyLiteral} then`,
294
+ " select s",
295
+ ` write text ${commandLiteral} to s`,
296
+ " return",
297
+ " end if",
298
+ " end try",
299
+ " end repeat",
300
+ " end repeat",
301
+ " end repeat",
302
+ ` error "No iTerm2 session found with tty: " & ${ttyLiteral}`,
303
+ "end tell",
304
+ ];
305
+
306
+ return runAppleScript(lines);
307
+ }
308
+
309
+ async injectMacTerminal(tty, command, meta = {}) {
310
+ if (process.platform !== "darwin") {
311
+ throw new Error("Terminal.app injection is only supported on macOS");
312
+ }
313
+
314
+ const terminalApp = String(meta.terminal_app || meta.terminalApp || "").trim().toLowerCase();
315
+ const shouldTryIterm = terminalApp === "iterm2" || (!terminalApp && this.isItermRunning());
316
+
317
+ if (shouldTryIterm) {
318
+ try {
319
+ logInject(`[inject] Using iTerm2 AppleScript for tty: ${tty}`);
320
+ await this.injectIterm(tty, command);
321
+ return;
322
+ } catch (err) {
323
+ if (terminalApp === "iterm2") {
324
+ throw err;
325
+ }
326
+ logInject(`[inject] iTerm2 failed: ${err.message}, trying Terminal.app`);
327
+ }
328
+ }
329
+
330
+ logInject(`[inject] Using Terminal.app AppleScript for tty: ${tty}`);
331
+ await this.injectTerminal(tty, command);
332
+ }
333
+
178
334
  /**
179
335
  * 获取订阅者的 inject socket 路径
180
336
  */
@@ -252,6 +408,7 @@ class Injector {
252
408
  * 优先级:
253
409
  * 1. PTY socket(直接写入,无需macOS权限)
254
410
  * 2. tmux send-keys(无需权限)
411
+ * 3. Terminal.app/iTerm2 tty lookup(terminal mode fallback)
255
412
  */
256
413
  async inject(subscriber, commandOverride = "") {
257
414
  if (String(subscriber || "").startsWith("ufoo-code:")) {
@@ -300,8 +457,10 @@ class Injector {
300
457
  }
301
458
  }
302
459
 
303
- // 读取 tty(tmux 需要)
304
- const tty = this.readTty(subscriber);
460
+ // 读取 tty(tmux/Terminal.app fallback 需要)
461
+ const recordedTty = this.readTty(subscriber);
462
+ const metaTty = isValidTty(meta.tty) ? meta.tty : "";
463
+ const tty = recordedTty || metaTty;
305
464
 
306
465
  // 2. 尝试 tmux(无需权限)
307
466
  // Launch mode may be temporarily missing/stale (e.g. rejoin from non-interactive context).
@@ -329,8 +488,17 @@ class Injector {
329
488
  }
330
489
  }
331
490
 
491
+ // 3. Plain Terminal.app/iTerm2 fallback: locate the window/tab by tty and paste input.
492
+ const allowMacTerminalFallback = (launchMode === "terminal" || (!launchMode && tty && isValidTty(tty)))
493
+ && tty
494
+ && isValidTty(tty);
495
+ if (allowMacTerminalFallback) {
496
+ await this.injectMacTerminal(tty, command, meta);
497
+ return;
498
+ }
499
+
332
500
  // 没有可用的注入方式
333
- throw new Error(`No inject method available for ${subscriber}. PTY socket or tmux required.`);
501
+ throw new Error(`No inject method available for ${subscriber}. PTY socket, tmux, or Terminal.app injection required.`);
334
502
  }
335
503
  }
336
504