@ai-devkit/agent-manager 0.2.0 → 0.3.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.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { AgentStatus } from './adapters/AgentAdapter';
5
5
  export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './adapters/AgentAdapter';
6
6
  export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusManager';
7
7
  export type { TerminalLocation } from './terminal/TerminalFocusManager';
8
+ export { TtyWriter } from './terminal/TtyWriter';
8
9
  export { listProcesses, getProcessCwd, getProcessTty, isProcessRunning, getProcessInfo } from './utils/process';
9
10
  export type { ListProcessesOptions } from './utils/process';
10
11
  export { readLastLines, readJsonLines, fileExists, readJson } from './utils/file';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACrF,YAAY,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAChH,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACrF,YAAY,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAChH,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readJson = exports.fileExists = exports.readJsonLines = exports.readLastLines = exports.getProcessInfo = exports.isProcessRunning = exports.getProcessTty = exports.getProcessCwd = exports.listProcesses = exports.TerminalType = exports.TerminalFocusManager = exports.AgentStatus = exports.CodexAdapter = exports.ClaudeCodeAdapter = exports.AgentManager = void 0;
3
+ exports.readJson = exports.fileExists = exports.readJsonLines = exports.readLastLines = exports.getProcessInfo = exports.isProcessRunning = exports.getProcessTty = exports.getProcessCwd = exports.listProcesses = exports.TtyWriter = exports.TerminalType = exports.TerminalFocusManager = exports.AgentStatus = exports.CodexAdapter = exports.ClaudeCodeAdapter = exports.AgentManager = void 0;
4
4
  var AgentManager_1 = require("./AgentManager");
5
5
  Object.defineProperty(exports, "AgentManager", { enumerable: true, get: function () { return AgentManager_1.AgentManager; } });
6
6
  var ClaudeCodeAdapter_1 = require("./adapters/ClaudeCodeAdapter");
@@ -12,6 +12,8 @@ Object.defineProperty(exports, "AgentStatus", { enumerable: true, get: function
12
12
  var TerminalFocusManager_1 = require("./terminal/TerminalFocusManager");
13
13
  Object.defineProperty(exports, "TerminalFocusManager", { enumerable: true, get: function () { return TerminalFocusManager_1.TerminalFocusManager; } });
14
14
  Object.defineProperty(exports, "TerminalType", { enumerable: true, get: function () { return TerminalFocusManager_1.TerminalType; } });
15
+ var TtyWriter_1 = require("./terminal/TtyWriter");
16
+ Object.defineProperty(exports, "TtyWriter", { enumerable: true, get: function () { return TtyWriter_1.TtyWriter; } });
15
17
  var process_1 = require("./utils/process");
16
18
  Object.defineProperty(exports, "listProcesses", { enumerable: true, get: function () { return process_1.listProcesses; } });
17
19
  Object.defineProperty(exports, "getProcessCwd", { enumerable: true, get: function () { return process_1.getProcessCwd; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,kEAAiE;AAAxD,sHAAA,iBAAiB,OAAA;AAC1B,wDAAuD;AAA9C,4GAAA,YAAY,OAAA;AACrB,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAGpB,wEAAqF;AAA5E,4HAAA,oBAAoB,OAAA;AAAE,oHAAA,YAAY,OAAA;AAG3C,2CAAgH;AAAvG,wGAAA,aAAa,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,2GAAA,gBAAgB,OAAA;AAAE,yGAAA,cAAc,OAAA;AAEtF,qCAAkF;AAAzE,qGAAA,aAAa,OAAA;AAAE,qGAAA,aAAa,OAAA;AAAE,kGAAA,UAAU,OAAA;AAAE,gGAAA,QAAQ,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,kEAAiE;AAAxD,sHAAA,iBAAiB,OAAA;AAC1B,wDAAuD;AAA9C,4GAAA,YAAY,OAAA;AACrB,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAGpB,wEAAqF;AAA5E,4HAAA,oBAAoB,OAAA;AAAE,oHAAA,YAAY,OAAA;AAE3C,kDAAiD;AAAxC,sGAAA,SAAS,OAAA;AAElB,2CAAgH;AAAvG,wGAAA,aAAa,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,2GAAA,gBAAgB,OAAA;AAAE,yGAAA,cAAc,OAAA;AAEtF,qCAAkF;AAAzE,qGAAA,aAAa,OAAA;AAAE,qGAAA,aAAa,OAAA;AAAE,kGAAA,UAAU,OAAA;AAAE,gGAAA,QAAQ,OAAA"}
@@ -0,0 +1,23 @@
1
+ import type { TerminalLocation } from './TerminalFocusManager';
2
+ export declare class TtyWriter {
3
+ /**
4
+ * Send a message as keyboard input to a terminal session.
5
+ *
6
+ * Dispatches to the correct mechanism based on terminal type:
7
+ * - tmux: `tmux send-keys`
8
+ * - iTerm2: AppleScript `write text`
9
+ * - Terminal.app: System Events `keystroke` + `key code 36` (Return)
10
+ *
11
+ * All AppleScript is executed via `execFile('osascript', ['-e', script])`
12
+ * to avoid shell interpolation and command injection.
13
+ *
14
+ * @param location Terminal location from TerminalFocusManager.findTerminal()
15
+ * @param message Text to send
16
+ * @throws Error if terminal type is unsupported or send fails
17
+ */
18
+ static send(location: TerminalLocation, message: string): Promise<void>;
19
+ private static sendViaTmux;
20
+ private static sendViaITerm2;
21
+ private static sendViaTerminalApp;
22
+ }
23
+ //# sourceMappingURL=TtyWriter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TtyWriter.d.ts","sourceRoot":"","sources":["../../src/terminal/TtyWriter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAa/D,qBAAa,SAAS;IAClB;;;;;;;;;;;;;;OAcG;WACU,IAAI,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;mBAgBxD,WAAW;mBAIX,aAAa;mBAuBb,kBAAkB;CAqC1C"}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TtyWriter = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const util_1 = require("util");
6
+ const TerminalFocusManager_1 = require("./TerminalFocusManager");
7
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
8
+ /**
9
+ * Escape a string for safe use inside an AppleScript double-quoted string.
10
+ * Backslashes and double quotes must be escaped.
11
+ */
12
+ function escapeAppleScript(text) {
13
+ return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
14
+ }
15
+ class TtyWriter {
16
+ /**
17
+ * Send a message as keyboard input to a terminal session.
18
+ *
19
+ * Dispatches to the correct mechanism based on terminal type:
20
+ * - tmux: `tmux send-keys`
21
+ * - iTerm2: AppleScript `write text`
22
+ * - Terminal.app: System Events `keystroke` + `key code 36` (Return)
23
+ *
24
+ * All AppleScript is executed via `execFile('osascript', ['-e', script])`
25
+ * to avoid shell interpolation and command injection.
26
+ *
27
+ * @param location Terminal location from TerminalFocusManager.findTerminal()
28
+ * @param message Text to send
29
+ * @throws Error if terminal type is unsupported or send fails
30
+ */
31
+ static async send(location, message) {
32
+ switch (location.type) {
33
+ case TerminalFocusManager_1.TerminalType.TMUX:
34
+ return TtyWriter.sendViaTmux(location.identifier, message);
35
+ case TerminalFocusManager_1.TerminalType.ITERM2:
36
+ return TtyWriter.sendViaITerm2(location.tty, message);
37
+ case TerminalFocusManager_1.TerminalType.TERMINAL_APP:
38
+ return TtyWriter.sendViaTerminalApp(location.tty, message);
39
+ default:
40
+ throw new Error(`Cannot send input: unsupported terminal type "${location.type}". ` +
41
+ 'Supported: tmux, iTerm2, Terminal.app.');
42
+ }
43
+ }
44
+ static async sendViaTmux(identifier, message) {
45
+ await execFileAsync('tmux', ['send-keys', '-t', identifier, message, 'Enter']);
46
+ }
47
+ static async sendViaITerm2(tty, message) {
48
+ const escaped = escapeAppleScript(message);
49
+ const script = `
50
+ tell application "iTerm"
51
+ repeat with w in windows
52
+ repeat with t in tabs of w
53
+ repeat with s in sessions of t
54
+ if tty of s is "${tty}" then
55
+ tell s to write text "${escaped}"
56
+ return "ok"
57
+ end if
58
+ end repeat
59
+ end repeat
60
+ end repeat
61
+ end tell
62
+ return "not_found"`;
63
+ const { stdout } = await execFileAsync('osascript', ['-e', script]);
64
+ if (stdout.trim() !== 'ok') {
65
+ throw new Error(`iTerm2 session not found for TTY ${tty}`);
66
+ }
67
+ }
68
+ static async sendViaTerminalApp(tty, message) {
69
+ const escaped = escapeAppleScript(message);
70
+ // Use System Events keystroke to type into the foreground process,
71
+ // NOT Terminal.app's "do script" which runs a new shell command.
72
+ // First activate Terminal and select the correct tab, then type via System Events.
73
+ const script = `
74
+ tell application "Terminal"
75
+ set targetFound to false
76
+ repeat with w in windows
77
+ repeat with i from 1 to count of tabs of w
78
+ set t to tab i of w
79
+ if tty of t is "${tty}" then
80
+ set selected tab of w to t
81
+ set index of w to 1
82
+ activate
83
+ set targetFound to true
84
+ exit repeat
85
+ end if
86
+ end repeat
87
+ if targetFound then exit repeat
88
+ end repeat
89
+ if not targetFound then return "not_found"
90
+ end tell
91
+ delay 0.1
92
+ tell application "System Events"
93
+ tell process "Terminal"
94
+ keystroke "${escaped}"
95
+ key code 36
96
+ end tell
97
+ end tell
98
+ return "ok"`;
99
+ const { stdout } = await execFileAsync('osascript', ['-e', script]);
100
+ if (stdout.trim() !== 'ok') {
101
+ throw new Error(`Terminal.app tab not found for TTY ${tty}`);
102
+ }
103
+ }
104
+ }
105
+ exports.TtyWriter = TtyWriter;
106
+ //# sourceMappingURL=TtyWriter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TtyWriter.js","sourceRoot":"","sources":["../../src/terminal/TtyWriter.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AACzC,+BAAiC;AAEjC,iEAAsD;AAEtD,MAAM,aAAa,GAAG,IAAA,gBAAS,EAAC,wBAAQ,CAAC,CAAC;AAE1C;;;GAGG;AACH,SAAS,iBAAiB,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC5D,CAAC;AAED,MAAa,SAAS;IAClB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAA0B,EAAE,OAAe;QACzD,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,mCAAY,CAAC,IAAI;gBAClB,OAAO,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC/D,KAAK,mCAAY,CAAC,MAAM;gBACpB,OAAO,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC1D,KAAK,mCAAY,CAAC,YAAY;gBAC1B,OAAO,SAAS,CAAC,kBAAkB,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC/D;gBACI,MAAM,IAAI,KAAK,CACX,iDAAiD,QAAQ,CAAC,IAAI,KAAK;oBACnE,wCAAwC,CAC3C,CAAC;QACV,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,UAAkB,EAAE,OAAe;QAChE,MAAM,aAAa,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACnF,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,GAAW,EAAE,OAAe;QAC3D,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG;;;;;0BAKG,GAAG;kCACK,OAAO;;;;;;;mBAOtB,CAAC;QAEZ,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACpE,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,oCAAoC,GAAG,EAAE,CAAC,CAAC;QAC/D,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAW,EAAE,OAAe;QAChE,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC3C,mEAAmE;QACnE,iEAAiE;QACjE,mFAAmF;QACnF,MAAM,MAAM,GAAG;;;;;;wBAMC,GAAG;;;;;;;;;;;;;;;iBAeV,OAAO;;;;YAIZ,CAAC;QAEL,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACpE,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,sCAAsC,GAAG,EAAE,CAAC,CAAC;QACjE,CAAC;IACL,CAAC;CACJ;AAhGD,8BAgGC"}
@@ -1,4 +1,5 @@
1
1
  export { TerminalFocusManager } from './TerminalFocusManager';
2
2
  export { TerminalType } from './TerminalFocusManager';
3
3
  export type { TerminalLocation } from './TerminalFocusManager';
4
+ export { TtyWriter } from './TtyWriter';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TerminalType = exports.TerminalFocusManager = void 0;
3
+ exports.TtyWriter = exports.TerminalType = exports.TerminalFocusManager = void 0;
4
4
  var TerminalFocusManager_1 = require("./TerminalFocusManager");
5
5
  Object.defineProperty(exports, "TerminalFocusManager", { enumerable: true, get: function () { return TerminalFocusManager_1.TerminalFocusManager; } });
6
6
  var TerminalFocusManager_2 = require("./TerminalFocusManager");
7
7
  Object.defineProperty(exports, "TerminalType", { enumerable: true, get: function () { return TerminalFocusManager_2.TerminalType; } });
8
+ var TtyWriter_1 = require("./TtyWriter");
9
+ Object.defineProperty(exports, "TtyWriter", { enumerable: true, get: function () { return TtyWriter_1.TtyWriter; } });
8
10
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":";;;AAAA,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,+DAAsD;AAA7C,oHAAA,YAAY,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/terminal/index.ts"],"names":[],"mappings":";;;AAAA,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,+DAAsD;AAA7C,oHAAA,YAAY,OAAA;AAErB,yCAAwC;AAA/B,sGAAA,SAAS,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-devkit/agent-manager",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Standalone agent detection and management utilities for AI DevKit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { TtyWriter } from '../../terminal/TtyWriter';
3
+ import { TerminalType } from '../../terminal/TerminalFocusManager';
4
+ import type { TerminalLocation } from '../../terminal/TerminalFocusManager';
5
+ import { execFile } from 'child_process';
6
+
7
+ jest.mock('child_process', () => {
8
+ const actual = jest.requireActual<typeof import('child_process')>('child_process');
9
+ return {
10
+ ...actual,
11
+ execFile: jest.fn(),
12
+ };
13
+ });
14
+
15
+ const mockedExecFile = execFile as unknown as jest.Mock;
16
+
17
+ function mockExecFileSuccess(stdout = '') {
18
+ mockedExecFile.mockImplementation((...args: unknown[]) => {
19
+ const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void;
20
+ cb(null, { stdout }, '');
21
+ });
22
+ }
23
+
24
+ function mockExecFileError(message: string) {
25
+ mockedExecFile.mockImplementation((...args: unknown[]) => {
26
+ const cb = args[args.length - 1] as (err: Error | null, result: null, stderr: string) => void;
27
+ cb(new Error(message), null, '');
28
+ });
29
+ }
30
+
31
+ describe('TtyWriter', () => {
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+ });
35
+
36
+ describe('tmux', () => {
37
+ const location: TerminalLocation = {
38
+ type: TerminalType.TMUX,
39
+ identifier: 'main:0.1',
40
+ tty: '/dev/ttys030',
41
+ };
42
+
43
+ it('sends message via tmux send-keys', async () => {
44
+ mockExecFileSuccess();
45
+
46
+ await TtyWriter.send(location, 'continue');
47
+
48
+ expect(mockedExecFile).toHaveBeenCalledWith(
49
+ 'tmux',
50
+ ['send-keys', '-t', 'main:0.1', 'continue', 'Enter'],
51
+ expect.any(Function),
52
+ );
53
+ });
54
+
55
+ it('throws on tmux failure', async () => {
56
+ mockExecFileError('tmux not running');
57
+
58
+ await expect(TtyWriter.send(location, 'hello'))
59
+ .rejects.toThrow('tmux not running');
60
+ });
61
+ });
62
+
63
+ describe('iTerm2', () => {
64
+ const location: TerminalLocation = {
65
+ type: TerminalType.ITERM2,
66
+ identifier: '/dev/ttys030',
67
+ tty: '/dev/ttys030',
68
+ };
69
+
70
+ it('sends message via osascript with execFile (no shell)', async () => {
71
+ mockExecFileSuccess('ok');
72
+
73
+ await TtyWriter.send(location, 'hello');
74
+
75
+ expect(mockedExecFile).toHaveBeenCalledWith(
76
+ 'osascript',
77
+ ['-e', expect.stringContaining('write text "hello"')],
78
+ expect.any(Function),
79
+ );
80
+ });
81
+
82
+ it('escapes special characters in message', async () => {
83
+ mockExecFileSuccess('ok');
84
+
85
+ await TtyWriter.send(location, 'say "hi" \\ there');
86
+
87
+ expect(mockedExecFile).toHaveBeenCalledWith(
88
+ 'osascript',
89
+ ['-e', expect.stringContaining('write text "say \\"hi\\" \\\\ there"')],
90
+ expect.any(Function),
91
+ );
92
+ });
93
+
94
+ it('throws when session not found', async () => {
95
+ mockExecFileSuccess('not_found');
96
+
97
+ await expect(TtyWriter.send(location, 'test'))
98
+ .rejects.toThrow('iTerm2 session not found');
99
+ });
100
+ });
101
+
102
+ describe('Terminal.app', () => {
103
+ const location: TerminalLocation = {
104
+ type: TerminalType.TERMINAL_APP,
105
+ identifier: '/dev/ttys030',
106
+ tty: '/dev/ttys030',
107
+ };
108
+
109
+ it('sends message via System Events keystroke (not do script)', async () => {
110
+ mockExecFileSuccess('ok');
111
+
112
+ await TtyWriter.send(location, 'hello');
113
+
114
+ const scriptArg = (mockedExecFile.mock.calls[0] as unknown[])[1] as string[];
115
+ const script = scriptArg[1];
116
+ // Must use keystroke, NOT do script
117
+ expect(script).toContain('keystroke "hello"');
118
+ expect(script).toContain('key code 36');
119
+ expect(script).not.toContain('do script');
120
+ });
121
+
122
+ it('uses execFile to avoid shell injection', async () => {
123
+ mockExecFileSuccess('ok');
124
+
125
+ await TtyWriter.send(location, "don't stop");
126
+
127
+ expect(mockedExecFile).toHaveBeenCalledWith(
128
+ 'osascript',
129
+ ['-e', expect.any(String)],
130
+ expect.any(Function),
131
+ );
132
+ });
133
+
134
+ it('throws when tab not found', async () => {
135
+ mockExecFileSuccess('not_found');
136
+
137
+ await expect(TtyWriter.send(location, 'test'))
138
+ .rejects.toThrow('Terminal.app tab not found');
139
+ });
140
+ });
141
+
142
+ describe('unsupported terminal', () => {
143
+ it('throws for unknown terminal type', async () => {
144
+ const location: TerminalLocation = {
145
+ type: TerminalType.UNKNOWN,
146
+ identifier: '',
147
+ tty: '/dev/ttys030',
148
+ };
149
+
150
+ await expect(TtyWriter.send(location, 'test'))
151
+ .rejects.toThrow('Cannot send input: unsupported terminal type');
152
+ });
153
+ });
154
+ });
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './adapters
7
7
 
8
8
  export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusManager';
9
9
  export type { TerminalLocation } from './terminal/TerminalFocusManager';
10
+ export { TtyWriter } from './terminal/TtyWriter';
10
11
 
11
12
  export { listProcesses, getProcessCwd, getProcessTty, isProcessRunning, getProcessInfo } from './utils/process';
12
13
  export type { ListProcessesOptions } from './utils/process';
@@ -0,0 +1,112 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import type { TerminalLocation } from './TerminalFocusManager';
4
+ import { TerminalType } from './TerminalFocusManager';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ /**
9
+ * Escape a string for safe use inside an AppleScript double-quoted string.
10
+ * Backslashes and double quotes must be escaped.
11
+ */
12
+ function escapeAppleScript(text: string): string {
13
+ return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
14
+ }
15
+
16
+ export class TtyWriter {
17
+ /**
18
+ * Send a message as keyboard input to a terminal session.
19
+ *
20
+ * Dispatches to the correct mechanism based on terminal type:
21
+ * - tmux: `tmux send-keys`
22
+ * - iTerm2: AppleScript `write text`
23
+ * - Terminal.app: System Events `keystroke` + `key code 36` (Return)
24
+ *
25
+ * All AppleScript is executed via `execFile('osascript', ['-e', script])`
26
+ * to avoid shell interpolation and command injection.
27
+ *
28
+ * @param location Terminal location from TerminalFocusManager.findTerminal()
29
+ * @param message Text to send
30
+ * @throws Error if terminal type is unsupported or send fails
31
+ */
32
+ static async send(location: TerminalLocation, message: string): Promise<void> {
33
+ switch (location.type) {
34
+ case TerminalType.TMUX:
35
+ return TtyWriter.sendViaTmux(location.identifier, message);
36
+ case TerminalType.ITERM2:
37
+ return TtyWriter.sendViaITerm2(location.tty, message);
38
+ case TerminalType.TERMINAL_APP:
39
+ return TtyWriter.sendViaTerminalApp(location.tty, message);
40
+ default:
41
+ throw new Error(
42
+ `Cannot send input: unsupported terminal type "${location.type}". ` +
43
+ 'Supported: tmux, iTerm2, Terminal.app.'
44
+ );
45
+ }
46
+ }
47
+
48
+ private static async sendViaTmux(identifier: string, message: string): Promise<void> {
49
+ await execFileAsync('tmux', ['send-keys', '-t', identifier, message, 'Enter']);
50
+ }
51
+
52
+ private static async sendViaITerm2(tty: string, message: string): Promise<void> {
53
+ const escaped = escapeAppleScript(message);
54
+ const script = `
55
+ tell application "iTerm"
56
+ repeat with w in windows
57
+ repeat with t in tabs of w
58
+ repeat with s in sessions of t
59
+ if tty of s is "${tty}" then
60
+ tell s to write text "${escaped}"
61
+ return "ok"
62
+ end if
63
+ end repeat
64
+ end repeat
65
+ end repeat
66
+ end tell
67
+ return "not_found"`;
68
+
69
+ const { stdout } = await execFileAsync('osascript', ['-e', script]);
70
+ if (stdout.trim() !== 'ok') {
71
+ throw new Error(`iTerm2 session not found for TTY ${tty}`);
72
+ }
73
+ }
74
+
75
+ private static async sendViaTerminalApp(tty: string, message: string): Promise<void> {
76
+ const escaped = escapeAppleScript(message);
77
+ // Use System Events keystroke to type into the foreground process,
78
+ // NOT Terminal.app's "do script" which runs a new shell command.
79
+ // First activate Terminal and select the correct tab, then type via System Events.
80
+ const script = `
81
+ tell application "Terminal"
82
+ set targetFound to false
83
+ repeat with w in windows
84
+ repeat with i from 1 to count of tabs of w
85
+ set t to tab i of w
86
+ if tty of t is "${tty}" then
87
+ set selected tab of w to t
88
+ set index of w to 1
89
+ activate
90
+ set targetFound to true
91
+ exit repeat
92
+ end if
93
+ end repeat
94
+ if targetFound then exit repeat
95
+ end repeat
96
+ if not targetFound then return "not_found"
97
+ end tell
98
+ delay 0.1
99
+ tell application "System Events"
100
+ tell process "Terminal"
101
+ keystroke "${escaped}"
102
+ key code 36
103
+ end tell
104
+ end tell
105
+ return "ok"`;
106
+
107
+ const { stdout } = await execFileAsync('osascript', ['-e', script]);
108
+ if (stdout.trim() !== 'ok') {
109
+ throw new Error(`Terminal.app tab not found for TTY ${tty}`);
110
+ }
111
+ }
112
+ }
@@ -1,3 +1,4 @@
1
1
  export { TerminalFocusManager } from './TerminalFocusManager';
2
2
  export { TerminalType } from './TerminalFocusManager';
3
3
  export type { TerminalLocation } from './TerminalFocusManager';
4
+ export { TtyWriter } from './TtyWriter';