@agent-native/core 0.176.3 → 0.176.4-nightly-20260901191433
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/client/terminal/AgentTerminal.d.ts +3 -1
- package/dist/client/terminal/AgentTerminal.js +36 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/terminal/cli-registry.d.ts +3 -0
- package/dist/terminal/cli-registry.js +54 -8
- package/dist/terminal/pty-server.d.ts +22 -0
- package/dist/terminal/pty-server.js +182 -55
- package/package.json +1 -1
|
@@ -22,6 +22,8 @@ export interface AgentTerminalProps {
|
|
|
22
22
|
theme?: Record<string, string>;
|
|
23
23
|
/** Font size. Default: 12 */
|
|
24
24
|
fontSize?: number;
|
|
25
|
+
/** Focus the terminal on mount and when it becomes active. Default: true */
|
|
26
|
+
autoFocus?: boolean;
|
|
25
27
|
/** CSS class for the container */
|
|
26
28
|
className?: string;
|
|
27
29
|
/** Inline styles for the container */
|
|
@@ -40,4 +42,4 @@ export interface AgentTerminalSubmitRequest {
|
|
|
40
42
|
text: string;
|
|
41
43
|
}
|
|
42
44
|
export declare function formatWebSocketHostname(hostname: string): string;
|
|
43
|
-
export declare function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame, theme, fontSize, className, style, onConnectionChange, onAgentRunningChange, submitRequest, onPromptSubmitted, }: AgentTerminalProps): import("react").JSX.Element | null;
|
|
45
|
+
export declare function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame, theme, fontSize, autoFocus, className, style, onConnectionChange, onAgentRunningChange, submitRequest, onPromptSubmitted, }: AgentTerminalProps): import("react").JSX.Element | null;
|
|
@@ -33,9 +33,20 @@ function injectXtermCss() {
|
|
|
33
33
|
.xterm .composition-view { display: none; position: absolute; white-space: nowrap; z-index: 1; }
|
|
34
34
|
.xterm .composition-view.active { display: block; }
|
|
35
35
|
.xterm .xterm-viewport {
|
|
36
|
-
background-color: var(--agent-terminal-background); overflow-y:
|
|
36
|
+
background-color: var(--agent-terminal-background); overflow-y: auto;
|
|
37
|
+
scrollbar-width: thin;
|
|
38
|
+
scrollbar-color: hsl(var(--muted-foreground) / 0.22) transparent;
|
|
37
39
|
cursor: default; position: absolute; right: 0; left: 0; top: 0; bottom: 0;
|
|
38
40
|
}
|
|
41
|
+
.xterm .xterm-viewport::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
42
|
+
.xterm .xterm-viewport::-webkit-scrollbar-track { background: transparent; }
|
|
43
|
+
.xterm .xterm-viewport::-webkit-scrollbar-thumb {
|
|
44
|
+
background: hsl(var(--muted-foreground) / 0.22);
|
|
45
|
+
border-radius: 999px;
|
|
46
|
+
}
|
|
47
|
+
.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
|
48
|
+
background: hsl(var(--muted-foreground) / 0.36);
|
|
49
|
+
}
|
|
39
50
|
.xterm .xterm-screen { position: relative; }
|
|
40
51
|
.xterm .xterm-screen canvas { position: absolute; left: 0; top: 0; }
|
|
41
52
|
.xterm .xterm-scroll-area { visibility: hidden; }
|
|
@@ -85,10 +96,13 @@ export function formatWebSocketHostname(hostname) {
|
|
|
85
96
|
? `[${hostname}]`
|
|
86
97
|
: hostname;
|
|
87
98
|
}
|
|
88
|
-
export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame = true, theme, fontSize = 12, className, style, onConnectionChange, onAgentRunningChange, submitRequest, onPromptSubmitted, }) {
|
|
99
|
+
export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame = true, theme, fontSize = 12, autoFocus = true, className, style, onConnectionChange, onAgentRunningChange, submitRequest, onPromptSubmitted, }) {
|
|
89
100
|
const termRef = useRef(null);
|
|
90
101
|
const submitPromptRef = useRef(null);
|
|
91
102
|
const pendingSubmitRequestRef = useRef(null);
|
|
103
|
+
const autoFocusRef = useRef(autoFocus);
|
|
104
|
+
autoFocusRef.current = autoFocus;
|
|
105
|
+
const focusTerminalRef = useRef(null);
|
|
92
106
|
const onPromptSubmittedRef = useRef(onPromptSubmitted);
|
|
93
107
|
onPromptSubmittedRef.current = onPromptSubmitted;
|
|
94
108
|
const [connected, setConnected] = useState(false);
|
|
@@ -111,6 +125,17 @@ export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame =
|
|
|
111
125
|
useEffect(() => {
|
|
112
126
|
onConnectionChange?.(connected);
|
|
113
127
|
}, [connected, onConnectionChange]);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (autoFocus) {
|
|
130
|
+
focusTerminalRef.current?.();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const activeElement = document.activeElement;
|
|
134
|
+
if (activeElement instanceof HTMLElement &&
|
|
135
|
+
termRef.current?.contains(activeElement)) {
|
|
136
|
+
activeElement.blur();
|
|
137
|
+
}
|
|
138
|
+
}, [autoFocus]);
|
|
114
139
|
// Main terminal setup
|
|
115
140
|
useEffect(() => {
|
|
116
141
|
if (typeof window === "undefined")
|
|
@@ -148,6 +173,10 @@ export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame =
|
|
|
148
173
|
term.loadAddon(fitAddon);
|
|
149
174
|
term.loadAddon(webLinksAddon);
|
|
150
175
|
term.open(container);
|
|
176
|
+
const focusTerminal = () => term.focus();
|
|
177
|
+
focusTerminalRef.current = focusTerminal;
|
|
178
|
+
if (autoFocusRef.current)
|
|
179
|
+
focusTerminal();
|
|
151
180
|
let fitPending = false;
|
|
152
181
|
function fitAndResize() {
|
|
153
182
|
if (fitPending)
|
|
@@ -190,6 +219,9 @@ export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame =
|
|
|
190
219
|
window.removeEventListener("focus", handleVisibilityOrFocus);
|
|
191
220
|
document.removeEventListener("visibilitychange", handleVisibilityOrFocus);
|
|
192
221
|
resizeObserver.disconnect();
|
|
222
|
+
if (focusTerminalRef.current === focusTerminal) {
|
|
223
|
+
focusTerminalRef.current = null;
|
|
224
|
+
}
|
|
193
225
|
term.dispose();
|
|
194
226
|
}
|
|
195
227
|
function disposeIfCancelled() {
|
|
@@ -236,7 +268,6 @@ export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame =
|
|
|
236
268
|
}
|
|
237
269
|
if (flags)
|
|
238
270
|
fullWsUrl.searchParams.set("flags", flags);
|
|
239
|
-
term.write(`\x1b[2m[terminal] Starting ${resolvedCommand || "CLI"}...\x1b[0m\r\n`);
|
|
240
271
|
// Connect WebSocket
|
|
241
272
|
let agentRunning = false;
|
|
242
273
|
let idleTimer = null;
|
|
@@ -284,6 +315,8 @@ export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame =
|
|
|
284
315
|
socket.onopen = () => {
|
|
285
316
|
setConnected(true);
|
|
286
317
|
setError(null);
|
|
318
|
+
if (autoFocusRef.current)
|
|
319
|
+
focusTerminal();
|
|
287
320
|
socket.send(JSON.stringify({
|
|
288
321
|
type: "resize",
|
|
289
322
|
cols: term.cols,
|
|
@@ -333,7 +366,6 @@ export function AgentTerminal({ command, flags, wsUrl: wsUrlProp, hideInFrame =
|
|
|
333
366
|
socket.onclose = () => {
|
|
334
367
|
setConnected(false);
|
|
335
368
|
if (connectionId === thisId && !disposed) {
|
|
336
|
-
term.write("\r\n\x1b[31m[terminal] Connection closed. Reconnecting in 3s...\x1b[0m\r\n");
|
|
337
369
|
setTimeout(() => {
|
|
338
370
|
if (connectionId === thisId && !disposed) {
|
|
339
371
|
connect(url);
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -3,25 +3,25 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
3
3
|
url?: string;
|
|
4
4
|
filename?: string;
|
|
5
5
|
}, {
|
|
6
|
-
id?: undefined;
|
|
7
6
|
error: string;
|
|
8
7
|
configured?: undefined;
|
|
9
8
|
connectPath?: undefined;
|
|
10
9
|
url?: undefined;
|
|
10
|
+
id?: undefined;
|
|
11
11
|
provider?: undefined;
|
|
12
12
|
} | {
|
|
13
|
-
id?: undefined;
|
|
14
13
|
error: string;
|
|
15
14
|
configured: boolean;
|
|
16
15
|
connectPath: string;
|
|
17
16
|
url?: undefined;
|
|
17
|
+
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
+
error?: undefined;
|
|
44
45
|
summary: import("./types.js").TraceSummary;
|
|
45
46
|
spans: import("./types.js").TraceSpan[];
|
|
46
47
|
id?: undefined;
|
|
47
|
-
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
+
error?: undefined;
|
|
50
51
|
summary?: undefined;
|
|
51
52
|
spans?: undefined;
|
|
52
53
|
id: string;
|
|
53
|
-
error?: undefined;
|
|
54
54
|
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
57
57
|
spans?: undefined;
|
|
58
|
-
id?: undefined;
|
|
59
58
|
error: any;
|
|
59
|
+
id?: undefined;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
+
error?: undefined;
|
|
62
63
|
summary?: undefined;
|
|
63
64
|
spans?: undefined;
|
|
64
|
-
id?: undefined;
|
|
65
65
|
ok: boolean;
|
|
66
|
-
|
|
66
|
+
id?: undefined;
|
|
67
67
|
}>>;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
|
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
expiresAt?: undefined;
|
|
27
27
|
ttlSeconds?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
error?: undefined;
|
|
30
29
|
token: string;
|
|
31
30
|
expiresAt: string;
|
|
32
31
|
ttlSeconds: number;
|
|
32
|
+
error?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -10,8 +10,11 @@ export interface CliEntry {
|
|
|
10
10
|
/** Env vars to strip when spawning (prevents nesting) */
|
|
11
11
|
stripEnv: string[];
|
|
12
12
|
}
|
|
13
|
+
export declare function terminalPath(environment?: NodeJS.ProcessEnv): Promise<string>;
|
|
13
14
|
export declare const CLI_REGISTRY: Record<string, CliEntry>;
|
|
14
15
|
/** Check if a command name is in the CLI_REGISTRY allowlist */
|
|
15
16
|
export declare function isAllowedCommand(cmd: string): boolean;
|
|
17
|
+
/** Resolve a CLI from the user's shell locations, including GUI launch paths. */
|
|
18
|
+
export declare function resolveCommandPath(cmd: string, environment?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
16
19
|
/** Check if a CLI command exists on PATH (safe — no shell interpolation) */
|
|
17
20
|
export declare function commandExists(cmd: string): Promise<boolean>;
|
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
* CLI Registry — known AI coding CLIs and their metadata.
|
|
3
3
|
* Used by the embedded terminal in the agent panel.
|
|
4
4
|
*/
|
|
5
|
+
export async function terminalPath(environment = process.env) {
|
|
6
|
+
const [fs, os, path] = await Promise.all([
|
|
7
|
+
import("node:fs"),
|
|
8
|
+
import("node:os"),
|
|
9
|
+
import("node:path"),
|
|
10
|
+
]);
|
|
11
|
+
const home = environment.HOME || os.homedir();
|
|
12
|
+
const nvmDirectories = home
|
|
13
|
+
? (() => {
|
|
14
|
+
try {
|
|
15
|
+
return fs
|
|
16
|
+
.readdirSync(path.join(home, ".nvm", "versions", "node"))
|
|
17
|
+
.map((version) => path.join(home, ".nvm", "versions", "node", version, "bin"));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === "ENOENT")
|
|
21
|
+
return [];
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
})()
|
|
25
|
+
: [];
|
|
26
|
+
const entries = [
|
|
27
|
+
...(environment.PATH ?? environment.Path ?? "")
|
|
28
|
+
.split(path.delimiter)
|
|
29
|
+
.filter(Boolean),
|
|
30
|
+
environment.PNPM_HOME,
|
|
31
|
+
home ? path.join(home, ".local", "bin") : undefined,
|
|
32
|
+
home ? path.join(home, ".local", "share", "pnpm") : undefined,
|
|
33
|
+
home ? path.join(home, "Library", "pnpm") : undefined,
|
|
34
|
+
home ? path.join(home, ".opencode", "bin") : undefined,
|
|
35
|
+
home ? path.join(home, ".cargo", "bin") : undefined,
|
|
36
|
+
...nvmDirectories,
|
|
37
|
+
"/opt/homebrew/bin",
|
|
38
|
+
"/usr/local/bin",
|
|
39
|
+
"/usr/bin",
|
|
40
|
+
"/bin",
|
|
41
|
+
].filter((entry) => Boolean(entry));
|
|
42
|
+
return [...new Set(entries)].join(path.delimiter);
|
|
43
|
+
}
|
|
5
44
|
export const CLI_REGISTRY = {
|
|
6
45
|
claude: {
|
|
7
46
|
label: "Claude Code",
|
|
@@ -38,14 +77,21 @@ export const CLI_REGISTRY = {
|
|
|
38
77
|
export function isAllowedCommand(cmd) {
|
|
39
78
|
return Object.prototype.hasOwnProperty.call(CLI_REGISTRY, cmd);
|
|
40
79
|
}
|
|
80
|
+
/** Resolve a CLI from the user's shell locations, including GUI launch paths. */
|
|
81
|
+
export async function resolveCommandPath(cmd, environment = process.env) {
|
|
82
|
+
const { spawnSync } = await import("node:child_process");
|
|
83
|
+
const resolver = process.platform === "win32" ? "where.exe" : "which";
|
|
84
|
+
const result = spawnSync(resolver, [cmd], {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
env: { ...environment, PATH: await terminalPath(environment) },
|
|
87
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
88
|
+
});
|
|
89
|
+
if (result.status !== 0)
|
|
90
|
+
return null;
|
|
91
|
+
const resolved = result.stdout?.trim().split(/\r?\n/)[0];
|
|
92
|
+
return resolved || cmd;
|
|
93
|
+
}
|
|
41
94
|
/** Check if a CLI command exists on PATH (safe — no shell interpolation) */
|
|
42
95
|
export async function commandExists(cmd) {
|
|
43
|
-
|
|
44
|
-
const { spawnSync } = await import("node:child_process");
|
|
45
|
-
const result = spawnSync("which", [cmd], { stdio: "ignore" });
|
|
46
|
-
return result.status === 0;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
96
|
+
return (await resolveCommandPath(cmd)) !== null;
|
|
51
97
|
}
|
|
@@ -21,9 +21,26 @@ export interface PtyServerOptions {
|
|
|
21
21
|
authCheck?: (req: IncomingMessage) => boolean | Promise<boolean>;
|
|
22
22
|
/** Trusted host arguments appended to each validated CLI command. */
|
|
23
23
|
getCommandArgs?: (command: string) => string[] | Promise<string[]>;
|
|
24
|
+
/** Trusted environment additions for the validated CLI command. */
|
|
25
|
+
getEnvironment?: (command: string) => NodeJS.ProcessEnv | Promise<NodeJS.ProcessEnv>;
|
|
26
|
+
/** Per-connection setup for trusted host capabilities and cleanup. */
|
|
27
|
+
getSessionSetup?: (command: string, context: PtySessionContext | null) => PtySessionSetup | Promise<PtySessionSetup>;
|
|
24
28
|
/** Log prefix for console output. Defaults to '[terminal]' */
|
|
25
29
|
logPrefix?: string;
|
|
26
30
|
}
|
|
31
|
+
export interface PtySessionContext {
|
|
32
|
+
appId: string;
|
|
33
|
+
path?: string;
|
|
34
|
+
view?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface PtySessionSetup {
|
|
37
|
+
/** Trusted arguments appended to the selected CLI command. */
|
|
38
|
+
commandArgs?: string[];
|
|
39
|
+
/** Trusted environment additions for the selected CLI command. */
|
|
40
|
+
environment?: NodeJS.ProcessEnv;
|
|
41
|
+
/** Releases per-connection resources such as scoped MCP relays. */
|
|
42
|
+
onClose?: () => void | Promise<void>;
|
|
43
|
+
}
|
|
27
44
|
export interface PtyServerResult {
|
|
28
45
|
/** The underlying HTTP server */
|
|
29
46
|
server: HttpServer;
|
|
@@ -33,3 +50,8 @@ export interface PtyServerResult {
|
|
|
33
50
|
close: () => void;
|
|
34
51
|
}
|
|
35
52
|
export declare function createPtyWebSocketServer(options?: PtyServerOptions): Promise<PtyServerResult>;
|
|
53
|
+
export declare function preparePtySpawn(commandPath: string, args: string[], platform?: NodeJS.Platform): {
|
|
54
|
+
command: string;
|
|
55
|
+
args: string[];
|
|
56
|
+
};
|
|
57
|
+
export declare function parseTerminalArguments(value: string, platform?: NodeJS.Platform): string[];
|
|
@@ -11,7 +11,7 @@ import * as fs from "node:fs";
|
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import os from "os";
|
|
13
13
|
import path from "path";
|
|
14
|
-
import { CLI_REGISTRY,
|
|
14
|
+
import { CLI_REGISTRY, isAllowedCommand, resolveCommandPath, terminalPath, } from "./cli-registry.js";
|
|
15
15
|
// Lazy singletons for Node-only modules (only available in Node.js)
|
|
16
16
|
let _cp;
|
|
17
17
|
async function getChildProcess() {
|
|
@@ -50,24 +50,6 @@ export function ensurePtySpawnHelperPermissions() {
|
|
|
50
50
|
console.warn("[terminal] Could not verify node-pty spawn-helper permissions:", err.message);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
function resolveTerminalShell() {
|
|
54
|
-
if (os.platform() === "win32") {
|
|
55
|
-
// config-ok: COMSPEC is an inherited host shell fact, not application configuration.
|
|
56
|
-
return process.env["COMSPEC"] || "cmd.exe";
|
|
57
|
-
}
|
|
58
|
-
// config-ok: SHELL is the user's inherited host shell, not application configuration.
|
|
59
|
-
const configuredShell = process.env.SHELL;
|
|
60
|
-
if (configuredShell && path.isAbsolute(configuredShell)) {
|
|
61
|
-
try {
|
|
62
|
-
if (fs.statSync(configuredShell).isFile())
|
|
63
|
-
return configuredShell;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// coercion-ok: an invalid SHELL falls through to known system shells.
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return (["/bin/zsh", "/bin/bash", "/bin/sh"].find((candidate) => fs.existsSync(candidate)) ?? "/bin/sh");
|
|
70
|
-
}
|
|
71
53
|
/**
|
|
72
54
|
* Kill a process and all its descendants.
|
|
73
55
|
* node-pty's kill() only sends a signal to the shell, but child processes
|
|
@@ -146,13 +128,12 @@ async function killProcessTree(pid, _logPrefix, killParent, isParentExited = ()
|
|
|
146
128
|
}, 500);
|
|
147
129
|
}
|
|
148
130
|
export async function createPtyWebSocketServer(options = {}) {
|
|
149
|
-
const { appDir = process.cwd(), command: defaultCommand = "claude", port = 0, authCheck, getCommandArgs, logPrefix = "[terminal]", } = options;
|
|
131
|
+
const { appDir = process.cwd(), command: defaultCommand = "claude", port = 0, authCheck, getCommandArgs, getEnvironment, getSessionSetup, logPrefix = "[terminal]", } = options;
|
|
150
132
|
ensurePtySpawnHelperPermissions();
|
|
151
133
|
// Dynamic imports for optional native dependencies
|
|
152
134
|
const { WebSocketServer, WebSocket } = await import("ws");
|
|
153
135
|
const pty = await import("node-pty");
|
|
154
136
|
const resolvedAppDir = path.resolve(appDir);
|
|
155
|
-
const shell = resolveTerminalShell();
|
|
156
137
|
const server = createHttpServer((req, res) => {
|
|
157
138
|
// CORS headers
|
|
158
139
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -225,6 +206,7 @@ export async function createPtyWebSocketServer(options = {}) {
|
|
|
225
206
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
226
207
|
const command = url.searchParams.get("command") || defaultCommand;
|
|
227
208
|
const extraFlags = url.searchParams.get("flags") || "";
|
|
209
|
+
const context = readPtySessionContext(url);
|
|
228
210
|
console.log(`${logPrefix} WebSocket connected for command: ${command}`);
|
|
229
211
|
const sendStatus = (status, message) => {
|
|
230
212
|
if (ws.readyState === WebSocket.OPEN) {
|
|
@@ -245,32 +227,42 @@ export async function createPtyWebSocketServer(options = {}) {
|
|
|
245
227
|
ws.close();
|
|
246
228
|
return;
|
|
247
229
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (registry?.installPackage) {
|
|
258
|
-
console.log(`${logPrefix} ${command} CLI not found, will use npx`);
|
|
259
|
-
useNpx = true;
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
sendStatus("not-found", `"${command}" not found on PATH. Please install it manually.`);
|
|
263
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
264
|
-
ws.close();
|
|
230
|
+
let connectionClosed = false;
|
|
231
|
+
const markConnectionClosed = () => {
|
|
232
|
+
connectionClosed = true;
|
|
233
|
+
};
|
|
234
|
+
ws.once("close", markConnectionClosed);
|
|
235
|
+
let sessionSetup;
|
|
236
|
+
let sessionSetupClosed = false;
|
|
237
|
+
const closeSessionSetup = () => {
|
|
238
|
+
if (!sessionSetup || sessionSetupClosed)
|
|
265
239
|
return;
|
|
266
|
-
|
|
267
|
-
|
|
240
|
+
sessionSetupClosed = true;
|
|
241
|
+
void Promise.resolve(sessionSetup.onClose?.()).catch((error) => {
|
|
242
|
+
console.warn(`${logPrefix} Session cleanup failed:`, error);
|
|
243
|
+
});
|
|
244
|
+
};
|
|
268
245
|
let commandArgs = [];
|
|
246
|
+
let commandEnvironment = {};
|
|
269
247
|
try {
|
|
270
|
-
if (
|
|
271
|
-
|
|
248
|
+
if (getSessionSetup) {
|
|
249
|
+
sessionSetup = await getSessionSetup(command, context);
|
|
250
|
+
commandArgs = sessionSetup.commandArgs ?? [];
|
|
251
|
+
commandEnvironment = sessionSetup.environment ?? {};
|
|
252
|
+
}
|
|
253
|
+
if (getCommandArgs) {
|
|
254
|
+
const additionalArgs = await getCommandArgs(command);
|
|
255
|
+
commandArgs = [...commandArgs, ...additionalArgs];
|
|
256
|
+
}
|
|
257
|
+
if (getEnvironment) {
|
|
258
|
+
commandEnvironment = {
|
|
259
|
+
...commandEnvironment,
|
|
260
|
+
...(await getEnvironment(command)),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
272
263
|
}
|
|
273
264
|
catch (error) {
|
|
265
|
+
closeSessionSetup();
|
|
274
266
|
if (closed) {
|
|
275
267
|
ws.close();
|
|
276
268
|
return;
|
|
@@ -282,32 +274,93 @@ export async function createPtyWebSocketServer(options = {}) {
|
|
|
282
274
|
ws.close();
|
|
283
275
|
return;
|
|
284
276
|
}
|
|
285
|
-
if (closed) {
|
|
277
|
+
if (connectionClosed || ws.readyState !== WebSocket.OPEN || closed) {
|
|
278
|
+
closeSessionSetup();
|
|
279
|
+
ws.close();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
let extraArgumentList = [];
|
|
283
|
+
try {
|
|
284
|
+
extraArgumentList = parseTerminalArguments(extraFlags);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
closeSessionSetup();
|
|
288
|
+
sendStatus("failed", error instanceof Error ? error.message : "Invalid terminal flags.");
|
|
289
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
290
|
+
ws.close();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (connectionClosed || ws.readyState !== WebSocket.OPEN || closed) {
|
|
294
|
+
closeSessionSetup();
|
|
286
295
|
ws.close();
|
|
287
296
|
return;
|
|
288
297
|
}
|
|
289
|
-
// Build the command — use npx if CLI not found locally
|
|
290
|
-
const baseCommand = useNpx
|
|
291
|
-
? `npx --yes ${CLI_REGISTRY[command].installPackage}`
|
|
292
|
-
: command;
|
|
293
|
-
const generatedFlags = commandArgs.map(shellQuote).join(" ");
|
|
294
|
-
const fullCommand = [baseCommand, generatedFlags, extraFlags]
|
|
295
|
-
.filter(Boolean)
|
|
296
|
-
.join(" ");
|
|
297
|
-
console.log(`${logPrefix} Spawning PTY for ${command}`);
|
|
298
298
|
// Build env, stripping CLI-specific nesting vars
|
|
299
299
|
const registry = CLI_REGISTRY[command];
|
|
300
300
|
const env = {
|
|
301
301
|
...process.env,
|
|
302
|
+
...commandEnvironment,
|
|
302
303
|
TERM: "xterm-256color",
|
|
303
304
|
};
|
|
304
305
|
if (registry) {
|
|
305
306
|
for (const v of registry.stripEnv)
|
|
306
307
|
delete env[v];
|
|
307
308
|
}
|
|
309
|
+
let commandPath;
|
|
310
|
+
try {
|
|
311
|
+
env.PATH = await terminalPath(env);
|
|
312
|
+
commandPath = await resolveCommandPath(command, env);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
closeSessionSetup();
|
|
316
|
+
if (closed) {
|
|
317
|
+
ws.close();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
sendStatus("failed", error instanceof Error
|
|
321
|
+
? error.message
|
|
322
|
+
: "The terminal command could not be resolved.");
|
|
323
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
324
|
+
ws.close();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (connectionClosed || ws.readyState !== WebSocket.OPEN || closed) {
|
|
328
|
+
closeSessionSetup();
|
|
329
|
+
ws.close();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let spawnCommand = commandPath;
|
|
333
|
+
let spawnArgs = [...commandArgs, ...extraArgumentList];
|
|
334
|
+
if (!spawnCommand) {
|
|
335
|
+
if (!registry?.installPackage) {
|
|
336
|
+
closeSessionSetup();
|
|
337
|
+
sendStatus("not-found", `"${command}" not found on PATH. Please install it manually.`);
|
|
338
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
339
|
+
ws.close();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const npxPath = await resolveCommandPath("npx", env);
|
|
343
|
+
if (connectionClosed || ws.readyState !== WebSocket.OPEN || closed) {
|
|
344
|
+
closeSessionSetup();
|
|
345
|
+
ws.close();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!npxPath) {
|
|
349
|
+
closeSessionSetup();
|
|
350
|
+
sendStatus("not-found", `"${command}" not found on PATH and npx is unavailable.`);
|
|
351
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
352
|
+
ws.close();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
console.log(`${logPrefix} ${command} CLI not found, will use npx`);
|
|
356
|
+
spawnCommand = npxPath;
|
|
357
|
+
spawnArgs = ["--yes", registry.installPackage, ...spawnArgs];
|
|
358
|
+
}
|
|
359
|
+
console.log(`${logPrefix} Spawning PTY for ${command}`);
|
|
360
|
+
const preparedSpawn = preparePtySpawn(spawnCommand, spawnArgs);
|
|
308
361
|
let ptyProcess;
|
|
309
362
|
try {
|
|
310
|
-
ptyProcess = pty.spawn(
|
|
363
|
+
ptyProcess = pty.spawn(preparedSpawn.command, preparedSpawn.args, {
|
|
311
364
|
name: "xterm-256color",
|
|
312
365
|
cols: 120,
|
|
313
366
|
rows: 40,
|
|
@@ -321,6 +374,7 @@ export async function createPtyWebSocketServer(options = {}) {
|
|
|
321
374
|
sendStatus("failed", `Failed to spawn PTY: ${err instanceof Error ? err.message : String(err)}`);
|
|
322
375
|
ws.close();
|
|
323
376
|
}
|
|
377
|
+
closeSessionSetup();
|
|
324
378
|
return;
|
|
325
379
|
}
|
|
326
380
|
let disposed = false;
|
|
@@ -330,6 +384,7 @@ export async function createPtyWebSocketServer(options = {}) {
|
|
|
330
384
|
return;
|
|
331
385
|
disposed = true;
|
|
332
386
|
activeDisposers.delete(dispose);
|
|
387
|
+
closeSessionSetup();
|
|
333
388
|
const killPty = () => {
|
|
334
389
|
try {
|
|
335
390
|
ptyProcess.kill();
|
|
@@ -435,6 +490,78 @@ export async function createPtyWebSocketServer(options = {}) {
|
|
|
435
490
|
});
|
|
436
491
|
});
|
|
437
492
|
}
|
|
438
|
-
function
|
|
439
|
-
|
|
493
|
+
function readPtySessionContext(url) {
|
|
494
|
+
const appId = url.searchParams.get("appId")?.trim();
|
|
495
|
+
if (!appId)
|
|
496
|
+
return null;
|
|
497
|
+
const pathValue = url.searchParams.get("path")?.trim();
|
|
498
|
+
const view = url.searchParams.get("view")?.trim();
|
|
499
|
+
return {
|
|
500
|
+
appId,
|
|
501
|
+
...(pathValue ? { path: pathValue } : {}),
|
|
502
|
+
...(view ? { view } : {}),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
export function preparePtySpawn(commandPath, args, platform = process.platform) {
|
|
506
|
+
if (platform !== "win32")
|
|
507
|
+
return { command: commandPath, args };
|
|
508
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
509
|
+
if (extension !== ".cmd" && extension !== ".bat") {
|
|
510
|
+
return { command: commandPath, args };
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
command: "cmd.exe",
|
|
514
|
+
args: ["/d", "/s", "/c", `"${commandPath}"`, ...args],
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
export function parseTerminalArguments(value, platform = process.platform) {
|
|
518
|
+
if (!value)
|
|
519
|
+
return [];
|
|
520
|
+
const args = [];
|
|
521
|
+
let current = "";
|
|
522
|
+
let quote = null;
|
|
523
|
+
let escaped = false;
|
|
524
|
+
const preserveBackslashes = platform === "win32";
|
|
525
|
+
const pushCurrent = () => {
|
|
526
|
+
if (current)
|
|
527
|
+
args.push(current);
|
|
528
|
+
current = "";
|
|
529
|
+
};
|
|
530
|
+
for (let index = 0; index < value.length; index++) {
|
|
531
|
+
const character = value[index];
|
|
532
|
+
if (escaped) {
|
|
533
|
+
current += character;
|
|
534
|
+
escaped = false;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (character === "\\" && quote !== "'") {
|
|
538
|
+
if (!preserveBackslashes || value[index + 1] === '"') {
|
|
539
|
+
escaped = true;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
current += character;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (quote) {
|
|
546
|
+
if (character === quote)
|
|
547
|
+
quote = null;
|
|
548
|
+
else
|
|
549
|
+
current += character;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (character === '"' || character === "'") {
|
|
553
|
+
quote = character;
|
|
554
|
+
}
|
|
555
|
+
else if (/\s/.test(character)) {
|
|
556
|
+
pushCurrent();
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
current += character;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (escaped || quote) {
|
|
563
|
+
throw new Error("Invalid flags: unterminated terminal argument");
|
|
564
|
+
}
|
|
565
|
+
pushCurrent();
|
|
566
|
+
return args;
|
|
440
567
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.176.
|
|
3
|
+
"version": "0.176.4-nightly-20260901191433",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|