xmux-bridge 1.0.39
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/README.md +219 -0
- package/bin/xmux +9 -0
- package/bridge-mcp-server.js +407 -0
- package/dist/bin/xmux-mailbox.js +6 -0
- package/dist/mailbox/cli.js +3 -0
- package/dist/mailbox/core.js +3 -0
- package/package.json +38 -0
- package/scripts/setup_claude_mcp.js +149 -0
- package/scripts/setup_copilot_mcp.js +87 -0
- package/scripts/setup_gemini_mcp.js +89 -0
- package/scripts/setup_xmux_codex_mcp.js +799 -0
- package/scripts/trust_codex_project.js +44 -0
- package/scripts/trust_copilot_project.js +49 -0
- package/scripts/trust_gemini_project.js +47 -0
- package/src/mailbox/cli.js +251 -0
- package/src/mailbox/core.js +887 -0
- package/src/runtime/errors.js +12 -0
- package/src/runtime/json.js +75 -0
- package/src/runtime/lock.js +56 -0
- package/src/runtime/time.js +27 -0
- package/xmux-bridge.zsh +481 -0
- package/xmux-lead-mcp-server.js +486 -0
- package/xmux.zsh +5146 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { MailboxError } = require('./errors');
|
|
8
|
+
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return Boolean(value) && Object.prototype.toString.call(value) === '[object Object]';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sortKeys(value) {
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.map(sortKeys);
|
|
16
|
+
}
|
|
17
|
+
if (isPlainObject(value)) {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const key of Object.keys(value).sort()) {
|
|
20
|
+
out[key] = sortKeys(value[key]);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureAscii(text) {
|
|
28
|
+
return text.replace(/[^\x00-\x7f]/g, (ch) => {
|
|
29
|
+
const code = ch.charCodeAt(0);
|
|
30
|
+
return `\\u${code.toString(16).padStart(4, '0')}`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toJsonString(data, pretty = false) {
|
|
35
|
+
const normalized = sortKeys(data);
|
|
36
|
+
const json = JSON.stringify(normalized, null, pretty ? 2 : 0);
|
|
37
|
+
return ensureAscii(json);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readJson(filePath, defaultValue) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err && err.code === 'ENOENT') {
|
|
46
|
+
return defaultValue;
|
|
47
|
+
}
|
|
48
|
+
if (err instanceof SyntaxError) {
|
|
49
|
+
throw new MailboxError(`invalid JSON in ${filePath}: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function atomicWriteJson(filePath, data) {
|
|
56
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
57
|
+
const tmpPath = path.join(
|
|
58
|
+
path.dirname(filePath),
|
|
59
|
+
`.tmp-${crypto.randomBytes(6).toString('hex')}.json`,
|
|
60
|
+
);
|
|
61
|
+
fs.writeFileSync(tmpPath, `${toJsonString(data, true)}\n`, 'utf8');
|
|
62
|
+
fs.renameSync(tmpPath, filePath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printJson(data) {
|
|
66
|
+
process.stdout.write(`${toJsonString(data, false)}\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
atomicWriteJson,
|
|
71
|
+
isPlainObject,
|
|
72
|
+
printJson,
|
|
73
|
+
readJson,
|
|
74
|
+
toJsonString,
|
|
75
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { sleepMs } = require('./time');
|
|
7
|
+
|
|
8
|
+
function withFileLock(targetPath, fn, attempts = 200, sleepDelayMs = 25) {
|
|
9
|
+
const lockDir = `${targetPath}.lock.d`;
|
|
10
|
+
const parentDir = path.dirname(lockDir) || '.';
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
14
|
+
} catch (_) {
|
|
15
|
+
// Parent creation can fail for permission/race reasons; mkdir for lock
|
|
16
|
+
// will surface the actionable error.
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let acquired = false;
|
|
20
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(lockDir);
|
|
23
|
+
acquired = true;
|
|
24
|
+
break;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err && err.code === 'EEXIST') {
|
|
27
|
+
sleepMs(sleepDelayMs);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (err && err.code === 'ENOENT') {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!acquired) {
|
|
38
|
+
throw new Error(`could not acquire lock on ${targetPath}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return fn();
|
|
43
|
+
} finally {
|
|
44
|
+
try {
|
|
45
|
+
fs.rmdirSync(lockDir);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (!err || err.code !== 'ENOENT') {
|
|
48
|
+
// Ignore lock cleanup races; stale lock handling is best-effort.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
withFileLock,
|
|
56
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function nowTs() {
|
|
4
|
+
return new Date().toISOString();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sleepMs(ms) {
|
|
8
|
+
const waitMs = Math.max(0, Number(ms) || 0);
|
|
9
|
+
if (waitMs <= 0) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const buffer = new SharedArrayBuffer(4);
|
|
14
|
+
const view = new Int32Array(buffer);
|
|
15
|
+
Atomics.wait(view, 0, 0, waitMs);
|
|
16
|
+
} catch (_) {
|
|
17
|
+
const end = Date.now() + waitMs;
|
|
18
|
+
while (Date.now() < end) {
|
|
19
|
+
// busy wait fallback
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
nowTs,
|
|
26
|
+
sleepMs,
|
|
27
|
+
};
|
package/xmux-bridge.zsh
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
# xmux-bridge.zsh
|
|
3
|
+
# Provider-neutral XMux inbox relay. It polls a teammate inbox under
|
|
4
|
+
# <project>/.codex/xmux/teams/<team>/inboxes/<agent>.json and pastes unread
|
|
5
|
+
# messages into the target tmux pane.
|
|
6
|
+
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
if [[ -n "${XMUX_BRIDGE_PATH:-}" ]]; then
|
|
10
|
+
PATH="$XMUX_BRIDGE_PATH"
|
|
11
|
+
else
|
|
12
|
+
PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
|
13
|
+
fi
|
|
14
|
+
_XMUX_BRIDGE_SOURCED_DIR="${${(%):-%x}:A:h}"
|
|
15
|
+
TMUX_BIN="${XMUX_TMUX_BIN:-tmux}"
|
|
16
|
+
NODE_BIN="${XMUX_NODE_BIN:-node}"
|
|
17
|
+
|
|
18
|
+
_xmux_tmux() {
|
|
19
|
+
command "$TMUX_BIN" "$@"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if [[ -n "${XMUX_INSTALL_DIR:-}" ]]; then
|
|
23
|
+
XMUX_INSTALL_DIR="${XMUX_INSTALL_DIR:A}"
|
|
24
|
+
else
|
|
25
|
+
XMUX_INSTALL_DIR="$_XMUX_BRIDGE_SOURCED_DIR"
|
|
26
|
+
fi
|
|
27
|
+
export XMUX_INSTALL_DIR
|
|
28
|
+
XMUX_MAILBOX_NODE_CLI="$XMUX_INSTALL_DIR/dist/bin/xmux-mailbox.js"
|
|
29
|
+
|
|
30
|
+
_xmux_node() {
|
|
31
|
+
command "$NODE_BIN" "$@"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_xmux_mailbox_cli() {
|
|
35
|
+
[[ -f "$XMUX_MAILBOX_NODE_CLI" ]] || return 127
|
|
36
|
+
_xmux_node "$XMUX_MAILBOX_NODE_CLI" "$@"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_xmux_bridge_project_root() {
|
|
40
|
+
local dir="${1:-$PWD}"
|
|
41
|
+
dir="${dir:A}"
|
|
42
|
+
while [[ "$dir" != "/" && -n "$dir" ]]; do
|
|
43
|
+
if [[ -e "$dir/.git" ]]; then
|
|
44
|
+
print -r -- "$dir"
|
|
45
|
+
return
|
|
46
|
+
fi
|
|
47
|
+
dir="${dir:h}"
|
|
48
|
+
done
|
|
49
|
+
print -r -- "${1:-$PWD}"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if [[ -n "${XMUX_PROJECT_DIR:-}" ]]; then
|
|
53
|
+
XMUX_PROJECT_DIR="${XMUX_PROJECT_DIR:A}"
|
|
54
|
+
else
|
|
55
|
+
XMUX_PROJECT_DIR="$(_xmux_bridge_project_root "$PWD")"
|
|
56
|
+
fi
|
|
57
|
+
export XMUX_PROJECT_DIR
|
|
58
|
+
|
|
59
|
+
if [[ -n "${XMUX_STATE_DIR:-}" ]]; then
|
|
60
|
+
XMUX_STATE_DIR="${XMUX_STATE_DIR:A}"
|
|
61
|
+
else
|
|
62
|
+
XMUX_STATE_DIR="$XMUX_PROJECT_DIR/.codex/xmux"
|
|
63
|
+
fi
|
|
64
|
+
export XMUX_STATE_DIR
|
|
65
|
+
unset XMUX_DIR XMUX_HOME 2>/dev/null || true
|
|
66
|
+
|
|
67
|
+
XMUX_LEAD_AGENT="${XMUX_LEAD_AGENT:-codex-lead}"
|
|
68
|
+
|
|
69
|
+
PANE_ID=""
|
|
70
|
+
TEAM_NAME=""
|
|
71
|
+
AGENT_NAME=""
|
|
72
|
+
PROVIDER=""
|
|
73
|
+
INBOX=""
|
|
74
|
+
TIMEOUT=60
|
|
75
|
+
IDLE_PATTERN=""
|
|
76
|
+
POLL_INTERVAL=0.5
|
|
77
|
+
SUBMIT_DELAY="${XMUX_SUBMIT_DELAY:-0.2}"
|
|
78
|
+
|
|
79
|
+
while getopts "p:T:a:i:x:w:d:P:" opt; do
|
|
80
|
+
case "$opt" in
|
|
81
|
+
p) PANE_ID="$OPTARG" ;;
|
|
82
|
+
T) TEAM_NAME="$OPTARG" ;;
|
|
83
|
+
a) AGENT_NAME="$OPTARG" ;;
|
|
84
|
+
P) PROVIDER="$OPTARG" ;;
|
|
85
|
+
i) INBOX="$OPTARG" ;;
|
|
86
|
+
x) TIMEOUT="$OPTARG" ;;
|
|
87
|
+
w) IDLE_PATTERN="$OPTARG" ;;
|
|
88
|
+
d) SUBMIT_DELAY="$OPTARG" ;;
|
|
89
|
+
*) echo "Usage: $0 -p <pane_id> -T <team> -a <agent> [-P <provider>] [-i <inbox>] [-x <timeout>] [-w <idle_pattern>] [-d <submit_delay>]" >&2; exit 1 ;;
|
|
90
|
+
esac
|
|
91
|
+
done
|
|
92
|
+
|
|
93
|
+
[[ -n "$PANE_ID" ]] || { echo "error: -p <pane_id> required" >&2; exit 1; }
|
|
94
|
+
[[ -n "$TEAM_NAME" ]] || { echo "error: -T <team> required" >&2; exit 1; }
|
|
95
|
+
[[ -n "$AGENT_NAME" ]] || { echo "error: -a <agent> required" >&2; exit 1; }
|
|
96
|
+
|
|
97
|
+
TEAM_DIR="$XMUX_STATE_DIR/teams/$TEAM_NAME"
|
|
98
|
+
INBOX_DIR="$TEAM_DIR/inboxes"
|
|
99
|
+
BRIDGE_PID_FILE="$TEAM_DIR/.${AGENT_NAME}-bridge.pid"
|
|
100
|
+
BRIDGE_ENV_FILE="$TEAM_DIR/.bridge-${AGENT_NAME}.env"
|
|
101
|
+
[[ -n "$INBOX" ]] || INBOX="$INBOX_DIR/$AGENT_NAME.json"
|
|
102
|
+
OUTBOX="$INBOX_DIR/$XMUX_LEAD_AGENT.json"
|
|
103
|
+
|
|
104
|
+
mkdir -p "$INBOX_DIR"
|
|
105
|
+
[[ -f "$INBOX" ]] || print -r -- '[]' > "$INBOX"
|
|
106
|
+
[[ -f "$OUTBOX" ]] || print -r -- '[]' > "$OUTBOX"
|
|
107
|
+
|
|
108
|
+
wait_for_idle() {
|
|
109
|
+
[[ -z "$IDLE_PATTERN" ]] && return 0
|
|
110
|
+
local elapsed=0
|
|
111
|
+
while (( elapsed < TIMEOUT )); do
|
|
112
|
+
if _xmux_tmux capture-pane -t "$PANE_ID" -p 2>/dev/null | grep -v '^[[:space:]]*$' | tail -8 | grep -qE "$IDLE_PATTERN"; then
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
sleep 1
|
|
116
|
+
(( elapsed++ ))
|
|
117
|
+
done
|
|
118
|
+
echo "[xmux-bridge] warning: idle timeout after ${TIMEOUT}s for $AGENT_NAME" >&2
|
|
119
|
+
return 1
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
read_unread() {
|
|
123
|
+
_xmux_node - "$INBOX" <<'NODE'
|
|
124
|
+
const fs = require('fs');
|
|
125
|
+
|
|
126
|
+
const inboxPath = process.argv[2];
|
|
127
|
+
let raw = '';
|
|
128
|
+
try {
|
|
129
|
+
raw = fs.readFileSync(inboxPath, 'utf8');
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error && error.code === 'ENOENT') {
|
|
132
|
+
process.stdout.write('');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let messages;
|
|
139
|
+
try {
|
|
140
|
+
messages = JSON.parse(raw);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const detail = error && error.message ? error.message : String(error);
|
|
143
|
+
console.error(`read_unread: JSON parse error in ${inboxPath}: ${detail}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!Array.isArray(messages)) {
|
|
148
|
+
process.stdout.write('');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const message of messages) {
|
|
153
|
+
if (message && typeof message === 'object' && !message.read) {
|
|
154
|
+
process.stdout.write(JSON.stringify(message));
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
NODE
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
parse_message() {
|
|
162
|
+
_xmux_node - "$1" <<'NODE'
|
|
163
|
+
const message = JSON.parse(process.argv[2] || '{}');
|
|
164
|
+
|
|
165
|
+
const hasText = Object.prototype.hasOwnProperty.call(message, 'text');
|
|
166
|
+
const hasMessage = Object.prototype.hasOwnProperty.call(message, 'message');
|
|
167
|
+
const rawText = hasText ? message.text : (hasMessage ? message.message : '');
|
|
168
|
+
let nested = null;
|
|
169
|
+
let text = '';
|
|
170
|
+
|
|
171
|
+
if (rawText && typeof rawText === 'object') {
|
|
172
|
+
text = JSON.stringify(rawText);
|
|
173
|
+
if (!Array.isArray(rawText)) nested = rawText;
|
|
174
|
+
} else if (typeof rawText === 'string') {
|
|
175
|
+
text = rawText;
|
|
176
|
+
try {
|
|
177
|
+
const candidate = JSON.parse(rawText);
|
|
178
|
+
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
179
|
+
nested = candidate;
|
|
180
|
+
}
|
|
181
|
+
} catch (_) {
|
|
182
|
+
nested = null;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
text = String(rawText ?? '');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const requestId =
|
|
189
|
+
message.request_id ||
|
|
190
|
+
message.requestId ||
|
|
191
|
+
((nested || {}).request_id || (nested || {}).requestId) ||
|
|
192
|
+
'';
|
|
193
|
+
const messageType = message.type || (nested || {}).type || '';
|
|
194
|
+
const fields = [
|
|
195
|
+
Buffer.from(text, 'utf8').toString('base64'),
|
|
196
|
+
String(message.timestamp ?? ''),
|
|
197
|
+
String(message.from ?? 'lead'),
|
|
198
|
+
String(requestId),
|
|
199
|
+
String(messageType),
|
|
200
|
+
];
|
|
201
|
+
process.stdout.write(fields.join('\n'));
|
|
202
|
+
NODE
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
decode_b64() {
|
|
206
|
+
_xmux_node - "$1" <<'NODE'
|
|
207
|
+
const encoded = process.argv[2] || '';
|
|
208
|
+
if (!encoded) process.exit(0);
|
|
209
|
+
process.stdout.write(Buffer.from(encoded, 'base64').toString('utf8'));
|
|
210
|
+
NODE
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mark_read_inline() {
|
|
214
|
+
_xmux_node - "$INBOX" "$1" "$2" <<'NODE'
|
|
215
|
+
const fs = require('fs');
|
|
216
|
+
const path = require('path');
|
|
217
|
+
|
|
218
|
+
const inboxPath = process.argv[2];
|
|
219
|
+
const timestamp = process.argv[3] || '';
|
|
220
|
+
const requestId = process.argv[4] || '';
|
|
221
|
+
|
|
222
|
+
function entryRequestId(entry) {
|
|
223
|
+
const direct = entry.request_id || entry.requestId || '';
|
|
224
|
+
const rawText = Object.prototype.hasOwnProperty.call(entry, 'text')
|
|
225
|
+
? entry.text
|
|
226
|
+
: (Object.prototype.hasOwnProperty.call(entry, 'message') ? entry.message : '');
|
|
227
|
+
if (direct || typeof rawText !== 'string') return direct;
|
|
228
|
+
try {
|
|
229
|
+
const nested = JSON.parse(rawText);
|
|
230
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
231
|
+
return nested.request_id || nested.requestId || '';
|
|
232
|
+
}
|
|
233
|
+
} catch (_) {
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let messages;
|
|
240
|
+
try {
|
|
241
|
+
messages = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error && error.code === 'ENOENT') process.exit(0);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!Array.isArray(messages)) process.exit(0);
|
|
248
|
+
|
|
249
|
+
for (const message of messages) {
|
|
250
|
+
if (!message || typeof message !== 'object' || message.read) continue;
|
|
251
|
+
if (timestamp && message.timestamp === timestamp) {
|
|
252
|
+
message.read = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (requestId && entryRequestId(message) === requestId) {
|
|
256
|
+
message.read = true;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (!timestamp && !requestId) {
|
|
260
|
+
message.read = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const resolved = path.resolve(inboxPath);
|
|
266
|
+
const dirName = path.dirname(resolved);
|
|
267
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
268
|
+
const tmpPath = path.join(
|
|
269
|
+
dirName,
|
|
270
|
+
`.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
|
271
|
+
);
|
|
272
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(messages, null, 2)}\n`, 'utf8');
|
|
273
|
+
fs.renameSync(tmpPath, resolved);
|
|
274
|
+
NODE
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
mark_read() {
|
|
278
|
+
local timestamp="$1" request_id="$2"
|
|
279
|
+
_xmux_mailbox_cli mark-read "$TEAM_NAME" "$AGENT_NAME" --timestamp "$timestamp" --request-id "$request_id" >/dev/null 2>&1 && return 0
|
|
280
|
+
mark_read_inline "$timestamp" "$request_id"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
append_to_lead_inline() {
|
|
284
|
+
_xmux_node - "$OUTBOX" "$AGENT_NAME" "$1" "$2" <<'NODE'
|
|
285
|
+
const fs = require('fs');
|
|
286
|
+
const path = require('path');
|
|
287
|
+
|
|
288
|
+
const outboxPath = process.argv[2];
|
|
289
|
+
const agent = process.argv[3];
|
|
290
|
+
const text = process.argv[4];
|
|
291
|
+
const requestId = process.argv[5];
|
|
292
|
+
let messages = [];
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
messages = JSON.parse(fs.readFileSync(outboxPath, 'utf8'));
|
|
296
|
+
} catch (_) {
|
|
297
|
+
messages = [];
|
|
298
|
+
}
|
|
299
|
+
if (!Array.isArray(messages)) messages = [];
|
|
300
|
+
|
|
301
|
+
const entry = {
|
|
302
|
+
from: agent,
|
|
303
|
+
text,
|
|
304
|
+
timestamp: new Date().toISOString(),
|
|
305
|
+
read: false,
|
|
306
|
+
};
|
|
307
|
+
if (requestId) entry.request_id = requestId;
|
|
308
|
+
messages.push(entry);
|
|
309
|
+
|
|
310
|
+
const resolved = path.resolve(outboxPath);
|
|
311
|
+
const dirName = path.dirname(resolved);
|
|
312
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
313
|
+
const tmpPath = path.join(
|
|
314
|
+
dirName,
|
|
315
|
+
`.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
|
316
|
+
);
|
|
317
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(messages, null, 2)}\n`, 'utf8');
|
|
318
|
+
fs.renameSync(tmpPath, resolved);
|
|
319
|
+
NODE
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
append_to_lead() {
|
|
323
|
+
local text="$1" request_id="$2"
|
|
324
|
+
if [[ -n "$request_id" ]]; then
|
|
325
|
+
_xmux_mailbox_cli write-response "$TEAM_NAME" --from "$AGENT_NAME" --text "$text" --request-id "$request_id" >/dev/null 2>&1 && return 0
|
|
326
|
+
else
|
|
327
|
+
_xmux_mailbox_cli write-response "$TEAM_NAME" --from "$AGENT_NAME" --text "$text" >/dev/null 2>&1 && return 0
|
|
328
|
+
fi
|
|
329
|
+
append_to_lead_inline "$text" "$request_id"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
focus_target_pane() {
|
|
333
|
+
[[ "$PROVIDER" == "copilot" ]] || return 0
|
|
334
|
+
_xmux_tmux send-keys -t "$PANE_ID" Escape '[' I 2>/dev/null || return 1
|
|
335
|
+
sleep 0.05
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
mark_member_inactive() {
|
|
339
|
+
_xmux_mailbox_cli update-member "$TEAM_NAME" "$AGENT_NAME" --active false >/dev/null 2>&1 && return 0
|
|
340
|
+
_xmux_node - "$TEAM_DIR/team.json" "$AGENT_NAME" <<'NODE'
|
|
341
|
+
const fs = require('fs');
|
|
342
|
+
|
|
343
|
+
const teamPath = process.argv[2];
|
|
344
|
+
const agentName = process.argv[3];
|
|
345
|
+
let config;
|
|
346
|
+
try {
|
|
347
|
+
config = JSON.parse(fs.readFileSync(teamPath, 'utf8'));
|
|
348
|
+
} catch (_) {
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const members = config.members;
|
|
353
|
+
if (!members || typeof members !== 'object' || !members[agentName] || typeof members[agentName] !== 'object') {
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
members[agentName].active = false;
|
|
358
|
+
members[agentName].updated_at = new Date().toISOString();
|
|
359
|
+
const tmpPath = `${teamPath}.tmp`;
|
|
360
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
361
|
+
fs.renameSync(tmpPath, teamPath);
|
|
362
|
+
NODE
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
paste_text() {
|
|
366
|
+
local text="$1"
|
|
367
|
+
local text_len=${#text}
|
|
368
|
+
local pos=0
|
|
369
|
+
local chunk_size=300
|
|
370
|
+
local chunk buf
|
|
371
|
+
|
|
372
|
+
focus_target_pane || return 1
|
|
373
|
+
_xmux_tmux send-keys -t "$PANE_ID" C-u 2>/dev/null || return 1
|
|
374
|
+
sleep 0.05
|
|
375
|
+
|
|
376
|
+
while (( pos < text_len )); do
|
|
377
|
+
chunk="${text:$pos:$chunk_size}"
|
|
378
|
+
buf="xmux-${$}-${RANDOM}"
|
|
379
|
+
if ! printf '%s' "$chunk" | _xmux_tmux load-buffer -b "$buf" - 2>/dev/null; then
|
|
380
|
+
echo "[xmux-bridge] error: load-buffer failed for $PANE_ID" >&2
|
|
381
|
+
return 1
|
|
382
|
+
fi
|
|
383
|
+
if ! _xmux_tmux paste-buffer -d -p -b "$buf" -t "$PANE_ID" 2>/dev/null; then
|
|
384
|
+
_xmux_tmux delete-buffer -b "$buf" 2>/dev/null
|
|
385
|
+
echo "[xmux-bridge] error: paste-buffer failed for $PANE_ID" >&2
|
|
386
|
+
return 1
|
|
387
|
+
fi
|
|
388
|
+
(( pos += chunk_size ))
|
|
389
|
+
sleep 0.05
|
|
390
|
+
done
|
|
391
|
+
|
|
392
|
+
sleep "$SUBMIT_DELAY"
|
|
393
|
+
focus_target_pane || return 1
|
|
394
|
+
buf="xmux-${$}-${RANDOM}"
|
|
395
|
+
if ! printf '\r' | _xmux_tmux load-buffer -b "$buf" - 2>/dev/null; then
|
|
396
|
+
echo "[xmux-bridge] error: load-buffer failed for submit on $PANE_ID" >&2
|
|
397
|
+
return 1
|
|
398
|
+
fi
|
|
399
|
+
if ! _xmux_tmux paste-buffer -d -b "$buf" -t "$PANE_ID" 2>/dev/null; then
|
|
400
|
+
_xmux_tmux delete-buffer -b "$buf" 2>/dev/null
|
|
401
|
+
echo "[xmux-bridge] error: paste-buffer submit failed for $PANE_ID" >&2
|
|
402
|
+
return 1
|
|
403
|
+
fi
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
cleanup() {
|
|
407
|
+
local recorded_pid=""
|
|
408
|
+
[[ -f "$BRIDGE_PID_FILE" ]] && recorded_pid="$(< "$BRIDGE_PID_FILE")"
|
|
409
|
+
if [[ "$recorded_pid" == "$$" ]]; then
|
|
410
|
+
rm -f "$BRIDGE_PID_FILE"
|
|
411
|
+
rm -f "$BRIDGE_ENV_FILE"
|
|
412
|
+
mark_member_inactive
|
|
413
|
+
fi
|
|
414
|
+
}
|
|
415
|
+
trap 'cleanup; exit 0' INT TERM EXIT
|
|
416
|
+
|
|
417
|
+
echo "[xmux-bridge] started - pane:$PANE_ID agent:$AGENT_NAME team:$TEAM_NAME"
|
|
418
|
+
|
|
419
|
+
defer_count=0
|
|
420
|
+
while true; do
|
|
421
|
+
if ! _xmux_tmux list-panes -a -F '#{pane_id}' 2>/dev/null | grep -qx -- "$PANE_ID"; then
|
|
422
|
+
append_to_lead "$AGENT_NAME pane exited." ""
|
|
423
|
+
exit 0
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
msg="$(read_unread || true)"
|
|
427
|
+
if [[ -n "$msg" ]]; then
|
|
428
|
+
parsed="$(parse_message "$msg" 2>/dev/null || true)"
|
|
429
|
+
if [[ -z "$parsed" ]]; then
|
|
430
|
+
echo "[xmux-bridge] warning: failed to parse unread message for $AGENT_NAME" >&2
|
|
431
|
+
sleep 2
|
|
432
|
+
continue
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
fields=("${(@f)parsed}")
|
|
436
|
+
text="$(decode_b64 "${fields[1]:-}")"
|
|
437
|
+
timestamp="${fields[2]:-}"
|
|
438
|
+
from_agent="${fields[3]:-lead}"
|
|
439
|
+
request_id="${fields[4]:-}"
|
|
440
|
+
msg_type="${fields[5]:-}"
|
|
441
|
+
|
|
442
|
+
if [[ "$msg_type" == "shutdown_request" ]]; then
|
|
443
|
+
mark_read "$timestamp" "$request_id"
|
|
444
|
+
if [[ -n "$request_id" ]]; then
|
|
445
|
+
append_to_lead "{\"type\":\"shutdown_approved\",\"from\":\"$AGENT_NAME\",\"request_id\":\"$request_id\"}" "$request_id"
|
|
446
|
+
else
|
|
447
|
+
append_to_lead "{\"type\":\"shutdown_approved\",\"from\":\"$AGENT_NAME\"}" ""
|
|
448
|
+
fi
|
|
449
|
+
_xmux_tmux kill-pane -t "$PANE_ID" 2>/dev/null
|
|
450
|
+
exit 0
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
if [[ -n "$request_id" ]]; then
|
|
454
|
+
text="[request_id: $request_id]
|
|
455
|
+
$text"
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
if (( ${#text} > 120 )); then
|
|
459
|
+
echo "[xmux-bridge] delivering from '$from_agent' to '$AGENT_NAME' (${#text} chars)"
|
|
460
|
+
else
|
|
461
|
+
echo "[xmux-bridge] delivering from '$from_agent' to '$AGENT_NAME': $text"
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
if ! wait_for_idle; then
|
|
465
|
+
(( defer_count++ ))
|
|
466
|
+
echo "[xmux-bridge] warning: $AGENT_NAME not idle, deferring (${defer_count})" >&2
|
|
467
|
+
sleep 3
|
|
468
|
+
continue
|
|
469
|
+
fi
|
|
470
|
+
defer_count=0
|
|
471
|
+
|
|
472
|
+
if paste_text "$text"; then
|
|
473
|
+
mark_read "$timestamp" "$request_id"
|
|
474
|
+
else
|
|
475
|
+
sleep 2
|
|
476
|
+
continue
|
|
477
|
+
fi
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
sleep "$POLL_INTERVAL"
|
|
481
|
+
done
|