@ai-devkit/agent-manager 0.22.1 → 0.25.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 (59) hide show
  1. package/README.md +3 -0
  2. package/dist/__tests__/AgentManager.test.js +9 -1
  3. package/dist/__tests__/AgentManager.test.js.map +1 -1
  4. package/dist/__tests__/adapters/GrokCliAdapter.test.js +403 -0
  5. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -0
  6. package/dist/__tests__/terminal/TerminalFocusManager.test.js +180 -0
  7. package/dist/__tests__/terminal/TerminalFocusManager.test.js.map +1 -1
  8. package/dist/__tests__/terminal/TtyWriter.test.js +206 -0
  9. package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -1
  10. package/dist/__tests__/utils/agent-requests.test.js +90 -0
  11. package/dist/__tests__/utils/agent-requests.test.js.map +1 -0
  12. package/dist/__tests__/utils/agents.test.js +6 -0
  13. package/dist/__tests__/utils/agents.test.js.map +1 -1
  14. package/dist/adapters/AgentAdapter.d.ts +1 -1
  15. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  16. package/dist/adapters/AgentAdapter.js.map +1 -1
  17. package/dist/adapters/GrokCliAdapter.d.ts +79 -0
  18. package/dist/adapters/GrokCliAdapter.d.ts.map +1 -0
  19. package/dist/adapters/GrokCliAdapter.js +306 -0
  20. package/dist/adapters/GrokCliAdapter.js.map +1 -0
  21. package/dist/adapters/index.d.ts +1 -0
  22. package/dist/adapters/index.d.ts.map +1 -1
  23. package/dist/adapters/index.js +1 -0
  24. package/dist/adapters/index.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/terminal/TerminalFocusManager.d.ts +11 -0
  30. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
  31. package/dist/terminal/TerminalFocusManager.js +84 -10
  32. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  33. package/dist/terminal/TtyWriter.d.ts +19 -0
  34. package/dist/terminal/TtyWriter.d.ts.map +1 -1
  35. package/dist/terminal/TtyWriter.js +167 -1
  36. package/dist/terminal/TtyWriter.js.map +1 -1
  37. package/dist/utils/agent-requests.d.ts +10 -0
  38. package/dist/utils/agent-requests.d.ts.map +1 -0
  39. package/dist/utils/agent-requests.js +22 -0
  40. package/dist/utils/agent-requests.js.map +1 -0
  41. package/dist/utils/agents.d.ts +1 -1
  42. package/dist/utils/agents.d.ts.map +1 -1
  43. package/dist/utils/agents.js +4 -0
  44. package/dist/utils/agents.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/__tests__/AgentManager.test.ts +7 -1
  47. package/src/__tests__/adapters/GrokCliAdapter.test.ts +307 -0
  48. package/src/__tests__/terminal/TerminalFocusManager.test.ts +187 -0
  49. package/src/__tests__/terminal/TtyWriter.test.ts +234 -0
  50. package/src/__tests__/utils/agent-requests.test.ts +74 -0
  51. package/src/__tests__/utils/agents.test.ts +7 -0
  52. package/src/adapters/AgentAdapter.ts +1 -1
  53. package/src/adapters/GrokCliAdapter.ts +394 -0
  54. package/src/adapters/index.ts +1 -0
  55. package/src/index.ts +4 -0
  56. package/src/terminal/TerminalFocusManager.ts +103 -11
  57. package/src/terminal/TtyWriter.ts +161 -1
  58. package/src/utils/agent-requests.ts +28 -0
  59. package/src/utils/agents.ts +2 -1
@@ -6,6 +6,19 @@ import { escapeAppleScript } from '../utils/applescript.js';
6
6
 
7
7
  const execFileAsync = promisify(execFile);
8
8
 
9
+ /**
10
+ * Carriage return byte (0x0d). Sent as a fixed discrete argv element with
11
+ * `--no-paste` to deliver Enter literally (shell equivalent: $'\x0d').
12
+ */
13
+ const CARRIAGE_RETURN = '\x0d';
14
+
15
+ /**
16
+ * Escape byte (0x1b). Recognized by `sendKey` and translated to the
17
+ * backend-native representation (`Escape` for tmux, `key code 53` for
18
+ * AppleScript, the literal byte for WezTerm).
19
+ */
20
+ const ESCAPE_BYTE = '\x1b';
21
+
9
22
  export class TtyWriter {
10
23
  /**
11
24
  * Send a message as keyboard input to a terminal session.
@@ -26,6 +39,8 @@ export class TtyWriter {
26
39
  switch (location.type) {
27
40
  case TerminalType.TMUX:
28
41
  return TtyWriter.sendViaTmux(location.identifier, message);
42
+ case TerminalType.WEZTERM:
43
+ return TtyWriter.sendViaWezterm(location.identifier, message);
29
44
  case TerminalType.ITERM2:
30
45
  return TtyWriter.sendViaITerm2(location.tty, message);
31
46
  case TerminalType.TERMINAL_APP:
@@ -33,11 +48,146 @@ export class TtyWriter {
33
48
  default:
34
49
  throw new Error(
35
50
  `Cannot send input: unsupported terminal type "${location.type}". ` +
36
- 'Supported: tmux, iTerm2, Terminal.app.'
51
+ 'Supported: tmux, WezTerm, iTerm2, Terminal.app.'
37
52
  );
38
53
  }
39
54
  }
40
55
 
56
+ /**
57
+ * Send a single raw key (e.g. "1", "Enter", "Up") to the terminal as a
58
+ * keystroke — bypassing bracketed paste and without auto-appending Enter.
59
+ *
60
+ * Use this when the target TUI distinguishes between typed text and raw
61
+ * keypresses (e.g. an `AskUserQuestion` picker that selects on digit-key
62
+ * press, not on a pasted digit followed by Enter).
63
+ *
64
+ * - tmux: `tmux send-keys -t <id> <key>` — direct keystroke, no paste buffer.
65
+ * - WezTerm: `wezterm cli send-text --pane-id <id> --no-paste <key>`.
66
+ * - iTerm2 / Terminal.app: AppleScript via System Events. Requires
67
+ * Accessibility permissions.
68
+ */
69
+ static async sendKey(location: TerminalLocation, key: string): Promise<void> {
70
+ switch (location.type) {
71
+ case TerminalType.TMUX:
72
+ return TtyWriter.sendKeyViaTmux(location.identifier, key);
73
+ case TerminalType.WEZTERM:
74
+ return TtyWriter.sendKeyViaWezterm(location.identifier, key);
75
+ case TerminalType.ITERM2:
76
+ return TtyWriter.sendKeyViaITerm2(location.tty, key);
77
+ case TerminalType.TERMINAL_APP:
78
+ return TtyWriter.sendKeyViaTerminalApp(location.tty, key);
79
+ default:
80
+ throw new Error(
81
+ `Cannot send key: unsupported terminal type "${location.type}". ` +
82
+ 'Supported: tmux, WezTerm, iTerm2, Terminal.app.'
83
+ );
84
+ }
85
+ }
86
+
87
+ private static async sendKeyViaTmux(identifier: string, key: string): Promise<void> {
88
+ // tmux send-keys interprets named keys (Enter, Up, Escape, ...) and
89
+ // passes literals through. No bracketed paste, no auto-Enter.
90
+ const arg = key === ESCAPE_BYTE ? 'Escape' : key;
91
+ await execFileAsync('tmux', ['send-keys', '-t', identifier, arg]);
92
+ }
93
+
94
+ private static async sendKeyViaWezterm(paneId: string, key: string): Promise<void> {
95
+ // --no-paste delivers the key bytes literally outside bracketed-paste
96
+ // markers; the TUI sees a raw keystroke. For Esc (`\x1b`), wezterm
97
+ // accepts the byte directly.
98
+ await execFileAsync('wezterm', [
99
+ 'cli', 'send-text', '--pane-id', paneId, '--no-paste', key,
100
+ ]);
101
+ }
102
+
103
+ private static async sendKeyViaITerm2(tty: string, key: string): Promise<void> {
104
+ // Focus the target session, then press the key via System Events so the
105
+ // inner TUI sees a raw keystroke (not a bracketed-paste text run).
106
+ const action = appleScriptKeyAction(key);
107
+ const script = `
108
+ tell application "iTerm"
109
+ set targetSession to missing value
110
+ repeat with w in windows
111
+ repeat with t in tabs of w
112
+ repeat with s in sessions of t
113
+ if tty of s is "${tty}" then
114
+ set targetSession to s
115
+ set frontmost of w to true
116
+ tell t to select
117
+ tell s to select
118
+ exit repeat
119
+ end if
120
+ end repeat
121
+ if targetSession is not missing value then exit repeat
122
+ end repeat
123
+ if targetSession is not missing value then exit repeat
124
+ end repeat
125
+ if targetSession is missing value then return "not_found"
126
+ activate
127
+ end tell
128
+ tell application "System Events" to ${action}
129
+ return "ok"`;
130
+
131
+ const { stdout } = await execFileAsync('osascript', ['-e', script]);
132
+ if (stdout.trim() !== 'ok') {
133
+ throw new Error(`iTerm2 session not found for TTY ${tty}`);
134
+ }
135
+ }
136
+
137
+ private static async sendKeyViaTerminalApp(tty: string, key: string): Promise<void> {
138
+ const action = appleScriptKeyAction(key);
139
+ const script = `
140
+ tell application "Terminal"
141
+ set targetTab to missing value
142
+ set targetWindow to missing value
143
+ repeat with w in windows
144
+ repeat with i from 1 to count of tabs of w
145
+ set t to tab i of w
146
+ if tty of t is "${tty}" then
147
+ set targetTab to t
148
+ set targetWindow to w
149
+ exit repeat
150
+ end if
151
+ end repeat
152
+ if targetTab is not missing value then exit repeat
153
+ end repeat
154
+ if targetTab is missing value then return "not_found"
155
+ set selected of targetTab to true
156
+ set frontmost of targetWindow to true
157
+ activate
158
+ end tell
159
+ tell application "System Events" to ${action}
160
+ return "ok"`;
161
+
162
+ const { stdout } = await execFileAsync('osascript', ['-e', script]);
163
+ if (stdout.trim() !== 'ok') {
164
+ throw new Error(`Terminal.app tab not found for TTY ${tty}`);
165
+ }
166
+ }
167
+
168
+ private static async sendViaWezterm(paneId: string, message: string): Promise<void> {
169
+ // Two explicit CLI calls, mirroring the text-then-Enter convention used
170
+ // by tmux / iTerm2 / Terminal.app so a bracketed-paste-aware TUI still
171
+ // sees Enter as a submit.
172
+ //
173
+ // Step 1 (text): write the message to stdin so prompt contents are not
174
+ // exposed through process arguments. execFile still spawns wezterm
175
+ // directly (no shell), so shell metacharacters remain inert.
176
+ // Step 2 (Enter): pass a fixed carriage return (0x0d) as a discrete
177
+ // argv element (the JS char '\x0d') with --no-paste, so the CR is
178
+ // delivered literally rather than wrapped in paste brackets. The
179
+ // equivalent shell command is:
180
+ // wezterm cli send-text --pane-id <id> --no-paste $'\x0d'
181
+ // (ANSI-C quoting, note the leading $).
182
+ await TtyWriter.execFileWithInput('wezterm', [
183
+ 'cli', 'send-text', '--pane-id', paneId,
184
+ ], message);
185
+ await new Promise((resolve) => setTimeout(resolve, 150));
186
+ await execFileAsync('wezterm', [
187
+ 'cli', 'send-text', '--pane-id', paneId, '--no-paste', CARRIAGE_RETURN,
188
+ ]);
189
+ }
190
+
41
191
  private static async sendViaTmux(identifier: string, message: string): Promise<void> {
42
192
  // Paste the message body using tmux bracketed paste, then send Enter as
43
193
  // a separate key so the inner TUI treats it as submission rather than
@@ -175,3 +325,13 @@ return "ok"`;
175
325
  }
176
326
  }
177
327
  }
328
+
329
+ /**
330
+ * AppleScript `keystroke` only delivers typeable characters; non-typeable
331
+ * keys (Esc, arrows, F-keys, …) must be sent via `key code <N>`. Add more
332
+ * mappings here as new special keys are needed.
333
+ */
334
+ function appleScriptKeyAction(key: string): string {
335
+ if (key === ESCAPE_BYTE) return 'key code 53';
336
+ return `keystroke "${escapeAppleScript(key)}"`;
337
+ }
@@ -0,0 +1,28 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export interface AgentRequest {
5
+ sessionId: string;
6
+ toolName: string;
7
+ toolInput: Record<string, unknown>;
8
+ timestamp: string;
9
+ }
10
+
11
+ export function getAgentRequestPath(homeDir: string, sessionId: string): string {
12
+ return path.join(homeDir, '.ai-devkit', 'agent-requests', `${sessionId}.json`);
13
+ }
14
+
15
+ export function readLatestAgentRequest(homeDir: string, sessionId: string): AgentRequest | null {
16
+ try {
17
+ const raw = fs.readFileSync(getAgentRequestPath(homeDir, sessionId), 'utf-8');
18
+ return JSON.parse(raw) as AgentRequest;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function writeAgentRequest(homeDir: string, entry: AgentRequest): void {
25
+ const filePath = getAgentRequestPath(homeDir, entry.sessionId);
26
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
27
+ fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), 'utf-8');
28
+ }
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import type { AgentType } from '../adapters/AgentAdapter.js';
3
3
 
4
- export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;
4
+ export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'grok_cli' | 'opencode' | 'pi'>;
5
5
 
6
6
  export interface AgentConfig {
7
7
  /** Shell command to launch the agent (sent to tmux via `send-keys`). */
@@ -20,6 +20,7 @@ export const AGENTS: Record<StartableAgentType, AgentConfig> = {
20
20
  codex: { command: 'codex', matches: matchArgv0('codex') },
21
21
  copilot: { command: 'copilot', matches: matchArgv0Name('copilot-cli') },
22
22
  gemini_cli: { command: 'gemini', matches: matchAnyToken('gemini') },
23
+ grok_cli: { command: 'grok', matches: matchArgv0('grok') },
23
24
  opencode: { command: 'opencode', matches: matchArgv0('opencode') },
24
25
  pi: { command: 'pi', matches: matchAnyBasename(['pi']) },
25
26
  };