@agent-native/core 0.176.3 → 0.176.4-nightly-20260901193441

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/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8819
34
+ - template files: 8820
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-01
4
+ ---
5
+
6
+ Calendar keeps the learn-more link clear of the language control on every screen size.
@@ -14,6 +14,7 @@ export default createAuthPlugin({
14
14
  screenshotWidth: 914,
15
15
  screenshotHeight: 818,
16
16
  learnMoreUrl: "https://agent-native.com/apps/calendar",
17
+ learnMorePlacement: "bottom-right",
17
18
  tagline:
18
19
  "Your AI agent schedules, reschedules, and manages your calendar so you never have to.",
19
20
  features: [
@@ -10,6 +10,7 @@ export interface AuthMarketingProps {
10
10
  screenshotWidth?: number;
11
11
  screenshotHeight?: number;
12
12
  learnMoreUrl?: string;
13
+ learnMorePlacement?: "top-right" | "bottom-right";
13
14
  }
14
15
  export interface AuthLocaleOption {
15
16
  value: string;
@@ -1831,9 +1831,15 @@ export function AuthPage(props) {
1831
1831
  React.createElement("span", { "aria-hidden": "true" }, " - "),
1832
1832
  React.createElement("span", { className: "auth-marketing-learn-more-link" }, t("learnMore")))) : null, auth: React.createElement(React.Fragment, null,
1833
1833
  authCard,
1834
- localNote), className: marketingCopy.screenshotSrc
1835
- ? "auth-marketing-home has-product-screenshot"
1836
- : "auth-marketing-home" }, marketingCopy.screenshotSrc ? (React.createElement("div", { className: "auth-marketing-screenshot-wrap" },
1834
+ localNote), className: [
1835
+ "auth-marketing-home",
1836
+ marketingCopy.screenshotSrc ? "has-product-screenshot" : "",
1837
+ marketingCopy.learnMorePlacement === "bottom-right"
1838
+ ? "has-bottom-right-learn-more"
1839
+ : "",
1840
+ ]
1841
+ .filter(Boolean)
1842
+ .join(" ") }, marketingCopy.screenshotSrc ? (React.createElement("div", { className: "auth-marketing-screenshot-wrap" },
1837
1843
  React.createElement("img", { className: "auth-marketing-screenshot", src: marketingCopy.screenshotSrc, alt: `${marketingCopy.appName} preview`, width: marketingCopy.screenshotWidth, height: marketingCopy.screenshotHeight, fetchPriority: "high", decoding: "async" }))) : (React.createElement("div", { className: "marketing-content" },
1838
1844
  React.createElement("h2", { className: "app-name" },
1839
1845
  React.createElement("img", { className: "brand-mark", src: brandMarkSrc, alt: "", "aria-hidden": "true" }),
@@ -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;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  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
- id?: undefined;
47
- error?: undefined;
48
47
  ok?: undefined;
48
+ id?: 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;
60
59
  ok?: undefined;
60
+ id?: 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
  }>>;
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
37
37
  ok?: undefined;
38
38
  status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
44
  ok?: undefined;
45
45
  error: string;
46
46
  removed?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
58
58
  error: string;
59
59
  note?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
@@ -34,10 +34,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
34
34
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
35
35
  error: any;
36
36
  } | {
37
+ error?: undefined;
37
38
  ok: boolean;
38
39
  key: string;
39
40
  baseUrlKey?: string;
40
41
  scope: AgentEngineApiKeyScope;
41
- error?: undefined;
42
42
  }>>;
43
43
  export {};
@@ -7,6 +7,7 @@ export interface AuthMarketingContent {
7
7
  screenshotWidth?: number;
8
8
  screenshotHeight?: number;
9
9
  learnMoreUrl?: string;
10
+ learnMorePlacement?: "top-right" | "bottom-right";
10
11
  /** @deprecated Local execution is no longer offered from auth pages. */
11
12
  runLocalCommand?: string;
12
13
  signupLocalModeNote?: {
@@ -32,6 +32,7 @@ export const BUILT_IN_AUTH_MARKETING = {
32
32
  "Manages availability and booking links automatically",
33
33
  "Answers schedule questions and resolves conflicts instantly",
34
34
  ],
35
+ learnMorePlacement: "bottom-right",
35
36
  },
36
37
  clips: {
37
38
  appName: "Agent-Native Clips",
@@ -132,6 +132,7 @@ export interface AuthOptions {
132
132
  screenshotWidth?: number;
133
133
  screenshotHeight?: number;
134
134
  learnMoreUrl?: string;
135
+ learnMorePlacement?: "top-right" | "bottom-right";
135
136
  /** @deprecated Local execution is no longer offered from auth pages. */
136
137
  runLocalCommand?: string;
137
138
  };
@@ -33,6 +33,7 @@ export interface OnboardingHtmlOptions {
33
33
  screenshotWidth?: number;
34
34
  screenshotHeight?: number;
35
35
  learnMoreUrl?: string;
36
+ learnMorePlacement?: "top-right" | "bottom-right";
36
37
  /** @deprecated Local execution is no longer offered from auth pages. */
37
38
  runLocalCommand?: string;
38
39
  };
@@ -1281,6 +1281,7 @@ export function getOnboardingHtml(opts = {}) {
1281
1281
  : undefined,
1282
1282
  screenshotWidth: marketing.screenshotWidth,
1283
1283
  screenshotHeight: marketing.screenshotHeight,
1284
+ learnMorePlacement: marketing.learnMorePlacement,
1284
1285
  learnMoreUrl: marketing.learnMoreUrl ??
1285
1286
  (marketingSlug
1286
1287
  ? `https://agent-native.com/apps/${marketingSlug}`
@@ -1350,13 +1351,11 @@ export function getOnboardingHtml(opts = {}) {
1350
1351
  width: 2rem;
1351
1352
  height: 2rem;
1352
1353
  padding: 0;
1353
- background: rgba(20,20,20,0.82);
1354
+ background: transparent;
1354
1355
  color: #e5e5e5;
1355
- border: 1px solid rgba(255,255,255,0.12);
1356
- border-radius: 8px;
1356
+ border: 0;
1357
1357
  cursor: pointer;
1358
1358
  outline: none;
1359
- backdrop-filter: blur(12px);
1360
1359
  }
1361
1360
  .locale-trigger svg {
1362
1361
  width: 1rem;
@@ -1369,11 +1368,10 @@ export function getOnboardingHtml(opts = {}) {
1369
1368
  }
1370
1369
  .locale-trigger:hover,
1371
1370
  .locale-trigger[aria-expanded="true"] {
1372
- border-color: rgba(255,255,255,0.22);
1373
- background: rgba(28,28,28,0.92);
1371
+ border-color: transparent;
1372
+ background: transparent;
1374
1373
  }
1375
1374
  .locale-trigger:focus {
1376
- border-color: rgba(255,255,255,0.42);
1377
1375
  box-shadow: 0 0 0 3px rgba(255,255,255,0.08);
1378
1376
  }
1379
1377
  .locale-menu {
@@ -1940,13 +1938,17 @@ ${embeddedAuthCss}
1940
1938
  justify-content: flex-end;
1941
1939
  align-items: center;
1942
1940
  position: absolute;
1943
- height: 2rem;
1944
1941
  padding: 0;
1945
1942
  top: max(1rem, env(safe-area-inset-top));
1946
1943
  inset-inline-end: calc(max(1rem, env(safe-area-inset-right)) + 2.5rem);
1947
1944
  z-index: 2;
1948
1945
  }
1949
- .auth-marketing-learn-more { font-size: 0.9rem; }
1946
+ .auth-marketing-learn-more { font-size: 0.8rem; }
1947
+ .auth-marketing-home.has-bottom-right-learn-more .auth-marketing-top-right {
1948
+ top: auto;
1949
+ bottom: max(1rem, env(safe-area-inset-bottom));
1950
+ inset-inline-end: max(1rem, env(safe-area-inset-right));
1951
+ }
1950
1952
  .auth-marketing-home .auth-marketing-layout {
1951
1953
  min-height: 100vh;
1952
1954
  display: flex;
@@ -1981,9 +1983,17 @@ ${embeddedAuthCss}
1981
1983
  }
1982
1984
  .auth-marketing-home .auth-marketing-top-right {
1983
1985
  top: max(1rem, env(safe-area-inset-top));
1986
+ bottom: auto;
1984
1987
  inset-inline-start: max(1rem, env(safe-area-inset-left));
1985
1988
  inset-inline-end: auto;
1986
1989
  }
1990
+ .auth-marketing-home.has-bottom-right-learn-more .auth-marketing-top-right {
1991
+ top: auto;
1992
+ bottom: max(1rem, env(safe-area-inset-bottom));
1993
+ inset-inline-start: 50%;
1994
+ inset-inline-end: auto;
1995
+ transform: translateX(-50%);
1996
+ }
1987
1997
  .auth-marketing-home .auth-marketing-layout { min-height: auto; }
1988
1998
  .auth-marketing-home .auth-marketing-shell { display: block; }
1989
1999
  .auth-marketing-home .auth-marketing-shell-with-top-right { display: flex; }
@@ -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-20260901193441",
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": {