@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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts +23 -0
- package/dist/terminal/TtyWriter.d.ts.map +1 -0
- package/dist/terminal/TtyWriter.js +106 -0
- package/dist/terminal/TtyWriter.js.map +1 -0
- package/dist/terminal/index.d.ts +1 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +3 -1
- package/dist/terminal/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/terminal/TtyWriter.test.ts +154 -0
- package/src/index.ts +1 -0
- package/src/terminal/TtyWriter.ts +112 -0
- package/src/terminal/index.ts +1 -0
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
package/dist/terminal/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/terminal/index.js
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
package/src/terminal/index.ts
CHANGED