@agent-native/core 0.174.1 → 0.174.2-nightly-20260825214843
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/forms/.agents/skills/form-building/SKILL.md +4 -0
- package/corpus/templates/forms/actions/create-form.ts +3 -1
- package/corpus/templates/forms/actions/patch-form-fields.ts +12 -3
- package/corpus/templates/forms/actions/update-form.ts +3 -1
- package/corpus/templates/forms/changelog/2026-08-25-forms-agents-retry-invalid-field-edits.md +5 -0
- package/corpus/templates/forms/server/lib/validate-fields.ts +47 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/onboarding-html.js +186 -13
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/vite/client.js +51 -10
- package/docs/content/cloneable-saas.mdx +1 -1
- package/docs/content/environment-variables.mdx +7 -0
- package/docs/content/key-concepts.mdx +2 -0
- package/docs/content/syncing-template-changes.mdx +3 -1
- package/docs/content/what-is-agent-native.mdx +4 -0
- package/docs/content/writing-agent-instructions.mdx +8 -0
- package/package.json +1 -1
- package/docs/content/faq.mdx +0 -257
package/corpus/README.md
CHANGED
|
@@ -68,6 +68,10 @@ Each field is a JSON object:
|
|
|
68
68
|
- `label` — display label
|
|
69
69
|
- `required` — boolean
|
|
70
70
|
|
|
71
|
+
Always pass fields as these complete objects. Never encode a field as shorthand
|
|
72
|
+
text such as `text: Enter a name`; `patch-form-fields` upserts also require the
|
|
73
|
+
field `id` so they cannot be ambiguous.
|
|
74
|
+
|
|
71
75
|
### Optional properties
|
|
72
76
|
- `placeholder` — input placeholder text
|
|
73
77
|
- `description` — help text below the field
|
|
@@ -49,7 +49,9 @@ export default defineAction({
|
|
|
49
49
|
fields: z
|
|
50
50
|
.union([z.string(), z.array(z.any())])
|
|
51
51
|
.optional()
|
|
52
|
-
.describe(
|
|
52
|
+
.describe(
|
|
53
|
+
"Array of complete field objects with id, type, label, and required (or JSON string of the same); never use shorthand strings such as 'text: Enter a name'.",
|
|
54
|
+
),
|
|
53
55
|
settings: z
|
|
54
56
|
.union([z.string(), z.record(z.string(), z.any())])
|
|
55
57
|
.optional()
|
|
@@ -24,7 +24,10 @@ import { z } from "zod";
|
|
|
24
24
|
import { getDb, schema } from "../server/db/index.js";
|
|
25
25
|
import { applyFieldOps } from "../server/lib/merge-fields.js";
|
|
26
26
|
import { invalidatePublicFormCache } from "../server/lib/public-form-ssr.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
assertValidFields,
|
|
29
|
+
normalizePersistedFields,
|
|
30
|
+
} from "../server/lib/validate-fields.js";
|
|
28
31
|
import type { FormField } from "../shared/types.js";
|
|
29
32
|
import { assertPublishableForm } from "./lib/assert-publishable-form.js";
|
|
30
33
|
|
|
@@ -61,7 +64,11 @@ export function withFormLock<T>(
|
|
|
61
64
|
const fieldOpSchema = z.union([
|
|
62
65
|
z.object({
|
|
63
66
|
op: z.literal("upsert"),
|
|
64
|
-
field: z
|
|
67
|
+
field: z
|
|
68
|
+
.record(z.string(), z.any())
|
|
69
|
+
.describe(
|
|
70
|
+
"Complete field object with id, type, label, and required; never use shorthand strings.",
|
|
71
|
+
),
|
|
65
72
|
}),
|
|
66
73
|
z.object({
|
|
67
74
|
op: z.literal("remove"),
|
|
@@ -117,7 +124,9 @@ export default defineAction({
|
|
|
117
124
|
// Parse current fields from the DB row.
|
|
118
125
|
let currentFields: FormField[] = [];
|
|
119
126
|
try {
|
|
120
|
-
currentFields =
|
|
127
|
+
currentFields = normalizePersistedFields(
|
|
128
|
+
JSON.parse(existing.fields),
|
|
129
|
+
) as FormField[];
|
|
121
130
|
} catch {
|
|
122
131
|
currentFields = [];
|
|
123
132
|
}
|
|
@@ -35,7 +35,9 @@ export default defineAction({
|
|
|
35
35
|
fields: z
|
|
36
36
|
.union([z.string(), z.array(z.any())])
|
|
37
37
|
.optional()
|
|
38
|
-
.describe(
|
|
38
|
+
.describe(
|
|
39
|
+
"Array of complete field objects with id, type, label, and required (or JSON string of the same); never use shorthand strings such as 'text: Enter a name'.",
|
|
40
|
+
),
|
|
39
41
|
settings: z
|
|
40
42
|
.union([z.string(), z.record(z.string(), z.any())])
|
|
41
43
|
.optional()
|
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
// runtime — an unrestricted id like `x" onfocus="alert(1)` would otherwise
|
|
5
5
|
// stored-XSS every anonymous submitter of a published form.
|
|
6
6
|
export const FIELD_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
7
|
+
const FIELD_TYPES = new Set([
|
|
8
|
+
"text",
|
|
9
|
+
"email",
|
|
10
|
+
"number",
|
|
11
|
+
"textarea",
|
|
12
|
+
"select",
|
|
13
|
+
"multiselect",
|
|
14
|
+
"checkbox",
|
|
15
|
+
"radio",
|
|
16
|
+
"date",
|
|
17
|
+
"rating",
|
|
18
|
+
"scale",
|
|
19
|
+
]);
|
|
7
20
|
const CONDITIONAL_OPERATORS = new Set(["equals", "not_equals", "contains"]);
|
|
8
21
|
|
|
9
22
|
/**
|
|
@@ -49,6 +62,28 @@ export function normalizeFieldIds(fields: unknown): unknown {
|
|
|
49
62
|
});
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Keeps granular edits compatible with fields written before the current
|
|
67
|
+
* schema. The UI already renders unknown types as text and treats a missing
|
|
68
|
+
* required flag as false, so patch reads must make the same repair before the
|
|
69
|
+
* strict persistence check runs.
|
|
70
|
+
*/
|
|
71
|
+
export function normalizePersistedFields(fields: unknown): unknown {
|
|
72
|
+
if (!Array.isArray(fields)) return fields;
|
|
73
|
+
return fields.map((field) => {
|
|
74
|
+
if (field == null || typeof field !== "object" || Array.isArray(field)) {
|
|
75
|
+
return field;
|
|
76
|
+
}
|
|
77
|
+
const f = field as Record<string, unknown>;
|
|
78
|
+
return {
|
|
79
|
+
...f,
|
|
80
|
+
type:
|
|
81
|
+
typeof f.type === "string" && FIELD_TYPES.has(f.type) ? f.type : "text",
|
|
82
|
+
required: f.required === undefined ? false : f.required,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
52
87
|
export function assertValidFields(fields: unknown): void {
|
|
53
88
|
if (!Array.isArray(fields)) {
|
|
54
89
|
throw new Error("fields must be an array");
|
|
@@ -71,6 +106,18 @@ export function assertValidFields(fields: unknown): void {
|
|
|
71
106
|
}
|
|
72
107
|
seenIds.add(id);
|
|
73
108
|
|
|
109
|
+
if (typeof f.type !== "string" || !FIELD_TYPES.has(f.type)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`field #${idx + 1} has an invalid type ${JSON.stringify(f.type)}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (typeof f.label !== "string") {
|
|
115
|
+
throw new Error(`field #${idx + 1} label must be a string`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof f.required !== "boolean") {
|
|
118
|
+
throw new Error(`field #${idx + 1} required must be a boolean`);
|
|
119
|
+
}
|
|
120
|
+
|
|
74
121
|
const cond = f.conditional;
|
|
75
122
|
if (cond !== undefined) {
|
|
76
123
|
if (cond == null || typeof cond !== "object") {
|
|
@@ -11,6 +11,7 @@ export const FORMS_SYSTEM_PROMPT = `## Agent-Native Forms
|
|
|
11
11
|
You are the in-app agent for Agent-Native Forms, a chat-first form builder and response workspace. Help users create, edit, publish, share, and analyze forms through the registered actions. The actions are the source of truth for database access, validation, permissions, UI sync, and business logic.
|
|
12
12
|
|
|
13
13
|
Core rules:
|
|
14
|
+
- Build every field as a complete object with id, type, label, and required; never send shorthand strings such as "text: Enter a name". If a form or field action rejects its payload, correct the arguments and retry in the same turn, then verify the saved fields. If a draft was created before the failure, repair that draft instead of explaining the fix and stopping.
|
|
14
15
|
- Use Forms actions instead of raw database access. Raw database tools are not available in this app.
|
|
15
16
|
- Never hardcode API keys, tokens, webhook URLs, signing secrets, customer data, or credential-looking literals. Use placeholders or configured integrations.
|
|
16
17
|
- Inspect the current state when it matters. Use \`view-screen\` when the active form, selected field, publish state, response table, or current route is unclear.
|
|
@@ -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;
|
|
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
+
error?: undefined;
|
|
45
|
+
ok?: undefined;
|
|
44
46
|
summary: import("./types.js").TraceSummary;
|
|
45
47
|
spans: import("./types.js").TraceSpan[];
|
|
46
48
|
id?: undefined;
|
|
49
|
+
} | {
|
|
47
50
|
error?: undefined;
|
|
48
51
|
ok?: undefined;
|
|
49
|
-
} | {
|
|
50
52
|
summary?: undefined;
|
|
51
53
|
spans?: undefined;
|
|
52
54
|
id: string;
|
|
53
|
-
error?: undefined;
|
|
54
|
-
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
|
+
ok?: undefined;
|
|
56
57
|
summary?: undefined;
|
|
57
58
|
spans?: undefined;
|
|
58
59
|
id?: undefined;
|
|
59
60
|
error: any;
|
|
60
|
-
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
+
error?: undefined;
|
|
62
63
|
summary?: undefined;
|
|
63
64
|
spans?: undefined;
|
|
64
65
|
id?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
|
-
error?: undefined;
|
|
67
67
|
}>>;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -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
|
-
status?: undefined;
|
|
38
37
|
ok?: undefined;
|
|
38
|
+
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
+
ok?: undefined;
|
|
44
45
|
error: string;
|
|
45
46
|
removed?: undefined;
|
|
46
|
-
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
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;
|
|
57
58
|
error: string;
|
|
58
59
|
note?: undefined;
|
|
59
|
-
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -34,10 +34,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
34
34
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
35
35
|
error: any;
|
|
36
36
|
} | {
|
|
37
|
+
error?: undefined;
|
|
37
38
|
ok: boolean;
|
|
38
39
|
key: string;
|
|
39
40
|
baseUrlKey?: string;
|
|
40
41
|
scope: AgentEngineApiKeyScope;
|
|
41
|
-
error?: undefined;
|
|
42
42
|
}>>;
|
|
43
43
|
export {};
|
|
@@ -2593,7 +2593,10 @@ ${signInJourneyInlineScript()}
|
|
|
2593
2593
|
}
|
|
2594
2594
|
}
|
|
2595
2595
|
function __anFinishOAuthExchange(ret, flowId, sessionToken) {
|
|
2596
|
+
__anStopOAuthPopupWatch();
|
|
2597
|
+
__anStopNativeOAuthAbandonment();
|
|
2596
2598
|
__anGoogleSignInInFlight = false;
|
|
2599
|
+
__anClearGoogleSignInFlow();
|
|
2597
2600
|
__anMagicLinkInFlight = false;
|
|
2598
2601
|
if (__anIsBuilderPreview()) {
|
|
2599
2602
|
if (sessionToken) {
|
|
@@ -2863,8 +2866,18 @@ ${identitySsoScript}
|
|
|
2863
2866
|
return __anIsAgentNativeDesktop() ? 'redirect' : 'popup';
|
|
2864
2867
|
}
|
|
2865
2868
|
var __anOAuthPollTimer = null;
|
|
2869
|
+
var __anOAuthPopupWatchTimer = null;
|
|
2870
|
+
var __anOAuthPopupCloseGraceTimer = null;
|
|
2871
|
+
var __anOAuthPopupCloseGraceMs = 5000;
|
|
2872
|
+
var __anNativeOAuthFlowId = null;
|
|
2873
|
+
var __anNativeOAuthRequestPending = false;
|
|
2874
|
+
var __anNativeOAuthReturnObserved = false;
|
|
2875
|
+
var __anNativeOAuthAbandonGraceTimer = null;
|
|
2876
|
+
var __anNativeOAuthAbandonGraceMs = 5000;
|
|
2866
2877
|
var __anOAuthPollCount = 0;
|
|
2867
2878
|
var __anGoogleSignInInFlight = false;
|
|
2879
|
+
var __anGoogleSignInFlowId = null;
|
|
2880
|
+
var __anGoogleSignInRecoveryFlowId = null;
|
|
2868
2881
|
var __anMagicLinkInFlight = false;
|
|
2869
2882
|
var __anGoogleRecoverBound = false;
|
|
2870
2883
|
function __anNewOAuthFlowId() {
|
|
@@ -2921,8 +2934,139 @@ ${identitySsoScript}
|
|
|
2921
2934
|
__anOAuthPollTimer = null;
|
|
2922
2935
|
}
|
|
2923
2936
|
}
|
|
2937
|
+
function __anStopOAuthPopupWatch() {
|
|
2938
|
+
if (__anOAuthPopupWatchTimer) {
|
|
2939
|
+
clearInterval(__anOAuthPopupWatchTimer);
|
|
2940
|
+
__anOAuthPopupWatchTimer = null;
|
|
2941
|
+
}
|
|
2942
|
+
if (__anOAuthPopupCloseGraceTimer) {
|
|
2943
|
+
clearTimeout(__anOAuthPopupCloseGraceTimer);
|
|
2944
|
+
__anOAuthPopupCloseGraceTimer = null;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
function __anStopNativeOAuthAbandonment() {
|
|
2948
|
+
__anCancelNativeOAuthAbandonment();
|
|
2949
|
+
__anNativeOAuthFlowId = null;
|
|
2950
|
+
__anNativeOAuthRequestPending = false;
|
|
2951
|
+
}
|
|
2952
|
+
function __anCancelNativeOAuthAbandonment() {
|
|
2953
|
+
if (__anNativeOAuthAbandonGraceTimer) {
|
|
2954
|
+
clearTimeout(__anNativeOAuthAbandonGraceTimer);
|
|
2955
|
+
__anNativeOAuthAbandonGraceTimer = null;
|
|
2956
|
+
}
|
|
2957
|
+
__anNativeOAuthReturnObserved = false;
|
|
2958
|
+
}
|
|
2959
|
+
function __anBeginNativeOAuth(flowId) {
|
|
2960
|
+
__anStopNativeOAuthAbandonment();
|
|
2961
|
+
__anNativeOAuthFlowId = flowId;
|
|
2962
|
+
__anNativeOAuthRequestPending = true;
|
|
2963
|
+
__anNativeOAuthReturnObserved = false;
|
|
2964
|
+
}
|
|
2965
|
+
function __anMarkNativeOAuthPolling(flowId) {
|
|
2966
|
+
if (__anNativeOAuthFlowId !== flowId) return;
|
|
2967
|
+
__anNativeOAuthRequestPending = false;
|
|
2968
|
+
}
|
|
2969
|
+
function __anIsCurrentNativeOAuth(flowId) {
|
|
2970
|
+
return __anNativeOAuthFlowId === flowId && __anIsCurrentGoogleSignInFlow(flowId);
|
|
2971
|
+
}
|
|
2972
|
+
function __anFinalizeNativeOAuthAbandonment(flowId) {
|
|
2973
|
+
if (!__anIsCurrentNativeOAuth(flowId) || !__anOAuthPollTimer) return;
|
|
2974
|
+
if (document.visibilityState === 'hidden') {
|
|
2975
|
+
__anCancelNativeOAuthAbandonment();
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
__anStopNativeOAuthAbandonment();
|
|
2979
|
+
if (__anInvalidateGoogleSignInFlow(flowId)) {
|
|
2980
|
+
__anStopOAuthExchangePolling();
|
|
2981
|
+
__anRecoverGoogleSignInAfterReturn(flowId);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
function __anScheduleNativeOAuthAbandonment(flowId) {
|
|
2985
|
+
// Returning focus is the only signal that the system-browser ceremony was
|
|
2986
|
+
// abandoned. Preserve the exchange poll briefly for a late deep-link.
|
|
2987
|
+
if (
|
|
2988
|
+
!__anIsCurrentNativeOAuth(flowId) ||
|
|
2989
|
+
__anNativeOAuthRequestPending ||
|
|
2990
|
+
!__anNativeOAuthReturnObserved ||
|
|
2991
|
+
!__anOAuthPollTimer ||
|
|
2992
|
+
__anNativeOAuthAbandonGraceTimer
|
|
2993
|
+
) return;
|
|
2994
|
+
__anNativeOAuthAbandonGraceTimer = setTimeout(function() {
|
|
2995
|
+
__anNativeOAuthAbandonGraceTimer = null;
|
|
2996
|
+
__anFinalizeNativeOAuthAbandonment(flowId);
|
|
2997
|
+
}, __anNativeOAuthAbandonGraceMs);
|
|
2998
|
+
}
|
|
2999
|
+
function __anBeginGoogleSignInFlow(flowId) {
|
|
3000
|
+
__anGoogleSignInFlowId = flowId;
|
|
3001
|
+
__anGoogleSignInRecoveryFlowId = null;
|
|
3002
|
+
__anGoogleSignInInFlight = true;
|
|
3003
|
+
}
|
|
3004
|
+
function __anIsCurrentGoogleSignInFlow(flowId) {
|
|
3005
|
+
return __anGoogleSignInInFlight && __anGoogleSignInFlowId === flowId;
|
|
3006
|
+
}
|
|
3007
|
+
function __anClearGoogleSignInFlow() {
|
|
3008
|
+
__anGoogleSignInFlowId = null;
|
|
3009
|
+
__anGoogleSignInRecoveryFlowId = null;
|
|
3010
|
+
}
|
|
3011
|
+
function __anInvalidateGoogleSignInFlow(flowId) {
|
|
3012
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) return false;
|
|
3013
|
+
__anGoogleSignInFlowId = null;
|
|
3014
|
+
__anGoogleSignInRecoveryFlowId = flowId;
|
|
3015
|
+
return true;
|
|
3016
|
+
}
|
|
3017
|
+
function __anCanRecoverGoogleSignInFlow(flowId, expectedFlowId) {
|
|
3018
|
+
// Focus can return while either the system-browser exchange or the popup
|
|
3019
|
+
// exchange is still active. Let that exchange finish before recovering.
|
|
3020
|
+
if (!flowId && (__anNativeOAuthFlowId || __anOAuthPollTimer || __anOAuthPopupWatchTimer)) return false;
|
|
3021
|
+
if (flowId) {
|
|
3022
|
+
return __anGoogleSignInInFlight &&
|
|
3023
|
+
!__anGoogleSignInFlowId &&
|
|
3024
|
+
__anGoogleSignInRecoveryFlowId === flowId;
|
|
3025
|
+
}
|
|
3026
|
+
return __anGoogleSignInInFlight && __anGoogleSignInFlowId === expectedFlowId;
|
|
3027
|
+
}
|
|
3028
|
+
function __anFinalizeOAuthPopupClose(flowId) {
|
|
3029
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
3030
|
+
__anOAuthPopupCloseGraceTimer = null;
|
|
3031
|
+
if (__anInvalidateGoogleSignInFlow(flowId)) {
|
|
3032
|
+
__anStopOAuthExchangePolling();
|
|
3033
|
+
__anRecoverGoogleSignInAfterReturn(flowId);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
function __anHandleOAuthPopupClosed(flowId) {
|
|
3037
|
+
__anStopOAuthPopupWatch();
|
|
3038
|
+
// The success callback closes its tab 250ms after staging the token, but
|
|
3039
|
+
// the opener polls once per second. Let an active exchange win that race.
|
|
3040
|
+
if (__anOAuthPollTimer) {
|
|
3041
|
+
__anOAuthPopupCloseGraceTimer = setTimeout(function() {
|
|
3042
|
+
__anFinalizeOAuthPopupClose(flowId);
|
|
3043
|
+
}, __anOAuthPopupCloseGraceMs);
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
__anFinalizeOAuthPopupClose(flowId);
|
|
3047
|
+
}
|
|
3048
|
+
function __anWatchOAuthPopupClose(popup, flowId) {
|
|
3049
|
+
__anStopOAuthPopupWatch();
|
|
3050
|
+
__anOAuthPopupWatchTimer = setInterval(function() {
|
|
3051
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) {
|
|
3052
|
+
__anStopOAuthPopupWatch();
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
var closed = false;
|
|
3056
|
+
try {
|
|
3057
|
+
closed = popup.closed === true;
|
|
3058
|
+
} catch(e) {
|
|
3059
|
+
__anHandleOAuthPopupClosed(flowId);
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
if (!closed) return;
|
|
3063
|
+
__anHandleOAuthPopupClosed(flowId);
|
|
3064
|
+
}, 500);
|
|
3065
|
+
}
|
|
2924
3066
|
function __anShowAuthExchangeError(err, btn, message, kind) {
|
|
2925
3067
|
__anStopOAuthExchangePolling();
|
|
3068
|
+
__anStopOAuthPopupWatch();
|
|
3069
|
+
__anStopNativeOAuthAbandonment();
|
|
2926
3070
|
err.textContent = message;
|
|
2927
3071
|
err.classList.add('show', 'error');
|
|
2928
3072
|
btn.disabled = false;
|
|
@@ -2931,22 +3075,30 @@ ${identitySsoScript}
|
|
|
2931
3075
|
showMagicLinkForm();
|
|
2932
3076
|
} else {
|
|
2933
3077
|
__anGoogleSignInInFlight = false;
|
|
3078
|
+
__anClearGoogleSignInFlow();
|
|
2934
3079
|
}
|
|
2935
3080
|
}
|
|
2936
3081
|
function __anShowOAuthError(err, btn, message) {
|
|
2937
3082
|
__anShowAuthExchangeError(err, btn, message, 'google');
|
|
2938
3083
|
}
|
|
2939
|
-
function __anRecoverGoogleSignInAfterReturn() {
|
|
3084
|
+
function __anRecoverGoogleSignInAfterReturn(flowId) {
|
|
2940
3085
|
// The user left for the Google sign-in window and came back. If the flow
|
|
2941
3086
|
// never completed (e.g. they closed the window to switch profiles), the
|
|
2942
3087
|
// button is stuck disabled with no error path firing for up to 5 minutes.
|
|
2943
3088
|
// Re-enable it so they can retry. Wait briefly first so a genuinely
|
|
2944
3089
|
// in-flight exchange can still finish and navigate without a flicker.
|
|
2945
|
-
if (!
|
|
3090
|
+
if (!flowId && __anNativeOAuthFlowId) {
|
|
3091
|
+
__anNativeOAuthReturnObserved = true;
|
|
3092
|
+
__anScheduleNativeOAuthAbandonment(__anNativeOAuthFlowId);
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
var expectedFlowId = flowId || __anGoogleSignInFlowId;
|
|
3096
|
+
if (!__anCanRecoverGoogleSignInFlow(flowId, expectedFlowId)) return;
|
|
2946
3097
|
setTimeout(function() {
|
|
3098
|
+
if (!__anCanRecoverGoogleSignInFlow(flowId, expectedFlowId)) return;
|
|
2947
3099
|
__anMaybeRedirectSignedIn(__anResumeHref()).then(function(redirected) {
|
|
3100
|
+
if (!__anCanRecoverGoogleSignInFlow(flowId, expectedFlowId)) return;
|
|
2948
3101
|
if (redirected) return;
|
|
2949
|
-
if (!__anGoogleSignInInFlight) return;
|
|
2950
3102
|
var btn = document.getElementById('google-btn');
|
|
2951
3103
|
if (!btn || !btn.disabled) return;
|
|
2952
3104
|
// Keep the desktop-exchange poll alive. Agent Native Desktop opens
|
|
@@ -2954,15 +3106,25 @@ ${identitySsoScript}
|
|
|
2954
3106
|
// callback has stored the session token.
|
|
2955
3107
|
btn.disabled = false;
|
|
2956
3108
|
__anGoogleSignInInFlight = false;
|
|
3109
|
+
__anClearGoogleSignInFlow();
|
|
2957
3110
|
});
|
|
2958
3111
|
}, 1200);
|
|
2959
3112
|
}
|
|
2960
3113
|
function __anBindGoogleRecover() {
|
|
2961
3114
|
if (__anGoogleRecoverBound) return;
|
|
2962
3115
|
__anGoogleRecoverBound = true;
|
|
2963
|
-
window.addEventListener('focus',
|
|
3116
|
+
window.addEventListener('focus', function() {
|
|
3117
|
+
__anRecoverGoogleSignInAfterReturn();
|
|
3118
|
+
});
|
|
3119
|
+
window.addEventListener('blur', function() {
|
|
3120
|
+
__anCancelNativeOAuthAbandonment();
|
|
3121
|
+
});
|
|
2964
3122
|
document.addEventListener('visibilitychange', function() {
|
|
2965
|
-
if (document.visibilityState === 'visible')
|
|
3123
|
+
if (document.visibilityState === 'visible') {
|
|
3124
|
+
__anRecoverGoogleSignInAfterReturn();
|
|
3125
|
+
} else {
|
|
3126
|
+
__anCancelNativeOAuthAbandonment();
|
|
3127
|
+
}
|
|
2966
3128
|
});
|
|
2967
3129
|
}
|
|
2968
3130
|
function __anHandlePopupOAuthFailure(ret, btn, err, flowId, redirectReason, builderFrameMessage) {
|
|
@@ -2990,6 +3152,7 @@ ${identitySsoScript}
|
|
|
2990
3152
|
var isMagicLink = kind === 'magic-link';
|
|
2991
3153
|
__anOAuthPollCount = 0;
|
|
2992
3154
|
async function check() {
|
|
3155
|
+
if (!isMagicLink && !__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
2993
3156
|
__anOAuthPollCount++;
|
|
2994
3157
|
try {
|
|
2995
3158
|
var exchangeParams = '?flow_id=' + encodeURIComponent(flowId);
|
|
@@ -2999,6 +3162,7 @@ ${identitySsoScript}
|
|
|
2999
3162
|
}
|
|
3000
3163
|
var res = await fetch(__anPath('/_agent-native/auth/desktop-exchange') + exchangeParams, exchangeOptions);
|
|
3001
3164
|
var data = await res.json().catch(function() { return {}; });
|
|
3165
|
+
if (!isMagicLink && !__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
3002
3166
|
if (data && (data.email || data.token)) {
|
|
3003
3167
|
if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);
|
|
3004
3168
|
__anOAuthPollTimer = null;
|
|
@@ -3062,8 +3226,7 @@ ${identitySsoScript}
|
|
|
3062
3226
|
}
|
|
3063
3227
|
return data.url;
|
|
3064
3228
|
}
|
|
3065
|
-
function __anStartPopupOAuth(ret, btn, err) {
|
|
3066
|
-
var flowId = __anNewOAuthFlowId();
|
|
3229
|
+
function __anStartPopupOAuth(ret, btn, err, flowId) {
|
|
3067
3230
|
var verifier = __anNewOAuthVerifier();
|
|
3068
3231
|
if (!verifier) {
|
|
3069
3232
|
__anShowOAuthError(err, btn, __anT('failedToConnect'));
|
|
@@ -3080,11 +3243,13 @@ ${identitySsoScript}
|
|
|
3080
3243
|
return;
|
|
3081
3244
|
}
|
|
3082
3245
|
try { popup.opener = null; } catch(e) {}
|
|
3246
|
+
__anWatchOAuthPopupClose(popup, flowId);
|
|
3083
3247
|
} catch(e) {
|
|
3084
3248
|
__anHandlePopupOAuthFailure(ret, btn, err, flowId, 'Could not open Google popup; falling back to redirect', 'Could not open Google popup.');
|
|
3085
3249
|
return;
|
|
3086
3250
|
}
|
|
3087
3251
|
__anRequestDesktopOAuthUrl(flowId, verifier, oauthReturn).then(function(url) {
|
|
3252
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
3088
3253
|
try {
|
|
3089
3254
|
popup.location.href = url;
|
|
3090
3255
|
__anSetOAuthDebug('Google popup opened; waiting for callback', flowId);
|
|
@@ -3093,24 +3258,29 @@ ${identitySsoScript}
|
|
|
3093
3258
|
throw e;
|
|
3094
3259
|
}
|
|
3095
3260
|
}).catch(function(e) {
|
|
3261
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
3096
3262
|
try { popup.close(); } catch(closeErr) {}
|
|
3097
3263
|
__anShowOAuthError(err, btn, e && e.message ? e.message : __anT('failedToConnect'));
|
|
3098
3264
|
});
|
|
3099
3265
|
}
|
|
3100
|
-
function __anStartNativeDesktopOAuth(ret, btn, err) {
|
|
3101
|
-
var flowId = __anNewOAuthFlowId();
|
|
3266
|
+
function __anStartNativeDesktopOAuth(ret, btn, err, flowId) {
|
|
3102
3267
|
var verifier = __anNewOAuthVerifier();
|
|
3103
3268
|
if (!verifier) {
|
|
3104
3269
|
__anShowOAuthError(err, btn, __anT('failedToConnect'));
|
|
3105
3270
|
return;
|
|
3106
3271
|
}
|
|
3107
3272
|
var oauthReturn = ret;
|
|
3273
|
+
__anBeginNativeOAuth(flowId);
|
|
3108
3274
|
__anSetOAuthDebug('Preparing Google sign-in in system browser', flowId);
|
|
3109
3275
|
__anRequestDesktopOAuthUrl(flowId, verifier, oauthReturn).then(function(url) {
|
|
3276
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
3277
|
+
__anMarkNativeOAuthPolling(flowId);
|
|
3110
3278
|
__anSetOAuthDebug('Opening Google sign-in in system browser', flowId);
|
|
3111
3279
|
__anOpenOAuthUrl(url);
|
|
3112
3280
|
__anWaitForOAuthExchange(flowId, ret, btn, err, 'google', verifier);
|
|
3281
|
+
__anScheduleNativeOAuthAbandonment(flowId);
|
|
3113
3282
|
}).catch(function(e) {
|
|
3283
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
3114
3284
|
__anShowOAuthError(err, btn, e && e.message ? e.message : __anT('failedToConnect'));
|
|
3115
3285
|
});
|
|
3116
3286
|
}
|
|
@@ -3831,20 +4001,20 @@ ${renderGoogleButton
|
|
|
3831
4001
|
var btn = document.getElementById('google-btn');
|
|
3832
4002
|
var err = document.getElementById('google-err');
|
|
3833
4003
|
var ret = __anResumeHref();
|
|
4004
|
+
var flowId = __anNewOAuthFlowId();
|
|
3834
4005
|
btn.disabled = true;
|
|
3835
|
-
|
|
4006
|
+
__anBeginGoogleSignInFlow(flowId);
|
|
3836
4007
|
__anBindGoogleRecover();
|
|
3837
4008
|
err.classList.remove('show');
|
|
3838
4009
|
if (__anResolveAuthFlow() === 'popup') {
|
|
3839
|
-
__anStartPopupOAuth(ret, btn, err);
|
|
4010
|
+
__anStartPopupOAuth(ret, btn, err, flowId);
|
|
3840
4011
|
return;
|
|
3841
4012
|
}
|
|
3842
4013
|
if (__anIsAgentNativeDesktop()) {
|
|
3843
|
-
__anStartNativeDesktopOAuth(ret, btn, err);
|
|
4014
|
+
__anStartNativeDesktopOAuth(ret, btn, err, flowId);
|
|
3844
4015
|
return;
|
|
3845
4016
|
}
|
|
3846
4017
|
if (__anIsBuilderPreview()) {
|
|
3847
|
-
var flowId = __anNewOAuthFlowId();
|
|
3848
4018
|
__anStartRedirectOAuth(ret, btn, err, flowId, 'Opening Google sign-in redirect from Builder preview');
|
|
3849
4019
|
return;
|
|
3850
4020
|
}
|
|
@@ -3852,6 +4022,7 @@ ${renderGoogleButton
|
|
|
3852
4022
|
var authUrl = __anGoogleAuthUrlPath() + '?return=' + encodeURIComponent(ret);
|
|
3853
4023
|
var res = await fetch(authUrl);
|
|
3854
4024
|
var data = await res.json();
|
|
4025
|
+
if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
|
|
3855
4026
|
if (data.url) {
|
|
3856
4027
|
__anOpenOAuthUrl(data.url);
|
|
3857
4028
|
} else {
|
|
@@ -3859,12 +4030,14 @@ ${renderGoogleButton
|
|
|
3859
4030
|
err.classList.add('show');
|
|
3860
4031
|
btn.disabled = false;
|
|
3861
4032
|
__anGoogleSignInInFlight = false;
|
|
4033
|
+
__anClearGoogleSignInFlow();
|
|
3862
4034
|
}
|
|
3863
4035
|
} catch (e) {
|
|
3864
4036
|
err.textContent = __anT('failedToConnect');
|
|
3865
4037
|
err.classList.add('show');
|
|
3866
4038
|
btn.disabled = false;
|
|
3867
4039
|
__anGoogleSignInInFlight = false;
|
|
4040
|
+
__anClearGoogleSignInFlow();
|
|
3868
4041
|
}
|
|
3869
4042
|
}`
|
|
3870
4043
|
: ""}
|
package/dist/vite/client.js
CHANGED
|
@@ -2254,7 +2254,11 @@ function isNitroEnvironmentUnavailable(error) {
|
|
|
2254
2254
|
typeof candidate.message === "string" &&
|
|
2255
2255
|
/Vite environment .+ is unavailable/.test(candidate.message));
|
|
2256
2256
|
}
|
|
2257
|
-
|
|
2257
|
+
// Nitro's worker can expose a transformed module graph before its entry
|
|
2258
|
+
// module has finished importing. Keep the recovery page up for the same
|
|
2259
|
+
// cold-start window instead of letting the first poll render a 500 error.
|
|
2260
|
+
const NITRO_STARTUP_SETTLE_MS = 3_000;
|
|
2261
|
+
const NITRO_STARTUP_POLL_INTERVAL_MS = 100;
|
|
2258
2262
|
const NITRO_STARTUP_TIMEOUT_MS = 30_000;
|
|
2259
2263
|
const NITRO_STARTUP_RETRY_DELAY_MS = 1_000;
|
|
2260
2264
|
const NITRO_STARTUP_RETRY_MAX_DELAY_MS = 3_000;
|
|
@@ -2366,15 +2370,24 @@ function nitroStartupGate(options = {}) {
|
|
|
2366
2370
|
let graphSignature = null;
|
|
2367
2371
|
let graphStableAt;
|
|
2368
2372
|
let startupComplete = false;
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
+
let readinessTimer;
|
|
2374
|
+
const completeStartup = () => {
|
|
2375
|
+
startupComplete = true;
|
|
2376
|
+
if (readinessTimer) {
|
|
2377
|
+
clearInterval(readinessTimer);
|
|
2378
|
+
readinessTimer = undefined;
|
|
2373
2379
|
}
|
|
2380
|
+
};
|
|
2381
|
+
// Observe the graph independently of the first document request. A
|
|
2382
|
+
// server can finish compiling while the shell is still on its loading
|
|
2383
|
+
// screen; measuring stability only from the first request adds the full
|
|
2384
|
+
// settle window to an otherwise-ready server.
|
|
2385
|
+
const observeStartup = () => {
|
|
2386
|
+
if (startupComplete)
|
|
2387
|
+
return;
|
|
2374
2388
|
const timestamp = now();
|
|
2375
2389
|
if (timestamp - startedAt >= timeoutMs) {
|
|
2376
|
-
|
|
2377
|
-
next();
|
|
2390
|
+
completeStartup();
|
|
2378
2391
|
return;
|
|
2379
2392
|
}
|
|
2380
2393
|
const nextGraphSignature = nitroModuleGraphSignature(server.environments?.nitro);
|
|
@@ -2385,15 +2398,43 @@ function nitroStartupGate(options = {}) {
|
|
|
2385
2398
|
}
|
|
2386
2399
|
else if (graphStableAt !== undefined &&
|
|
2387
2400
|
timestamp - graphStableAt >= settleMs) {
|
|
2388
|
-
|
|
2389
|
-
next();
|
|
2390
|
-
return;
|
|
2401
|
+
completeStartup();
|
|
2391
2402
|
}
|
|
2392
2403
|
}
|
|
2393
2404
|
else {
|
|
2394
2405
|
graphSignature = null;
|
|
2395
2406
|
graphStableAt = undefined;
|
|
2396
2407
|
}
|
|
2408
|
+
};
|
|
2409
|
+
// Start watching before any HTML request arrives so readiness time is
|
|
2410
|
+
// measured from server startup, not from the user's first navigation.
|
|
2411
|
+
observeStartup();
|
|
2412
|
+
if (!startupComplete) {
|
|
2413
|
+
readinessTimer = setInterval(observeStartup, NITRO_STARTUP_POLL_INTERVAL_MS);
|
|
2414
|
+
readinessTimer.unref?.();
|
|
2415
|
+
server.httpServer?.once("close", () => {
|
|
2416
|
+
if (readinessTimer) {
|
|
2417
|
+
clearInterval(readinessTimer);
|
|
2418
|
+
readinessTimer = undefined;
|
|
2419
|
+
}
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
server.middlewares.use((req, res, next) => {
|
|
2423
|
+
if (startupComplete || !isHtmlDocumentRequest(req)) {
|
|
2424
|
+
next();
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
const timestamp = now();
|
|
2428
|
+
if (timestamp - startedAt >= timeoutMs) {
|
|
2429
|
+
completeStartup();
|
|
2430
|
+
next();
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
observeStartup();
|
|
2434
|
+
if (startupComplete) {
|
|
2435
|
+
next();
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2397
2438
|
sendNitroStartingResponse(req, res);
|
|
2398
2439
|
});
|
|
2399
2440
|
},
|
|
@@ -214,7 +214,7 @@ Four steps take you from template to deployed app:
|
|
|
214
214
|
|
|
215
215
|
1. **Pick an app.** Use the CLI picker, or browse the docs.
|
|
216
216
|
2. **Brand it.** Change the name, colors, logo, and copy. Most example apps expose this in a single config file.
|
|
217
|
-
3. **Customize it.** Make source changes in your own repository and development workflow. An agent can help when its frame is intentionally granted workspace and write tooling; otherwise the embedded agent operates the resulting app through its actions, data, and configured integrations.
|
|
217
|
+
3. **Customize it.** Make source changes in your own repository and development workflow: "add a priority field to forms," "connect to our Salesforce instance," "change the color scheme to match our brand." An agent can help when its frame is intentionally granted workspace and write tooling; otherwise the embedded agent operates the resulting app through its actions, data, and configured integrations.
|
|
218
218
|
4. **Ship it.** Run the deploy command. You now have a production app at your own domain.
|
|
219
219
|
|
|
220
220
|
Steps 2–4 typically take a few days. Step 3 has no fixed end point — the app is yours to evolve, while the embedded agent remains available to operate the deployed product and can assist with source changes when code access is explicitly enabled.
|
|
@@ -168,6 +168,13 @@ multi-user apps should prefer the scoped credential and connection store.
|
|
|
168
168
|
Every provider key below selects its engine on its own: dropping one in is
|
|
169
169
|
enough, and `AGENT_ENGINE` is only needed to pick between several.
|
|
170
170
|
|
|
171
|
+
Supported models span Anthropic Claude, OpenAI (GPT-5 family), Google Gemini,
|
|
172
|
+
and any provider that speaks the OpenAI API shape via `OPENAI_BASE_URL`,
|
|
173
|
+
including OpenAI-compatible gateways like LiteLLM. The model is chosen in
|
|
174
|
+
Settings, so switching providers is a configuration change, not a code
|
|
175
|
+
rewrite. Claude is the framework's most heavily tested path and the default
|
|
176
|
+
recommendation.
|
|
177
|
+
|
|
171
178
|
| Variable | What it controls | Supported values or format |
|
|
172
179
|
| ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------- |
|
|
173
180
|
| `ANTHROPIC_API_KEY` | Claude model fallback for the built-in AI engine. | An Anthropic API key. |
|
|
@@ -370,6 +370,8 @@ Every consumer calls the same underlying function, so there is only one implemen
|
|
|
370
370
|
|
|
371
371
|
When the agent changes data, the UI needs to reflect that without a manual refresh. `useDbSync()` is what makes that automatic. Same-process writes stream over `/_agent-native/events`; `/_agent-native/poll` remains the cross-process and serverless fallback. When the agent writes to the database (application state, settings, or domain data), a version counter increments and the client invalidates the relevant React Query caches.
|
|
372
372
|
|
|
373
|
+
SSE covers the common case instead of WebSockets because it gives same-process writes an immediate path to the browser with plain HTTP, while the version-counter poll stays as a fallback that works in every deployment environment, including serverless and edge, where persistent sockets may not be available.
|
|
374
|
+
|
|
373
375
|
```ts
|
|
374
376
|
// Client: subscribe to agent/UI data changes once near the app shell
|
|
375
377
|
import { useDbSync } from "@agent-native/core/client/hooks";
|
|
@@ -25,7 +25,9 @@ agent-native upgrade
|
|
|
25
25
|
agent-native template sync
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`sync` defaults to the template version matching your installed `@agent-native/core`, so running it after `upgrade` keeps code and packages on the same version.
|
|
28
|
+
`sync` defaults to the template version matching your installed `@agent-native/core`, so running it after `upgrade` keeps code and packages on the same version. Running the bare `agent-native upgrade` bumps `@agent-native/*` dependencies to `latest`, installs, refreshes scaffold skills (`skills update scaffold --project`), and runs typecheck when available. Use `agent-native upgrade check` first for a doctor-only report of framework overrides/patches and pending bumps.
|
|
29
|
+
|
|
30
|
+
Do **not** fix a broken upgrade with `pnpm.overrides`, `patchedDependencies`, or edits under `node_modules/@agent-native/*` (especially core/dispatch); those mask the real incompatibility instead of resolving it. If upgrade or typecheck fails, fix app-level code or ask, then re-run upgrade.
|
|
29
31
|
|
|
30
32
|
Look before you merge:
|
|
31
33
|
|
|
@@ -102,6 +102,14 @@ data and the same actions.
|
|
|
102
102
|
|
|
103
103
|
Keep one canonical instructions file: `AGENTS.md`. If a client expects `CLAUDE.md`, make it a symlink to `AGENTS.md` rather than a second copy. Two hand-maintained files drift, and the agent ends up with contradictory rules. One source of truth, linked where needed.
|
|
104
104
|
|
|
105
|
+
`AGENTS.md` is the universal standard the framework builds on, so any AI coding tool that reads project instructions works out of the box:
|
|
106
|
+
|
|
107
|
+
- **Claude Code**: reads `CLAUDE.md` (symlinked from `AGENTS.md` by the CLI setup)
|
|
108
|
+
- **Cursor**: reads `AGENTS.md` directly, or `.cursorrules` (Cursor's legacy location) if present
|
|
109
|
+
- **Windsurf**: reads `AGENTS.md` directly, or `.windsurfrules` (Windsurf's legacy location) if present
|
|
110
|
+
- **Codex, Gemini, and others**: work via the embedded agent panel
|
|
111
|
+
- **Builder.io**: cloud-hosted agent with visual editing and collaboration
|
|
112
|
+
|
|
105
113
|
## SKILL.md frontmatter must say what AND when {#skill-frontmatter}
|
|
106
114
|
|
|
107
115
|
The `description` is the only thing the agent sees when deciding whether to read a skill. It must answer two questions: what the skill covers, and when to trigger it. A description that only describes the topic will not fire.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.174.
|
|
3
|
+
"version": "0.174.2-nightly-20260825214843",
|
|
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": {
|
package/docs/content/faq.mdx
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "FAQ"
|
|
3
|
-
description: "Common questions about Agent-Native — what it is, who it's for, what you can build, and how it works."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# FAQ
|
|
7
|
-
|
|
8
|
-
Common questions about Agent-Native, organized from "I'm just looking" to "I'm wiring up auth right now."
|
|
9
|
-
|
|
10
|
-
## The basics {#general}
|
|
11
|
-
|
|
12
|
-
### What is Agent-Native? {#what-is-agent-native}
|
|
13
|
-
|
|
14
|
-
Agent-Native is a framework for building apps where the AI agent and the product surface around it are equal partners. The usual path starts with chat, adds typed actions, renders structured results inline, and grows into durable pages around the same SQL state. The invariant is that agents and humans share the same actions, database, and state. See [What is Agent-Native?](/docs/what-is-agent-native) for the full explanation.
|
|
15
|
-
|
|
16
|
-
### Who is this for? {#who-is-this-for}
|
|
17
|
-
|
|
18
|
-
Agent-Native is for people who want a real app and an AI agent to work from the same data and actions. The common paths are:
|
|
19
|
-
|
|
20
|
-
- **Use a hosted app** if you want Mail, Calendar, Forms, Plan, or another finished template with no setup — start at the [template gallery](/templates).
|
|
21
|
-
- **Start with Chat** if you want the default from-scratch path: users can talk to the agent immediately, then you extend with actions, native results, and screens — start with [Getting Started](/docs/getting-started) or [Chat](/docs/template-chat).
|
|
22
|
-
- **Start automation-first** if you are building scheduled jobs, queues, scripts, integration workers, or external-agent workflows with no browser UI yet — start with [Automation-First Apps](/docs/pure-agent-apps).
|
|
23
|
-
- **Start from a template and customize it** if you want your own SaaS product with auth, database, UI, and agent actions already wired — see [Templates](/docs/cloneable-saas).
|
|
24
|
-
- **Build from scratch** if you want the framework primitives for a new agent-driven product — start with [Getting Started](/docs/getting-started).
|
|
25
|
-
- **Connect another agent or code tool** if you want Claude, ChatGPT, Codex, Cursor, or GitHub Copilot / VS Code to use an Agent-Native app — see [External Agents](/docs/external-agents) and [Skills Guide](/docs/skills-guide).
|
|
26
|
-
|
|
27
|
-
### How is this different from adding AI to an existing app? {#how-is-this-different}
|
|
28
|
-
|
|
29
|
-
Most apps bolt AI on as an afterthought that can't actually _do_ things in the app. In an Agent-Native app the agent is a first-class citizen that shares the same actions, database, and state as the UI, so it can do anything the buttons can. See [Why build apps this way](/docs/what-is-agent-native#why-build-apps-this-way).
|
|
30
|
-
|
|
31
|
-
<Diagram id="doc-block-1s6b04" title="Bolted-on AI vs. Agent-Native" summary="A bolted-on chat sidebar lives in its own world. An Agent-Native agent shares the same actions, database, and state as the UI.">
|
|
32
|
-
|
|
33
|
-
```html
|
|
34
|
-
<div class="diagram-vs">
|
|
35
|
-
<div class="diagram-col">
|
|
36
|
-
<span class="diagram-pill warn">Bolted-on AI</span>
|
|
37
|
-
<div class="diagram-node">Chat sidebar</div>
|
|
38
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">↓</div>
|
|
39
|
-
<div class="diagram-box" data-rough>
|
|
40
|
-
separate AI world<br /><small class="diagram-muted"
|
|
41
|
-
>can't touch the app</small
|
|
42
|
-
>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="diagram-box diagram-muted">App UI & data</div>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="diagram-divider" aria-hidden="true"></div>
|
|
47
|
-
<div class="diagram-col">
|
|
48
|
-
<span class="diagram-pill ok">Agent-Native</span>
|
|
49
|
-
<div class="diagram-row2">
|
|
50
|
-
<div class="diagram-node">UI</div>
|
|
51
|
-
<div class="diagram-node">Agent</div>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">↓</div>
|
|
54
|
-
<div class="diagram-box" data-rough>shared actions, DB & state</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
```css
|
|
60
|
-
.diagram-vs {
|
|
61
|
-
display: flex;
|
|
62
|
-
align-items: stretch;
|
|
63
|
-
gap: 18px;
|
|
64
|
-
flex-wrap: wrap;
|
|
65
|
-
}
|
|
66
|
-
.diagram-vs .diagram-col {
|
|
67
|
-
display: flex;
|
|
68
|
-
flex-direction: column;
|
|
69
|
-
gap: 8px;
|
|
70
|
-
align-items: center;
|
|
71
|
-
flex: 1;
|
|
72
|
-
min-width: 200px;
|
|
73
|
-
}
|
|
74
|
-
.diagram-vs .diagram-row2 {
|
|
75
|
-
display: flex;
|
|
76
|
-
gap: 8px;
|
|
77
|
-
}
|
|
78
|
-
.diagram-vs .diagram-arrow {
|
|
79
|
-
font-size: 20px;
|
|
80
|
-
line-height: 1;
|
|
81
|
-
}
|
|
82
|
-
.diagram-vs .diagram-divider {
|
|
83
|
-
width: 1px;
|
|
84
|
-
align-self: stretch;
|
|
85
|
-
background: currentColor;
|
|
86
|
-
opacity: 0.15;
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
</Diagram>
|
|
91
|
-
|
|
92
|
-
### Is it open source? {#is-this-open-source}
|
|
93
|
-
|
|
94
|
-
Yes. The framework and all templates are open source. You can run everything locally, self-host, or use Builder.io's cloud for managed hosting, collaboration, and team features.
|
|
95
|
-
|
|
96
|
-
### How much does it cost? {#how-much}
|
|
97
|
-
|
|
98
|
-
The framework itself is free. The two costs you'll see in practice:
|
|
99
|
-
|
|
100
|
-
- **AI usage.** You bring your own API key (Anthropic, OpenAI, etc.) and pay the model provider directly. There's no markup from us.
|
|
101
|
-
- **Hosting.** Whatever your host charges. Most templates run fine on free tiers (Netlify, Vercel, Cloudflare) for small workloads.
|
|
102
|
-
|
|
103
|
-
If you'd rather not manage any of this, the hosted version on `agent-native.com` (operated by Builder.io) bundles inference and hosting into a per-seat plan.
|
|
104
|
-
|
|
105
|
-
### Can I host this myself? {#can-i-self-host}
|
|
106
|
-
|
|
107
|
-
Yes. Pick any host that runs Node — Netlify, Vercel, Cloudflare, AWS, Deno Deploy, your own server — and any SQL database (Postgres, SQLite, Turso, D1). The framework is built to be portable. Start with the [local Docker quickstart](/docs/deployment#self-host-quickstart), then use [Deployment](/docs/deployment) for production.
|
|
108
|
-
|
|
109
|
-
### What AI models does it support? {#what-models}
|
|
110
|
-
|
|
111
|
-
Anthropic Claude, OpenAI (GPT-5 family), Google Gemini, and any provider that speaks the OpenAI API shape. For OpenAI-compatible gateways like LiteLLM, choose OpenAI in Settings and add the gateway URL under Advanced. You configure the model in settings; switching is a config change, not a code rewrite. The framework's heaviest tested path is Claude, so that's the default recommendation.
|
|
112
|
-
|
|
113
|
-
### Do I need to know AI/ML? {#do-i-need-to-know-ai}
|
|
114
|
-
|
|
115
|
-
No. You don't train models, fine-tune, or deal with embeddings. You build a regular web app — and on the hosted version, you barely build anything at all. The framework handles the agent integration: routing messages, running actions, syncing state.
|
|
116
|
-
|
|
117
|
-
### Can I migrate an existing app to Agent-Native? {#can-i-use-existing-code}
|
|
118
|
-
|
|
119
|
-
You can, but Agent-Native works best when built from the ground up. The architecture — shared database, polling sync, actions, application state — needs to be integrated throughout. Starting from an app and customizing it is the recommended path. Think of it like the shift from desktop-first to mobile-first: you _can_ retrofit, but building native is better.
|
|
120
|
-
|
|
121
|
-
## Templates and what you can build {#templates}
|
|
122
|
-
|
|
123
|
-
### What templates are available? {#what-templates-are-available}
|
|
124
|
-
|
|
125
|
-
The framework ships with production-ready templates including [Chat](/docs/template-chat), [Mail](/docs/template-mail), [Calendar](/docs/template-calendar), [Forms](/docs/template-forms), [Plan](/docs/template-plan) (visual plans and PR recaps), [Analytics](/docs/template-analytics), [Dispatch](/docs/template-dispatch), and more. Each is a complete app with UI, agent actions, database schema, and AI instructions ready to go. See [Templates](/docs/cloneable-saas) for the full catalog.
|
|
126
|
-
|
|
127
|
-
### Can I customize templates? {#can-i-customize-templates}
|
|
128
|
-
|
|
129
|
-
That's the whole point. Start from a template and customize it in your own repository and development workflow. "Add a priority field to forms." "Connect to our Salesforce instance." "Change the color scheme to match our brand." An agent can edit the source when its frame is intentionally granted workspace and write tooling; otherwise the embedded agent operates the resulting app through its actions and data surface.
|
|
130
|
-
|
|
131
|
-
### Can I build something the templates don't cover? {#build-from-scratch}
|
|
132
|
-
|
|
133
|
-
Yes. If you want a basic agentic app, run `npx @agent-native/core@latest create my-chat-app --template chat`; you get durable chat threads, actions, auth, SQL-backed runtime state, native inline result rendering, and room to add your own screens. If you are building a no-browser automation, run `npx @agent-native/core@latest create my-agent --headless`. See [Getting Started](/docs/getting-started), [Automation-First Apps](/docs/pure-agent-apps), and [Chat](/docs/template-chat).
|
|
134
|
-
|
|
135
|
-
### Can I try it without starting from a template? {#try-with-a-skill}
|
|
136
|
-
|
|
137
|
-
Yes — install a skill into a coding agent you already use with one command and no scaffold required. See the [Skills Guide](/docs/skills-guide#app-backed-skills) for the walkthrough.
|
|
138
|
-
|
|
139
|
-
## Agent capabilities {#agent-capabilities}
|
|
140
|
-
|
|
141
|
-
### Does the embedded agent edit the app's source code? {#can-the-agent-modify-code}
|
|
142
|
-
|
|
143
|
-
Not by default. The embedded agent normally works through actions, SQL-backed state, and configured integrations. It can edit components, routes, styles, or actions only when its frame is intentionally granted repository workspace and write tooling. Otherwise use your normal development workflow or a separate code-capable development/Builder frame. For runtime customization without source changes, use [Extensions](/docs/extensions).
|
|
144
|
-
|
|
145
|
-
### Can users talk to the agent from outside the app? {#external-channels}
|
|
146
|
-
|
|
147
|
-
Yes. The same agent runs in your web UI, in Slack, in Telegram, over email, and from other agents (via [A2A](/docs/a2a-protocol)). It's the same agent with the same memory and the same actions, just reached through different channels. See [Messaging the agent](/docs/messaging).
|
|
148
|
-
|
|
149
|
-
### Can agents talk to each other? {#can-agents-talk-to-each-other}
|
|
150
|
-
|
|
151
|
-
Yes, via the [A2A (Agent-to-Agent) protocol](/docs/a2a-protocol). Every Agent-Native app automatically gets an A2A endpoint. From the mail app, you can tag the analytics agent to query data. An agent discovers what other agents are available, calls them over the protocol, and shows results in the UI. No configuration needed — the agent card is auto-generated from your template's actions.
|
|
152
|
-
|
|
153
|
-
### What can the agent see in the app? {#what-can-the-agent-see}
|
|
154
|
-
|
|
155
|
-
The agent always knows what the user is currently viewing. The UI writes navigation state to the database on every route change — which view is open, which item is selected. The agent reads this before taking action. If an email is open, the agent knows which email. If a slide is selected, the agent knows which slide. See [Context Awareness](/docs/context-awareness).
|
|
156
|
-
|
|
157
|
-
## Development questions {#development}
|
|
158
|
-
|
|
159
|
-
### Which AI coding tools work with Agent-Native? {#which-ai-tools-work}
|
|
160
|
-
|
|
161
|
-
Any AI coding tool that reads project instructions. The framework uses AGENTS.md as the universal standard:
|
|
162
|
-
|
|
163
|
-
- **Claude Code** — reads CLAUDE.md (symlinked from AGENTS.md by the CLI setup)
|
|
164
|
-
- **Cursor** — reads AGENTS.md directly, or `.cursorrules` (Cursor's legacy location) if present in your project
|
|
165
|
-
- **Windsurf** — reads AGENTS.md directly, or `.windsurfrules` (Windsurf's legacy location) if present in your project
|
|
166
|
-
- **Codex, Gemini, and others** — work via the embedded agent panel
|
|
167
|
-
- **Builder.io** — cloud-hosted agent with visual editing and collaboration
|
|
168
|
-
|
|
169
|
-
### Can I use my own database? {#can-i-use-my-own-database}
|
|
170
|
-
|
|
171
|
-
Yes. Set `DATABASE_URL` and the framework auto-detects it. Supported databases include SQLite, Postgres (Neon, Supabase, plain), Turso (libSQL), and Cloudflare D1. All SQL is dialect-agnostic via Drizzle ORM — the same code works everywhere.
|
|
172
|
-
|
|
173
|
-
### Where can I deploy? {#where-can-i-deploy}
|
|
174
|
-
|
|
175
|
-
Anywhere. The server runs on Nitro, which compiles to any deployment target: Node.js, Cloudflare Workers/Pages, Netlify, Vercel, Deno Deploy, AWS Lambda, and Bun. You can also use Builder.io's hosting for managed deployments. See the [Deployment guide](/docs/deployment).
|
|
176
|
-
|
|
177
|
-
## Architecture {#architecture}
|
|
178
|
-
|
|
179
|
-
### Why SSE plus polling instead of WebSockets? {#why-polling-not-websockets}
|
|
180
|
-
|
|
181
|
-
SSE gives same-process writes an immediate path to the browser, and a lightweight version-counter poll remains the fallback because it works in every deployment environment — including serverless and edge, where persistent sockets may not be available. See [Key Concepts — Live sync](/docs/key-concepts#polling-sync).
|
|
182
|
-
|
|
183
|
-
<Diagram id="doc-block-1et8179" title="SSE first, polling fallback" summary={"Same-process writes stream instantly; a version-counter poll keeps serverless, edge, and cross-process writes convergent."}>
|
|
184
|
-
|
|
185
|
-
```html
|
|
186
|
-
<div class="diagram-transport">
|
|
187
|
-
<div class="diagram-box" data-rough>DB write</div>
|
|
188
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</div>
|
|
189
|
-
<div class="diagram-col">
|
|
190
|
-
<div class="diagram-node">
|
|
191
|
-
SSE<br /><small class="diagram-muted"
|
|
192
|
-
>/_agent-native/events · instant</small
|
|
193
|
-
>
|
|
194
|
-
</div>
|
|
195
|
-
<div class="diagram-node">
|
|
196
|
-
Poll<br /><small class="diagram-muted"
|
|
197
|
-
>/_agent-native/poll · universal fallback</small
|
|
198
|
-
>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</div>
|
|
202
|
-
<div class="diagram-box" data-rough>Browser refetch</div>
|
|
203
|
-
</div>
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
```css
|
|
207
|
-
.diagram-transport {
|
|
208
|
-
display: flex;
|
|
209
|
-
align-items: center;
|
|
210
|
-
gap: 12px;
|
|
211
|
-
flex-wrap: wrap;
|
|
212
|
-
}
|
|
213
|
-
.diagram-transport .diagram-col {
|
|
214
|
-
display: flex;
|
|
215
|
-
flex-direction: column;
|
|
216
|
-
gap: 8px;
|
|
217
|
-
}
|
|
218
|
-
.diagram-transport .diagram-arrow {
|
|
219
|
-
font-size: 22px;
|
|
220
|
-
line-height: 1;
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
</Diagram>
|
|
225
|
-
|
|
226
|
-
### Why can't the UI call an LLM directly? {#why-no-inline-llm-calls}
|
|
227
|
-
|
|
228
|
-
AI is non-deterministic, so you need conversation flow to give feedback and iterate — not one-shot buttons — and the agent already has your codebase, instructions, skills, and history that an inline call lacks. Routing everything through the agent is also what lets the app be driven from Slack, Telegram, or another agent. See [Key Concepts — Agent chat bridge](/docs/key-concepts#agent-chat-bridge).
|
|
229
|
-
|
|
230
|
-
### How do I upgrade an older Agent-Native app? {#upgrade-older-app}
|
|
231
|
-
|
|
232
|
-
Run the supported upgrade path from the app or workspace root:
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
npx @agent-native/core@latest upgrade
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
That bumps `@agent-native/*` dependencies to `latest`, installs, refreshes
|
|
239
|
-
scaffold skills (`skills update scaffold --project`), and runs typecheck when
|
|
240
|
-
available. Use `upgrade check` first if you want a doctor-only report of
|
|
241
|
-
framework overrides/patches and pending bumps.
|
|
242
|
-
|
|
243
|
-
Do **not** fix a broken upgrade with `pnpm.overrides`, `patchedDependencies`,
|
|
244
|
-
or edits under `node_modules/@agent-native/*` (especially core/dispatch). If
|
|
245
|
-
upgrade or typecheck fails, fix app-level code or ask — then re-run upgrade.
|
|
246
|
-
|
|
247
|
-
### Why is this a framework and not a library? {#why-framework-not-library}
|
|
248
|
-
|
|
249
|
-
The shared database, live sync, actions system, and application state only work because they're wired together from the ground up — the UI reacts to agent changes instantly, agents communicate, and the agent understands what the user is looking at. A library gives you pieces; this is an architecture. See [Key Concepts](/docs/key-concepts).
|
|
250
|
-
|
|
251
|
-
## What's next
|
|
252
|
-
|
|
253
|
-
- [**Getting Started**](/docs/getting-started) — build your first action and render it inline in chat
|
|
254
|
-
- [**What is Agent-Native?**](/docs/what-is-agent-native) — the full explanation behind the answers above
|
|
255
|
-
- [**Templates**](/docs/cloneable-saas) — start from a complete SaaS product instead of starting from scratch
|
|
256
|
-
- [**Key Concepts**](/docs/key-concepts) — the architecture: SQL, actions, polling sync, and portability
|
|
257
|
-
- [**Deployment**](/docs/deployment) — host on your own domain or any Nitro-compatible provider
|