@agent-native/core 0.198.2 → 0.198.3-nightly-20260930154911

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.
Files changed (47) hide show
  1. package/dist/catalog.json +1 -1
  2. package/dist/cli/create-tui.js +31 -9
  3. package/dist/cli/design-connect.js +10 -2
  4. package/dist/client/chat/agentkit-protocol.js +4 -1
  5. package/dist/collab/awareness.d.ts +2 -2
  6. package/dist/collab/routes.d.ts +1 -1
  7. package/dist/collab/struct-routes.d.ts +1 -1
  8. package/dist/deploy/workspace-deploy.js +18 -5
  9. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  10. package/dist/framework-tools.js +2 -0
  11. package/dist/localization/core-messages/supplemental/ar-SA.js +2 -0
  12. package/dist/localization/core-messages/supplemental/de-DE.js +2 -0
  13. package/dist/localization/core-messages/supplemental/en-US.js +2 -0
  14. package/dist/localization/core-messages/supplemental/es-ES.js +2 -0
  15. package/dist/localization/core-messages/supplemental/fr-FR.js +2 -0
  16. package/dist/localization/core-messages/supplemental/hi-IN.js +2 -0
  17. package/dist/localization/core-messages/supplemental/ja-JP.js +2 -0
  18. package/dist/localization/core-messages/supplemental/ko-KR.js +2 -0
  19. package/dist/localization/core-messages/supplemental/pt-BR.js +2 -0
  20. package/dist/localization/core-messages/supplemental/zh-CN.js +2 -0
  21. package/dist/localization/core-messages/supplemental/zh-TW.js +2 -0
  22. package/dist/localization/mcp-settings-messages.d.ts +2 -0
  23. package/dist/mcp/connect-route.js +57 -10
  24. package/dist/mcp/oauth-route.js +4 -21
  25. package/dist/mcp/org-choice.d.ts +26 -0
  26. package/dist/mcp/org-choice.js +36 -0
  27. package/dist/notifications/routes.d.ts +1 -1
  28. package/dist/org/handlers.d.ts +1 -1
  29. package/dist/progress/routes.d.ts +1 -1
  30. package/dist/provider-api/actions/custom-provider-registration.d.ts +3 -3
  31. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  32. package/dist/server/action-discovery.js +5 -0
  33. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  34. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  35. package/dist/server/core-routes-plugin.d.ts +1 -1
  36. package/dist/server/credential-provider.js +5 -0
  37. package/dist/server/realtime-token.d.ts +1 -1
  38. package/dist/usage/actions/get-usage-insights.d.ts +7 -0
  39. package/dist/usage/actions/get-usage-insights.js +23 -0
  40. package/dist/usage/actions/get-usage-run.d.ts +7 -0
  41. package/dist/usage/actions/get-usage-run.js +23 -0
  42. package/dist/usage/insights-store.d.ts +127 -0
  43. package/dist/usage/insights-store.js +462 -0
  44. package/dist/usage/metrics-store.d.ts +21 -0
  45. package/dist/usage/metrics-store.js +67 -3
  46. package/dist/vite/action-types-plugin.js +8 -0
  47. package/package.json +6 -4
package/dist/catalog.json CHANGED
@@ -62,6 +62,6 @@
62
62
  "vite": "8.1.0",
63
63
  "vitest": "^4.1.5",
64
64
  "y-protocols": "^1.0.7",
65
- "yjs": "^13.6.32",
65
+ "yjs": "^13.6.33",
66
66
  "zod": "^4.3.6"
67
67
  }
@@ -253,18 +253,37 @@ function fitRows(items, activeIndex, rows) {
253
253
  const start = Math.max(0, Math.min(activeIndex - rows + 1, items.length - rows));
254
254
  return items.slice(start, start + rows);
255
255
  }
256
+ // Ink hands every key in one stdin chunk to the same input handler before
257
+ // React re-renders, so an input handler must read `latest` and update through
258
+ // the setter: the rendered value is stale from the chunk's second key on.
259
+ function useKeypressState(initial) {
260
+ const [rendered, setRendered] = React.useState(initial);
261
+ const latest = React.useRef(rendered);
262
+ const update = (next) => {
263
+ latest.current = next(latest.current);
264
+ setRendered(latest.current);
265
+ };
266
+ return [rendered, update, latest];
267
+ }
268
+ function wizardChoices(state, installedApps) {
269
+ const apps = availableTemplates(state.kind, installedApps);
270
+ const choices = state.step === "start" ? START_CHOICES : apps;
271
+ return {
272
+ multiSelect: state.kind === "chat-workspace" ||
273
+ state.kind === "first-party" ||
274
+ state.kind === "workspace-add",
275
+ apps,
276
+ required: new Set(requiredTemplateNames(state.kind)),
277
+ choices,
278
+ };
279
+ }
256
280
  export function CreateWizard({ options, onFinish, }) {
257
281
  const { exit } = useApp();
258
282
  const { stdout } = useStdout();
259
- const [state, setState] = React.useState(() => initialWizardState(options));
283
+ const [state, setState, latest] = useKeypressState(() => initialWizardState(options));
260
284
  const terminalColumns = Math.max(16, stdout.columns ?? 80);
261
285
  const terminalRows = Math.max(14, stdout.rows ?? 24);
262
- const multiSelect = state.kind === "chat-workspace" ||
263
- state.kind === "first-party" ||
264
- state.kind === "workspace-add";
265
- const apps = availableTemplates(state.kind, options.installedApps ?? []);
266
- const required = new Set(requiredTemplateNames(state.kind));
267
- const choices = state.step === "start" ? START_CHOICES : apps;
286
+ const { multiSelect, required, choices } = wizardChoices(state, options.installedApps ?? []);
268
287
  const listRows = Math.max(3, Math.min(choices.length, Math.floor((terminalRows - 13) / 2)));
269
288
  const visibleChoices = fitRows(choices, state.activeIndex, listRows);
270
289
  const visibleOffset = Math.max(0, choices.indexOf(visibleChoices[0]));
@@ -280,6 +299,7 @@ export function CreateWizard({ options, onFinish, }) {
280
299
  exit();
281
300
  };
282
301
  const complete = () => {
302
+ const state = latest.current;
283
303
  const answer = {
284
304
  kind: state.kind ?? "chat-workspace",
285
305
  name: state.name,
@@ -291,6 +311,8 @@ export function CreateWizard({ options, onFinish, }) {
291
311
  exit();
292
312
  };
293
313
  useInput((input, key) => {
314
+ const state = latest.current;
315
+ const { multiSelect, apps, required, choices } = wizardChoices(state, options.installedApps ?? []);
294
316
  if (state.step === "cancelled")
295
317
  return;
296
318
  if ((key.ctrl && input.toLowerCase() === "c") || key.escape) {
@@ -569,7 +591,7 @@ export async function runCreateWizard(options) {
569
591
  }
570
592
  function InkChoicePrompt({ message, choices, onFinish, }) {
571
593
  const { exit } = useApp();
572
- const [activeIndex, setActiveIndex] = React.useState(0);
594
+ const [activeIndex, setActiveIndex, latestIndex] = useKeypressState(() => 0);
573
595
  useInput((input, key) => {
574
596
  if (key.escape || (key.ctrl && input.toLowerCase() === "c")) {
575
597
  onFinish(null);
@@ -581,7 +603,7 @@ function InkChoicePrompt({ message, choices, onFinish, }) {
581
603
  else if (key.downArrow)
582
604
  setActiveIndex((index) => Math.min(choices.length - 1, index + 1));
583
605
  else if (key.return) {
584
- onFinish(choices[activeIndex]?.value ?? null);
606
+ onFinish(choices[latestIndex.current]?.value ?? null);
585
607
  exit();
586
608
  }
587
609
  });
@@ -645,6 +645,13 @@ function isLoopbackOrigin(parsed) {
645
645
  hostname === "::1" ||
646
646
  /^127(?:\.\d{1,3}){3}$/.test(hostname));
647
647
  }
648
+ function sameDevServerOrigin(a, b) {
649
+ return (a.origin === b.origin ||
650
+ (a.protocol === b.protocol &&
651
+ a.port === b.port &&
652
+ isLoopbackOrigin(a) &&
653
+ isLoopbackOrigin(b)));
654
+ }
648
655
  function isApprovedDesignOrigin(rawOrigin, configuredOrigins, opaquePreviewAuthorized = false) {
649
656
  // Sandboxed loopback preview documents have an opaque `null` origin. It is
650
657
  // only approved when this request carries the non-cookie preview token;
@@ -976,16 +983,17 @@ class PreviewSessionCookieJar {
976
983
  }
977
984
  function resolvePreviewSnapshotUrl(devServerUrl, rawUrl) {
978
985
  const base = normalizeHttpUrl(devServerUrl);
986
+ const baseUrl = new URL(base);
979
987
  const parsed = new URL(rawUrl?.trim() || "/", `${base}/`);
980
988
  parsed.hash = "";
981
989
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
982
990
  throw new Error("Snapshot URL must use http(s).");
983
991
  }
984
- if (!sameOrigin(parsed.toString(), base)) {
992
+ if (!sameDevServerOrigin(parsed, baseUrl)) {
985
993
  throw new Error("Snapshot URL must stay on the connected dev server.");
986
994
  }
987
995
  const search = stripQueryPair(parsed.search, FRAME_BRIDGE_KEY_PARAM);
988
- return `${parsed.origin}${parsed.pathname}${search}`;
996
+ return `${baseUrl.origin}${parsed.pathname}${search}`;
989
997
  }
990
998
  function stripPreviewTokenQueryParam(search) {
991
999
  if (!search.includes("previewToken"))
@@ -1528,7 +1528,10 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1528
1528
  {
1529
1529
  type: "suggestions.updated",
1530
1530
  ...base,
1531
- suggestions: event.suggestions,
1531
+ suggestions: event.suggestions.map((suggestion) => ({
1532
+ ...suggestion,
1533
+ runId: run.runId,
1534
+ })),
1532
1535
  },
1533
1536
  ];
1534
1537
  case "annotation": {
@@ -38,19 +38,19 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
38
38
  error: string;
39
39
  states?: undefined;
40
40
  } | {
41
+ error?: undefined;
41
42
  states: {
42
43
  clientId: number;
43
44
  state: string;
44
45
  }[];
45
- error?: undefined;
46
46
  }>>;
47
47
  export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
48
48
  error: string;
49
49
  users?: undefined;
50
50
  } | {
51
+ error?: undefined;
51
52
  users: {
52
53
  clientId: number;
53
54
  lastSeen: number;
54
55
  }[];
55
- error?: undefined;
56
56
  }>>;
@@ -8,8 +8,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
8
8
  state: string;
9
9
  }>>;
10
10
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
- error: string;
12
11
  ok?: undefined;
12
+ error: string;
13
13
  } | {
14
14
  error?: undefined;
15
15
  ok: boolean;
@@ -1,6 +1,6 @@
1
1
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
- ok?: undefined;
3
2
  error: string;
3
+ ok?: undefined;
4
4
  } | {
5
5
  error?: undefined;
6
6
  ok: boolean;
@@ -18,6 +18,12 @@ import { collectImmutableAssetPaths, IMMUTABLE_ASSET_CACHE_HEADERS, } from "./im
18
18
  function workspaceFrameworkRoutePrefixEnv() {
19
19
  return (process.env.AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX?.trim() || "");
20
20
  }
21
+ function workspaceFrameworkRoutePrefixEnvSnippet() {
22
+ const frameworkRoutePrefix = workspaceFrameworkRoutePrefixEnv();
23
+ return frameworkRoutePrefix
24
+ ? ` processRef.env.AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX = ${JSON.stringify(frameworkRoutePrefix)};\n`
25
+ : " delete processRef.env.AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX;\n";
26
+ }
21
27
  function workspaceFrameworkRoutePrefix() {
22
28
  return normalizeFrameworkRoutePrefix(workspaceFrameworkRoutePrefixEnv() || undefined, "AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX");
23
29
  }
@@ -167,6 +173,7 @@ function buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApp
167
173
  ? workspaceGatewayUrl
168
174
  : null);
169
175
  const workspaceOAuthUrl = workspaceOAuthOrigin(workspaceGatewayUrl);
176
+ const frameworkRoutePrefix = workspaceFrameworkRoutePrefixEnv();
170
177
  const env = {
171
178
  ...process.env,
172
179
  NITRO_PRESET: preset,
@@ -184,7 +191,6 @@ function buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApp
184
191
  : {}),
185
192
  APP_BASE_PATH: `/${app}`,
186
193
  VITE_APP_BASE_PATH: `/${app}`,
187
- AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX: workspaceFrameworkRoutePrefixEnv(),
188
194
  AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: workspaceAppAudience,
189
195
  AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: JSON.stringify(workspaceAppRouteAccess.publicPaths),
190
196
  AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: JSON.stringify(workspaceAppRouteAccess.protectedPaths),
@@ -206,6 +212,13 @@ function buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApp
206
212
  : {}),
207
213
  [WORKSPACE_APPS_ENV_KEY]: JSON.stringify(workspaceApps),
208
214
  };
215
+ if (frameworkRoutePrefix) {
216
+ env.AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX =
217
+ frameworkRoutePrefix;
218
+ }
219
+ else {
220
+ delete env.AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX;
221
+ }
209
222
  if (preset === "netlify" && appUsesNetlifyUnpooledDatabaseUrl(appDir)) {
210
223
  env.DATABASE_URL =
211
224
  process.env.NETLIFY_DATABASE_URL_UNPOOLED ??
@@ -641,13 +654,13 @@ ${workspaceDirectoryEnvSnippet(workspaceApps)}
641
654
  VITE_AGENT_NATIVE_WORKSPACE_AUTH_MODE: ${JSON.stringify(workspaceAuthMode)},
642
655
  VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
643
656
  VITE_APP_BASE_PATH: basePath,
644
- AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX: ${JSON.stringify(workspaceFrameworkRoutePrefixEnv())},
645
657
  VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
646
658
  VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
647
659
  VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
648
660
  VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
649
661
  ${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
650
662
  });
663
+ ${workspaceFrameworkRoutePrefixEnvSnippet()}
651
664
  }
652
665
 
653
666
  setBasePathEnv();
@@ -734,13 +747,13 @@ ${workspaceDirectoryEnvSnippet(workspaceApps)}
734
747
  VITE_AGENT_NATIVE_WORKSPACE_AUTH_MODE: ${JSON.stringify(workspaceAuthMode)},
735
748
  VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
736
749
  VITE_APP_BASE_PATH: basePath,
737
- AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX: ${JSON.stringify(workspaceFrameworkRoutePrefixEnv())},
738
750
  VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
739
751
  VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
740
752
  VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
741
753
  VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
742
754
  ${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
743
755
  });
756
+ ${workspaceFrameworkRoutePrefixEnvSnippet()}
744
757
  }
745
758
 
746
759
  function enabled() {
@@ -865,13 +878,13 @@ ${workspaceDirectoryEnvSnippet(workspaceApps)}
865
878
  VITE_AGENT_NATIVE_WORKSPACE_AUTH_MODE: ${JSON.stringify(workspaceAuthMode)},
866
879
  VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
867
880
  VITE_APP_BASE_PATH: basePath,
868
- AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX: ${JSON.stringify(workspaceFrameworkRoutePrefixEnv())},
869
881
  VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
870
882
  VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
871
883
  VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
872
884
  VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
873
885
  ${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
874
886
  });
887
+ ${workspaceFrameworkRoutePrefixEnvSnippet()}
875
888
  }
876
889
 
877
890
  setBasePathEnv();
@@ -928,13 +941,13 @@ ${workspaceDirectoryEnvSnippet(workspaceApps)}
928
941
  VITE_AGENT_NATIVE_WORKSPACE_AUTH_MODE: ${JSON.stringify(workspaceAuthMode)},
929
942
  VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
930
943
  VITE_APP_BASE_PATH: basePath,
931
- AGENT_NATIVE_CONFIG_RUNTIME_FRAMEWORK_ROUTE_PREFIX: ${JSON.stringify(workspaceFrameworkRoutePrefixEnv())},
932
944
  VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
933
945
  VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
934
946
  VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
935
947
  VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
936
948
  ${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
937
949
  });
950
+ ${workspaceFrameworkRoutePrefixEnvSnippet()}
938
951
  }
939
952
 
940
953
  function normalizeBasePathArgs(args) {
@@ -35,9 +35,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
35
35
  } | {
36
36
  error: string;
37
37
  url?: undefined;
38
- id?: undefined;
39
38
  provider?: undefined;
40
39
  configured?: undefined;
40
+ id?: undefined;
41
41
  } | {
42
42
  url: string;
43
43
  id: string;
@@ -46,9 +46,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
46
46
  error?: undefined;
47
47
  } | {
48
48
  url?: undefined;
49
- id?: undefined;
50
49
  provider?: undefined;
51
50
  error: string;
52
51
  configured: boolean;
52
+ id?: undefined;
53
53
  }>;
54
54
  export default _default;
@@ -132,6 +132,8 @@ export const CORE_ACTION_GROUPS = {
132
132
  "get-usage-alerts": "automation",
133
133
  "manage-usage-alert": "automation",
134
134
  "get-usage-metrics": "automation",
135
+ "get-usage-insights": "automation",
136
+ "get-usage-run": "automation",
135
137
  "get-builder-credit-usage": "automation",
136
138
  "get-builder-credit-status": "automation",
137
139
  "get-builder-referral-info": "automation",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "إلغاء",
38
38
  couldNotRevoke: "تعذر إلغاء الرمز.",
39
39
  authorizeDevice: "تخويل الجهاز",
40
+ organization: "المؤسسة",
41
+ invalidOrganization: "اختر مؤسسة أنت عضو فيها.",
40
42
  fullCatalogRequested: "يطلب هذا الجهاز الوصول إلى كتالوج الإجراءات الكامل.",
41
43
  createToken: "إنشاء رمز اتصال",
42
44
  authorizingDevice: "جارٍ تخويل الجهاز...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "Widerrufen",
38
38
  couldNotRevoke: "Token konnte nicht widerrufen werden.",
39
39
  authorizeDevice: "Gerät autorisieren",
40
+ organization: "Organisation",
41
+ invalidOrganization: "Wähle eine Organisation, der du angehörst.",
40
42
  fullCatalogRequested: "Dieses Gerät fordert Zugriff auf den vollständigen Aktionskatalog an.",
41
43
  createToken: "Verbindungstoken erstellen",
42
44
  authorizingDevice: "Gerät wird autorisiert...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "Revoke",
38
38
  couldNotRevoke: "Could not revoke token.",
39
39
  authorizeDevice: "Authorize device",
40
+ organization: "Organization",
41
+ invalidOrganization: "Choose an organization you belong to.",
40
42
  fullCatalogRequested: "This device is requesting access to the full action catalog.",
41
43
  createToken: "Create connection token",
42
44
  authorizingDevice: "Authorizing device...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "Revocar",
38
38
  couldNotRevoke: "No se pudo revocar el token.",
39
39
  authorizeDevice: "Autorizar dispositivo",
40
+ organization: "Organización",
41
+ invalidOrganization: "Elige una organización a la que pertenezcas.",
40
42
  fullCatalogRequested: "Este dispositivo solicita acceso al catálogo completo de acciones.",
41
43
  createToken: "Crear token de conexión",
42
44
  authorizingDevice: "Autorizando dispositivo...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "Révoquer",
38
38
  couldNotRevoke: "Impossible de révoquer le jeton.",
39
39
  authorizeDevice: "Autoriser l’appareil",
40
+ organization: "Organisation",
41
+ invalidOrganization: "Choisissez une organisation dont vous êtes membre.",
40
42
  fullCatalogRequested: "Cet appareil demande l’accès au catalogue complet des actions.",
41
43
  createToken: "Créer un jeton de connexion",
42
44
  authorizingDevice: "Autorisation de l’appareil...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "Revoke करें",
38
38
  couldNotRevoke: "Token revoke नहीं किया जा सका।",
39
39
  authorizeDevice: "डिवाइस को अनुमति दें",
40
+ organization: "संगठन",
41
+ invalidOrganization: "ऐसा संगठन चुनें जिसके आप सदस्य हैं।",
40
42
  fullCatalogRequested: "यह डिवाइस पूरे एक्शन कैटलॉग तक पहुंच का अनुरोध कर रहा है।",
41
43
  createToken: "कनेक्शन token बनाएँ",
42
44
  authorizingDevice: "डिवाइस को अनुमति दी जा रही है...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "取り消す",
38
38
  couldNotRevoke: "トークンを取り消せませんでした。",
39
39
  authorizeDevice: "デバイスを承認",
40
+ organization: "組織",
41
+ invalidOrganization: "所属している組織を選択してください。",
40
42
  fullCatalogRequested: "このデバイスは、すべてのアクションカタログへのアクセスを要求しています。",
41
43
  createToken: "接続トークンを作成",
42
44
  authorizingDevice: "デバイスを承認中...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "취소",
38
38
  couldNotRevoke: "토큰을 취소할 수 없습니다.",
39
39
  authorizeDevice: "기기 승인",
40
+ organization: "조직",
41
+ invalidOrganization: "소속된 조직을 선택하세요.",
40
42
  fullCatalogRequested: "이 기기는 전체 작업 카탈로그에 대한 액세스를 요청하고 있습니다.",
41
43
  createToken: "연결 토큰 생성",
42
44
  authorizingDevice: "기기 승인 중...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "Revogar",
38
38
  couldNotRevoke: "Não foi possível revogar o token.",
39
39
  authorizeDevice: "Autorizar dispositivo",
40
+ organization: "Organização",
41
+ invalidOrganization: "Escolha uma organização da qual você faz parte.",
40
42
  fullCatalogRequested: "Este dispositivo está solicitando acesso ao catálogo completo de ações.",
41
43
  createToken: "Criar token de conexão",
42
44
  authorizingDevice: "Autorizando dispositivo...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "撤销",
38
38
  couldNotRevoke: "无法撤销令牌。",
39
39
  authorizeDevice: "授权设备",
40
+ organization: "组织",
41
+ invalidOrganization: "请选择你所属的组织。",
40
42
  fullCatalogRequested: "此设备正在请求访问完整的操作目录。",
41
43
  createToken: "创建连接令牌",
42
44
  authorizingDevice: "正在授权设备...",
@@ -37,6 +37,8 @@ export const mcpConnectMessages = {
37
37
  revoke: "撤銷",
38
38
  couldNotRevoke: "無法撤銷權杖。",
39
39
  authorizeDevice: "授權裝置",
40
+ organization: "組織",
41
+ invalidOrganization: "請選擇你所屬的組織。",
40
42
  fullCatalogRequested: "此裝置正在要求存取完整的動作目錄。",
41
43
  createToken: "建立連線權杖",
42
44
  authorizingDevice: "正在授權裝置...",
@@ -24,6 +24,8 @@ export interface McpConnectMessages {
24
24
  revoke: string;
25
25
  couldNotRevoke: string;
26
26
  authorizeDevice: string;
27
+ organization: string;
28
+ invalidOrganization: string;
27
29
  fullCatalogRequested: string;
28
30
  createToken: string;
29
31
  authorizingDevice: string;
@@ -11,6 +11,7 @@ import { readBody } from "../server/h3-helpers.js";
11
11
  import { MCP_CONNECT_MCP_URL_TEMPLATE, getMcpConnectGuides, getMcpStaticTokenFallback, interpolateMcpConnectTemplate, resolveMcpConnectGuideId, } from "../shared/mcp-connect-content.js";
12
12
  import { recordMintedToken, listTokens, revokeToken, normalizeServiceName, serviceIdentityEmail, createDeviceCode, getDeviceCode, getDeviceCodeByUserCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, releaseDeviceCodeMint, expireDeviceCode, MCP_CONNECT_OAUTH_CLIENT_ID, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
13
13
  import { MCP_OAUTH_DEFAULT_SCOPE, signMcpOAuthAccessToken, } from "./oauth-token.js";
14
+ import { resolveMcpOrgChoices } from "./org-choice.js";
14
15
  import { MCP_PUBLIC_ROUTE_PREFIX } from "./route-paths.js";
15
16
  const DEVICE_POLL_INTERVAL_S = 3;
16
17
  const USER_CODE_RE = /^[A-Z2-7]{4}-[A-Z2-7]{4}$/;
@@ -287,7 +288,7 @@ function renderConnectGuide(guide, activeGuideId, values, copyLabel) {
287
288
  return `<div class="tab-panel${guide.id === activeGuideId ? " is-active" : ""}" role="tabpanel" id="mcp-guide-panel-${guideId}" aria-labelledby="mcp-guide-tab-${guideId}" data-panel="${guideId}">${content}</div>`;
288
289
  }
289
290
  function renderConnectPage(params) {
290
- const { connectBasePath, email, appName, appUrl, serverId, userCode, catalogScope, locale, requestedGuide, } = params;
291
+ const { connectBasePath, email, appName, appUrl, serverId, userCode, catalogScope, locale, requestedGuide, organizations, defaultOrganizationId, } = params;
291
292
  const direction = localeDirection(locale);
292
293
  const messages = mcpSettingsMessagesForLocale(locale);
293
294
  const connectMessages = messages.mcpConnect;
@@ -316,6 +317,19 @@ function renderConnectPage(params) {
316
317
  const guidePanelsHtml = guides
317
318
  .map((guide) => renderConnectGuide(guide, activeGuideId, connectTemplateValues, messages.mcpCopy))
318
319
  .join("\n");
320
+ const organizationSelectorHtml = safeUserCode && organizations.length > 1
321
+ ? `<div class="field">
322
+ <label for="organizationId">${localize(connectMessages.organization)}</label>
323
+ <div class="select-wrap">
324
+ <select id="organizationId">
325
+ ${organizations
326
+ .map((organization) => `<option value="${escapeHtml(organization.id)}"${organization.id === defaultOrganizationId ? " selected" : ""}>${escapeHtml(organization.name)}${organization.domain ? ` (${escapeHtml(organization.domain)})` : ""}</option>`)
327
+ .join("")}
328
+ </select>
329
+ <span class="chev" aria-hidden="true"></span>
330
+ </div>
331
+ </div>`
332
+ : "";
319
333
  const setupHtml = safeUserCode
320
334
  ? ""
321
335
  : `
@@ -426,7 +440,7 @@ function renderConnectPage(params) {
426
440
  display: flex; align-items: center; justify-content: space-between;
427
441
  gap: 0.75rem; border: 1px solid var(--border);
428
442
  border-radius: 8px; padding: 0.5rem 0.65rem; margin: 0 0 0.9rem;
429
- background: var(--panel-soft); color: var(--muted);
443
+ background: var(--panel-soft); color: var(--muted); line-height: 1.2rem;
430
444
  }
431
445
  .device-strip .label {
432
446
  font-size: 0.76rem; font-weight: 560; color: var(--subtle);
@@ -503,12 +517,25 @@ function renderConnectPage(params) {
503
517
  .field:last-child { margin-bottom: 0; }
504
518
  .field label { display: block; font-size: 0.78rem; color: var(--muted);
505
519
  margin-bottom: 0.35rem; }
506
- .field input {
520
+ .field input, .field select {
507
521
  width: 100%; padding: 0.6rem 0.7rem; font: inherit; color: var(--text);
508
522
  background: var(--panel-2); border: 1px solid var(--border-strong);
509
523
  border-radius: 8px;
510
524
  }
511
- .field input:focus-visible {
525
+ .select-wrap { position: relative; }
526
+ .select-wrap select {
527
+ appearance: none; -webkit-appearance: none;
528
+ padding: 0.5rem 2.4rem 0.5rem 0.65rem; font-size: 0.78rem;
529
+ line-height: 1.2rem; cursor: pointer;
530
+ }
531
+ .select-wrap .chev {
532
+ position: absolute; top: 50%; right: 1rem; width: 7px; height: 7px;
533
+ border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
534
+ transform: translateY(-70%) rotate(45deg); pointer-events: none;
535
+ transition: transform 0.15s ease;
536
+ }
537
+ .select-wrap:has(select:open) .chev { transform: translateY(-30%) rotate(225deg); }
538
+ .field input:focus-visible, .field select:focus-visible {
512
539
  outline: none; border-color: var(--ring);
513
540
  box-shadow: 0 0 0 3px rgba(250,250,250,0.12);
514
541
  }
@@ -718,6 +745,7 @@ function renderConnectPage(params) {
718
745
  ? `<div id="staticTokenMint">
719
746
  <div id="msg" class="msg" role="status" aria-live="polite"></div>
720
747
  <div id="mintForm">
748
+ ${organizationSelectorHtml}
721
749
  <button id="authorizeBtn" class="primary">${localize(connectMessages.authorizeDevice)}</button>
722
750
  </div>
723
751
  </div>`
@@ -864,6 +892,7 @@ function renderConnectPage(params) {
864
892
  if (response.status === 404) return COPY.unknownDeviceCode;
865
893
  if (response.status === 410) return COPY.expiredDeviceCode;
866
894
  if (response.status === 409) return COPY.alreadyUsedDeviceCode;
895
+ if (response.status === 403) return COPY.invalidOrganization;
867
896
  return COPY.couldNotAuthorize;
868
897
  }
869
898
 
@@ -930,7 +959,13 @@ function renderConnectPage(params) {
930
959
  clearMsg();
931
960
  try {
932
961
  if (USER_CODE) {
933
- var a = await postJson("/device/authorize", { user_code: USER_CODE });
962
+ var orgSelect = document.getElementById("organizationId");
963
+ var a = await postJson(
964
+ "/device/authorize",
965
+ orgSelect
966
+ ? { user_code: USER_CODE, org_id: orgSelect.value }
967
+ : { user_code: USER_CODE },
968
+ );
934
969
  if (!a.ok) {
935
970
  resetButtonLoading(btn);
936
971
  showMsg(deviceAuthorizationError(a));
@@ -1050,6 +1085,8 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1050
1085
  catalogScope: null,
1051
1086
  locale,
1052
1087
  requestedGuide: requestUrl?.searchParams.get("guide") ?? null,
1088
+ organizations: [],
1089
+ defaultOrganizationId: undefined,
1053
1090
  }));
1054
1091
  }
1055
1092
  let userCode = null;
@@ -1064,6 +1101,9 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1064
1101
  deviceCode.expiresAt >= Date.now()
1065
1102
  ? deviceCode.catalogScope
1066
1103
  : null;
1104
+ const { organizations, defaultOrganizationId } = userCode
1105
+ ? await resolveMcpOrgChoices(event, session)
1106
+ : { organizations: [], defaultOrganizationId: undefined };
1067
1107
  return html(renderConnectPage({
1068
1108
  connectBasePath: basePath,
1069
1109
  email: session.email,
@@ -1074,6 +1114,8 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1074
1114
  catalogScope,
1075
1115
  locale,
1076
1116
  requestedGuide: requestUrl?.searchParams.get("guide") ?? null,
1117
+ organizations,
1118
+ defaultOrganizationId,
1077
1119
  }));
1078
1120
  }
1079
1121
  if (sub === "/token") {
@@ -1094,9 +1136,10 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1094
1136
  ? "full"
1095
1137
  : undefined;
1096
1138
  try {
1139
+ const { defaultOrganizationId } = await resolveMcpOrgChoices(event, session);
1097
1140
  const { token } = await mintConnectToken({
1098
1141
  email: session.email,
1099
- orgId: session.orgId,
1142
+ orgId: defaultOrganizationId,
1100
1143
  label,
1101
1144
  ttlDays,
1102
1145
  appUrl,
@@ -1157,10 +1200,14 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1157
1200
  if (!USER_CODE_RE.test(userCode)) {
1158
1201
  return json({ error: "Invalid user code." }, 400);
1159
1202
  }
1160
- const orgId = typeof session.orgId === "string" && session.orgId.trim()
1161
- ? session.orgId.trim()
1162
- : null;
1163
- const result = await approveDeviceCode(userCode, session.email, orgId);
1203
+ const requestedOrgId = typeof body.org_id === "string" && body.org_id ? body.org_id : undefined;
1204
+ const { organizations, defaultOrganizationId } = await resolveMcpOrgChoices(event, session, requestedOrgId);
1205
+ if (requestedOrgId !== undefined &&
1206
+ !organizations.some(({ id }) => id === requestedOrgId)) {
1207
+ return json({ error: "Choose an organization you belong to." }, 403);
1208
+ }
1209
+ const selectedOrgId = requestedOrgId ?? defaultOrganizationId ?? null;
1210
+ const result = await approveDeviceCode(userCode, session.email, selectedOrgId);
1164
1211
  if (result === "not_found") {
1165
1212
  return json({ error: "Unknown device code." }, 404);
1166
1213
  }
@@ -1,12 +1,13 @@
1
1
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { getHeader, getMethod, getQuery, setResponseStatus } from "h3";
3
- import { getActiveOrgSettingForEvent, getOrgContext, getOrgDomain, listOrgMembershipsForEvent, } from "../org/context.js";
3
+ import { getOrgDomain } from "../org/context.js";
4
4
  import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
5
5
  import { getAuthSecret } from "../server/better-auth-instance.js";
6
6
  import { readBody } from "../server/h3-helpers.js";
7
7
  import { applicationTypeForRedirectUris, isAllowedOAuthRedirectUri, isUrlBasedOAuthClientId, matchesRegisteredRedirectUri, resolveOAuthClientMetadataDocument, } from "./oauth-client-metadata.js";
8
8
  import { createOAuthCode, createOAuthRefreshToken, consumeOAuthCode, generateOpaqueToken, getOAuthClient, getOAuthCode, getOAuthRefreshToken, registerOAuthClient, touchOAuthRefreshToken, } from "./oauth-store.js";
9
9
  import { MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS, MCP_OAUTH_SCOPES, normalizeOAuthScope, signMcpOAuthAccessToken, } from "./oauth-token.js";
10
+ import { resolveMcpOrgChoices } from "./org-choice.js";
10
11
  import { MCP_LEGACY_ROUTE_PREFIX, MCP_PUBLIC_ROUTE_PREFIX, MCP_ROUTE_PREFIXES, } from "./route-paths.js";
11
12
  const MCP_OAUTH_RESOURCE_SCOPES = MCP_OAUTH_SCOPES.filter((scope) => scope !== "offline_access");
12
13
  const MCP_OAUTH_RESOURCE_SCOPE = MCP_OAUTH_RESOURCE_SCOPES.join(" ");
@@ -597,27 +598,9 @@ async function handleAuthorize(event, options) {
597
598
  error: "invalid_scope",
598
599
  });
599
600
  }
600
- const activeOrgSetting = await getActiveOrgSettingForEvent(event, session.email);
601
- const requestedOrganizationId = method === "POST" && params.organization_id !== undefined
601
+ const { organizations, defaultOrganizationId } = await resolveMcpOrgChoices(event, session, method === "POST" && params.organization_id !== undefined
602
602
  ? params.organization_id || null
603
- : (activeOrgSetting?.orgId ?? session.orgId ?? null);
604
- let memberships = await listOrgMembershipsForEvent(event, session.email, requestedOrganizationId);
605
- // A token issued with no org stays org-less for its whole life, even after
606
- // the app later creates the org, so resolve an account without one to its
607
- // domain or default org before offering the choice.
608
- const ensuredOrgId = memberships?.length === 0 ? (await getOrgContext(event)).orgId : null;
609
- if (ensuredOrgId) {
610
- memberships = await listOrgMembershipsForEvent(event, session.email, ensuredOrgId);
611
- }
612
- const organizations = memberships?.map((membership) => ({
613
- id: membership.orgId,
614
- name: membership.orgName,
615
- domain: membership.allowedDomain,
616
- })) ??
617
- (session.orgId
618
- ? [{ id: session.orgId, name: "Organization", domain: null }]
619
- : []);
620
- const defaultOrganizationId = [activeOrgSetting?.orgId, ensuredOrgId, session.orgId].find((id) => id && organizations.some((org) => org.id === id)) ?? organizations[0]?.id;
603
+ : undefined);
621
604
  if (method === "GET") {
622
605
  return html(renderConsentPage({
623
606
  appName: options.appName || options.appId || "Agent-Native",