@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.
@@ -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: scroll;
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);
@@ -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
- error?: undefined;
66
+ id?: undefined;
67
67
  }>>;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -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
  }>>;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- text: string;
24
23
  error?: undefined;
24
+ text: string;
25
25
  }>>;
@@ -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
- try {
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, commandExists, isAllowedCommand, } from "./cli-registry.js";
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
- // Check if CLI is installed; if not, use npx to run it
249
- let useNpx = false;
250
- const commandInstalled = await commandExists(command);
251
- if (closed) {
252
- ws.close();
253
- return;
254
- }
255
- if (!commandInstalled) {
256
- const registry = CLI_REGISTRY[command];
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 (getCommandArgs)
271
- commandArgs = await getCommandArgs(command);
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(shell, ["-l", "-c", fullCommand], {
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 shellQuote(value) {
439
- return `'${value.replaceAll("'", `'"'"'`)}'`;
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",
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": {