@ai-devkit/agent-manager 0.7.0 → 0.9.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.
@@ -7,10 +7,13 @@ const execFileAsync = promisify(execFile);
7
7
 
8
8
  /**
9
9
  * Escape a string for safe use inside an AppleScript double-quoted string.
10
- * Backslashes and double quotes must be escaped.
10
+ * Backslashes, double quotes, and newlines must be escaped.
11
11
  */
12
12
  function escapeAppleScript(text: string): string {
13
- return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
13
+ return text
14
+ .replace(/\\/g, '\\\\')
15
+ .replace(/"/g, '\\"')
16
+ .replace(/\r\n|\r|\n/g, '\\n');
14
17
  }
15
18
 
16
19
  export class TtyWriter {
@@ -19,8 +22,8 @@ export class TtyWriter {
19
22
  *
20
23
  * Dispatches to the correct mechanism based on terminal type:
21
24
  * - tmux: `tmux send-keys`
22
- * - iTerm2: AppleScript `write text`
23
- * - Terminal.app: System Events `keystroke` + `key code 36` (Return)
25
+ * - iTerm2: Two separate AppleScript `write text` calls (text then newline)
26
+ * - Terminal.app: Two separate AppleScript `do script` calls (text then newline)
24
27
  *
25
28
  * All AppleScript is executed via `execFile('osascript', ['-e', script])`
26
29
  * to avoid shell interpolation and command injection.
@@ -56,12 +59,13 @@ export class TtyWriter {
56
59
  await execFileAsync('tmux', ['send-keys', '-t', identifier, 'Enter']);
57
60
  }
58
61
 
59
- private static async sendViaITerm2(tty: string, message: string): Promise<void> {
60
- const escaped = escapeAppleScript(message);
61
- // Send text WITHOUT a trailing newline to avoid the newline being swallowed
62
- // by bracketed paste mode. Then simulate pressing Return separately so that
63
- // Claude Code (and other interactive TUIs) treat it as a real submit action.
64
- const script = `
62
+ /**
63
+ * Build an AppleScript that finds an iTerm2 session by TTY and runs a
64
+ * command against it. The `sessionCommand` is inserted inside a
65
+ * `tell targetSession` block.
66
+ */
67
+ private static iterm2SessionScript(tty: string, sessionCommand: string): string {
68
+ return `
65
69
  tell application "iTerm"
66
70
  set targetSession to missing value
67
71
  repeat with w in windows
@@ -77,66 +81,90 @@ tell application "iTerm"
77
81
  if targetSession is not missing value then exit repeat
78
82
  end repeat
79
83
  if targetSession is missing value then return "not_found"
80
- tell targetSession to write text "${escaped}" newline no
81
- end tell
82
- tell application "iTerm" to activate
83
- delay 0.15
84
- tell application "System Events"
85
- tell process "iTerm2"
86
- key code 36
87
- end tell
84
+ tell targetSession to ${sessionCommand}
88
85
  end tell
89
86
  return "ok"`;
87
+ }
88
+
89
+ private static async sendViaITerm2(tty: string, message: string): Promise<void> {
90
+ const escaped = escapeAppleScript(message);
91
+ // Send text and Enter as two separate write text calls so the newline
92
+ // is delivered outside the bracketed paste sequence of the message body.
93
+ // iTerm2 appends the newline after the paste-end marker (\e[201~), so
94
+ // the inner TUI (Claude Code, Codex) sees it as a real submit action.
95
+ const textScript = TtyWriter.iterm2SessionScript(tty, `write text "${escaped}" newline no`);
90
96
 
91
- const { stdout } = await execFileAsync('osascript', ['-e', script]);
92
- if (stdout.trim() !== 'ok') {
97
+ const { stdout: textResult } = await execFileAsync('osascript', ['-e', textScript]);
98
+ if (textResult.trim() !== 'ok') {
93
99
  throw new Error(`iTerm2 session not found for TTY ${tty}`);
94
100
  }
101
+
102
+ // Wait for the paste to complete before sending Enter separately
103
+ await new Promise((resolve) => setTimeout(resolve, 150));
104
+
105
+ const enterScript = TtyWriter.iterm2SessionScript(tty, 'write text "" newline yes');
106
+ const { stdout: enterResult } = await execFileAsync('osascript', ['-e', enterScript]);
107
+ if (enterResult.trim() !== 'ok') {
108
+ throw new Error(`iTerm2 session disappeared before Enter could be sent for TTY ${tty}`);
109
+ }
95
110
  }
96
111
 
97
112
  private static async sendViaTerminalApp(tty: string, message: string): Promise<void> {
98
113
  const escaped = escapeAppleScript(message);
99
- // Use System Events keystroke to type into the foreground process,
100
- // NOT Terminal.app's "do script" which runs a new shell command.
101
- // First activate Terminal and select the correct tab, then type via System Events.
102
- // Send the text first, then wait for the paste/input to complete before pressing
103
- // Return separately this ensures interactive TUIs (like Claude Code) see the
104
- // Return as a real submit action, not part of a bracketed paste.
105
- const script = `
114
+ // Use Terminal.app's `do script` to send text to the correct tab by TTY.
115
+ // We avoid System Events `keystroke` + `key code 36` because it requires
116
+ // accessibility permissions and unreliably delivers the Return key.
117
+ //
118
+ // `do script` with `in` targets a specific tab without opening a new one.
119
+ // We send text and Enter as two separate calls so the newline arrives
120
+ // outside of bracketed paste mode — same pattern as iTerm2 and tmux.
121
+ const textScript = `
106
122
  tell application "Terminal"
107
- set targetFound to false
123
+ set targetTab to missing value
108
124
  repeat with w in windows
109
125
  repeat with i from 1 to count of tabs of w
110
126
  set t to tab i of w
111
127
  if tty of t is "${tty}" then
112
- set selected tab of w to t
113
- set index of w to 1
114
- activate
115
- set targetFound to true
128
+ set targetTab to t
116
129
  exit repeat
117
130
  end if
118
131
  end repeat
119
- if targetFound then exit repeat
132
+ if targetTab is not missing value then exit repeat
120
133
  end repeat
121
- if not targetFound then return "not_found"
122
- end tell
123
- delay 0.1
124
- tell application "System Events"
125
- tell process "Terminal"
126
- keystroke "${escaped}"
127
- end tell
128
- end tell
129
- delay 0.15
130
- tell application "System Events"
131
- tell process "Terminal"
132
- key code 36
133
- end tell
134
+ if targetTab is missing value then return "not_found"
135
+ do script "${escaped}" in targetTab
134
136
  end tell
135
137
  return "ok"`;
136
138
 
137
- const { stdout } = await execFileAsync('osascript', ['-e', script]);
138
- if (stdout.trim() !== 'ok') {
139
+ const { stdout: textResult } = await execFileAsync('osascript', ['-e', textScript]);
140
+ if (textResult.trim() !== 'ok') {
139
141
  throw new Error(`Terminal.app tab not found for TTY ${tty}`);
140
142
  }
143
+
144
+ // Wait for the text to be delivered before sending Enter
145
+ await new Promise((resolve) => setTimeout(resolve, 150));
146
+
147
+ const enterScript = `
148
+ tell application "Terminal"
149
+ set targetTab to missing value
150
+ repeat with w in windows
151
+ repeat with i from 1 to count of tabs of w
152
+ set t to tab i of w
153
+ if tty of t is "${tty}" then
154
+ set targetTab to t
155
+ exit repeat
156
+ end if
157
+ end repeat
158
+ if targetTab is not missing value then exit repeat
159
+ end repeat
160
+ if targetTab is missing value then return "not_found"
161
+ do script "" in targetTab
162
+ end tell
163
+ return "ok"`;
164
+
165
+ const { stdout: enterResult } = await execFileAsync('osascript', ['-e', enterScript]);
166
+ if (enterResult.trim() !== 'ok') {
167
+ throw new Error(`Terminal.app tab disappeared before Enter could be sent for TTY ${tty}`);
168
+ }
141
169
  }
142
170
  }