@agent-native/core 0.168.1 → 0.168.3
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 +1 -1
- package/corpus/templates/analytics/netlify.toml +4 -4
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +32 -5
- package/corpus/templates/analytics/server/lib/scoped-settings.ts +2 -3
- package/corpus/templates/assets/netlify.toml +4 -1
- package/corpus/templates/brain/netlify.toml +4 -1
- package/corpus/templates/calendar/app/root.tsx +2 -0
- package/corpus/templates/calendar/netlify.toml +4 -1
- package/corpus/templates/chat/netlify.toml +4 -1
- package/corpus/templates/clips/desktop/src/app.tsx +4 -8
- package/corpus/templates/clips/netlify.toml +4 -1
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +17 -11
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -5
- package/corpus/templates/content/netlify.toml +4 -1
- package/corpus/templates/crm/netlify.toml +6 -1
- package/corpus/templates/design/app/root.tsx +5 -1
- package/corpus/templates/design/netlify.toml +4 -1
- package/corpus/templates/dispatch/netlify.toml +4 -1
- package/corpus/templates/factory/netlify.toml +4 -1
- package/corpus/templates/forms/netlify.toml +4 -1
- package/corpus/templates/macros/netlify.toml +3 -3
- package/corpus/templates/mail/actions/view-screen.ts +80 -8
- package/corpus/templates/mail/app/components/email/EmailList.tsx +3 -1
- package/corpus/templates/mail/app/components/email/EmailListItem.tsx +1 -35
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +16 -8
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +146 -87
- package/corpus/templates/mail/app/global.css +21 -0
- package/corpus/templates/mail/app/hooks/use-emails.ts +6 -0
- package/corpus/templates/mail/app/hooks/use-navigation-state.ts +2 -0
- package/corpus/templates/mail/app/lib/inbox-tabs.ts +5 -0
- package/corpus/templates/mail/app/lib/label-colors.ts +47 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +25 -8
- package/corpus/templates/mail/netlify.toml +4 -1
- package/corpus/templates/mail/server/handlers/emails.ts +27 -2
- package/corpus/templates/plan/netlify.toml +4 -1
- package/corpus/templates/slides/netlify.toml +4 -1
- package/corpus/templates/tasks/netlify.toml +6 -1
- package/dist/app-config/migration.d.ts +5 -0
- package/dist/app-config/migration.js +11 -0
- package/dist/app-config/schema.d.ts +4 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/app-config/store.d.ts +8 -0
- package/dist/app-config/store.js +19 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/migrations.js +10 -2
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/netlify-static-headers.d.ts +3 -0
- package/dist/deploy/netlify-static-headers.js +60 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +17 -17
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.js +1 -1
- package/dist/settings/org-settings.js +7 -3
- package/dist/templates/chat/netlify.toml +4 -1
- package/dist/vite/client.js +2 -0
- package/package.json +2 -1
- package/src/templates/chat/netlify.toml +4 -1
|
@@ -43,6 +43,20 @@
|
|
|
43
43
|
--mail-toast-surface: hsl(var(--card));
|
|
44
44
|
--mail-toast-border: hsl(var(--border));
|
|
45
45
|
--mail-overlay-scrim: hsl(0 0% 0% / 0.2);
|
|
46
|
+
--mail-label-automated-bg: 330 81% 60% / 0.2;
|
|
47
|
+
--mail-label-automated-text: 336 69% 36%;
|
|
48
|
+
--mail-label-social-bg: 217 91% 60% / 0.2;
|
|
49
|
+
--mail-label-social-text: 224 76% 48%;
|
|
50
|
+
--mail-label-updates-bg: 45 93% 47% / 0.2;
|
|
51
|
+
--mail-label-updates-text: 32 95% 44%;
|
|
52
|
+
--mail-label-promotions-bg: 142 71% 45% / 0.2;
|
|
53
|
+
--mail-label-promotions-text: 142 72% 29%;
|
|
54
|
+
--mail-label-forums-bg: 199 89% 48% / 0.2;
|
|
55
|
+
--mail-label-forums-text: 201 96% 32%;
|
|
56
|
+
--mail-label-finance-bg: 160 84% 39% / 0.2;
|
|
57
|
+
--mail-label-finance-text: 163 94% 24%;
|
|
58
|
+
--mail-label-travel-bg: 187 85% 53% / 0.2;
|
|
59
|
+
--mail-label-travel-text: 192 91% 36%;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
.dark {
|
|
@@ -81,6 +95,13 @@
|
|
|
81
95
|
--mail-toast-surface: hsl(0 0% 15%);
|
|
82
96
|
--mail-toast-border: hsl(0 0% 26%);
|
|
83
97
|
--mail-overlay-scrim: hsl(0 0% 6% / 0.32);
|
|
98
|
+
--mail-label-automated-text: 336 75% 75%;
|
|
99
|
+
--mail-label-social-text: 213 94% 68%;
|
|
100
|
+
--mail-label-updates-text: 48 96% 64%;
|
|
101
|
+
--mail-label-promotions-text: 141 76% 73%;
|
|
102
|
+
--mail-label-forums-text: 199 95% 74%;
|
|
103
|
+
--mail-label-finance-text: 152 76% 70%;
|
|
104
|
+
--mail-label-travel-text: 188 86% 74%;
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
@layer base {
|
|
@@ -1453,10 +1453,16 @@ export function useContacts() {
|
|
|
1453
1453
|
|
|
1454
1454
|
// ─── Labels ──────────────────────────────────────────────────────────────────
|
|
1455
1455
|
|
|
1456
|
+
export const EMPTY_LABELS: Label[] = [];
|
|
1457
|
+
|
|
1456
1458
|
export function useLabels() {
|
|
1457
1459
|
return useQuery<Label[]>({
|
|
1458
1460
|
queryKey: ["labels"],
|
|
1459
1461
|
queryFn: () => apiFetch("/api/labels"),
|
|
1462
|
+
// A failed background refresh must not erase the last complete label map.
|
|
1463
|
+
// The layout still surfaces isError so an initial failure has an explicit
|
|
1464
|
+
// retry path instead of looking like an empty mailbox.
|
|
1465
|
+
placeholderData: (previousData) => previousData,
|
|
1460
1466
|
staleTime: 60_000,
|
|
1461
1467
|
});
|
|
1462
1468
|
}
|
|
@@ -24,6 +24,11 @@ export const COLLAPSIBLE_VIEW_IDS = [
|
|
|
24
24
|
"trash",
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
+
// Keep the synthetic remainder tab's UI identity separate from user-label IDs.
|
|
28
|
+
// Its public URL remains `tab=other` for existing links and agent commands.
|
|
29
|
+
export const OTHER_INBOX_TAB_ID = "__inbox_other__";
|
|
30
|
+
export const OTHER_INBOX_TAB_PARAM = "other";
|
|
31
|
+
|
|
27
32
|
/** Pinned labels include a virtual "important" tab when Google is connected. */
|
|
28
33
|
export function resolvePinnedLabels(
|
|
29
34
|
userPinnedLabels: readonly string[],
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type LabelStyle = {
|
|
2
|
+
bg: string;
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const labelColors: Record<string, LabelStyle> = {
|
|
7
|
+
automated: {
|
|
8
|
+
bg: "bg-[hsl(var(--mail-label-automated-bg))]",
|
|
9
|
+
text: "text-[hsl(var(--mail-label-automated-text))]",
|
|
10
|
+
},
|
|
11
|
+
social: {
|
|
12
|
+
bg: "bg-[hsl(var(--mail-label-social-bg))]",
|
|
13
|
+
text: "text-[hsl(var(--mail-label-social-text))]",
|
|
14
|
+
},
|
|
15
|
+
updates: {
|
|
16
|
+
bg: "bg-[hsl(var(--mail-label-updates-bg))]",
|
|
17
|
+
text: "text-[hsl(var(--mail-label-updates-text))]",
|
|
18
|
+
},
|
|
19
|
+
promotions: {
|
|
20
|
+
bg: "bg-[hsl(var(--mail-label-promotions-bg))]",
|
|
21
|
+
text: "text-[hsl(var(--mail-label-promotions-text))]",
|
|
22
|
+
},
|
|
23
|
+
forums: {
|
|
24
|
+
bg: "bg-[hsl(var(--mail-label-forums-bg))]",
|
|
25
|
+
text: "text-[hsl(var(--mail-label-forums-text))]",
|
|
26
|
+
},
|
|
27
|
+
finance: {
|
|
28
|
+
bg: "bg-[hsl(var(--mail-label-finance-bg))]",
|
|
29
|
+
text: "text-[hsl(var(--mail-label-finance-text))]",
|
|
30
|
+
},
|
|
31
|
+
travel: {
|
|
32
|
+
bg: "bg-[hsl(var(--mail-label-travel-bg))]",
|
|
33
|
+
text: "text-[hsl(var(--mail-label-travel-text))]",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function getLabelStyle(labelId: string): LabelStyle {
|
|
38
|
+
const normalized = labelId.toLowerCase().replace(/^label:/, "");
|
|
39
|
+
if (labelColors[normalized]) return labelColors[normalized];
|
|
40
|
+
|
|
41
|
+
let hash = 0;
|
|
42
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
43
|
+
hash = normalized.charCodeAt(i) + ((hash << 5) - hash);
|
|
44
|
+
}
|
|
45
|
+
const options = Object.values(labelColors);
|
|
46
|
+
return options[Math.abs(hash) % options.length];
|
|
47
|
+
}
|
|
@@ -23,6 +23,7 @@ import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
|
|
23
23
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
24
24
|
import { useNavigationState } from "@/hooks/use-navigation-state";
|
|
25
25
|
import {
|
|
26
|
+
OTHER_INBOX_TAB_PARAM,
|
|
26
27
|
resolvePinnedLabels,
|
|
27
28
|
pinnedTriageLabels,
|
|
28
29
|
augmentSelfSentLabels,
|
|
@@ -260,6 +261,7 @@ export function InboxPage() {
|
|
|
260
261
|
const { data: settings } = useSettings();
|
|
261
262
|
const [searchParams] = useSearchParams();
|
|
262
263
|
const activeLabel = searchParams.get("label");
|
|
264
|
+
const activeInboxTab = searchParams.get("tab");
|
|
263
265
|
const routeSearchSuffix = searchParams.toString()
|
|
264
266
|
? `?${searchParams.toString()}`
|
|
265
267
|
: "";
|
|
@@ -304,6 +306,10 @@ export function InboxPage() {
|
|
|
304
306
|
view === "inbox" &&
|
|
305
307
|
mailLabelsInclude(triageLabels, activeLabel);
|
|
306
308
|
const clientSliceTab = isPinnedTab && !searchQuery;
|
|
309
|
+
const isOtherTab =
|
|
310
|
+
view === "inbox" &&
|
|
311
|
+
activeInboxTab === OTHER_INBOX_TAB_PARAM &&
|
|
312
|
+
!searchQuery;
|
|
307
313
|
const effectiveLabel = clientSliceTab
|
|
308
314
|
? undefined
|
|
309
315
|
: (activeLabel ?? undefined);
|
|
@@ -349,12 +355,7 @@ export function InboxPage() {
|
|
|
349
355
|
return filterInboxTabEmails(filtered, activeLabel, pinnedLabels);
|
|
350
356
|
}
|
|
351
357
|
// "Other" tab — the inbox remainder, same partition as its badge.
|
|
352
|
-
if (
|
|
353
|
-
!searchQuery &&
|
|
354
|
-
view === "inbox" &&
|
|
355
|
-
!activeLabel &&
|
|
356
|
-
triageLabels.length > 0
|
|
357
|
-
) {
|
|
358
|
+
if (isOtherTab) {
|
|
358
359
|
return filterInboxTabEmails(filtered, null, pinnedLabels);
|
|
359
360
|
}
|
|
360
361
|
|
|
@@ -407,6 +408,7 @@ export function InboxPage() {
|
|
|
407
408
|
view,
|
|
408
409
|
searchQuery,
|
|
409
410
|
activeLabel,
|
|
411
|
+
isOtherTab,
|
|
410
412
|
clientSliceTab,
|
|
411
413
|
pinnedLabels,
|
|
412
414
|
triageLabels,
|
|
@@ -419,7 +421,10 @@ export function InboxPage() {
|
|
|
419
421
|
// Clear multi-selection when switching views or label tabs. Do NOT clear on
|
|
420
422
|
// threadId changes — shift+j/k in detail view navigates between threads while
|
|
421
423
|
// extending the selection, so selection must persist across thread nav.
|
|
422
|
-
useEffect(
|
|
424
|
+
useEffect(
|
|
425
|
+
() => setSelectedIds(new Set()),
|
|
426
|
+
[view, activeLabel, activeInboxTab],
|
|
427
|
+
);
|
|
423
428
|
|
|
424
429
|
// Sync current navigation state to file (write-only, so agent can read it)
|
|
425
430
|
const searchQ = searchParams.get("q") ?? undefined;
|
|
@@ -430,10 +435,22 @@ export function InboxPage() {
|
|
|
430
435
|
focusedEmailId: focusedId ?? undefined,
|
|
431
436
|
search: searchQ,
|
|
432
437
|
label: activeLabel ?? undefined,
|
|
438
|
+
activeInboxTab: activeInboxTab ?? undefined,
|
|
439
|
+
activeAccounts:
|
|
440
|
+
activeAccounts.size > 0 ? Array.from(activeAccounts) : undefined,
|
|
433
441
|
selectedThreadIds:
|
|
434
442
|
selectedThreadIds.length > 0 ? selectedThreadIds : undefined,
|
|
435
443
|
});
|
|
436
|
-
}, [
|
|
444
|
+
}, [
|
|
445
|
+
view,
|
|
446
|
+
threadId,
|
|
447
|
+
focusedId,
|
|
448
|
+
searchQ,
|
|
449
|
+
activeLabel,
|
|
450
|
+
activeInboxTab,
|
|
451
|
+
activeAccounts,
|
|
452
|
+
selectedThreadIds,
|
|
453
|
+
]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
437
454
|
|
|
438
455
|
// One-shot agent navigation: agent writes navigate.json, UI reads it, navigates, deletes it
|
|
439
456
|
const { data: navCommand } = navState.command;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build]
|
|
2
2
|
ignore = "node ./scripts/netlify-ignore-build.mjs mail"
|
|
3
|
-
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && DATABASE_URL=${NETLIFY_DATABASE_URL_UNPOOLED:-$DATABASE_URL} NITRO_PRESET=netlify pnpm --filter mail build && if [ \"${CONTEXT:-}\" = \"production\" ]; then DATABASE_URL=${NETLIFY_DATABASE_URL_UNPOOLED:-$DATABASE_URL} pnpm --filter mail migrate:production; fi"
|
|
3
|
+
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && DATABASE_URL=${NETLIFY_DATABASE_URL_UNPOOLED:-$DATABASE_URL} NITRO_PRESET=netlify pnpm --filter mail build && if [ \"${CONTEXT:-}\" = \"production\" ] || [ \"${AGENT_NATIVE_RUN_RELEASE_MIGRATIONS:-}\" = \"1\" ]; then DATABASE_URL=${NETLIFY_DATABASE_URL_UNPOOLED:-$DATABASE_URL} pnpm --filter mail migrate:production; fi"
|
|
4
4
|
publish = "templates/mail/dist"
|
|
5
5
|
functions = "templates/mail/.netlify/functions-internal"
|
|
6
6
|
|
|
@@ -14,6 +14,9 @@ NPM_CONFIG_PRODUCTION = "false"
|
|
|
14
14
|
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
15
15
|
AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
|
|
16
16
|
AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
|
|
17
|
+
# Production release migrations own schema setup; request functions must skip
|
|
18
|
+
# migration probes on every serverless invocation.
|
|
19
|
+
AGENT_NATIVE_RELEASE_MIGRATIONS = "1"
|
|
17
20
|
|
|
18
21
|
# A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
|
|
19
22
|
# The framework does not impose a default agent-run timeout; if a template sets
|
|
@@ -1743,10 +1743,13 @@ export const listLabels = defineEventHandler(async (_event: H3Event) => {
|
|
|
1743
1743
|
totalCount: number;
|
|
1744
1744
|
}
|
|
1745
1745
|
>();
|
|
1746
|
+
let successfulAccountReads = 0;
|
|
1747
|
+
let failedAccountReads = 0;
|
|
1746
1748
|
// Fetch labels from each account sequentially to avoid race conditions on the shared map
|
|
1747
1749
|
for (const { accessToken } of accountTokens) {
|
|
1748
1750
|
try {
|
|
1749
1751
|
const res = await gmailListLabels(accessToken);
|
|
1752
|
+
successfulAccountReads += 1;
|
|
1750
1753
|
for (const label of res.labels || []) {
|
|
1751
1754
|
if (!label.id || !label.name) continue;
|
|
1752
1755
|
const gmailId = label.id;
|
|
@@ -1791,7 +1794,25 @@ export const listLabels = defineEventHandler(async (_event: H3Event) => {
|
|
|
1791
1794
|
});
|
|
1792
1795
|
}
|
|
1793
1796
|
}
|
|
1794
|
-
} catch {
|
|
1797
|
+
} catch {
|
|
1798
|
+
// A partial label map is not safe to return because it drops labels
|
|
1799
|
+
// from accounts that failed and makes counts look authoritative.
|
|
1800
|
+
failedAccountReads += 1;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (failedAccountReads > 0) {
|
|
1804
|
+
console.warn(
|
|
1805
|
+
`[listLabels] ${failedAccountReads} Gmail account label read(s) failed`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
if (
|
|
1809
|
+
accountTokens.length === 0 ||
|
|
1810
|
+
successfulAccountReads === 0 ||
|
|
1811
|
+
failedAccountReads > 0
|
|
1812
|
+
) {
|
|
1813
|
+
console.error("[listLabels] Gmail label fetch failed");
|
|
1814
|
+
setResponseStatus(_event, 502);
|
|
1815
|
+
return { error: "Unable to load Gmail labels. Please retry." };
|
|
1795
1816
|
}
|
|
1796
1817
|
const labels: Label[] = Array.from(labelMap.values());
|
|
1797
1818
|
|
|
@@ -1821,7 +1842,11 @@ export const listLabels = defineEventHandler(async (_event: H3Event) => {
|
|
|
1821
1842
|
}
|
|
1822
1843
|
|
|
1823
1844
|
return labels;
|
|
1824
|
-
} catch {
|
|
1845
|
+
} catch {
|
|
1846
|
+
console.error("[listLabels] Gmail label request failed");
|
|
1847
|
+
setResponseStatus(_event, 502);
|
|
1848
|
+
return { error: "Unable to load Gmail labels. Please retry." };
|
|
1849
|
+
}
|
|
1825
1850
|
}
|
|
1826
1851
|
return readLabels(email);
|
|
1827
1852
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build]
|
|
2
2
|
ignore = "node ./scripts/netlify-ignore-build.mjs plan"
|
|
3
|
-
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter plan build && if [ \"${CONTEXT:-}\" = \"production\" ]; then pnpm --filter plan migrate:production; fi"
|
|
3
|
+
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter plan build && if [ \"${CONTEXT:-}\" = \"production\" ] || [ \"${AGENT_NATIVE_RUN_RELEASE_MIGRATIONS:-}\" = \"1\" ]; then pnpm --filter plan migrate:production; fi"
|
|
4
4
|
publish = "templates/plan/dist"
|
|
5
5
|
functions = "templates/plan/.netlify/functions-internal"
|
|
6
6
|
|
|
@@ -15,6 +15,9 @@ AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
|
15
15
|
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
16
16
|
AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
|
|
17
17
|
AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
|
|
18
|
+
# Production release migrations own schema setup; request functions must skip
|
|
19
|
+
# migration probes on every serverless invocation.
|
|
20
|
+
AGENT_NATIVE_RELEASE_MIGRATIONS = "1"
|
|
18
21
|
|
|
19
22
|
# A2A/MCP calls may include plan generation, artifact serialization, and
|
|
20
23
|
# feedback polling. Keep this aligned with other agent-heavy hosted templates.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
ignore = "node ./scripts/netlify-ignore-build.mjs slides"
|
|
3
3
|
# Rebuild Core explicitly so a cached Netlify install cannot bundle an older
|
|
4
4
|
# agent loop or A2A handler into Slides' server functions.
|
|
5
|
-
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && pnpm --filter @agent-native/core build && NITRO_PRESET=netlify pnpm --filter slides build && if [ \"${CONTEXT:-}\" = \"production\" ]; then pnpm --filter slides migrate:production; fi"
|
|
5
|
+
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && pnpm --filter @agent-native/core build && NITRO_PRESET=netlify pnpm --filter slides build && if [ \"${CONTEXT:-}\" = \"production\" ] || [ \"${AGENT_NATIVE_RUN_RELEASE_MIGRATIONS:-}\" = \"1\" ]; then pnpm --filter slides migrate:production; fi"
|
|
6
6
|
publish = "templates/slides/dist"
|
|
7
7
|
functions = "templates/slides/.netlify/functions-internal"
|
|
8
8
|
|
|
@@ -18,6 +18,9 @@ AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
|
18
18
|
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
19
19
|
AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
|
|
20
20
|
AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
|
|
21
|
+
# Production release migrations own schema setup; request functions must skip
|
|
22
|
+
# migration probes on every serverless invocation.
|
|
23
|
+
AGENT_NATIVE_RELEASE_MIGRATIONS = "1"
|
|
21
24
|
|
|
22
25
|
# Function wall for the non-agent long-request paths (deck import/export,
|
|
23
26
|
# image generation). Agent chat opts into the separate Netlify background
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build]
|
|
2
2
|
ignore = "node ./scripts/netlify-ignore-build.mjs tasks"
|
|
3
|
-
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter tasks build && if [ \"${CONTEXT:-}\" = \"production\" ]; then pnpm --filter tasks migrate:production; fi"
|
|
3
|
+
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter tasks build && if [ \"${CONTEXT:-}\" = \"production\" ] || [ \"${AGENT_NATIVE_RUN_RELEASE_MIGRATIONS:-}\" = \"1\" ]; then pnpm --filter tasks migrate:production; fi"
|
|
4
4
|
publish = "templates/tasks/dist"
|
|
5
5
|
functions = "templates/tasks/.netlify/functions-internal"
|
|
6
6
|
|
|
@@ -9,3 +9,8 @@ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
|
|
|
9
9
|
GTM_CONTAINER_ID = "GTM-N3WSTXZ"
|
|
10
10
|
NITRO_PRESET = "netlify"
|
|
11
11
|
NPM_CONFIG_PRODUCTION = "false"
|
|
12
|
+
|
|
13
|
+
[context.production.environment]
|
|
14
|
+
# Production release migrations own schema setup; request functions must skip
|
|
15
|
+
# migration probes on every serverless invocation.
|
|
16
|
+
AGENT_NATIVE_RELEASE_MIGRATIONS = "1"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const migrationConfig = z.object({
|
|
3
|
+
releaseMigrations: z.boolean().default(false).meta({
|
|
4
|
+
env: "AGENT_NATIVE_RELEASE_MIGRATIONS",
|
|
5
|
+
doc: "Treat database migrations as release-owned so request runtimes only probe an already-prepared schema.",
|
|
6
|
+
}),
|
|
7
|
+
betaSchemaOwner: z.string().trim().min(1).optional().meta({
|
|
8
|
+
env: "AGENT_NATIVE_BETA_SCHEMA_OWNER",
|
|
9
|
+
doc: "Schema owner marker embedded in a prebuilt beta server bundle.",
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
@@ -40,6 +40,10 @@ export declare const appConfigSchema: z.ZodObject<{
|
|
|
40
40
|
integrations: z.ZodPrefault<z.ZodObject<{
|
|
41
41
|
allowUnverifiedWebhooks: z.ZodDefault<z.ZodBoolean>;
|
|
42
42
|
}, z.core.$strip>>;
|
|
43
|
+
migration: z.ZodPrefault<z.ZodObject<{
|
|
44
|
+
releaseMigrations: z.ZodDefault<z.ZodBoolean>;
|
|
45
|
+
betaSchemaOwner: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>>;
|
|
43
47
|
privateBlob: z.ZodPrefault<z.ZodObject<{
|
|
44
48
|
provider: z.ZodOptional<z.ZodString>;
|
|
45
49
|
publicUploadFallback: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -4,6 +4,7 @@ import { agentConfig } from "./agent.js";
|
|
|
4
4
|
import { appConfig } from "./app.js";
|
|
5
5
|
import { authConfig } from "./auth.js";
|
|
6
6
|
import { integrationsConfig } from "./integrations.js";
|
|
7
|
+
import { migrationConfig } from "./migration.js";
|
|
7
8
|
import { privateBlobConfig } from "./private-blob.js";
|
|
8
9
|
import { workspaceConfig } from "./workspace.js";
|
|
9
10
|
/**
|
|
@@ -28,6 +29,7 @@ export const appConfigSchema = z.object({
|
|
|
28
29
|
app: appConfig.prefault({}),
|
|
29
30
|
auth: authConfig.prefault({}),
|
|
30
31
|
integrations: integrationsConfig.prefault({}),
|
|
32
|
+
migration: migrationConfig.prefault({}),
|
|
31
33
|
privateBlob: privateBlobConfig.prefault({}),
|
|
32
34
|
workspace: workspaceConfig.prefault({}),
|
|
33
35
|
});
|
|
@@ -8,6 +8,14 @@ import { type AppConfig, type AppConfigInput } from "./schema.js";
|
|
|
8
8
|
*/
|
|
9
9
|
declare const LAYER_ORDER: readonly ["env", "legacy", "app"];
|
|
10
10
|
export type AppConfigLayer = (typeof LAYER_ORDER)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Nitro embeds build-only deployment markers into direct env reads. Netlify's
|
|
13
|
+
* prebuilt beta lane has those markers while building, but does not copy them
|
|
14
|
+
* into the deployed Function environment. Keep the embedded values as the
|
|
15
|
+
* fallback for this one config boundary so a later dynamic `env[key]` read
|
|
16
|
+
* cannot erase the build decision from the server bundle.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readConfigEnvironment(embedded?: Record<string, string | undefined>): Record<string, string | undefined>;
|
|
11
19
|
/**
|
|
12
20
|
* Writes one layer of the ladder. Framework-internal: the deprecated setters
|
|
13
21
|
* use it to keep working without reintroducing a second namespace.
|
package/dist/app-config/store.js
CHANGED
|
@@ -48,6 +48,24 @@ function resolve(envLayer) {
|
|
|
48
48
|
}
|
|
49
49
|
return appConfigSchema.parse(merged);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Nitro embeds build-only deployment markers into direct env reads. Netlify's
|
|
53
|
+
* prebuilt beta lane has those markers while building, but does not copy them
|
|
54
|
+
* into the deployed Function environment. Keep the embedded values as the
|
|
55
|
+
* fallback for this one config boundary so a later dynamic `env[key]` read
|
|
56
|
+
* cannot erase the build decision from the server bundle.
|
|
57
|
+
*/
|
|
58
|
+
export function readConfigEnvironment(embedded = {
|
|
59
|
+
AGENT_NATIVE_RELEASE_MIGRATIONS: process.env.AGENT_NATIVE_RELEASE_MIGRATIONS,
|
|
60
|
+
AGENT_NATIVE_BETA_SCHEMA_OWNER: process.env.AGENT_NATIVE_BETA_SCHEMA_OWNER,
|
|
61
|
+
}) {
|
|
62
|
+
const env = { ...process.env };
|
|
63
|
+
for (const [key, value] of Object.entries(embedded)) {
|
|
64
|
+
if (!env[key] && value)
|
|
65
|
+
env[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return env;
|
|
68
|
+
}
|
|
51
69
|
/**
|
|
52
70
|
* Writes one layer of the ladder. Framework-internal: the deprecated setters
|
|
53
71
|
* use it to keep working without reintroducing a second namespace.
|
|
@@ -84,7 +102,7 @@ export function defineAppConfig(config) {
|
|
|
84
102
|
* remove.
|
|
85
103
|
*/
|
|
86
104
|
export function getAppConfig() {
|
|
87
|
-
const envLayer = readEnvConfigLayer(appConfigSchema,
|
|
105
|
+
const envLayer = readEnvConfigLayer(appConfigSchema, readConfigEnvironment());
|
|
88
106
|
const signature = JSON.stringify(envLayer);
|
|
89
107
|
if (state.resolved && state.envSignature === signature)
|
|
90
108
|
return state.resolved;
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
-
error?: undefined;
|
|
66
65
|
states: {
|
|
67
66
|
clientId: number;
|
|
68
67
|
state: string;
|
|
69
68
|
}[];
|
|
69
|
+
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
-
error?: undefined;
|
|
81
80
|
users: {
|
|
82
81
|
clientId: number;
|
|
83
82
|
lastSeen: number;
|
|
84
83
|
}[];
|
|
84
|
+
error?: undefined;
|
|
85
85
|
}>>;
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -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;
|
package/dist/db/migrations.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAppConfig } from "../app-config/index.js";
|
|
1
2
|
import { getDbExec, createDbExec, isPostgres, getDialect, getMigrationDatabaseUrl, getCloudflareD1Binding, retrySqliteBusy, } from "./client.js";
|
|
2
3
|
import { isMigrationAuthorizedRuntime } from "./migration-runtime.js";
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -250,8 +251,15 @@ function isServerlessRequestRuntime() {
|
|
|
250
251
|
* migration skipping is safe only when that runner owns schema setup.
|
|
251
252
|
*/
|
|
252
253
|
function appMigratesAtRelease() {
|
|
253
|
-
const
|
|
254
|
-
|
|
254
|
+
const { migration } = getAppConfig();
|
|
255
|
+
if (migration.releaseMigrations)
|
|
256
|
+
return true;
|
|
257
|
+
// Clips beta uses the production-owned schema but its masked prebuilt build
|
|
258
|
+
// must not run migrate:production against the fake build database. The
|
|
259
|
+
// owner marker is embedded into the deployed server bundle and therefore
|
|
260
|
+
// carries the same request-path skip decision without pretending the masked
|
|
261
|
+
// build performed the release migration.
|
|
262
|
+
return migration.betaSchemaOwner?.toLowerCase() === "production";
|
|
255
263
|
}
|
|
256
264
|
/**
|
|
257
265
|
* Run an explicit release-time migration job with migration duty enabled.
|
package/dist/deploy/build.js
CHANGED
|
@@ -34,6 +34,7 @@ import { generateActionRegistryForProject } from "../vite/action-types-plugin.js
|
|
|
34
34
|
import { createAgentNativeConfigContext, loadResolvedAgentNativeConfig, } from "../vite/agent-native-config-loader.js";
|
|
35
35
|
import { cloneServerBundleForFunction, copyDir } from "./function-bundle.js";
|
|
36
36
|
import { collectImmutableAssetPaths, IMMUTABLE_ASSET_CACHE_CONTROL, IMMUTABLE_ASSET_CACHE_HEADERS, prefixAssetPath, } from "./immutable-assets.js";
|
|
37
|
+
import { writeNetlifyStaticHeaders } from "./netlify-static-headers.js";
|
|
37
38
|
import { discoverApiRoutes, discoverPlugins, discoverActionFiles, getMissingDefaultPlugins, DEFAULT_PLUGIN_REGISTRY, } from "./route-discovery.js";
|
|
38
39
|
import { getWorkspaceCoreExports, } from "./workspace-core.js";
|
|
39
40
|
const cwd = process.cwd();
|
|
@@ -3892,6 +3893,7 @@ export function resolveNitroBuildReplacements(env = process.env, deploymentEnvir
|
|
|
3892
3893
|
// a separate server build, so embed release migration ownership here as
|
|
3893
3894
|
// well as in the Vite server bundle.
|
|
3894
3895
|
"process.env.AGENT_NATIVE_RELEASE_MIGRATIONS": JSON.stringify(env.AGENT_NATIVE_RELEASE_MIGRATIONS?.trim() || ""),
|
|
3896
|
+
"process.env.AGENT_NATIVE_BETA_SCHEMA_OWNER": JSON.stringify(env.AGENT_NATIVE_BETA_SCHEMA_OWNER?.trim() || ""),
|
|
3895
3897
|
"process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT": JSON.stringify(env.CONTEXT?.trim() || env.NETLIFY_CONTEXT?.trim() || ""),
|
|
3896
3898
|
// Whether the recurring-jobs scheduled function exists is decided HERE, by
|
|
3897
3899
|
// the build env. `scheduledTriggerAvailability` cannot re-derive it later —
|
|
@@ -4087,6 +4089,10 @@ export default bundle;
|
|
|
4087
4089
|
}
|
|
4088
4090
|
}
|
|
4089
4091
|
writeSingleTemplateNetlifyRedirects(cwd);
|
|
4092
|
+
// React Router prerendered pages bypass the SSR function and are served
|
|
4093
|
+
// directly from Netlify's static backing store. Keep that artifact on the
|
|
4094
|
+
// same public SWR policy as runtime SSR and .data responses.
|
|
4095
|
+
writeNetlifyStaticHeaders(path.join(cwd, "dist"));
|
|
4090
4096
|
assertSingleTemplateNetlifyBuildOutput(cwd);
|
|
4091
4097
|
}
|
|
4092
4098
|
// Resolve remaining bare npm imports by bundling them into _libs/.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const GENERATED_NETLIFY_HEADERS_MARKER = "# Generated by @agent-native/core deploy/netlify-static-headers.ts";
|
|
2
|
+
export declare function renderNetlifyStaticHeaders(env?: Record<string, string | undefined>): string;
|
|
3
|
+
export declare function writeNetlifyStaticHeaders(publishDir: string, env?: Record<string, string | undefined>): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveSsrCacheHeaders } from "../shared/cache-control.js";
|
|
4
|
+
import { IMMUTABLE_ASSET_CACHE_CONTROL } from "./immutable-assets.js";
|
|
5
|
+
export const GENERATED_NETLIFY_HEADERS_MARKER = "# Generated by @agent-native/core deploy/netlify-static-headers.ts";
|
|
6
|
+
const LEGACY_GENERATED_NETLIFY_HEADERS_MARKER = "# Generated by @agent-native/docs scripts/generate-netlify-headers.ts";
|
|
7
|
+
function renderHeaderBlock(pathname, headers) {
|
|
8
|
+
return [
|
|
9
|
+
pathname,
|
|
10
|
+
...headers.map(([name, value]) => ` ${name}: ${value}`),
|
|
11
|
+
].join("\n");
|
|
12
|
+
}
|
|
13
|
+
export function renderNetlifyStaticHeaders(env = process.env) {
|
|
14
|
+
const ssrHeaders = resolveSsrCacheHeaders(env);
|
|
15
|
+
const ssrHeaderEntries = [
|
|
16
|
+
["Cache-Control", ssrHeaders["cache-control"]],
|
|
17
|
+
["CDN-Cache-Control", ssrHeaders["cdn-cache-control"]],
|
|
18
|
+
["Netlify-CDN-Cache-Control", ssrHeaders["netlify-cdn-cache-control"]],
|
|
19
|
+
];
|
|
20
|
+
const immutableAssetHeaders = [
|
|
21
|
+
["Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL],
|
|
22
|
+
["CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL],
|
|
23
|
+
["Netlify-CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL],
|
|
24
|
+
];
|
|
25
|
+
const internalHeaders = [
|
|
26
|
+
["Cache-Control", "no-cache, no-store"],
|
|
27
|
+
["CDN-Cache-Control", "no-cache, no-store"],
|
|
28
|
+
["Netlify-CDN-Cache-Control", "no-cache, no-store"],
|
|
29
|
+
];
|
|
30
|
+
return ([
|
|
31
|
+
GENERATED_NETLIFY_HEADERS_MARKER,
|
|
32
|
+
renderHeaderBlock("/*", ssrHeaderEntries),
|
|
33
|
+
renderHeaderBlock("/assets/*", immutableAssetHeaders),
|
|
34
|
+
renderHeaderBlock("/_agent-native/*", internalHeaders),
|
|
35
|
+
].join("\n\n") + "\n");
|
|
36
|
+
}
|
|
37
|
+
export function writeNetlifyStaticHeaders(publishDir, env = process.env) {
|
|
38
|
+
if (!fs.existsSync(publishDir)) {
|
|
39
|
+
throw new Error(`Netlify publish directory does not exist: ${publishDir}`);
|
|
40
|
+
}
|
|
41
|
+
const headersPath = path.join(publishDir, "_headers");
|
|
42
|
+
const existing = fs.existsSync(headersPath)
|
|
43
|
+
? fs.readFileSync(headersPath, "utf8").trim()
|
|
44
|
+
: "";
|
|
45
|
+
const generated = renderNetlifyStaticHeaders(env).trim();
|
|
46
|
+
const generatedStart = [
|
|
47
|
+
GENERATED_NETLIFY_HEADERS_MARKER,
|
|
48
|
+
LEGACY_GENERATED_NETLIFY_HEADERS_MARKER,
|
|
49
|
+
]
|
|
50
|
+
.map((marker) => existing.indexOf(marker))
|
|
51
|
+
.filter((index) => index >= 0)
|
|
52
|
+
.sort((a, b) => a - b)[0];
|
|
53
|
+
const existingWithoutGenerated = generatedStart === undefined
|
|
54
|
+
? existing
|
|
55
|
+
: existing.slice(0, generatedStart).trimEnd();
|
|
56
|
+
const contents = existingWithoutGenerated
|
|
57
|
+
? `${existingWithoutGenerated}\n\n${generated}`
|
|
58
|
+
: generated;
|
|
59
|
+
fs.writeFileSync(headersPath, `${contents}\n`);
|
|
60
|
+
}
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* DELETE /_agent-native/notifications/:id — delete
|
|
12
12
|
*/
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
|
-
error?: undefined;
|
|
15
14
|
count: number;
|
|
16
15
|
updated?: undefined;
|
|
16
|
+
error?: undefined;
|
|
17
17
|
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
error?: undefined;
|
|
20
19
|
count?: undefined;
|
|
21
20
|
updated: number;
|
|
21
|
+
error?: undefined;
|
|
22
22
|
ok?: undefined;
|
|
23
23
|
} | {
|
|
24
24
|
count?: undefined;
|
|
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
error: string;
|
|
27
27
|
ok?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
error?: undefined;
|
|
30
29
|
count?: undefined;
|
|
31
30
|
updated?: undefined;
|
|
31
|
+
error?: undefined;
|
|
32
32
|
ok: boolean;
|
|
33
33
|
}>>;
|
|
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
error?: undefined;
|
|
45
44
|
summary: import("./types.js").TraceSummary;
|
|
46
45
|
spans: import("./types.js").TraceSpan[];
|
|
47
46
|
id?: undefined;
|
|
47
|
+
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
-
error?: undefined;
|
|
51
50
|
summary?: undefined;
|
|
52
51
|
spans?: undefined;
|
|
53
52
|
id: string;
|
|
53
|
+
error?: undefined;
|
|
54
54
|
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
59
59
|
error: any;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
65
|
+
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|