@agent-native/core 0.163.0 → 0.163.2

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 (43) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/plugins/collab.ts +1 -0
  3. package/corpus/templates/slides/DEVELOPING.md +17 -0
  4. package/corpus/templates/slides/actions/get-deck-access-status.ts +67 -0
  5. package/corpus/templates/slides/actions/request-deck-access.ts +569 -0
  6. package/corpus/templates/slides/agent-native.json +3 -0
  7. package/corpus/templates/slides/app/components/layout/Header.tsx +2 -0
  8. package/corpus/templates/slides/app/hooks/use-deck-access.ts +51 -0
  9. package/corpus/templates/slides/app/i18n/en-US.ts +12 -0
  10. package/corpus/templates/slides/app/lib/deck-editor-loading.ts +7 -0
  11. package/corpus/templates/slides/app/pages/DeckEditor.tsx +172 -32
  12. package/corpus/templates/slides/changelog/2026-08-18-private-deck-links-now-explain-access-and-let-viewers-notify.md +6 -0
  13. package/corpus/templates/slides/server/db/schema.ts +10 -0
  14. package/corpus/templates/slides/server/plugins/db.ts +17 -0
  15. package/dist/client/analytics.d.ts +1 -0
  16. package/dist/client/analytics.js +19 -5
  17. package/dist/client/session-replay.js +349 -100
  18. package/dist/collab/routes.d.ts +1 -1
  19. package/dist/collab/storage.d.ts +2 -0
  20. package/dist/collab/storage.js +7 -0
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/config.js +11 -1
  23. package/dist/deploy/build.d.ts +1 -1
  24. package/dist/deploy/build.js +89 -12
  25. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  26. package/dist/observability/routes.d.ts +1 -1
  27. package/dist/observability/tracking-identity.js +2 -0
  28. package/dist/progress/routes.d.ts +1 -1
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/secrets/routes.d.ts +9 -9
  31. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  32. package/dist/server/collab-plugin.d.ts +6 -0
  33. package/dist/server/collab-plugin.js +27 -6
  34. package/dist/server/deploy-environment.d.ts +1 -1
  35. package/dist/server/deploy-environment.js +40 -2
  36. package/dist/server/realtime-token.d.ts +1 -1
  37. package/dist/server/sentry-config.js +8 -4
  38. package/dist/server/sentry.js +4 -0
  39. package/dist/server/transcribe-voice.d.ts +1 -1
  40. package/dist/tracking/registry.js +8 -6
  41. package/dist/vite/client.js +5 -0
  42. package/docs/content/deployment.mdx +19 -5
  43. package/package.json +1 -1
@@ -18,6 +18,8 @@ export declare function trySaveYDocState(docId: string, state: Uint8Array, textS
18
18
  export declare function saveYDocState(docId: string, state: Uint8Array, textSnapshot: string): Promise<void>;
19
19
  /** Check if a document has collaborative state. */
20
20
  export declare function hasCollabState(docId: string): Promise<boolean>;
21
+ /** Load all existing document ids in one query for startup reconciliation. */
22
+ export declare function listCollabDocIds(): Promise<Set<string>>;
21
23
  /** Delete collaborative state for a document. */
22
24
  export declare function deleteCollabState(docId: string): Promise<void>;
23
25
  declare function uint8ArrayToBase64(arr: Uint8Array): string;
@@ -148,6 +148,13 @@ export async function hasCollabState(docId) {
148
148
  });
149
149
  return rows.length > 0;
150
150
  }
151
+ /** Load all existing document ids in one query for startup reconciliation. */
152
+ export async function listCollabDocIds() {
153
+ await ensureTable();
154
+ const client = getDbExec();
155
+ const { rows } = await client.execute("SELECT doc_id FROM _collab_docs");
156
+ return new Set(rows.flatMap((row) => typeof row.doc_id === "string" && row.doc_id ? [row.doc_id] : []));
157
+ }
151
158
  /** Delete collaborative state for a document. */
152
159
  export async function deleteCollabState(docId) {
153
160
  await ensureTable();
@@ -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
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
package/dist/config.js CHANGED
@@ -374,14 +374,24 @@ export function isAgentNativeDeploymentEnvironment(value) {
374
374
  * never parses process.env itself.
375
375
  */
376
376
  export function inferAgentNativeDeploymentEnvironment(env, mode) {
377
+ const explicit = env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT?.trim().toLowerCase();
378
+ if (explicit && !isAgentNativeDeploymentEnvironment(explicit)) {
379
+ throw new Error('AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT must be "local", "beta", "production", or "preview"');
380
+ }
381
+ if (isAgentNativeDeploymentEnvironment(explicit))
382
+ return explicit;
377
383
  const context = env.CONTEXT?.trim().toLowerCase();
378
384
  const branch = env.BRANCH?.trim().toLowerCase();
379
- if (branch === "production" || context === "production") {
385
+ const vercelEnv = env.VERCEL_ENV?.trim().toLowerCase();
386
+ if (branch === "production" ||
387
+ (context === "production" && branch !== "beta")) {
380
388
  return "production";
381
389
  }
382
390
  if (branch === "beta" || (context === "branch-deploy" && branch === "main")) {
383
391
  return "beta";
384
392
  }
393
+ if (vercelEnv === "preview")
394
+ return "preview";
385
395
  if (context === "deploy-preview" ||
386
396
  context === "branch-deploy" ||
387
397
  branch?.startsWith("deploy-preview")) {
@@ -337,4 +337,4 @@ export declare function resolveNitroBundledYjsEntry(): string;
337
337
  * small set above through the controlled post-build pass.
338
338
  */
339
339
  export declare function nitroNoExternalsForPreset(targetPreset: string): true | readonly string[];
340
- export declare function resolveNitroBuildReplacements(env?: NodeJS.ProcessEnv): Record<string, string>;
340
+ export declare function resolveNitroBuildReplacements(env?: NodeJS.ProcessEnv, deploymentEnvironment?: string): Record<string, string>;
@@ -891,6 +891,77 @@ function isTruthyRuntimeValue(value) {
891
891
  );
892
892
  }
893
893
 
894
+ function normalizeExplicitDeploymentEnvironment(value) {
895
+ const normalized = firstNonEmpty(value)?.toLowerCase();
896
+ if (!normalized) return;
897
+ if (
898
+ normalized !== "local" &&
899
+ normalized !== "beta" &&
900
+ normalized !== "production" &&
901
+ normalized !== "preview"
902
+ ) {
903
+ throw new Error(
904
+ 'AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT must be "local", "beta", "production", or "preview"',
905
+ );
906
+ }
907
+ return normalized;
908
+ }
909
+
910
+ function normalizeFallbackDeploymentEnvironment(value) {
911
+ const normalized = firstNonEmpty(value)?.toLowerCase();
912
+ if (normalized === "development" || normalized === "test") return "local";
913
+ return normalized === "local" ||
914
+ normalized === "beta" ||
915
+ normalized === "production" ||
916
+ normalized === "preview"
917
+ ? normalized
918
+ : undefined;
919
+ }
920
+
921
+ function resolveDeploymentEnvironment() {
922
+ const env = globalThis.process?.env || {};
923
+ const explicit = normalizeExplicitDeploymentEnvironment(
924
+ env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT,
925
+ );
926
+ if (explicit) return explicit;
927
+
928
+ const context = firstNonEmpty(
929
+ typeof env.CONTEXT === "string" ? env.CONTEXT : undefined,
930
+ typeof env.NETLIFY_CONTEXT === "string" ? env.NETLIFY_CONTEXT : undefined,
931
+ typeof env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT === "string"
932
+ ? env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT
933
+ : undefined,
934
+ )?.toLowerCase();
935
+ const branch =
936
+ typeof env.BRANCH === "string" ? env.BRANCH.trim().toLowerCase() : "";
937
+ const vercelEnv = String(env.VERCEL_ENV || "").trim().toLowerCase();
938
+ const sentryEnvironment = firstNonEmpty(env.SENTRY_ENVIRONMENT);
939
+ if (branch === "beta") return "beta";
940
+ if (
941
+ branch === "production" ||
942
+ (context === "production" && branch !== "beta")
943
+ ) {
944
+ return "production";
945
+ }
946
+ if (context === "branch-deploy" && branch === "main") return "beta";
947
+ if (
948
+ context === "deploy-preview" ||
949
+ context === "branch-deploy" ||
950
+ branch.startsWith("deploy-preview") ||
951
+ vercelEnv === "preview"
952
+ ) {
953
+ return "preview";
954
+ }
955
+ if (!context && !branch && !vercelEnv && sentryEnvironment) {
956
+ return normalizeFallbackDeploymentEnvironment(sentryEnvironment) || "production";
957
+ }
958
+ return (
959
+ normalizeFallbackDeploymentEnvironment(firstNonEmpty(context, vercelEnv)) ||
960
+ normalizeFallbackDeploymentEnvironment(env.NODE_ENV) ||
961
+ "production"
962
+ );
963
+ }
964
+
894
965
  function getSentryClientConfigScript() {
895
966
  const env = globalThis.process?.env || {};
896
967
  const key = firstNonEmpty(env.SENTRY_CLIENT_KEY, env.VITE_SENTRY_CLIENT_KEY);
@@ -909,16 +980,15 @@ function getSentryClientConfigScript() {
909
980
  env.VITE_SENTRY_DSN,
910
981
  env.SENTRY_DSN,
911
982
  ) || (key && projectId && host ? "https://" + key + "@" + host + "/" + projectId : undefined);
912
- if (!dsn) return null;
983
+ const deploymentEnvironment = resolveDeploymentEnvironment();
913
984
  const config = {
914
- sentryDsn: dsn,
915
- sentryEnvironment:
916
- firstNonEmpty(
917
- env.SENTRY_ENVIRONMENT,
918
- env.NETLIFY_CONTEXT,
919
- env.VERCEL_ENV,
920
- env.NODE_ENV,
921
- ) || "production",
985
+ ...(dsn
986
+ ? {
987
+ sentryDsn: dsn,
988
+ sentryEnvironment: deploymentEnvironment,
989
+ }
990
+ : {}),
991
+ deploymentEnvironment,
922
992
  };
923
993
  return (
924
994
  '<script data-agent-native-sentry-config>' +
@@ -3742,7 +3812,9 @@ function createBrowserOnlyServerStubPlugin() {
3742
3812
  },
3743
3813
  };
3744
3814
  }
3745
- export function resolveNitroBuildReplacements(env = process.env) {
3815
+ export function resolveNitroBuildReplacements(env = process.env, deploymentEnvironment) {
3816
+ const configuredDeploymentEnvironment = deploymentEnvironment?.trim() ||
3817
+ env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT?.trim();
3746
3818
  return {
3747
3819
  // Netlify exposes DEPLOY_ID only while building. Embed it into the Nitro
3748
3820
  // function so preview OAuth relays can target this immutable deployment
@@ -3755,7 +3827,12 @@ export function resolveNitroBuildReplacements(env = process.env) {
3755
3827
  // a separate server build, so embed release migration ownership here as
3756
3828
  // well as in the Vite server bundle.
3757
3829
  "process.env.AGENT_NATIVE_RELEASE_MIGRATIONS": JSON.stringify(env.AGENT_NATIVE_RELEASE_MIGRATIONS?.trim() || ""),
3758
- "process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT": JSON.stringify(env.CONTEXT?.trim() || ""),
3830
+ "process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT": JSON.stringify(env.CONTEXT?.trim() || env.NETLIFY_CONTEXT?.trim() || ""),
3831
+ ...(configuredDeploymentEnvironment
3832
+ ? {
3833
+ "process.env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT": JSON.stringify(configuredDeploymentEnvironment),
3834
+ }
3835
+ : {}),
3759
3836
  };
3760
3837
  }
3761
3838
  async function buildWithNitro() {
@@ -3841,7 +3918,7 @@ export default bundle;
3841
3918
  virtual: {
3842
3919
  "virtual:agents-bundle": agentsBundleModuleSource,
3843
3920
  },
3844
- replace: resolveNitroBuildReplacements(),
3921
+ replace: resolveNitroBuildReplacements(process.env, nitroAgentConfig.deployment?.environment),
3845
3922
  // Replace browser-only renderers (Excalidraw/Mermaid) with an inert proxy in
3846
3923
  // the server bundle. Without this, Nitro's Rolldown build pulls the real
3847
3924
  // Excalidraw into a shared vendor chunk imported statically by the SSR render
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
- error?: undefined;
21
20
  configured?: undefined;
22
21
  connectPath?: undefined;
23
22
  url: string;
24
23
  id: string;
25
24
  provider: string;
25
+ error?: undefined;
26
26
  }>;
27
27
  export default _default;
@@ -62,6 +62,6 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
62
62
  summary?: undefined;
63
63
  spans?: undefined;
64
64
  id?: undefined;
65
- ok: boolean;
66
65
  error?: undefined;
66
+ ok: boolean;
67
67
  }>>;
@@ -1,3 +1,4 @@
1
+ import { resolveDeployEnvironment } from "../server/deploy-environment.js";
1
2
  function normalizeTrackingSlug(value) {
2
3
  const raw = value?.trim().toLowerCase();
3
4
  if (!raw)
@@ -44,6 +45,7 @@ export function trackingIdentityProperties() {
44
45
  normalizeTrackingSlug(process.env.VITE_APP_TEMPLATE) ||
45
46
  app;
46
47
  return {
48
+ deployment_environment: resolveDeployEnvironment(),
47
49
  ...(app ? { app, agent_native_app: app } : {}),
48
50
  ...(template ? { template, agent_native_template: template } : {}),
49
51
  };
@@ -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
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- ok?: undefined;
38
37
  status?: undefined;
38
+ ok?: undefined;
39
39
  } | {
40
- error?: undefined;
41
40
  ok: boolean;
42
41
  status: string;
42
+ error?: undefined;
43
43
  } | {
44
- ok?: undefined;
45
44
  error: string;
46
45
  removed?: undefined;
46
+ ok?: undefined;
47
47
  } | {
48
- error?: undefined;
49
48
  ok: boolean;
50
49
  removed: boolean;
50
+ error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
- ok?: undefined;
58
57
  error: string;
59
58
  note?: undefined;
59
+ ok?: undefined;
60
60
  } | {
61
- error?: undefined;
62
61
  ok: boolean;
63
62
  note?: undefined;
64
- } | {
65
63
  error?: undefined;
64
+ } | {
66
65
  ok: boolean;
67
66
  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;
99
98
  ok: boolean;
100
99
  key: string;
101
- } | {
102
100
  error?: undefined;
101
+ } | {
103
102
  ok: boolean;
104
103
  removed: boolean;
104
+ error?: undefined;
105
105
  }>>;
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -41,6 +41,8 @@ export interface CollabPluginOptions {
41
41
  idColumn?: string;
42
42
  /** Whether to auto-seed existing documents on startup. Default: true */
43
43
  autoSeed?: boolean;
44
+ /** Map a source-table id to the id used by the collab document store. */
45
+ resolveCollabDocumentId?: (sourceId: string) => string;
44
46
  /**
45
47
  * Callback invoked after a collab update to sync the content column.
46
48
  * If not provided, the plugin auto-syncs using table/contentColumn/idColumn.
@@ -78,4 +80,8 @@ export interface CollabPluginOptions {
78
80
  }
79
81
  export declare function normalizeCollabAccess(options: Pick<CollabPluginOptions, "access" | "resourceType" | "resolveResourceId">): NormalizedCollabAccess;
80
82
  export declare function createCollabPlugin(options?: CollabPluginOptions): NitroPluginDef;
83
+ export declare function selectUnseededCollabRows(rows: ReadonlyArray<Record<string, unknown>>, idColumn: string, existingDocIds: ReadonlySet<string>, resolveCollabDocumentId?: (sourceId: string) => string): Array<{
84
+ row: Record<string, unknown>;
85
+ docId: string;
86
+ }>;
81
87
  export {};
@@ -16,7 +16,7 @@ import { defineEventHandler, getMethod, setResponseStatus, } from "h3";
16
16
  import { postAwareness, getActiveUsers } from "../collab/awareness.js";
17
17
  import { getCollabEmitter } from "../collab/emitter.js";
18
18
  import { getCollabState, postCollabUpdate, postCollabText, postCollabSearchReplace, } from "../collab/routes.js";
19
- import { hasCollabState } from "../collab/storage.js";
19
+ import { listCollabDocIds } from "../collab/storage.js";
20
20
  import { postCollabJson, getCollabJson, postCollabPatch, } from "../collab/struct-routes.js";
21
21
  import { seedFromText, seedFromJson } from "../collab/ydoc-manager.js";
22
22
  import { getDbExec } from "../db/client.js";
@@ -82,6 +82,7 @@ function warnForImplicitAllAuthenticatedAccess(table) {
82
82
  export function createCollabPlugin(options = {}) {
83
83
  const normalizedAccess = normalizeCollabAccess(options);
84
84
  const { table = "documents", contentColumn = "content", idColumn = "id", autoSeed = true, maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES, } = options;
85
+ const resolveCollabDocumentId = options.resolveCollabDocumentId ?? ((sourceId) => sourceId);
85
86
  const resourceType = normalizedAccess.mode === "resource"
86
87
  ? normalizedAccess.resourceType
87
88
  : undefined;
@@ -308,13 +309,14 @@ export function createCollabPlugin(options = {}) {
308
309
  // Run in background so it doesn't block startup
309
310
  setTimeout(async () => {
310
311
  try {
312
+ // Read existing ids once, then apply the plugin's document-id
313
+ // mapping before filtering. A source-table SQL predicate can use
314
+ // the wrong id for integrations such as Analytics' dash-* docs.
315
+ const existingDocIds = await listCollabDocIds();
311
316
  const client = getDbExec();
312
317
  const { rows } = await client.execute(`SELECT ${idColumn}, ${seedColumn} FROM ${table}`);
313
- for (const row of rows) {
314
- const docId = row[idColumn];
315
- const exists = await hasCollabState(docId);
316
- if (exists)
317
- continue;
318
+ for (const { row, docId } of selectUnseededCollabRows(rows, idColumn, existingDocIds, resolveCollabDocumentId)) {
319
+ let seeded = false;
318
320
  if (isJson) {
319
321
  const raw = row[seedColumn] ?? "{}";
320
322
  try {
@@ -323,6 +325,7 @@ export function createCollabPlugin(options = {}) {
323
325
  ? "array"
324
326
  : "map";
325
327
  await seedFromJson(docId, parsed, "data", inferredType);
328
+ seeded = true;
326
329
  }
327
330
  catch {
328
331
  // Invalid JSON — skip
@@ -331,7 +334,10 @@ export function createCollabPlugin(options = {}) {
331
334
  else {
332
335
  const content = row[seedColumn] ?? "";
333
336
  await seedFromText(docId, content);
337
+ seeded = true;
334
338
  }
339
+ if (seeded)
340
+ existingDocIds.add(docId);
335
341
  }
336
342
  }
337
343
  catch {
@@ -341,3 +347,18 @@ export function createCollabPlugin(options = {}) {
341
347
  }
342
348
  };
343
349
  }
350
+ export function selectUnseededCollabRows(rows, idColumn, existingDocIds, resolveCollabDocumentId = (sourceId) => sourceId) {
351
+ const seenDocIds = new Set(existingDocIds);
352
+ const unseeded = [];
353
+ for (const row of rows) {
354
+ const sourceId = String(row[idColumn] ?? "");
355
+ if (!sourceId)
356
+ continue;
357
+ const docId = resolveCollabDocumentId(sourceId);
358
+ if (seenDocIds.has(docId))
359
+ continue;
360
+ seenDocIds.add(docId);
361
+ unseeded.push({ row, docId });
362
+ }
363
+ return unseeded;
364
+ }
@@ -5,7 +5,7 @@
5
5
  * not the vendor, and a second backend that reports errors without them
6
6
  * produces issues nobody can tie to a release.
7
7
  */
8
- /** The deploy environment name, e.g. `production`, `deploy-preview`. */
8
+ /** The deploy environment name, e.g. `production`, `beta`, or `preview`. */
9
9
  export declare function resolveDeployEnvironment(): string;
10
10
  /**
11
11
  * Resolve the agent-native version baked into core's package.json so the
@@ -8,6 +8,7 @@
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { isAgentNativeDeploymentEnvironment } from "../config.js";
11
12
  function firstNonEmpty(...values) {
12
13
  for (const value of values) {
13
14
  const trimmed = value?.trim();
@@ -16,9 +17,46 @@ function firstNonEmpty(...values) {
16
17
  }
17
18
  return undefined;
18
19
  }
19
- /** The deploy environment name, e.g. `production`, `deploy-preview`. */
20
+ function normalizeFallbackEnvironment(value) {
21
+ const normalized = firstNonEmpty(value)?.toLowerCase();
22
+ if (normalized === "development" || normalized === "test")
23
+ return "local";
24
+ return isAgentNativeDeploymentEnvironment(normalized)
25
+ ? normalized
26
+ : undefined;
27
+ }
28
+ /** The deploy environment name, e.g. `production`, `beta`, or `preview`. */
20
29
  export function resolveDeployEnvironment() {
21
- return (firstNonEmpty(process.env.SENTRY_ENVIRONMENT, process.env.NETLIFY_CONTEXT, process.env.VERCEL_ENV, process.env.NODE_ENV) ?? "production");
30
+ const explicit = firstNonEmpty(process.env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT)?.toLowerCase();
31
+ if (explicit) {
32
+ if (!isAgentNativeDeploymentEnvironment(explicit)) {
33
+ throw new Error('AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT must be "local", "beta", "production", or "preview"');
34
+ }
35
+ return explicit;
36
+ }
37
+ const context = firstNonEmpty(process.env.CONTEXT, process.env.NETLIFY_CONTEXT, process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT)?.toLowerCase();
38
+ const branch = process.env.BRANCH?.trim().toLowerCase();
39
+ const vercelEnv = process.env.VERCEL_ENV?.trim().toLowerCase();
40
+ if (branch === "beta")
41
+ return "beta";
42
+ if (branch === "production" ||
43
+ (context === "production" && branch !== "beta")) {
44
+ return "production";
45
+ }
46
+ if (context === "branch-deploy" && branch === "main")
47
+ return "beta";
48
+ if (context === "deploy-preview" ||
49
+ context === "branch-deploy" ||
50
+ branch?.startsWith("deploy-preview") ||
51
+ vercelEnv === "preview") {
52
+ return "preview";
53
+ }
54
+ if (!context && !branch && !vercelEnv) {
55
+ return (normalizeFallbackEnvironment(firstNonEmpty(process.env.SENTRY_ENVIRONMENT, process.env.NODE_ENV)) ?? "production");
56
+ }
57
+ return (normalizeFallbackEnvironment(firstNonEmpty(context, vercelEnv)) ??
58
+ normalizeFallbackEnvironment(process.env.NODE_ENV) ??
59
+ "production");
22
60
  }
23
61
  /**
24
62
  * Resolve the agent-native version baked into core's package.json so the
@@ -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
  }>>;
@@ -28,11 +28,15 @@ export function resolvePublicSentryDsn() {
28
28
  }
29
29
  export function getSentryClientConfigScript() {
30
30
  const dsn = resolvePublicSentryDsn();
31
- if (!dsn)
32
- return null;
31
+ const deploymentEnvironment = resolveDeployEnvironment();
33
32
  const config = {
34
- sentryDsn: dsn,
35
- sentryEnvironment: resolveSentryEnvironment(),
33
+ ...(dsn
34
+ ? {
35
+ sentryDsn: dsn,
36
+ sentryEnvironment: resolveSentryEnvironment(),
37
+ }
38
+ : {}),
39
+ deploymentEnvironment,
36
40
  };
37
41
  return [
38
42
  "<script data-agent-native-sentry-config>",
@@ -62,6 +62,10 @@ export function initServerSentry() {
62
62
  // cookies, or process.env contents to Sentry without explicit consent.
63
63
  sendDefaultPii: false,
64
64
  beforeSend(event) {
65
+ event.tags = {
66
+ ...event.tags,
67
+ deployment_environment: resolveDeployEnvironment(),
68
+ };
65
69
  // Drop rules live in `error-noise-filter.ts` so every backend applies
66
70
  // the same ones — they describe which server errors are non-bugs in
67
71
  // this framework, not a Sentry preference.
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -1,3 +1,4 @@
1
+ import { resolveDeployEnvironment } from "../server/deploy-environment.js";
1
2
  import { getRequestContext } from "../server/request-context.js";
2
3
  import { ANALYTICS_CLIENT_PLATFORM_PROPERTY } from "../shared/analytics-platform.js";
3
4
  const REGISTRY_KEY = Symbol.for("@agent-native/core/tracking.registry");
@@ -46,12 +47,13 @@ function resolveTrackingSource(source) {
46
47
  export function track(name, properties, source) {
47
48
  const { userId, anonymousId, sessionId } = resolveTrackingSource(source);
48
49
  const clientPlatform = getRequestContext()?.clientPlatform;
49
- const trackedProperties = clientPlatform
50
- ? {
51
- ...(properties ?? {}),
52
- [ANALYTICS_CLIENT_PLATFORM_PROPERTY]: clientPlatform,
53
- }
54
- : properties;
50
+ const trackedProperties = {
51
+ ...(properties ?? {}),
52
+ deployment_environment: resolveDeployEnvironment(),
53
+ ...(clientPlatform
54
+ ? { [ANALYTICS_CLIENT_PLATFORM_PROPERTY]: clientPlatform }
55
+ : {}),
56
+ };
55
57
  const event = {
56
58
  name,
57
59
  properties: trackedProperties,
@@ -2913,6 +2913,11 @@ function createAgentNativeConfig(options = {}, command, userConfig = {}, mode =
2913
2913
  // netlify.toml environment is available to the build but not injected
2914
2914
  // into deployed Functions, so embed the decision in the server bundle.
2915
2915
  "process.env.AGENT_NATIVE_RELEASE_MIGRATIONS": JSON.stringify(process.env.AGENT_NATIVE_RELEASE_MIGRATIONS?.trim() || ""),
2916
+ ...(resolvedAppConfig.deployment?.environment
2917
+ ? {
2918
+ "process.env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT": JSON.stringify(resolvedAppConfig.deployment.environment),
2919
+ }
2920
+ : {}),
2916
2921
  __AGENT_NATIVE_BUILD_GTM_CONTAINER_ID__: JSON.stringify(process.env.GTM_CONTAINER_ID?.trim() || ""),
2917
2922
  "process.env.AGENT_NATIVE_BUILD_GTM_CONTAINER_ID": JSON.stringify(process.env.GTM_CONTAINER_ID?.trim() || ""),
2918
2923
  // Framework route warmup controls how SSR `.data` routes are fetched:
@@ -26,11 +26,11 @@ pnpm install
26
26
 
27
27
  The install step creates the lockfile that the production Dockerfile below copies into its build stage.
28
28
 
29
- ### 2. Add a local Compose file
29
+ ### 2. Add the local Docker files
30
30
 
31
- Create `docker-compose.yml` in the app root. It uses the [Dockerfile](#docker) in the next section and starts Postgres only on the private Compose network:
31
+ Create `Dockerfile`, `.dockerignore`, and `docker-compose.yml` in the app root. `docker compose up --build` needs the Dockerfile, and `.dockerignore` keeps host-only files such as `node_modules` out of the Linux build context. Compose starts Postgres only on the private Compose network.
32
32
 
33
- You can also download the [Dockerfile](https://www.agent-native.com/examples/self-hosted-chat/Dockerfile), [Compose file](https://www.agent-native.com/examples/self-hosted-chat/docker-compose.yml), and [environment template](https://www.agent-native.com/examples/self-hosted-chat/env.example) as a starter fixture.
33
+ You can also download the [Dockerfile](https://www.agent-native.com/examples/self-hosted-chat/Dockerfile), [.dockerignore](https://www.agent-native.com/examples/self-hosted-chat/.dockerignore), [Compose file](https://www.agent-native.com/examples/self-hosted-chat/docker-compose.yml), and [environment template](https://www.agent-native.com/examples/self-hosted-chat/env.example) as a starter fixture. The [Dockerfile](#docker) below matches the fixture.
34
34
 
35
35
  ```yaml filename="docker-compose.yml" maxLines=40
36
36
  services:
@@ -53,7 +53,7 @@ services:
53
53
  POSTGRES_USER: agent_native
54
54
  POSTGRES_HOST_AUTH_METHOD: trust
55
55
  volumes:
56
- - postgres-data:/var/lib/postgresql
56
+ - postgres-data-v18:/var/lib/postgresql
57
57
  healthcheck:
58
58
  test: ["CMD-SHELL", "pg_isready -U agent_native -d agent_native"]
59
59
  interval: 5s
@@ -61,9 +61,11 @@ services:
61
61
  retries: 10
62
62
 
63
63
  volumes:
64
- postgres-data:
64
+ postgres-data-v18:
65
65
  ```
66
66
 
67
+ PostgreSQL 18 uses `/var/lib/postgresql` as its volume mount path, unlike PostgreSQL 17 and earlier. The versioned volume name keeps this fresh fixture separate from older local stacks. If you are upgrading an existing PostgreSQL 17 or earlier stack, back up and migrate its data before changing the image or mount path. Do not point this fixture at the old volume without a migration.
68
+
67
69
  ### 3. Run it
68
70
 
69
71
  ```bash
@@ -322,6 +324,18 @@ EXPOSE 3000
322
324
  CMD ["node", ".output/server/index.mjs"]
323
325
  ```
324
326
 
327
+ Create `.dockerignore` alongside the Dockerfile:
328
+
329
+ ```text filename=".dockerignore"
330
+ node_modules
331
+ .output
332
+ data
333
+ .env
334
+ .env.*
335
+ !.env.example
336
+ .git
337
+ ```
338
+
325
339
  ### Self-hosted workspace with Docker Compose {#workspace-docker-compose}
326
340
 
327
341
  The Dockerfile above is for one standalone app. A workspace is not one Nitro
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.163.0",
3
+ "version": "0.163.2",
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": {