agent-relay 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +22 -0
- package/PROTOCOL.md +319 -0
- package/README.md +791 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1591 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/daemon/connection.d.ts +60 -0
- package/dist/daemon/connection.d.ts.map +1 -0
- package/dist/daemon/connection.js +245 -0
- package/dist/daemon/connection.js.map +1 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +4 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/router.d.ts +72 -0
- package/dist/daemon/router.d.ts.map +1 -0
- package/dist/daemon/router.js +183 -0
- package/dist/daemon/router.js.map +1 -0
- package/dist/daemon/server.d.ts +52 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +186 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/dashboard/public/index.html +690 -0
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +220 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/games/index.d.ts +2 -0
- package/dist/games/index.d.ts.map +1 -0
- package/dist/games/index.js +2 -0
- package/dist/games/index.js.map +1 -0
- package/dist/games/tictactoe.d.ts +24 -0
- package/dist/games/tictactoe.d.ts.map +1 -0
- package/dist/games/tictactoe.js +160 -0
- package/dist/games/tictactoe.js.map +1 -0
- package/dist/hooks/inbox-check/hook.d.ts +28 -0
- package/dist/hooks/inbox-check/hook.d.ts.map +1 -0
- package/dist/hooks/inbox-check/hook.js +97 -0
- package/dist/hooks/inbox-check/hook.js.map +1 -0
- package/dist/hooks/inbox-check/index.d.ts +8 -0
- package/dist/hooks/inbox-check/index.d.ts.map +1 -0
- package/dist/hooks/inbox-check/index.js +8 -0
- package/dist/hooks/inbox-check/index.js.map +1 -0
- package/dist/hooks/inbox-check/types.d.ts +31 -0
- package/dist/hooks/inbox-check/types.d.ts.map +1 -0
- package/dist/hooks/inbox-check/types.js +5 -0
- package/dist/hooks/inbox-check/types.js.map +1 -0
- package/dist/hooks/inbox-check/utils.d.ts +44 -0
- package/dist/hooks/inbox-check/utils.d.ts.map +1 -0
- package/dist/hooks/inbox-check/utils.js +107 -0
- package/dist/hooks/inbox-check/utils.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/framing.d.ts +32 -0
- package/dist/protocol/framing.d.ts.map +1 -0
- package/dist/protocol/framing.js +71 -0
- package/dist/protocol/framing.js.map +1 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/types.d.ts +104 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +6 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/state/agent-state.d.ts +40 -0
- package/dist/state/agent-state.d.ts.map +1 -0
- package/dist/state/agent-state.js +120 -0
- package/dist/state/agent-state.js.map +1 -0
- package/dist/storage/adapter.d.ts +29 -0
- package/dist/storage/adapter.d.ts.map +1 -0
- package/dist/storage/adapter.js +2 -0
- package/dist/storage/adapter.js.map +1 -0
- package/dist/storage/sqlite-adapter.d.ts +15 -0
- package/dist/storage/sqlite-adapter.d.ts.map +1 -0
- package/dist/storage/sqlite-adapter.js +116 -0
- package/dist/storage/sqlite-adapter.js.map +1 -0
- package/dist/supervisor/inbox.d.ts +38 -0
- package/dist/supervisor/inbox.d.ts.map +1 -0
- package/dist/supervisor/inbox.js +162 -0
- package/dist/supervisor/inbox.js.map +1 -0
- package/dist/supervisor/index.d.ts +10 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +10 -0
- package/dist/supervisor/index.js.map +1 -0
- package/dist/supervisor/spawner.d.ts +54 -0
- package/dist/supervisor/spawner.d.ts.map +1 -0
- package/dist/supervisor/spawner.js +282 -0
- package/dist/supervisor/spawner.js.map +1 -0
- package/dist/supervisor/state.d.ts +132 -0
- package/dist/supervisor/state.d.ts.map +1 -0
- package/dist/supervisor/state.js +465 -0
- package/dist/supervisor/state.js.map +1 -0
- package/dist/supervisor/supervisor.d.ts +67 -0
- package/dist/supervisor/supervisor.d.ts.map +1 -0
- package/dist/supervisor/supervisor.js +263 -0
- package/dist/supervisor/supervisor.js.map +1 -0
- package/dist/supervisor/types.d.ts +139 -0
- package/dist/supervisor/types.d.ts.map +1 -0
- package/dist/supervisor/types.js +12 -0
- package/dist/supervisor/types.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/name-generator.d.ts +17 -0
- package/dist/utils/name-generator.d.ts.map +1 -0
- package/dist/utils/name-generator.js +52 -0
- package/dist/utils/name-generator.js.map +1 -0
- package/dist/webhook/spawner.d.ts +79 -0
- package/dist/webhook/spawner.d.ts.map +1 -0
- package/dist/webhook/spawner.js +288 -0
- package/dist/webhook/spawner.js.map +1 -0
- package/dist/wrapper/client.d.ts +72 -0
- package/dist/wrapper/client.d.ts.map +1 -0
- package/dist/wrapper/client.js +306 -0
- package/dist/wrapper/client.js.map +1 -0
- package/dist/wrapper/inbox.d.ts +37 -0
- package/dist/wrapper/inbox.d.ts.map +1 -0
- package/dist/wrapper/inbox.js +73 -0
- package/dist/wrapper/inbox.js.map +1 -0
- package/dist/wrapper/index.d.ts +4 -0
- package/dist/wrapper/index.d.ts.map +1 -0
- package/dist/wrapper/index.js +7 -0
- package/dist/wrapper/index.js.map +1 -0
- package/dist/wrapper/parser.d.ts +94 -0
- package/dist/wrapper/parser.d.ts.map +1 -0
- package/dist/wrapper/parser.js +360 -0
- package/dist/wrapper/parser.js.map +1 -0
- package/dist/wrapper/pty-wrapper.d.ts +125 -0
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -0
- package/dist/wrapper/pty-wrapper.js +494 -0
- package/dist/wrapper/pty-wrapper.js.map +1 -0
- package/dist/wrapper/tmux-wrapper.d.ts +131 -0
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -0
- package/dist/wrapper/tmux-wrapper.js +427 -0
- package/dist/wrapper/tmux-wrapper.js.map +1 -0
- package/install.sh +69 -0
- package/package.json +82 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TmuxWrapper - Attach-based tmux wrapper
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* 1. Start agent in detached tmux session
|
|
6
|
+
* 2. Attach user to tmux (they see real terminal)
|
|
7
|
+
* 3. Background: poll capture-pane silently (no stdout writes)
|
|
8
|
+
* 4. Background: parse @relay commands, send to daemon
|
|
9
|
+
* 5. Background: inject messages via send-keys
|
|
10
|
+
*
|
|
11
|
+
* The key insight: user sees the REAL tmux session, not a proxy.
|
|
12
|
+
* We just do background parsing and injection.
|
|
13
|
+
*/
|
|
14
|
+
export interface TmuxWrapperConfig {
|
|
15
|
+
name: string;
|
|
16
|
+
command: string;
|
|
17
|
+
args?: string[];
|
|
18
|
+
socketPath?: string;
|
|
19
|
+
cols?: number;
|
|
20
|
+
rows?: number;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
/** Use file-based inbox in addition to injection */
|
|
24
|
+
useInbox?: boolean;
|
|
25
|
+
/** Custom inbox directory */
|
|
26
|
+
inboxDir?: string;
|
|
27
|
+
/** Polling interval for capture-pane (ms) */
|
|
28
|
+
pollInterval?: number;
|
|
29
|
+
/** Enable debug logging to stderr */
|
|
30
|
+
debug?: boolean;
|
|
31
|
+
/** Throttle debug logs (ms) */
|
|
32
|
+
debugLogIntervalMs?: number;
|
|
33
|
+
/** Idle time after last output before injecting (ms) */
|
|
34
|
+
idleBeforeInjectMs?: number;
|
|
35
|
+
/** Retry interval while waiting for idle window (ms) */
|
|
36
|
+
injectRetryMs?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare class TmuxWrapper {
|
|
39
|
+
private config;
|
|
40
|
+
private sessionName;
|
|
41
|
+
private client;
|
|
42
|
+
private parser;
|
|
43
|
+
private inbox?;
|
|
44
|
+
private running;
|
|
45
|
+
private pollTimer?;
|
|
46
|
+
private attachProcess?;
|
|
47
|
+
private lastCapturedOutput;
|
|
48
|
+
private lastOutputTime;
|
|
49
|
+
private recentlySentMessages;
|
|
50
|
+
private sentMessageHashes;
|
|
51
|
+
private messageQueue;
|
|
52
|
+
private isInjecting;
|
|
53
|
+
private processedOutputLength;
|
|
54
|
+
private lastDebugLog;
|
|
55
|
+
constructor(config: TmuxWrapperConfig);
|
|
56
|
+
/**
|
|
57
|
+
* Log to stderr (safe - doesn't interfere with tmux display)
|
|
58
|
+
*/
|
|
59
|
+
private logStderr;
|
|
60
|
+
/**
|
|
61
|
+
* Build the full command with proper quoting
|
|
62
|
+
* Args containing spaces need to be quoted
|
|
63
|
+
*/
|
|
64
|
+
private buildCommand;
|
|
65
|
+
/**
|
|
66
|
+
* Check if tmux session exists
|
|
67
|
+
*/
|
|
68
|
+
private sessionExists;
|
|
69
|
+
/**
|
|
70
|
+
* Start the wrapped agent process
|
|
71
|
+
*/
|
|
72
|
+
start(): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Wait for tmux session to be ready
|
|
75
|
+
*/
|
|
76
|
+
private waitForSession;
|
|
77
|
+
/**
|
|
78
|
+
* Wait for shell prompt to appear (shell is ready for input)
|
|
79
|
+
*/
|
|
80
|
+
private waitForShellReady;
|
|
81
|
+
/**
|
|
82
|
+
* Attach user to tmux session
|
|
83
|
+
* This spawns tmux attach and lets it take over stdin/stdout
|
|
84
|
+
*/
|
|
85
|
+
private attachToSession;
|
|
86
|
+
/**
|
|
87
|
+
* Start silent polling for @relay commands
|
|
88
|
+
* Does NOT write to stdout - just parses and sends to daemon
|
|
89
|
+
*/
|
|
90
|
+
private startSilentPolling;
|
|
91
|
+
/**
|
|
92
|
+
* Poll for @relay commands in output (silent)
|
|
93
|
+
*/
|
|
94
|
+
private pollForRelayCommands;
|
|
95
|
+
/**
|
|
96
|
+
* Strip ANSI escape codes
|
|
97
|
+
*/
|
|
98
|
+
private stripAnsi;
|
|
99
|
+
/**
|
|
100
|
+
* Send relay command to daemon
|
|
101
|
+
*/
|
|
102
|
+
private sendRelayCommand;
|
|
103
|
+
/**
|
|
104
|
+
* Handle incoming message from relay
|
|
105
|
+
*/
|
|
106
|
+
private handleIncomingMessage;
|
|
107
|
+
/**
|
|
108
|
+
* Check if we should inject a message
|
|
109
|
+
*/
|
|
110
|
+
private checkForInjectionOpportunity;
|
|
111
|
+
/**
|
|
112
|
+
* Inject message via tmux send-keys
|
|
113
|
+
*/
|
|
114
|
+
private injectNextMessage;
|
|
115
|
+
/**
|
|
116
|
+
* Send special keys to tmux
|
|
117
|
+
*/
|
|
118
|
+
private sendKeys;
|
|
119
|
+
/**
|
|
120
|
+
* Send literal text to tmux
|
|
121
|
+
*/
|
|
122
|
+
private sendKeysLiteral;
|
|
123
|
+
private sleep;
|
|
124
|
+
/**
|
|
125
|
+
* Stop and cleanup
|
|
126
|
+
*/
|
|
127
|
+
stop(): void;
|
|
128
|
+
get isRunning(): boolean;
|
|
129
|
+
get name(): string;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=tmux-wrapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux-wrapper.d.ts","sourceRoot":"","sources":["../../src/wrapper/tmux-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+BAA+B;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,KAAK,CAAC,CAAe;IAC7B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAC,CAAiB;IACnC,OAAO,CAAC,aAAa,CAAC,CAAe;IACrC,OAAO,CAAC,kBAAkB,CAAM;IAChC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,oBAAoB,CAAkC;IAC9D,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,iBAAiB;IA2CrC;;OAEG;IACH,OAAO,CAAC,SAAS;IAejB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAiBpB;;OAEG;YACW,aAAa;IAS3B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkE5B;;OAEG;YACW,cAAc;IAc5B;;OAEG;YACW,iBAAiB;IAkC/B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAyBvB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAQ1B;;OAEG;YACW,oBAAoB;IAmClC;;OAEG;IACH,OAAO,CAAC,SAAS;IAKjB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAe7B;;OAEG;IACH,OAAO,CAAC,4BAA4B;IAgBpC;;OAEG;YACW,iBAAiB;IAyC/B;;OAEG;YACW,QAAQ;IAItB;;OAEG;YACW,eAAe;IAM7B,OAAO,CAAC,KAAK;IAIb;;OAEG;IACH,IAAI,IAAI,IAAI;IAqBZ,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TmuxWrapper - Attach-based tmux wrapper
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* 1. Start agent in detached tmux session
|
|
6
|
+
* 2. Attach user to tmux (they see real terminal)
|
|
7
|
+
* 3. Background: poll capture-pane silently (no stdout writes)
|
|
8
|
+
* 4. Background: parse @relay commands, send to daemon
|
|
9
|
+
* 5. Background: inject messages via send-keys
|
|
10
|
+
*
|
|
11
|
+
* The key insight: user sees the REAL tmux session, not a proxy.
|
|
12
|
+
* We just do background parsing and injection.
|
|
13
|
+
*/
|
|
14
|
+
import { exec, execSync, spawn } from 'node:child_process';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
16
|
+
import { RelayClient } from './client.js';
|
|
17
|
+
import { OutputParser } from './parser.js';
|
|
18
|
+
import { InboxManager } from './inbox.js';
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
export class TmuxWrapper {
|
|
21
|
+
config;
|
|
22
|
+
sessionName;
|
|
23
|
+
client;
|
|
24
|
+
parser;
|
|
25
|
+
inbox;
|
|
26
|
+
running = false;
|
|
27
|
+
pollTimer;
|
|
28
|
+
attachProcess;
|
|
29
|
+
lastCapturedOutput = '';
|
|
30
|
+
lastOutputTime = 0;
|
|
31
|
+
recentlySentMessages = new Map();
|
|
32
|
+
sentMessageHashes = new Set(); // Permanent dedup
|
|
33
|
+
messageQueue = [];
|
|
34
|
+
isInjecting = false;
|
|
35
|
+
// Track processed output to avoid re-parsing
|
|
36
|
+
processedOutputLength = 0;
|
|
37
|
+
lastDebugLog = 0;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.config = {
|
|
40
|
+
cols: process.stdout.columns || 120,
|
|
41
|
+
rows: process.stdout.rows || 40,
|
|
42
|
+
pollInterval: 200, // Slightly slower polling since we're not displaying
|
|
43
|
+
idleBeforeInjectMs: 1500,
|
|
44
|
+
injectRetryMs: 500,
|
|
45
|
+
debug: true,
|
|
46
|
+
debugLogIntervalMs: 0,
|
|
47
|
+
...config,
|
|
48
|
+
};
|
|
49
|
+
// Generate unique session name
|
|
50
|
+
this.sessionName = `relay-${config.name}-${process.pid}`;
|
|
51
|
+
this.client = new RelayClient({
|
|
52
|
+
agentName: config.name,
|
|
53
|
+
socketPath: config.socketPath,
|
|
54
|
+
});
|
|
55
|
+
this.parser = new OutputParser();
|
|
56
|
+
// Initialize inbox if using file-based messaging
|
|
57
|
+
if (config.useInbox) {
|
|
58
|
+
this.inbox = new InboxManager({
|
|
59
|
+
agentName: config.name,
|
|
60
|
+
inboxDir: config.inboxDir,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Handle incoming messages from relay
|
|
64
|
+
this.client.onMessage = (from, payload) => {
|
|
65
|
+
this.handleIncomingMessage(from, payload);
|
|
66
|
+
};
|
|
67
|
+
this.client.onStateChange = (state) => {
|
|
68
|
+
// Only log to stderr, never stdout (user is in tmux)
|
|
69
|
+
if (state === 'READY') {
|
|
70
|
+
this.logStderr(`Connected to relay daemon`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Log to stderr (safe - doesn't interfere with tmux display)
|
|
76
|
+
*/
|
|
77
|
+
logStderr(msg, force = false) {
|
|
78
|
+
if (!force && this.config.debug === false)
|
|
79
|
+
return;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (!force && this.config.debugLogIntervalMs && this.config.debugLogIntervalMs > 0) {
|
|
82
|
+
if (now - this.lastDebugLog < this.config.debugLogIntervalMs) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.lastDebugLog = now;
|
|
86
|
+
}
|
|
87
|
+
// Prefix with newline to avoid corrupting tmux status line
|
|
88
|
+
process.stderr.write(`\r[relay:${this.config.name}] ${msg}\n`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build the full command with proper quoting
|
|
92
|
+
* Args containing spaces need to be quoted
|
|
93
|
+
*/
|
|
94
|
+
buildCommand() {
|
|
95
|
+
if (!this.config.args || this.config.args.length === 0) {
|
|
96
|
+
return this.config.command;
|
|
97
|
+
}
|
|
98
|
+
// Quote any argument that contains spaces, quotes, or special chars
|
|
99
|
+
const quotedArgs = this.config.args.map(arg => {
|
|
100
|
+
if (arg.includes(' ') || arg.includes('"') || arg.includes("'") || arg.includes('$')) {
|
|
101
|
+
// Use double quotes and escape internal quotes
|
|
102
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
103
|
+
}
|
|
104
|
+
return arg;
|
|
105
|
+
});
|
|
106
|
+
return `${this.config.command} ${quotedArgs.join(' ')}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if tmux session exists
|
|
110
|
+
*/
|
|
111
|
+
async sessionExists() {
|
|
112
|
+
try {
|
|
113
|
+
await execAsync(`tmux has-session -t ${this.sessionName} 2>/dev/null`);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Start the wrapped agent process
|
|
122
|
+
*/
|
|
123
|
+
async start() {
|
|
124
|
+
if (this.running)
|
|
125
|
+
return;
|
|
126
|
+
// Initialize inbox if enabled
|
|
127
|
+
if (this.inbox) {
|
|
128
|
+
this.inbox.init();
|
|
129
|
+
}
|
|
130
|
+
// Connect to relay daemon (in background, don't block)
|
|
131
|
+
this.client.connect().catch(() => {
|
|
132
|
+
// Silent - relay connection is optional
|
|
133
|
+
});
|
|
134
|
+
// Kill any existing session with this name
|
|
135
|
+
try {
|
|
136
|
+
execSync(`tmux kill-session -t ${this.sessionName} 2>/dev/null`);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Session doesn't exist, that's fine
|
|
140
|
+
}
|
|
141
|
+
// Build the command - properly quote args that contain spaces
|
|
142
|
+
const fullCommand = this.buildCommand();
|
|
143
|
+
this.logStderr(`Command: ${fullCommand}`);
|
|
144
|
+
// Create tmux session
|
|
145
|
+
try {
|
|
146
|
+
execSync(`tmux new-session -d -s ${this.sessionName} -x ${this.config.cols} -y ${this.config.rows}`, {
|
|
147
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
148
|
+
stdio: 'pipe',
|
|
149
|
+
});
|
|
150
|
+
// Set environment variables
|
|
151
|
+
for (const [key, value] of Object.entries({
|
|
152
|
+
...this.config.env,
|
|
153
|
+
AGENT_RELAY_NAME: this.config.name,
|
|
154
|
+
TERM: 'xterm-256color',
|
|
155
|
+
})) {
|
|
156
|
+
const escaped = value.replace(/"/g, '\\"');
|
|
157
|
+
execSync(`tmux setenv -t ${this.sessionName} ${key} "${escaped}"`);
|
|
158
|
+
}
|
|
159
|
+
// Wait for shell to be ready (look for prompt)
|
|
160
|
+
await this.waitForShellReady();
|
|
161
|
+
// Send the command to run
|
|
162
|
+
await this.sendKeysLiteral(fullCommand);
|
|
163
|
+
await this.sleep(100);
|
|
164
|
+
await this.sendKeys('Enter');
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
throw new Error(`Failed to create tmux session: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
// Wait for session to be ready
|
|
170
|
+
await this.waitForSession();
|
|
171
|
+
this.running = true;
|
|
172
|
+
// Start background polling (silent - no stdout writes)
|
|
173
|
+
this.startSilentPolling();
|
|
174
|
+
// Attach user to tmux session
|
|
175
|
+
// This takes over stdin/stdout - user sees the real terminal
|
|
176
|
+
this.attachToSession();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wait for tmux session to be ready
|
|
180
|
+
*/
|
|
181
|
+
async waitForSession(maxWaitMs = 5000) {
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
184
|
+
if (await this.sessionExists()) {
|
|
185
|
+
await new Promise(r => setTimeout(r, 200));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await new Promise(r => setTimeout(r, 100));
|
|
189
|
+
}
|
|
190
|
+
throw new Error('Timeout waiting for tmux session');
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Wait for shell prompt to appear (shell is ready for input)
|
|
194
|
+
*/
|
|
195
|
+
async waitForShellReady(maxWaitMs = 10000) {
|
|
196
|
+
const startTime = Date.now();
|
|
197
|
+
// Common prompt endings: $, %, >, ➜, #
|
|
198
|
+
const promptPatterns = /[$%>#➜]\s*$/;
|
|
199
|
+
this.logStderr('Waiting for shell to initialize...');
|
|
200
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
201
|
+
try {
|
|
202
|
+
const { stdout } = await execAsync(`tmux capture-pane -t ${this.sessionName} -p 2>/dev/null`);
|
|
203
|
+
// Check if the last non-empty line looks like a prompt
|
|
204
|
+
const lines = stdout.split('\n').filter(l => l.trim());
|
|
205
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
206
|
+
if (promptPatterns.test(lastLine)) {
|
|
207
|
+
this.logStderr('Shell ready');
|
|
208
|
+
// Extra delay to ensure shell is fully ready
|
|
209
|
+
await this.sleep(200);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Session might not be ready yet
|
|
215
|
+
}
|
|
216
|
+
await this.sleep(200);
|
|
217
|
+
}
|
|
218
|
+
// Fallback: proceed anyway after timeout
|
|
219
|
+
this.logStderr('Shell ready timeout, proceeding anyway');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Attach user to tmux session
|
|
223
|
+
* This spawns tmux attach and lets it take over stdin/stdout
|
|
224
|
+
*/
|
|
225
|
+
attachToSession() {
|
|
226
|
+
this.attachProcess = spawn('tmux', ['attach-session', '-t', this.sessionName], {
|
|
227
|
+
stdio: 'inherit', // User's terminal connects directly to tmux
|
|
228
|
+
});
|
|
229
|
+
this.attachProcess.on('exit', (code) => {
|
|
230
|
+
this.logStderr(`Session ended (code: ${code})`, true);
|
|
231
|
+
this.stop();
|
|
232
|
+
process.exit(code ?? 0);
|
|
233
|
+
});
|
|
234
|
+
this.attachProcess.on('error', (err) => {
|
|
235
|
+
this.logStderr(`Attach error: ${err.message}`, true);
|
|
236
|
+
this.stop();
|
|
237
|
+
process.exit(1);
|
|
238
|
+
});
|
|
239
|
+
// Handle signals
|
|
240
|
+
const cleanup = () => {
|
|
241
|
+
this.stop();
|
|
242
|
+
};
|
|
243
|
+
process.on('SIGINT', cleanup);
|
|
244
|
+
process.on('SIGTERM', cleanup);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Start silent polling for @relay commands
|
|
248
|
+
* Does NOT write to stdout - just parses and sends to daemon
|
|
249
|
+
*/
|
|
250
|
+
startSilentPolling() {
|
|
251
|
+
this.pollTimer = setInterval(() => {
|
|
252
|
+
this.pollForRelayCommands().catch(() => {
|
|
253
|
+
// Ignore poll errors
|
|
254
|
+
});
|
|
255
|
+
}, this.config.pollInterval);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Poll for @relay commands in output (silent)
|
|
259
|
+
*/
|
|
260
|
+
async pollForRelayCommands() {
|
|
261
|
+
if (!this.running)
|
|
262
|
+
return;
|
|
263
|
+
try {
|
|
264
|
+
// Capture scrollback
|
|
265
|
+
const { stdout } = await execAsync(`tmux capture-pane -t ${this.sessionName} -p -S - 2>/dev/null`);
|
|
266
|
+
// Always parse the FULL capture for @relay commands
|
|
267
|
+
// This handles terminal UIs that rewrite content in place
|
|
268
|
+
const cleanContent = this.stripAnsi(stdout);
|
|
269
|
+
const { commands } = this.parser.parse(cleanContent);
|
|
270
|
+
// Track last output time for injection timing
|
|
271
|
+
if (stdout.length !== this.processedOutputLength) {
|
|
272
|
+
this.lastOutputTime = Date.now();
|
|
273
|
+
this.processedOutputLength = stdout.length;
|
|
274
|
+
}
|
|
275
|
+
// Send any commands found (deduplication handles repeats)
|
|
276
|
+
for (const cmd of commands) {
|
|
277
|
+
this.sendRelayCommand(cmd);
|
|
278
|
+
}
|
|
279
|
+
// Also check for injection opportunity
|
|
280
|
+
this.checkForInjectionOpportunity();
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (err.message?.includes('no such session')) {
|
|
284
|
+
this.stop();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Strip ANSI escape codes
|
|
290
|
+
*/
|
|
291
|
+
stripAnsi(str) {
|
|
292
|
+
// eslint-disable-next-line no-control-regex
|
|
293
|
+
return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Send relay command to daemon
|
|
297
|
+
*/
|
|
298
|
+
sendRelayCommand(cmd) {
|
|
299
|
+
const msgHash = `${cmd.to}:${cmd.body}`;
|
|
300
|
+
// Permanent dedup - never send the same message twice
|
|
301
|
+
if (this.sentMessageHashes.has(msgHash))
|
|
302
|
+
return;
|
|
303
|
+
const success = this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data);
|
|
304
|
+
if (success) {
|
|
305
|
+
this.sentMessageHashes.add(msgHash);
|
|
306
|
+
this.logStderr(`→ ${cmd.to}: ${cmd.body.substring(0, 40)}...`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Handle incoming message from relay
|
|
311
|
+
*/
|
|
312
|
+
handleIncomingMessage(from, payload) {
|
|
313
|
+
this.logStderr(`← ${from}: ${payload.body.substring(0, 40)}...`);
|
|
314
|
+
// Queue for injection
|
|
315
|
+
this.messageQueue.push({ from, body: payload.body });
|
|
316
|
+
// Write to inbox if enabled
|
|
317
|
+
if (this.inbox) {
|
|
318
|
+
this.inbox.addMessage(from, payload.body);
|
|
319
|
+
}
|
|
320
|
+
// Try to inject
|
|
321
|
+
this.checkForInjectionOpportunity();
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Check if we should inject a message
|
|
325
|
+
*/
|
|
326
|
+
checkForInjectionOpportunity() {
|
|
327
|
+
if (this.messageQueue.length === 0)
|
|
328
|
+
return;
|
|
329
|
+
if (this.isInjecting)
|
|
330
|
+
return;
|
|
331
|
+
if (!this.running)
|
|
332
|
+
return;
|
|
333
|
+
// Wait for output to settle (agent might be busy)
|
|
334
|
+
const timeSinceOutput = Date.now() - this.lastOutputTime;
|
|
335
|
+
if (timeSinceOutput < (this.config.idleBeforeInjectMs ?? 1500)) {
|
|
336
|
+
const retryMs = this.config.injectRetryMs ?? 500;
|
|
337
|
+
setTimeout(() => this.checkForInjectionOpportunity(), retryMs);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
this.injectNextMessage();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Inject message via tmux send-keys
|
|
344
|
+
*/
|
|
345
|
+
async injectNextMessage() {
|
|
346
|
+
const msg = this.messageQueue.shift();
|
|
347
|
+
if (!msg)
|
|
348
|
+
return;
|
|
349
|
+
this.isInjecting = true;
|
|
350
|
+
this.logStderr(`Injecting message from ${msg.from}`);
|
|
351
|
+
try {
|
|
352
|
+
let sanitizedBody = msg.body.replace(/[\r\n]+/g, ' ').trim();
|
|
353
|
+
// Truncate long messages to avoid display issues
|
|
354
|
+
const maxLen = 500;
|
|
355
|
+
if (sanitizedBody.length > maxLen) {
|
|
356
|
+
sanitizedBody = sanitizedBody.substring(0, maxLen) + '... [truncated]';
|
|
357
|
+
}
|
|
358
|
+
const injection = `Relay message from ${msg.from}: ${sanitizedBody}`;
|
|
359
|
+
// Clear any partial input
|
|
360
|
+
await this.sendKeys('Escape');
|
|
361
|
+
await this.sleep(30);
|
|
362
|
+
await this.sendKeys('C-u');
|
|
363
|
+
await this.sleep(30);
|
|
364
|
+
// Type the message
|
|
365
|
+
await this.sendKeysLiteral(injection);
|
|
366
|
+
await this.sleep(50);
|
|
367
|
+
// Submit
|
|
368
|
+
await this.sendKeys('Enter');
|
|
369
|
+
this.logStderr(`Injection complete`);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
this.logStderr(`Injection failed: ${err.message}`, true);
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
this.isInjecting = false;
|
|
376
|
+
if (this.messageQueue.length > 0) {
|
|
377
|
+
setTimeout(() => this.checkForInjectionOpportunity(), 1000);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Send special keys to tmux
|
|
383
|
+
*/
|
|
384
|
+
async sendKeys(keys) {
|
|
385
|
+
await execAsync(`tmux send-keys -t ${this.sessionName} ${keys}`);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Send literal text to tmux
|
|
389
|
+
*/
|
|
390
|
+
async sendKeysLiteral(text) {
|
|
391
|
+
// Escape for shell and use -l for literal
|
|
392
|
+
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
393
|
+
await execAsync(`tmux send-keys -t ${this.sessionName} -l "${escaped}"`);
|
|
394
|
+
}
|
|
395
|
+
sleep(ms) {
|
|
396
|
+
return new Promise(r => setTimeout(r, ms));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Stop and cleanup
|
|
400
|
+
*/
|
|
401
|
+
stop() {
|
|
402
|
+
if (!this.running)
|
|
403
|
+
return;
|
|
404
|
+
this.running = false;
|
|
405
|
+
// Stop polling
|
|
406
|
+
if (this.pollTimer) {
|
|
407
|
+
clearInterval(this.pollTimer);
|
|
408
|
+
this.pollTimer = undefined;
|
|
409
|
+
}
|
|
410
|
+
// Kill tmux session
|
|
411
|
+
try {
|
|
412
|
+
execSync(`tmux kill-session -t ${this.sessionName} 2>/dev/null`);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// Ignore
|
|
416
|
+
}
|
|
417
|
+
// Disconnect relay
|
|
418
|
+
this.client.destroy();
|
|
419
|
+
}
|
|
420
|
+
get isRunning() {
|
|
421
|
+
return this.running;
|
|
422
|
+
}
|
|
423
|
+
get name() {
|
|
424
|
+
return this.config.name;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
//# sourceMappingURL=tmux-wrapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux-wrapper.js","sourceRoot":"","sources":["../../src/wrapper/tmux-wrapper.ts"],"names":[],"mappings":"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"}
|
package/install.sh
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Agent Relay Installer
|
|
5
|
+
# Usage: curl -fsSL https://raw.githubusercontent.com/khaliqgant/agent-relay/main/install.sh | bash
|
|
6
|
+
|
|
7
|
+
REPO="khaliqgant/agent-relay"
|
|
8
|
+
INSTALL_DIR="${AGENT_RELAY_INSTALL_DIR:-$HOME/.agent-relay}"
|
|
9
|
+
BIN_DIR="${AGENT_RELAY_BIN_DIR:-$HOME/.local/bin}"
|
|
10
|
+
|
|
11
|
+
# Colors
|
|
12
|
+
RED='\033[0;31m'
|
|
13
|
+
GREEN='\033[0;32m'
|
|
14
|
+
YELLOW='\033[1;33m'
|
|
15
|
+
BLUE='\033[0;34m'
|
|
16
|
+
NC='\033[0m'
|
|
17
|
+
|
|
18
|
+
info() { echo -e "${BLUE}[info]${NC} $1"; }
|
|
19
|
+
success() { echo -e "${GREEN}[success]${NC} $1"; }
|
|
20
|
+
warn() { echo -e "${YELLOW}[warn]${NC} $1"; }
|
|
21
|
+
error() { echo -e "${RED}[error]${NC} $1"; exit 1; }
|
|
22
|
+
|
|
23
|
+
check_requirements() {
|
|
24
|
+
if ! command -v node &> /dev/null; then
|
|
25
|
+
error "Node.js is required. Please install Node.js 20+ first."
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
|
29
|
+
if [ "$NODE_VERSION" -lt 20 ]; then
|
|
30
|
+
error "Node.js 20+ required. Found: $(node -v)"
|
|
31
|
+
fi
|
|
32
|
+
info "Node.js $(node -v) detected"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
install_source() {
|
|
36
|
+
info "Installing from source..."
|
|
37
|
+
mkdir -p "$INSTALL_DIR" "$BIN_DIR"
|
|
38
|
+
|
|
39
|
+
if command -v git &> /dev/null; then
|
|
40
|
+
[ -d "$INSTALL_DIR/.git" ] && cd "$INSTALL_DIR" && git pull || git clone "https://github.com/$REPO.git" "$INSTALL_DIR"
|
|
41
|
+
else
|
|
42
|
+
curl -fsSL "https://github.com/$REPO/archive/main.tar.gz" | tar -xz -C "$INSTALL_DIR" --strip-components=1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
cd "$INSTALL_DIR" && npm ci && npm run build
|
|
46
|
+
ln -sf "$INSTALL_DIR/dist/cli/index.js" "$BIN_DIR/agent-relay"
|
|
47
|
+
chmod +x "$BIN_DIR/agent-relay"
|
|
48
|
+
|
|
49
|
+
[[ ":$PATH:" != *":$BIN_DIR:"* ]] && warn "Add to PATH: export PATH=\"\$PATH:$BIN_DIR\""
|
|
50
|
+
success "Installed to $INSTALL_DIR"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main() {
|
|
54
|
+
echo -e "\n${YELLOW}⚡ Agent Relay${NC} Installer\n"
|
|
55
|
+
check_requirements
|
|
56
|
+
install_source
|
|
57
|
+
echo -e "\nQuick Start:"
|
|
58
|
+
echo -e " # Start the daemon"
|
|
59
|
+
echo -e " agent-relay start -f"
|
|
60
|
+
echo -e ""
|
|
61
|
+
echo -e " # Wrap an agent (tmux mode is default)"
|
|
62
|
+
echo -e " agent-relay wrap -n MyAgent \"claude\""
|
|
63
|
+
echo -e ""
|
|
64
|
+
echo -e " # Open the dashboard"
|
|
65
|
+
echo -e " agent-relay dashboard"
|
|
66
|
+
echo -e ""
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main "$@"
|