@termhub/agent 0.4.4 → 0.5.1
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/cli.js +387 -56
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -39,12 +39,19 @@ var sessionName = z.string().min(1).max(128).regex(SESSION_RE);
|
|
|
39
39
|
var machinePath = z.string().min(1).max(4096).refine((p) => (p === "~" || p.startsWith("~/") || p.startsWith("/")) && !/[\0\n\r]/.test(p), "invalid path");
|
|
40
40
|
var pasteName = z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/);
|
|
41
41
|
var aiProvider = z.enum(["claude", "chatgpt", "gemini", "antigravity"]);
|
|
42
|
+
var UDID_RE = /^[A-Fa-f0-9-]{8,64}$/;
|
|
43
|
+
var udid = z.string().regex(UDID_RE);
|
|
44
|
+
function isWdaPort(port) {
|
|
45
|
+
return Number.isInteger(port) && (port >= 8100 && port <= 8199 || port >= 9100 && port <= 9199);
|
|
46
|
+
}
|
|
47
|
+
var wdaPort = z.number().int().refine(isWdaPort, "port outside the WDA ranges");
|
|
42
48
|
var TMUX_KEYS = ["Enter", "Escape", "C-c", "Up", "Down", "Tab", "y", "n", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
43
49
|
var tmuxKey = z.enum(TMUX_KEYS);
|
|
44
50
|
var TEXT_MAX_CHARS = 4e3;
|
|
45
51
|
var rpcErrorSchema = z.object({
|
|
46
|
-
/** `failed`: the operation ran on the machine and `message` says why it failed, in words meant for the user.
|
|
47
|
-
|
|
52
|
+
/** `failed`: the operation ran on the machine and `message` says why it failed, in words meant for the user.
|
|
53
|
+
* `refused`: a `tcp` open found nothing listening on the port (ECONNREFUSED). */
|
|
54
|
+
code: z.enum(["eperm", "notfound", "no_tmux", "timeout", "invalid", "internal", "failed", "refused"]),
|
|
48
55
|
message: z.string().max(2e3),
|
|
49
56
|
path: z.string().max(4096).optional()
|
|
50
57
|
});
|
|
@@ -90,7 +97,19 @@ var RPC = {
|
|
|
90
97
|
}), 15e3),
|
|
91
98
|
"hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3),
|
|
92
99
|
/** Installs `version` of @termhub/agent with npm; when the agent runs as a service it then exits so the service relaunches the new code (since agent 0.2.1). */
|
|
93
|
-
"agent.update": def(z.object({ version: z.string().regex(/^\d+\.\d+\.\d+$/) }), z.object({ installed_version: z.string(), restart: z.enum(["service", "manual"]) }), 18e4)
|
|
100
|
+
"agent.update": def(z.object({ version: z.string().regex(/^\d+\.\d+\.\d+$/) }), z.object({ installed_version: z.string(), restart: z.enum(["service", "manual"]) }), 18e4),
|
|
101
|
+
/** iOS simulator over the agent (spec 2026-09-24): raw `xcrun simctl list devices -j`; the server parses it. */
|
|
102
|
+
"sim.list": def(z.object({}), z.object({ stdout: z.string() }), 15e3),
|
|
103
|
+
/** `xcrun simctl boot`; combined output, "already booted" included — the server decides what is a failure. */
|
|
104
|
+
"sim.boot": def(z.object({ udid }), z.object({ stdout: z.string() }), 6e4),
|
|
105
|
+
/** Starts the WDA runner in its tmux session; `started: false` when the session already existed. */
|
|
106
|
+
"wda.runner.start": def(z.object({ udid, wda_port: wdaPort, mjpeg_port: wdaPort }), z.object({ started: z.boolean() }), 1e4),
|
|
107
|
+
"wda.runner.alive": def(z.object({ udid }), z.object({ alive: z.boolean() })),
|
|
108
|
+
"wda.runner.tail": def(z.object({ udid, lines: z.number().int().min(1).max(200) }), z.object({ lines: z.array(z.string()) })),
|
|
109
|
+
/** Writes ~/.termhub/wda-setup.sh and runs it in tmux `termhub-wda-setup`; `started: false` when already running. */
|
|
110
|
+
"wda.setup.start": def(z.object({}), z.object({ started: z.boolean() }), 1e4),
|
|
111
|
+
/** Raw `STATE:/VERSION:/TAIL:` text; `parseSetupOutput` on the server reads it. */
|
|
112
|
+
"wda.setup.state": def(z.object({}), z.object({ stdout: z.string() }))
|
|
94
113
|
};
|
|
95
114
|
var RPC_METHODS = Object.keys(RPC);
|
|
96
115
|
var rpcMethod = z.enum(RPC_METHODS);
|
|
@@ -100,9 +119,10 @@ var PROTOCOL_VERSION = 1;
|
|
|
100
119
|
var CLOSE = { UNAUTHORIZED: 4401, CONFLICT: 4409, VIOLATION: 1008 };
|
|
101
120
|
var channel = z2.number().int().min(1).max(4294967295);
|
|
102
121
|
var rpcId = z2.string().min(1).max(64);
|
|
103
|
-
var closedReason = z2.enum(["cli_missing", "run_failed", "killed", "missing_session", "cli_rejected"]);
|
|
122
|
+
var closedReason = z2.enum(["cli_missing", "run_failed", "killed", "missing_session", "cli_rejected", "reset"]);
|
|
104
123
|
var CAPABILITY_CLAUDE = "claude";
|
|
105
124
|
var CAPABILITY_CLAUDE_SYSTEM_PROMPT = "claude.system_prompt";
|
|
125
|
+
var CAPABILITY_SIM = "sim";
|
|
106
126
|
var helloMessage = z2.object({
|
|
107
127
|
type: z2.literal("hello"),
|
|
108
128
|
protocol: z2.number().int().min(1),
|
|
@@ -150,17 +170,21 @@ var claudeOpenParams = z2.object({
|
|
|
150
170
|
// `CAPABILITY_CLAUDE_SYSTEM_PROMPT`). Absent for the account-wide chat, whose argv must not change.
|
|
151
171
|
append_system_prompt: z2.string().max(4e3).nullable().optional()
|
|
152
172
|
});
|
|
173
|
+
var tcpOpenParams = z2.object({ port: wdaPort }).strict();
|
|
153
174
|
var openPty = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams });
|
|
154
175
|
var openClaude = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("claude"), params: claudeOpenParams });
|
|
176
|
+
var openTcp = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("tcp"), params: tcpOpenParams });
|
|
155
177
|
var serverMessage = z2.union([
|
|
156
178
|
z2.object({ type: z2.literal("rpc"), id: rpcId, method: rpcMethod, params: z2.unknown() }),
|
|
157
179
|
openPty,
|
|
158
180
|
openClaude,
|
|
181
|
+
openTcp,
|
|
159
182
|
z2.object({ type: z2.literal("resize"), ch: channel, cols: z2.number().int().min(2).max(500), rows: z2.number().int().min(2).max(200) }),
|
|
160
183
|
z2.object({ type: z2.literal("close"), ch: channel })
|
|
161
184
|
]);
|
|
162
185
|
|
|
163
186
|
// src/client.ts
|
|
187
|
+
var MAX_STREAM_PAYLOAD = MAX_FRAME - HEADER_BYTES;
|
|
164
188
|
var RevokedError = class extends Error {
|
|
165
189
|
};
|
|
166
190
|
var ProtocolMismatchError = class extends Error {
|
|
@@ -249,7 +273,14 @@ function connectOnce(opts, signal) {
|
|
|
249
273
|
}, opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS);
|
|
250
274
|
socket = {
|
|
251
275
|
sendControl: (msg) => ws.send(encodeFrame(CONTROL_CHANNEL, JSON.stringify(msg))),
|
|
252
|
-
sendStream: (ch, data) =>
|
|
276
|
+
sendStream: (ch, data) => {
|
|
277
|
+
if (data.length <= MAX_STREAM_PAYLOAD) {
|
|
278
|
+
ws.send(encodeFrame(ch, data));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
for (let off = 0; off < data.length; off += MAX_STREAM_PAYLOAD) ws.send(encodeFrame(ch, data.subarray(off, off + MAX_STREAM_PAYLOAD)));
|
|
282
|
+
},
|
|
283
|
+
bufferedAmount: () => ws.bufferedAmount
|
|
253
284
|
};
|
|
254
285
|
const hello = { type: "hello", protocol: PROTOCOL_VERSION, ...opts.hello };
|
|
255
286
|
socket.sendControl(hello);
|
|
@@ -622,7 +653,8 @@ function claudeConfigDirs(accountDirs) {
|
|
|
622
653
|
function expandHome(dir, home) {
|
|
623
654
|
return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
|
|
624
655
|
}
|
|
625
|
-
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Notification", "Stop", "SessionEnd"];
|
|
656
|
+
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd"];
|
|
657
|
+
var CLAUDE_TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PermissionRequest"]);
|
|
626
658
|
var CURSOR_HOOK_EVENTS = ["sessionStart", "beforeSubmitPrompt", "afterAgentResponse", "stop", "sessionEnd"];
|
|
627
659
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
628
660
|
# termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex / Cursor CLI hook
|
|
@@ -643,9 +675,21 @@ if [ "$TOOL" = codex ]; then EVENT="$2"; else EVENT=$(cat 2>/dev/null); fi
|
|
|
643
675
|
# screen, and only when the pair changed since the last one for this session \u2014 twenty edits in a row
|
|
644
676
|
# are one request as long as the verb stays the same (a new verb mid-run is a new request). The marker is per tmux
|
|
645
677
|
# session, under TMPDIR, with the session name reduced to filename-safe characters.
|
|
678
|
+
# AskUserQuestion is the one exception (below).
|
|
646
679
|
MARK="\${TMPDIR:-/tmp}/termhub-hook-$(printf '%s' "$SESSION" | tr -c 'A-Za-z0-9_-' '_')"
|
|
647
|
-
|
|
648
|
-
|
|
680
|
+
# The branch below is picked on the event's OWN hook_event_name \u2014 the FIRST "hook_event_name" key of
|
|
681
|
+
# the payload (Claude Code serialises it before tool_input, same reasoning as tool_name below) \u2014
|
|
682
|
+
# never a value a substring search could find nested inside a tool's input (e.g. a PermissionRequest
|
|
683
|
+
# whose tool_input happened to contain the text "hook_event_name":"PreToolUse").
|
|
684
|
+
KIND_REST=\${EVENT#*'"hook_event_name"'}
|
|
685
|
+
if [ "$KIND_REST" != "$EVENT" ]; then
|
|
686
|
+
KIND_REST=\${KIND_REST#*'"'}
|
|
687
|
+
KIND=\${KIND_REST%%'"'*}
|
|
688
|
+
else
|
|
689
|
+
KIND=
|
|
690
|
+
fi
|
|
691
|
+
case "$KIND" in
|
|
692
|
+
PreToolUse)
|
|
649
693
|
# The event's own tool name is the FIRST "tool_name" of the payload (Claude Code serialises it
|
|
650
694
|
# before tool_input), so the shortest prefix is cut \u2014 a "tool_name" nested in a tool's input
|
|
651
695
|
# must not win. Only letters, digits, "_", "." and "-" are posted (a bare Claude Code tool name,
|
|
@@ -657,35 +701,63 @@ case "$EVENT" in
|
|
|
657
701
|
REST=\${REST#*'"'}
|
|
658
702
|
NAME=\${REST%%'"'*}
|
|
659
703
|
case "$NAME" in '' | *[!A-Za-z0-9_.-]*) exit 0 ;; esac
|
|
660
|
-
#
|
|
661
|
-
#
|
|
662
|
-
#
|
|
663
|
-
#
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
# and
|
|
667
|
-
#
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
[ "
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
704
|
+
# AskUserQuestion's input is the question itself, written to be shown to the person (spec
|
|
705
|
+
# 2026-09-25 \xA74.1): the whole event goes as it came \u2014 the server keeps tool_use_id and
|
|
706
|
+
# tool_input and drops the rest \u2014 and the marker is neither read nor written, so two questions
|
|
707
|
+
# in a row are two questions. NAME is real only when $REST (everything from the first "tool_name"
|
|
708
|
+
# on) holds no SECOND "tool_name": a real question never repeats that key, so a second one means
|
|
709
|
+
# the first was actually nested inside another tool's tool_input (tool_input serialised before
|
|
710
|
+
# tool_name) and NAME does not name the real tool \u2014 fall back to the ordinary name-only path below
|
|
711
|
+
# (which, worst case, mislabels that one event; it never forwards the input).
|
|
712
|
+
ASK=false
|
|
713
|
+
if [ "$NAME" = AskUserQuestion ]; then
|
|
714
|
+
case "$REST" in
|
|
715
|
+
*'"tool_name"'*) ;;
|
|
716
|
+
*) ASK=true ;;
|
|
717
|
+
esac
|
|
718
|
+
fi
|
|
719
|
+
if [ "$ASK" != true ]; then
|
|
720
|
+
# Claude Code's spinner verb ("\u273B Moonwalking\u2026 (12s \xB7 esc to interrupt)"): the visible pane is
|
|
721
|
+
# read here, on the machine, and only the verb may leave it \u2014 one word of 2 to 24 ASCII letters
|
|
722
|
+
# right after a spinner glyph at column 0 and a single space, immediately followed by "\u2026" or
|
|
723
|
+
# "...", then the end of the line or a space. Column 0 because Claude Code draws its spinner
|
|
724
|
+
# there, while a draft in the input box or indented tool output can look just like one. The
|
|
725
|
+
# lowest such line of the last 24 non-blank rows wins (the live spinner sits above the todo list
|
|
726
|
+
# and the input box; blank rows under a short session are skipped). Both grep and sed run under
|
|
727
|
+
# LC_ALL=C: bytes the locale calls invalid then neither trip grep's "binary file matches" (which
|
|
728
|
+
# would also swallow the rest of the screen) nor sed's "illegal byte sequence" on macOS, and
|
|
729
|
+
# the match works the same on GNU, BSD and busybox. ASCII only on purpose: a customised verb
|
|
730
|
+
# with accents is dropped rather than half-matched. The case below checks the result again, so
|
|
731
|
+
# the hand-built JSON only ever gets letters.
|
|
732
|
+
VERB=$(tmux capture-pane -p -t "$TMUX_PANE" 2>/dev/null | LC_ALL=C grep -v '^[[:space:]]*$' 2>/dev/null | tail -n 24 |
|
|
733
|
+
LC_ALL=C sed -n -E 's/^(\xB7|\u2722|\u2733|\u2736|\u273B|\u273D|\\*) ([A-Za-z]{2,24})(\u2026|\\.\\.\\.)( .*)?$/\\2/p' | tail -n 1)
|
|
734
|
+
case "$VERB" in *[!A-Za-z]*) VERB= ;; esac
|
|
735
|
+
[ "\${#VERB}" -le 24 ] || VERB=
|
|
736
|
+
KEY="$NAME\${VERB:+ $VERB}"
|
|
737
|
+
[ "$(cat "$MARK" 2>/dev/null)" = "$KEY" ] && exit 0
|
|
738
|
+
printf '%s' "$KEY" 2>/dev/null > "$MARK"
|
|
739
|
+
if [ -n "$VERB" ]; then
|
|
740
|
+
EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s","verb":"%s"}' "$NAME" "$VERB")
|
|
741
|
+
else
|
|
742
|
+
EVENT=$(printf '{"hook_event_name":"PreToolUse","tool_name":"%s"}' "$NAME")
|
|
743
|
+
fi
|
|
683
744
|
fi
|
|
684
745
|
;;
|
|
746
|
+
PermissionRequest)
|
|
747
|
+
# A permission prompt: only the tool's name travels, exactly like a tool call (never its input,
|
|
748
|
+
# never the suggestions). AskUserQuestion's own prompt is dropped \u2014 its PreToolUse already carried
|
|
749
|
+
# the question. Same first-"tool_name" rule and character set as above.
|
|
750
|
+
REST=\${EVENT#*'"tool_name"'}
|
|
751
|
+
[ "$REST" != "$EVENT" ] || exit 0
|
|
752
|
+
REST=\${REST#*'"'}
|
|
753
|
+
NAME=\${REST%%'"'*}
|
|
754
|
+
case "$NAME" in '' | *[!A-Za-z0-9_.-]* | AskUserQuestion) exit 0 ;; esac
|
|
755
|
+
EVENT=$(printf '{"hook_event_name":"PermissionRequest","tool_name":"%s"}' "$NAME")
|
|
756
|
+
;;
|
|
685
757
|
# A new turn starts fresh, and so does an answered notification: a permission prompt takes the tab
|
|
686
758
|
# out of working, and the tool the person approves is the same one that set the marker, so without
|
|
687
759
|
# this reset the retry is suppressed and nothing says the tab is working again.
|
|
688
|
-
|
|
760
|
+
SessionStart | UserPromptSubmit | Notification)
|
|
689
761
|
rm -f "$MARK"
|
|
690
762
|
;;
|
|
691
763
|
esac
|
|
@@ -719,10 +791,10 @@ function mergeClaudeSettings(current, scriptPath, shown = "~/.claude/settings.js
|
|
|
719
791
|
throw new Error(`${shown}: o campo "hooks" n\xE3o \xE9 um objeto`);
|
|
720
792
|
const next = hooks ?? {};
|
|
721
793
|
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
722
|
-
const
|
|
723
|
-
const others =
|
|
794
|
+
const list4 = Array.isArray(next[event]) ? next[event] : [];
|
|
795
|
+
const others = list4.filter((e) => !isOurs(e));
|
|
724
796
|
const entry = { hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] };
|
|
725
|
-
if (event
|
|
797
|
+
if (CLAUDE_TOOL_EVENTS.has(event))
|
|
726
798
|
entry.matcher = "*";
|
|
727
799
|
others.push(entry);
|
|
728
800
|
next[event] = others;
|
|
@@ -860,6 +932,47 @@ function configDirsFromRc(text) {
|
|
|
860
932
|
return out;
|
|
861
933
|
}
|
|
862
934
|
|
|
935
|
+
// ../../packages/machine-ops/dist/simulator.js
|
|
936
|
+
var WDA_DIR = "$HOME/.termhub/WebDriverAgent";
|
|
937
|
+
var WDA_SETUP_SESSION = "termhub-wda-setup";
|
|
938
|
+
var UDID_RE2 = /^[A-Fa-f0-9-]{8,64}$/;
|
|
939
|
+
function runnerSessionName(udid2) {
|
|
940
|
+
return `termhub-wda-${udid2.slice(0, 8).toLowerCase()}`;
|
|
941
|
+
}
|
|
942
|
+
var SIMCTL_LIST_SCRIPT = "xcrun simctl list devices -j";
|
|
943
|
+
var SIMCTL_BOOT_SCRIPT = 'xcrun simctl boot "$UDID" 2>&1 || true';
|
|
944
|
+
var WDA_RUNNER_START_SCRIPT = `tmux new-session -d -s "$SESSION" "cd ${WDA_DIR} && xcodebuild test-without-building -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination id=$UDID -derivedDataPath DerivedData USE_PORT=$WDA_PORT MJPEG_SERVER_PORT=$MJPEG_PORT"`;
|
|
945
|
+
var WDA_RUNNER_ALIVE_SCRIPT = `tmux has-session -t "=$SESSION" 2>/dev/null && echo yes || echo no`;
|
|
946
|
+
var WDA_RUNNER_TAIL_SCRIPT = `tmux capture-pane -p -t "=$SESSION" 2>/dev/null | grep -v '^$' | tail -n "$LINES"`;
|
|
947
|
+
var WDA_SETUP_SH = [
|
|
948
|
+
"#!/bin/sh",
|
|
949
|
+
'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"',
|
|
950
|
+
'mkdir -p "$HOME/.termhub"',
|
|
951
|
+
'rm -f "$HOME/.termhub/wda-setup.status"',
|
|
952
|
+
"{",
|
|
953
|
+
` if [ -d "${WDA_DIR}/.git" ]; then git -C "${WDA_DIR}" pull --ff-only; else git clone --depth 1 https://github.com/appium/WebDriverAgent "${WDA_DIR}"; fi &&`,
|
|
954
|
+
` cd "${WDA_DIR}" &&`,
|
|
955
|
+
" xcodebuild build-for-testing -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'generic/platform=iOS Simulator' -derivedDataPath DerivedData CODE_SIGNING_ALLOWED=NO",
|
|
956
|
+
'} > "$HOME/.termhub/wda-setup.log" 2>&1',
|
|
957
|
+
'echo $? > "$HOME/.termhub/wda-setup.status"',
|
|
958
|
+
""
|
|
959
|
+
].join("\n");
|
|
960
|
+
var WDA_SETUP_START_SCRIPT = [
|
|
961
|
+
`if tmux has-session -t '=${WDA_SETUP_SESSION}' 2>/dev/null; then echo STARTED:no; exit 0; fi`,
|
|
962
|
+
`mkdir -p "$HOME/.termhub" && cat > "$HOME/.termhub/wda-setup.sh" <<'TERMHUB_EOF'`,
|
|
963
|
+
WDA_SETUP_SH + "TERMHUB_EOF",
|
|
964
|
+
`chmod +x "$HOME/.termhub/wda-setup.sh" && tmux new-session -d -s ${WDA_SETUP_SESSION} 'sh "$HOME/.termhub/wda-setup.sh"' && echo STARTED:yes`
|
|
965
|
+
].join("\n");
|
|
966
|
+
var WDA_SETUP_STATE_SCRIPT = `
|
|
967
|
+
if tmux has-session -t '=${WDA_SETUP_SESSION}' 2>/dev/null; then echo STATE:running;
|
|
968
|
+
elif [ -f "$HOME/.termhub/wda-setup.status" ]; then
|
|
969
|
+
if [ "$(cat "$HOME/.termhub/wda-setup.status")" = 0 ]; then echo STATE:ok; else echo STATE:failed; fi;
|
|
970
|
+
else echo STATE:idle; fi
|
|
971
|
+
echo VERSION:$(sed -n 's/.*"version": *"\\([^"]*\\)".*/\\1/p' "${WDA_DIR}/package.json" 2>/dev/null | head -1)
|
|
972
|
+
echo TAIL:
|
|
973
|
+
tail -n 40 "$HOME/.termhub/wda-setup.log" 2>/dev/null
|
|
974
|
+
exit 0`;
|
|
975
|
+
|
|
863
976
|
// src/claude-dirs.ts
|
|
864
977
|
import { readdir, readFile, stat } from "fs/promises";
|
|
865
978
|
import path2 from "path";
|
|
@@ -887,8 +1000,8 @@ async function fileNames(dir) {
|
|
|
887
1000
|
}
|
|
888
1001
|
async function candidateNames(home) {
|
|
889
1002
|
try {
|
|
890
|
-
const
|
|
891
|
-
return
|
|
1003
|
+
const list4 = await readdir(home, { withFileTypes: true });
|
|
1004
|
+
return list4.filter((d) => d.isDirectory() && d.name.startsWith(".claude")).map((d) => d.name);
|
|
892
1005
|
} catch {
|
|
893
1006
|
return [];
|
|
894
1007
|
}
|
|
@@ -933,12 +1046,12 @@ function tmuxPath() {
|
|
|
933
1046
|
return process.env.TMUX_PATH || "tmux";
|
|
934
1047
|
}
|
|
935
1048
|
function run(file, args, opts = {}) {
|
|
936
|
-
const { timeoutMs = DEFAULT_TIMEOUT_MS, input, env = agentEnv() } = opts;
|
|
1049
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, input, env: env2 = agentEnv() } = opts;
|
|
937
1050
|
return new Promise((resolve) => {
|
|
938
1051
|
const child = execFile(
|
|
939
1052
|
file,
|
|
940
1053
|
args,
|
|
941
|
-
{ timeout: timeoutMs, env, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" },
|
|
1054
|
+
{ timeout: timeoutMs, env: env2, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" },
|
|
942
1055
|
(err, stdout, stderr) => {
|
|
943
1056
|
const e = err;
|
|
944
1057
|
resolve({
|
|
@@ -1060,9 +1173,9 @@ async function install(params, home = os3.homedir()) {
|
|
|
1060
1173
|
}
|
|
1061
1174
|
async function heal(home = os3.homedir()) {
|
|
1062
1175
|
const scriptPath = path3.join(home, HOOK_SCRIPT_REL);
|
|
1063
|
-
const
|
|
1176
|
+
const env2 = await readOrEmpty2(path3.join(home, HOOK_ENV_REL));
|
|
1064
1177
|
const script = await readOrEmpty2(scriptPath);
|
|
1065
|
-
if (!
|
|
1178
|
+
if (!env2.trim() || !script) return [];
|
|
1066
1179
|
if (script !== HOOK_SCRIPT) await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
|
|
1067
1180
|
const steps = [healClaudeDirs, healCursor, healCodex];
|
|
1068
1181
|
const healed = [];
|
|
@@ -1252,9 +1365,9 @@ function createClaudeManager(deps) {
|
|
|
1252
1365
|
const runDir = dir;
|
|
1253
1366
|
socket.sendControl({ type: "opened", ch });
|
|
1254
1367
|
deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null });
|
|
1255
|
-
const
|
|
1256
|
-
if (params.config_dir === null) delete
|
|
1257
|
-
else
|
|
1368
|
+
const env2 = { ...deps.env ?? agentEnv() };
|
|
1369
|
+
if (params.config_dir === null) delete env2.CLAUDE_CONFIG_DIR;
|
|
1370
|
+
else env2.CLAUDE_CONFIG_DIR = params.config_dir;
|
|
1258
1371
|
const args = buildClaudeArgs({
|
|
1259
1372
|
session_id: params.session_id,
|
|
1260
1373
|
resume: params.resume,
|
|
@@ -1264,7 +1377,7 @@ function createClaudeManager(deps) {
|
|
|
1264
1377
|
});
|
|
1265
1378
|
let child;
|
|
1266
1379
|
try {
|
|
1267
|
-
child = spawn(CLI, args, { env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
1380
|
+
child = spawn(CLI, args, { env: env2, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
1268
1381
|
} catch (err) {
|
|
1269
1382
|
deps.log("claude run could not be started", { ch, cli: CLI, error: message(err) });
|
|
1270
1383
|
rmSync(runDir, { recursive: true, force: true });
|
|
@@ -1463,7 +1576,7 @@ function createDispatcher(deps) {
|
|
|
1463
1576
|
void handleRpc(msg, socket, deps.handlers, deps.log);
|
|
1464
1577
|
break;
|
|
1465
1578
|
case "open": {
|
|
1466
|
-
const opened = msg.kind === "claude" ? deps.claude.open(msg.ch, msg.params, socket) : deps.pty.open(msg.ch, msg.params, socket);
|
|
1579
|
+
const opened = msg.kind === "claude" ? deps.claude.open(msg.ch, msg.params, socket) : msg.kind === "tcp" ? deps.tcp.open(msg.ch, msg.params, socket) : deps.pty.open(msg.ch, msg.params, socket);
|
|
1467
1580
|
opened.catch((err) => {
|
|
1468
1581
|
deps.log(`${msg.kind}.open rejected unexpectedly`, { ch: msg.ch, error: err instanceof Error ? err.message : String(err) });
|
|
1469
1582
|
});
|
|
@@ -1475,6 +1588,7 @@ function createDispatcher(deps) {
|
|
|
1475
1588
|
case "close":
|
|
1476
1589
|
deps.pty.close(msg.ch);
|
|
1477
1590
|
deps.claude.close(msg.ch);
|
|
1591
|
+
deps.tcp.close(msg.ch);
|
|
1478
1592
|
break;
|
|
1479
1593
|
}
|
|
1480
1594
|
};
|
|
@@ -1566,13 +1680,13 @@ function createPtyManager(deps) {
|
|
|
1566
1680
|
try {
|
|
1567
1681
|
({ cols, rows } = clampSize(params));
|
|
1568
1682
|
const cwd = resolveCwd(params.cwd);
|
|
1569
|
-
const
|
|
1683
|
+
const env2 = {
|
|
1570
1684
|
...ptyEnv(agentEnv(), process.env.SHELL ?? "/bin/sh"),
|
|
1571
1685
|
TERMHUB_TAB_ID: params.session,
|
|
1572
1686
|
TERMHUB_SESSION: params.session
|
|
1573
1687
|
};
|
|
1574
1688
|
const spawn2 = await resolveSpawn();
|
|
1575
|
-
const spawnTmux = () => spawn2(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env });
|
|
1689
|
+
const spawnTmux = () => spawn2(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env: env2 });
|
|
1576
1690
|
try {
|
|
1577
1691
|
proc = spawnTmux();
|
|
1578
1692
|
} catch (err) {
|
|
@@ -1665,6 +1779,141 @@ function createPtyManager(deps) {
|
|
|
1665
1779
|
};
|
|
1666
1780
|
}
|
|
1667
1781
|
|
|
1782
|
+
// src/tcp.ts
|
|
1783
|
+
import net from "net";
|
|
1784
|
+
var DEFAULT_HIGH_WATER = 4 * 1024 * 1024;
|
|
1785
|
+
var DEFAULT_LOW_WATER = 1024 * 1024;
|
|
1786
|
+
var DEFAULT_RESUME_POLL_MS = 50;
|
|
1787
|
+
function isRefused(err) {
|
|
1788
|
+
return err?.code === "ECONNREFUSED";
|
|
1789
|
+
}
|
|
1790
|
+
function createTcpManager(deps) {
|
|
1791
|
+
const channels = /* @__PURE__ */ new Map();
|
|
1792
|
+
const allowPort = deps.allowPort ?? isWdaPort;
|
|
1793
|
+
const highWater = deps.highWater ?? DEFAULT_HIGH_WATER;
|
|
1794
|
+
const lowWater = deps.lowWater ?? DEFAULT_LOW_WATER;
|
|
1795
|
+
const resumePollMs = deps.resumePollMs ?? DEFAULT_RESUME_POLL_MS;
|
|
1796
|
+
const control = (socket, msg) => {
|
|
1797
|
+
try {
|
|
1798
|
+
socket.sendControl(msg);
|
|
1799
|
+
} catch (err) {
|
|
1800
|
+
deps.log("tcp control send failed", { type: msg.type, error: err instanceof Error ? err.message : String(err) });
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
const stopResumeTimer = (entry) => {
|
|
1804
|
+
if (entry.resumeTimer) clearInterval(entry.resumeTimer);
|
|
1805
|
+
entry.resumeTimer = null;
|
|
1806
|
+
};
|
|
1807
|
+
const drop = (ch, entry) => {
|
|
1808
|
+
entry.closed = true;
|
|
1809
|
+
stopResumeTimer(entry);
|
|
1810
|
+
if (channels.get(ch) === entry) channels.delete(ch);
|
|
1811
|
+
entry.sock.destroy();
|
|
1812
|
+
};
|
|
1813
|
+
return {
|
|
1814
|
+
async open(ch, params, socket) {
|
|
1815
|
+
if (channels.has(ch)) {
|
|
1816
|
+
control(socket, { type: "open_error", ch, error: { code: "invalid", message: "channel in use" } });
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (!allowPort(params.port)) {
|
|
1820
|
+
deps.log("tcp open refused: port not allowed", { ch, port: params.port });
|
|
1821
|
+
control(socket, { type: "open_error", ch, error: { code: "invalid", message: "port not allowed" } });
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
const sock = net.connect({ host: "127.0.0.1", port: params.port });
|
|
1825
|
+
let opened = false;
|
|
1826
|
+
let closedSent = false;
|
|
1827
|
+
const entry = {
|
|
1828
|
+
sock,
|
|
1829
|
+
paused: false,
|
|
1830
|
+
resumeTimer: null,
|
|
1831
|
+
closed: false,
|
|
1832
|
+
sendClosed: (reason) => {
|
|
1833
|
+
if (closedSent) return;
|
|
1834
|
+
closedSent = true;
|
|
1835
|
+
control(socket, reason ? { type: "closed", ch, code: null, reason } : { type: "closed", ch, code: null });
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
channels.set(ch, entry);
|
|
1839
|
+
await new Promise((resolve) => {
|
|
1840
|
+
sock.once("connect", () => {
|
|
1841
|
+
opened = true;
|
|
1842
|
+
deps.log("tcp opened", { ch, port: params.port });
|
|
1843
|
+
control(socket, { type: "opened", ch });
|
|
1844
|
+
resolve();
|
|
1845
|
+
});
|
|
1846
|
+
sock.once("error", (err) => {
|
|
1847
|
+
if (!opened) {
|
|
1848
|
+
channels.delete(ch);
|
|
1849
|
+
stopResumeTimer(entry);
|
|
1850
|
+
deps.log("tcp open failed", { ch, port: params.port, code: err.code ?? "unknown" });
|
|
1851
|
+
control(socket, { type: "open_error", ch, error: isRefused(err) ? { code: "refused", message: "connection refused" } : { code: "internal", message: "connect failed" } });
|
|
1852
|
+
resolve();
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
deps.log("tcp socket error", { ch, port: params.port, code: err.code ?? "unknown" });
|
|
1856
|
+
entry.sock.destroy();
|
|
1857
|
+
});
|
|
1858
|
+
});
|
|
1859
|
+
if (!channels.has(ch)) return;
|
|
1860
|
+
sock.on("data", (chunk) => {
|
|
1861
|
+
if (entry.closed) return;
|
|
1862
|
+
try {
|
|
1863
|
+
socket.sendStream(ch, chunk);
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
deps.log("tcp stream send failed", { ch, error: err instanceof Error ? err.message : String(err) });
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const queued = socket.bufferedAmount?.() ?? 0;
|
|
1869
|
+
if (queued > highWater && !entry.paused) {
|
|
1870
|
+
entry.paused = true;
|
|
1871
|
+
sock.pause();
|
|
1872
|
+
deps.log("tcp paused", { ch, queued });
|
|
1873
|
+
entry.resumeTimer = setInterval(() => {
|
|
1874
|
+
if (entry.closed) {
|
|
1875
|
+
stopResumeTimer(entry);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if ((socket.bufferedAmount?.() ?? 0) <= lowWater) {
|
|
1879
|
+
stopResumeTimer(entry);
|
|
1880
|
+
entry.paused = false;
|
|
1881
|
+
sock.resume();
|
|
1882
|
+
deps.log("tcp resumed", { ch });
|
|
1883
|
+
}
|
|
1884
|
+
}, resumePollMs);
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
sock.on("close", (hadError) => {
|
|
1888
|
+
if (!opened) return;
|
|
1889
|
+
stopResumeTimer(entry);
|
|
1890
|
+
if (channels.get(ch) === entry) channels.delete(ch);
|
|
1891
|
+
if (entry.closed) return;
|
|
1892
|
+
deps.log("tcp closed", { ch, port: params.port, hadError });
|
|
1893
|
+
entry.sendClosed(hadError ? "reset" : void 0);
|
|
1894
|
+
});
|
|
1895
|
+
},
|
|
1896
|
+
write(ch, data) {
|
|
1897
|
+
const entry = channels.get(ch);
|
|
1898
|
+
if (!entry || entry.closed) return false;
|
|
1899
|
+
entry.sock.write(data);
|
|
1900
|
+
return true;
|
|
1901
|
+
},
|
|
1902
|
+
close(ch) {
|
|
1903
|
+
const entry = channels.get(ch);
|
|
1904
|
+
if (!entry) return;
|
|
1905
|
+
drop(ch, entry);
|
|
1906
|
+
entry.sendClosed();
|
|
1907
|
+
},
|
|
1908
|
+
closeAll() {
|
|
1909
|
+
for (const [ch, entry] of channels) drop(ch, entry);
|
|
1910
|
+
},
|
|
1911
|
+
isPaused(ch) {
|
|
1912
|
+
return channels.get(ch)?.paused ?? false;
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1668
1917
|
// src/rpc/ai.ts
|
|
1669
1918
|
async function credential(params) {
|
|
1670
1919
|
let prefix;
|
|
@@ -1720,6 +1969,33 @@ async function pasteFile(params) {
|
|
|
1720
1969
|
return { path: path12 };
|
|
1721
1970
|
}
|
|
1722
1971
|
|
|
1972
|
+
// src/rpc/sim.ts
|
|
1973
|
+
function checkUdid(udid2) {
|
|
1974
|
+
if (!UDID_RE2.test(udid2)) throw new RpcFailure("invalid", "invalid udid");
|
|
1975
|
+
}
|
|
1976
|
+
function scriptFailure(r, what) {
|
|
1977
|
+
if (r.timedOut) return new RpcFailure("timeout", `${what} timed out`);
|
|
1978
|
+
if (r.error === "enoent") return new RpcFailure("internal", "/bin/sh not found");
|
|
1979
|
+
if (r.code !== 0) {
|
|
1980
|
+
const why2 = r.stderr.trim().split("\n")[0] || r.stdout.trim().split("\n")[0] || `${what} exited with code ${r.code}`;
|
|
1981
|
+
return /tmux: (command )?not found/.test(r.stderr) ? new RpcFailure("no_tmux", "tmux not found") : new RpcFailure("failed", why2);
|
|
1982
|
+
}
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1985
|
+
async function list2(_params) {
|
|
1986
|
+
const r = await sh(SIMCTL_LIST_SCRIPT, { timeoutMs: 14e3 });
|
|
1987
|
+
const failure = scriptFailure(r, "sim.list");
|
|
1988
|
+
if (failure) throw failure;
|
|
1989
|
+
return { stdout: r.stdout };
|
|
1990
|
+
}
|
|
1991
|
+
async function boot(params) {
|
|
1992
|
+
checkUdid(params.udid);
|
|
1993
|
+
const r = await sh(SIMCTL_BOOT_SCRIPT, { timeoutMs: 59e3, env: { ...agentEnv(), UDID: params.udid } });
|
|
1994
|
+
const failure = scriptFailure(r, "sim.boot");
|
|
1995
|
+
if (failure) throw failure;
|
|
1996
|
+
return { stdout: r.stdout + r.stderr };
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1723
1999
|
// src/rpc/tmux.ts
|
|
1724
2000
|
import { randomUUID } from "crypto";
|
|
1725
2001
|
var ENTER_PAUSE_MS = 300;
|
|
@@ -1729,7 +2005,7 @@ function processFailure2(r) {
|
|
|
1729
2005
|
if (r.error === "maxbuffer") return new RpcFailure("internal", "output too large");
|
|
1730
2006
|
return null;
|
|
1731
2007
|
}
|
|
1732
|
-
async function
|
|
2008
|
+
async function list3(_params) {
|
|
1733
2009
|
const r = await run(tmuxPath(), ["list-sessions", "-F", "#{session_name}"]);
|
|
1734
2010
|
const failure = processFailure2(r);
|
|
1735
2011
|
if (failure) throw failure;
|
|
@@ -2036,7 +2312,7 @@ async function status3() {
|
|
|
2036
2312
|
}
|
|
2037
2313
|
|
|
2038
2314
|
// src/version.ts
|
|
2039
|
-
var AGENT_VERSION = "0.
|
|
2315
|
+
var AGENT_VERSION = "0.5.1";
|
|
2040
2316
|
|
|
2041
2317
|
// src/rpc/update.ts
|
|
2042
2318
|
var PACKAGE = "@termhub/agent";
|
|
@@ -2100,9 +2376,52 @@ async function doUpdate(version, deps) {
|
|
|
2100
2376
|
}
|
|
2101
2377
|
var update = (params) => updateAgent(params);
|
|
2102
2378
|
|
|
2379
|
+
// src/rpc/wda.ts
|
|
2380
|
+
function env(vars) {
|
|
2381
|
+
return { ...agentEnv(), ...vars };
|
|
2382
|
+
}
|
|
2383
|
+
async function runnerStart(params) {
|
|
2384
|
+
checkUdid(params.udid);
|
|
2385
|
+
if (!isWdaPort(params.wda_port) || !isWdaPort(params.mjpeg_port)) throw new RpcFailure("invalid", "port outside the WDA ranges");
|
|
2386
|
+
const vars = { SESSION: runnerSessionName(params.udid), UDID: params.udid, WDA_PORT: String(params.wda_port), MJPEG_PORT: String(params.mjpeg_port) };
|
|
2387
|
+
const r = await sh(WDA_RUNNER_START_SCRIPT, { timeoutMs: 9e3, env: env(vars) });
|
|
2388
|
+
if (r.code !== 0 && r.stderr.includes("duplicate session")) return { started: false };
|
|
2389
|
+
const failure = scriptFailure(r, "wda.runner.start");
|
|
2390
|
+
if (failure) throw failure;
|
|
2391
|
+
return { started: true };
|
|
2392
|
+
}
|
|
2393
|
+
async function runnerAlive(params) {
|
|
2394
|
+
checkUdid(params.udid);
|
|
2395
|
+
const r = await sh(WDA_RUNNER_ALIVE_SCRIPT, { env: env({ SESSION: runnerSessionName(params.udid) }) });
|
|
2396
|
+
const failure = scriptFailure(r, "wda.runner.alive");
|
|
2397
|
+
if (failure) throw failure;
|
|
2398
|
+
return { alive: r.stdout.includes("yes") };
|
|
2399
|
+
}
|
|
2400
|
+
async function runnerTail(params) {
|
|
2401
|
+
checkUdid(params.udid);
|
|
2402
|
+
const r = await sh(WDA_RUNNER_TAIL_SCRIPT, { env: env({ SESSION: runnerSessionName(params.udid), LINES: String(params.lines) }) });
|
|
2403
|
+
const failure = scriptFailure(r, "wda.runner.tail");
|
|
2404
|
+
if (failure) throw failure;
|
|
2405
|
+
return { lines: r.stdout.split("\n").filter((l) => l.trim()) };
|
|
2406
|
+
}
|
|
2407
|
+
async function setupStart(_params) {
|
|
2408
|
+
const r = await sh(WDA_SETUP_START_SCRIPT, { timeoutMs: 9e3 });
|
|
2409
|
+
const failure = scriptFailure(r, "wda.setup.start");
|
|
2410
|
+
if (failure) throw failure;
|
|
2411
|
+
if (r.stdout.includes("STARTED:no")) return { started: false };
|
|
2412
|
+
if (r.stdout.includes("STARTED:yes")) return { started: true };
|
|
2413
|
+
throw new RpcFailure("failed", r.stderr.trim().split("\n")[0] || "setup did not start");
|
|
2414
|
+
}
|
|
2415
|
+
async function setupState(_params) {
|
|
2416
|
+
const r = await sh(WDA_SETUP_STATE_SCRIPT);
|
|
2417
|
+
const failure = scriptFailure(r, "wda.setup.state");
|
|
2418
|
+
if (failure) throw failure;
|
|
2419
|
+
return { stdout: r.stdout };
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2103
2422
|
// src/rpc/index.ts
|
|
2104
2423
|
var handlers = {
|
|
2105
|
-
"tmux.list":
|
|
2424
|
+
"tmux.list": list3,
|
|
2106
2425
|
"tmux.kill": kill,
|
|
2107
2426
|
"tmux.capture": capture,
|
|
2108
2427
|
"tmux.ensure": ensure,
|
|
@@ -2116,7 +2435,14 @@ var handlers = {
|
|
|
2116
2435
|
"file.paste": pasteFile,
|
|
2117
2436
|
"hooks.install": install,
|
|
2118
2437
|
"hooks.uninstall": uninstall,
|
|
2119
|
-
"agent.update": update
|
|
2438
|
+
"agent.update": update,
|
|
2439
|
+
"sim.list": list2,
|
|
2440
|
+
"sim.boot": boot,
|
|
2441
|
+
"wda.runner.start": runnerStart,
|
|
2442
|
+
"wda.runner.alive": runnerAlive,
|
|
2443
|
+
"wda.runner.tail": runnerTail,
|
|
2444
|
+
"wda.setup.start": setupStart,
|
|
2445
|
+
"wda.setup.state": setupState
|
|
2120
2446
|
};
|
|
2121
2447
|
|
|
2122
2448
|
// src/run.ts
|
|
@@ -2126,6 +2452,9 @@ function detectOs(platform = process.platform) {
|
|
|
2126
2452
|
return null;
|
|
2127
2453
|
}
|
|
2128
2454
|
var CAPABILITIES = [CAPABILITY_CLAUDE, CAPABILITY_CLAUDE_SYSTEM_PROMPT];
|
|
2455
|
+
function capabilitiesFor(osName) {
|
|
2456
|
+
return osName === "macos" ? [...CAPABILITIES, CAPABILITY_SIM] : [...CAPABILITIES];
|
|
2457
|
+
}
|
|
2129
2458
|
async function buildHello(osName) {
|
|
2130
2459
|
let tools = [];
|
|
2131
2460
|
try {
|
|
@@ -2141,7 +2470,7 @@ async function buildHello(osName) {
|
|
|
2141
2470
|
hostname: os6.hostname(),
|
|
2142
2471
|
tmux: tools.includes("tmux"),
|
|
2143
2472
|
tools,
|
|
2144
|
-
capabilities:
|
|
2473
|
+
capabilities: capabilitiesFor(osName)
|
|
2145
2474
|
};
|
|
2146
2475
|
}
|
|
2147
2476
|
var REVOKED_MESSAGE = "Token inv\xE1lido ou revogado";
|
|
@@ -2159,7 +2488,7 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
|
|
|
2159
2488
|
{
|
|
2160
2489
|
url: config.url,
|
|
2161
2490
|
token: config.token,
|
|
2162
|
-
hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os6.hostname(), tmux: false, tools: [], capabilities:
|
|
2491
|
+
hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os6.hostname(), tmux: false, tools: [], capabilities: capabilitiesFor(osName), probe: true },
|
|
2163
2492
|
onServerMessage: () => {
|
|
2164
2493
|
},
|
|
2165
2494
|
onStream: () => {
|
|
@@ -2202,7 +2531,8 @@ async function runAgent(config, opts) {
|
|
|
2202
2531
|
else if (!helper.executable) opts.log("spawn-helper is not executable and could not be fixed", { path: helper.path, error: helper.error });
|
|
2203
2532
|
const pty = createPtyManager({ log: opts.log });
|
|
2204
2533
|
const claude = createClaudeManager({ log: opts.log });
|
|
2205
|
-
const
|
|
2534
|
+
const tcp = createTcpManager({ log: opts.log });
|
|
2535
|
+
const dispatch = createDispatcher({ handlers, pty, claude, tcp, log: opts.log });
|
|
2206
2536
|
const healHooks = () => {
|
|
2207
2537
|
heal().then((dirs) => {
|
|
2208
2538
|
if (dirs.length) opts.log("monitor hooks repaired", { dirs: dirs.length });
|
|
@@ -2219,12 +2549,13 @@ async function runAgent(config, opts) {
|
|
|
2219
2549
|
// A frame belongs to whichever manager holds that channel: the claude one says so, and
|
|
2220
2550
|
// anything it does not own is a terminal's.
|
|
2221
2551
|
onStream: (ch, data) => {
|
|
2222
|
-
if (!claude.write(ch, data)) pty.write(ch, data);
|
|
2552
|
+
if (!claude.write(ch, data) && !tcp.write(ch, data)) pty.write(ch, data);
|
|
2223
2553
|
},
|
|
2224
2554
|
onConnect: healHooks,
|
|
2225
2555
|
onDisconnect: () => {
|
|
2226
2556
|
pty.closeAll();
|
|
2227
2557
|
claude.closeAll();
|
|
2558
|
+
tcp.closeAll();
|
|
2228
2559
|
},
|
|
2229
2560
|
log: opts.log
|
|
2230
2561
|
},
|
package/package.json
CHANGED