@agent-native/core 0.157.1 → 0.157.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/templates/calendar/server/handlers/bookings.ts +6 -1
- package/corpus/templates/clips/actions/request-recording-access.ts +47 -14
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +125 -15
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/docs/content/cloneable-saas.mdx +24 -26
- package/package.json +3 -3
|
@@ -483,7 +483,12 @@ async function resolveAvailabilityContext({
|
|
|
483
483
|
draft
|
|
484
484
|
? and(
|
|
485
485
|
eq(schema.bookingLinks.slug, slug),
|
|
486
|
-
accessFilter(
|
|
486
|
+
accessFilter(
|
|
487
|
+
schema.bookingLinks,
|
|
488
|
+
schema.bookingLinkShares,
|
|
489
|
+
undefined,
|
|
490
|
+
"editor",
|
|
491
|
+
),
|
|
487
492
|
)
|
|
488
493
|
: eq(schema.bookingLinks.slug, slug),
|
|
489
494
|
)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
import { defineAction } from "@agent-native/core";
|
|
2
4
|
import { notify } from "@agent-native/core/notifications";
|
|
3
5
|
import {
|
|
@@ -19,7 +21,7 @@ import { and, eq } from "drizzle-orm";
|
|
|
19
21
|
import { z } from "zod";
|
|
20
22
|
|
|
21
23
|
import { getDb, schema } from "../server/db/index.js";
|
|
22
|
-
import {
|
|
24
|
+
import { normalizeOwnerEmail } from "../server/lib/recordings.js";
|
|
23
25
|
import {
|
|
24
26
|
CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
|
|
25
27
|
recordingSharePath,
|
|
@@ -27,6 +29,20 @@ import {
|
|
|
27
29
|
|
|
28
30
|
export const CLIPS_ACCESS_REQUEST_EMAIL_ID = "clips.access-request";
|
|
29
31
|
|
|
32
|
+
function accessRequestEventId(
|
|
33
|
+
recordingId: string,
|
|
34
|
+
requesterEmail: string,
|
|
35
|
+
): string {
|
|
36
|
+
return (
|
|
37
|
+
"access-request-" +
|
|
38
|
+
createHash("sha256")
|
|
39
|
+
.update(recordingId)
|
|
40
|
+
.update("\0")
|
|
41
|
+
.update(requesterEmail)
|
|
42
|
+
.digest("hex")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
function httpError(message: string, statusCode: number): Error {
|
|
31
47
|
return Object.assign(new Error(message), { statusCode });
|
|
32
48
|
}
|
|
@@ -211,19 +227,36 @@ export default defineAction({
|
|
|
211
227
|
getRequestUserName()?.trim() ||
|
|
212
228
|
displayNameForEmail(normalizedRequesterEmail);
|
|
213
229
|
const requestedAt = new Date().toISOString();
|
|
214
|
-
await db
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
230
|
+
const [insertedRequest] = await db
|
|
231
|
+
.insert(schema.recordingEvents)
|
|
232
|
+
.values({
|
|
233
|
+
id: accessRequestEventId(recordingId, normalizedRequesterEmail),
|
|
234
|
+
recordingId,
|
|
235
|
+
viewerId: null,
|
|
236
|
+
kind: "access-request",
|
|
237
|
+
timestampMs: 0,
|
|
238
|
+
payload: JSON.stringify({
|
|
239
|
+
requesterEmail: normalizedRequesterEmail,
|
|
240
|
+
requesterName,
|
|
241
|
+
requestedAt,
|
|
242
|
+
}),
|
|
243
|
+
createdAt: requestedAt,
|
|
244
|
+
})
|
|
245
|
+
.onConflictDoNothing()
|
|
246
|
+
.returning({ id: schema.recordingEvents.id });
|
|
247
|
+
|
|
248
|
+
// Historical requests use random IDs, so the read above still handles
|
|
249
|
+
// them. New requests use a deterministic primary key so concurrent
|
|
250
|
+
// callers have one database-backed winner before notifications are sent.
|
|
251
|
+
if (!insertedRequest) {
|
|
252
|
+
return {
|
|
253
|
+
ok: true as const,
|
|
254
|
+
alreadyHasAccess: false,
|
|
255
|
+
alreadyRequested: true,
|
|
256
|
+
notifiedOwner: false,
|
|
257
|
+
message: "Your access request is already with the clip owner.",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
227
260
|
|
|
228
261
|
const ownerEmail = recording.ownerEmail
|
|
229
262
|
? normalizeOwnerEmail(recording.ownerEmail)
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
deleteResumableSession,
|
|
58
58
|
getResumableSession,
|
|
59
59
|
setResumableSession,
|
|
60
|
+
type StoredResumableSession,
|
|
60
61
|
} from "../../../../lib/resumable-session.js";
|
|
61
62
|
import { abortResumableUploadSession } from "../../../../lib/resumable-upload-cleanup.js";
|
|
62
63
|
import { shouldEnableStreamingUpload } from "../../../../lib/streaming-upload-mode.js";
|
|
@@ -74,6 +75,66 @@ interface CompressionMeta {
|
|
|
74
75
|
outputMimeType?: string;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
interface PendingResumableCleanup {
|
|
79
|
+
recordingId: string;
|
|
80
|
+
generationId: string | null;
|
|
81
|
+
ownerGenerationId: string | null;
|
|
82
|
+
claimId: string | null;
|
|
83
|
+
session: StoredResumableSession;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parsePendingResumableCleanup(
|
|
87
|
+
raw: Record<string, unknown> | null,
|
|
88
|
+
recordingId: string,
|
|
89
|
+
): PendingResumableCleanup | null {
|
|
90
|
+
if (!raw) return null;
|
|
91
|
+
const session = raw.session;
|
|
92
|
+
const generationId = raw.generationId;
|
|
93
|
+
const ownerGenerationId = raw.ownerGenerationId;
|
|
94
|
+
const claimId = raw.claimId;
|
|
95
|
+
if (
|
|
96
|
+
raw.recordingId !== recordingId ||
|
|
97
|
+
!session ||
|
|
98
|
+
typeof session !== "object" ||
|
|
99
|
+
Array.isArray(session) ||
|
|
100
|
+
!(generationId === null || typeof generationId === "string") ||
|
|
101
|
+
(ownerGenerationId !== undefined &&
|
|
102
|
+
ownerGenerationId !== null &&
|
|
103
|
+
typeof ownerGenerationId !== "string") ||
|
|
104
|
+
(claimId !== undefined && claimId !== null && typeof claimId !== "string")
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid resumable cleanup state for recording ${recordingId}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const candidate = session as Record<string, unknown>;
|
|
112
|
+
if (
|
|
113
|
+
typeof candidate.providerId !== "string" ||
|
|
114
|
+
typeof candidate.sessionId !== "string" ||
|
|
115
|
+
!candidate.meta ||
|
|
116
|
+
typeof candidate.meta !== "object" ||
|
|
117
|
+
Array.isArray(candidate.meta) ||
|
|
118
|
+
typeof candidate.bytesUploaded !== "number" ||
|
|
119
|
+
!Number.isFinite(candidate.bytesUploaded) ||
|
|
120
|
+
(candidate.lastCommittedIndex !== undefined &&
|
|
121
|
+
typeof candidate.lastCommittedIndex !== "number")
|
|
122
|
+
) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Invalid resumable session in cleanup state for recording ${recordingId}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
recordingId,
|
|
130
|
+
generationId,
|
|
131
|
+
ownerGenerationId:
|
|
132
|
+
ownerGenerationId === undefined ? null : ownerGenerationId,
|
|
133
|
+
claimId: claimId === undefined ? null : claimId,
|
|
134
|
+
session: candidate as unknown as StoredResumableSession,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
77
138
|
function normalizeVideoMimeType(value: unknown): string | null {
|
|
78
139
|
if (typeof value !== "string") return null;
|
|
79
140
|
const mimeType = value.split(";")[0]?.trim().toLowerCase();
|
|
@@ -219,10 +280,25 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
219
280
|
const nextGenerationId = useGenerationFence ? randomUUID() : null;
|
|
220
281
|
const uploadStateKey = `recording-upload-${recordingId}`;
|
|
221
282
|
const uploadStateSnapshot = await readAppState(uploadStateKey);
|
|
222
|
-
const
|
|
283
|
+
const cleanupStateKey = `recording-resumable-cleanup-${recordingId}`;
|
|
284
|
+
const cleanupStateSnapshot = await readAppState(cleanupStateKey);
|
|
285
|
+
const parsedCleanup = parsePendingResumableCleanup(
|
|
286
|
+
cleanupStateSnapshot,
|
|
223
287
|
recordingId,
|
|
224
|
-
existingGenerationId,
|
|
225
288
|
);
|
|
289
|
+
const pendingCleanup =
|
|
290
|
+
parsedCleanup &&
|
|
291
|
+
(parsedCleanup.ownerGenerationId === null
|
|
292
|
+
? !useGenerationFence
|
|
293
|
+
: parsedCleanup.ownerGenerationId === existingGenerationId)
|
|
294
|
+
? parsedCleanup
|
|
295
|
+
: null;
|
|
296
|
+
const discardedGenerationId = pendingCleanup
|
|
297
|
+
? pendingCleanup.generationId
|
|
298
|
+
: existingGenerationId;
|
|
299
|
+
const discardedResumableSession =
|
|
300
|
+
pendingCleanup?.session ??
|
|
301
|
+
(await getResumableSession(recordingId, existingGenerationId));
|
|
226
302
|
const reset = await db
|
|
227
303
|
.update(schema.recordings)
|
|
228
304
|
.set({
|
|
@@ -257,29 +333,63 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
257
333
|
};
|
|
258
334
|
}
|
|
259
335
|
|
|
336
|
+
let cleanupClaim: PendingResumableCleanup | null = null;
|
|
260
337
|
if (discardedResumableSession) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
338
|
+
// A cleanup claim belongs to the generation that won this fence. A
|
|
339
|
+
// losing reset must never resurrect a claim after the winner releases
|
|
340
|
+
// it, and a later winner must be able to transfer an unfinished claim.
|
|
341
|
+
const candidate: PendingResumableCleanup = {
|
|
342
|
+
recordingId,
|
|
343
|
+
generationId: discardedGenerationId,
|
|
344
|
+
ownerGenerationId: nextGenerationId,
|
|
345
|
+
claimId: randomUUID(),
|
|
346
|
+
session: discardedResumableSession,
|
|
347
|
+
};
|
|
348
|
+
const claimed = await compareAndSetAppState(
|
|
349
|
+
cleanupStateKey,
|
|
350
|
+
cleanupStateSnapshot,
|
|
351
|
+
candidate as unknown as Record<string, unknown>,
|
|
264
352
|
);
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
353
|
+
if (claimed) cleanupClaim = candidate;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (discardedResumableSession) {
|
|
357
|
+
if (cleanupClaim) {
|
|
358
|
+
const cleaned = await abortResumableUploadSession(
|
|
359
|
+
discardedResumableSession,
|
|
360
|
+
{ label: `reset-${recordingId}` },
|
|
361
|
+
);
|
|
362
|
+
if (!cleaned) {
|
|
363
|
+
setResponseStatus(event, 502);
|
|
364
|
+
return {
|
|
365
|
+
error:
|
|
366
|
+
"The previous recording upload could not be cleaned up. Retry the upload restart.",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const released = await compareAndSetAppState(
|
|
370
|
+
cleanupStateKey,
|
|
371
|
+
cleanupClaim as unknown as Record<string, unknown>,
|
|
372
|
+
null,
|
|
373
|
+
);
|
|
374
|
+
if (!released) {
|
|
375
|
+
console.warn(
|
|
376
|
+
`[reset-chunks-${recordingId}] cleanup claim changed while releasing it`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
271
379
|
}
|
|
272
380
|
}
|
|
273
381
|
const cleared = await deleteRecordingChunks(
|
|
274
382
|
ownerEmail,
|
|
275
383
|
recordingId,
|
|
276
|
-
|
|
384
|
+
discardedGenerationId,
|
|
277
385
|
);
|
|
278
386
|
// Clear any stale resumable session so a buffered retry does not
|
|
279
387
|
// accidentally route through handleResumableChunk with stale offsets.
|
|
280
|
-
|
|
281
|
-
()
|
|
282
|
-
|
|
388
|
+
if (!discardedResumableSession || cleanupClaim) {
|
|
389
|
+
await deleteResumableSession(recordingId, discardedGenerationId).catch(
|
|
390
|
+
() => {},
|
|
391
|
+
);
|
|
392
|
+
}
|
|
283
393
|
|
|
284
394
|
let uploadMode: UploadMode = "buffered";
|
|
285
395
|
let compensateStartedSession: (() => Promise<void>) | null = null;
|
|
@@ -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;
|
|
65
66
|
states: {
|
|
66
67
|
clientId: number;
|
|
67
68
|
state: string;
|
|
68
69
|
}[];
|
|
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;
|
|
80
81
|
users: {
|
|
81
82
|
clientId: number;
|
|
82
83
|
lastSeen: number;
|
|
83
84
|
}[];
|
|
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;
|
|
@@ -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;
|
|
14
15
|
count: number;
|
|
15
16
|
updated?: undefined;
|
|
16
|
-
error?: undefined;
|
|
17
17
|
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
|
+
error?: undefined;
|
|
19
20
|
count?: undefined;
|
|
20
21
|
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;
|
|
29
30
|
count?: undefined;
|
|
30
31
|
updated?: undefined;
|
|
31
|
-
error?: undefined;
|
|
32
32
|
ok: boolean;
|
|
33
33
|
}>>;
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Templates"
|
|
3
|
-
description: "Start from a
|
|
3
|
+
description: "Start from a complete SaaS product with a UI, database, auth, and agent already built."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Templates
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Agent-Native apps come in two shapes: templates, which have a full browser UI, and automation-first apps, which don't — a Slack bot, a nightly report, a webhook responder. This doc covers templates; see [Automation-First Apps](/docs/pure-agent-apps) for the other shape.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Building a workflow with no browser UI at all — a Slack bot, a nightly report, a webhook responder? See [Automation-First Apps](/docs/pure-agent-apps) instead; the templates on this page all ship a full browser UI alongside the agent.
|
|
10
|
+
A template is a complete SaaS product: a UI, database, auth, and agent, already built and wired together. Pick one, brand it, and deploy it as your own product instead of starting from a blank scaffold. This model is called Cloneable SaaS: you start from a finished product, not a scaffold.
|
|
13
11
|
|
|
14
12
|
## Example apps {#catalog}
|
|
15
13
|
|
|
16
|
-
Each
|
|
14
|
+
Each app is a complete product you can run today and customize into your own.
|
|
17
15
|
|
|
18
16
|
| App | What it is |
|
|
19
17
|
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -31,9 +29,9 @@ Each one is a real app you could use today, and the launching pad for your own v
|
|
|
31
29
|
| [**Plan**](/docs/template-plan) | Visual plans and PR recaps with diagrams, wireframes, and annotations. |
|
|
32
30
|
| [**Dispatch**](/docs/template-dispatch) | The workspace control plane: shared secrets, reusable integrations, Slack/Telegram, scheduled jobs. |
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
If none of these match your domain, start with [Chat](/docs/template-chat) and extend it with actions, native results, and screens. Use [Automation-First Apps](/docs/pure-agent-apps) if your workflow doesn't need a browser UI.
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
Each app has its own docs covering its architecture, actions, and customization. Browse live hosted versions at [agent-native.com/apps](/apps), or start with [Dispatch](/docs/template-dispatch) for a workspace-style app.
|
|
37
35
|
|
|
38
36
|
## Community templates {#community-templates}
|
|
39
37
|
|
|
@@ -151,20 +149,20 @@ details, the tested CLI command, and an optional hosted demo URL.
|
|
|
151
149
|
|
|
152
150
|
## What you get out of the box {#what-you-get}
|
|
153
151
|
|
|
154
|
-
Every example app
|
|
152
|
+
Every example app includes:
|
|
155
153
|
|
|
156
|
-
- **A working agent** —
|
|
157
|
-
- **Auth** — sign
|
|
158
|
-
- **A database** —
|
|
159
|
-
- **A real-time UI** — the screen stays in sync with
|
|
160
|
-
- **Deploy-ready** —
|
|
161
|
-
- **Branding hooks** — name, colors, logo, copy
|
|
154
|
+
- **A working agent** — wired into the app, able to take actions on your data, and aware of what you're looking at. See [Messaging the agent](/docs/messaging).
|
|
155
|
+
- **Auth** — sign-in, sessions, organizations, and multi-tenant isolation.
|
|
156
|
+
- **A database** — schema, queries, and migrations, ready to use. Bring your own SQL database (Postgres, SQLite, Turso, D1); the framework adapts.
|
|
157
|
+
- **A real-time UI** — the screen stays in sync with the agent. For example, drafting an email in chat updates the inbox immediately.
|
|
158
|
+
- **Deploy-ready** — deploy to Netlify, Vercel, Cloudflare, AWS, or any host that runs Node.
|
|
159
|
+
- **Branding hooks** — change the name, colors, logo, and copy.
|
|
162
160
|
|
|
163
|
-
|
|
161
|
+
The framework's maintainer runs his email, calendar, and analytics on the Mail, Calendar, and Analytics apps.
|
|
164
162
|
|
|
165
163
|
## What you do {#what-you-do}
|
|
166
164
|
|
|
167
|
-
|
|
165
|
+
Four steps take you from template to deployed app:
|
|
168
166
|
|
|
169
167
|
<Diagram id="doc-block-1ab5jb9" title="Start from a working app" summary="Pick a finished product, brand it, evolve it in plain English, and ship it to your own domain.">
|
|
170
168
|
|
|
@@ -214,21 +212,21 @@ The path from "I want my own SaaS" to "I have my own SaaS" is short:
|
|
|
214
212
|
|
|
215
213
|
</Diagram>
|
|
216
214
|
|
|
217
|
-
1. **Pick an
|
|
215
|
+
1. **Pick an app.** Use the CLI picker, or browse the docs.
|
|
218
216
|
2. **Brand it.** Change the name, colors, logo, and copy. Most example apps expose this in a single config file.
|
|
219
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.
|
|
220
|
-
4. **Ship it.** Run the deploy command. You now have
|
|
218
|
+
4. **Ship it.** Run the deploy command. You now have a production app at your own domain.
|
|
221
219
|
|
|
222
|
-
Steps 2–4 typically take
|
|
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.
|
|
223
221
|
|
|
224
222
|
## Why this is practical {#why}
|
|
225
223
|
|
|
226
|
-
A separate
|
|
224
|
+
A separate codebase per user doesn't scale — maintaining a fork of the app for every user becomes unmanageable. Two framework decisions avoid that:
|
|
227
225
|
|
|
228
226
|
1. **The app keeps the framework contract.** Your source owns the domain behavior, while Core keeps actions, SQL, state, and sync consistent as the app evolves.
|
|
229
227
|
2. **Per-user customization without per-user code.** Skills, memory, instructions, connected MCP servers, and sub-agents all live in SQL. Every user gets their own customization layer; the shared codebase hosts all of them at once.
|
|
230
228
|
|
|
231
|
-
The result:
|
|
229
|
+
The result: per-user customization without per-user deployments.
|
|
232
230
|
|
|
233
231
|
<Diagram id="doc-block-bhg7mw" title="Why per-user customization scales" summary={"Two ideas keep the start-from-a-working-app model practical: the app keeps the framework contract, and per-user customization lives in SQL — not in per-user code."}>
|
|
234
232
|
|
|
@@ -275,11 +273,11 @@ The result: Claude-Code-level flexibility for each user, with normal SaaS deploy
|
|
|
275
273
|
|
|
276
274
|
## Don't want to customize? {#hosted}
|
|
277
275
|
|
|
278
|
-
|
|
276
|
+
Every example app is also available as a hosted product on `agent-native.com` — `mail.agent-native.com`, `calendar.agent-native.com`, and so on. Use the hosted version for free or paid; create your own app from a template when you want to change something the hosted version doesn't expose.
|
|
279
277
|
|
|
280
278
|
## Try it with a skill {#try-with-a-skill}
|
|
281
279
|
|
|
282
|
-
|
|
280
|
+
You can add Agent-Native capabilities to a coding agent you already use, without scaffolding an app. See the [Skills Guide](/docs/skills-guide#app-backed-skills).
|
|
283
281
|
|
|
284
282
|
## What's next
|
|
285
283
|
|
|
@@ -292,7 +290,7 @@ Not ready to scaffold? You can add agent-native superpowers to a coding agent yo
|
|
|
292
290
|
|
|
293
291
|
### For developers {#dev-details}
|
|
294
292
|
|
|
295
|
-
|
|
293
|
+
To scaffold a new app:
|
|
296
294
|
|
|
297
295
|
```bash
|
|
298
296
|
npx @agent-native/core@latest create my-platform
|
|
@@ -306,7 +304,7 @@ scaffolded into `apps/<name>/` with every file you need. For a no-browser
|
|
|
306
304
|
automation instead of an app UI, use
|
|
307
305
|
`npx @agent-native/core@latest create my-agent --headless`.
|
|
308
306
|
|
|
309
|
-
Fill in `.env` (
|
|
307
|
+
Fill in `.env` (`ANTHROPIC_API_KEY` and `DATABASE_URL`), then run `pnpm install` and `pnpm dev`. There's no login to implement and no placeholder routes.
|
|
310
308
|
|
|
311
309
|
Deploy targets: any Nitro-compatible host (Node, Cloudflare, Netlify, Vercel, Deno, Lambda, Bun) and any Drizzle-compatible SQL database (SQLite, Postgres, Turso, D1, Supabase, Neon). For workspaces, `npx @agent-native/core@latest deploy` builds every app at once and ships them behind a single origin. See [Deployment](/docs/deployment).
|
|
312
310
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.157.
|
|
3
|
+
"version": "0.157.2",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -423,8 +423,8 @@
|
|
|
423
423
|
"y-protocols": "^1.0.7",
|
|
424
424
|
"yjs": "^13.6.31",
|
|
425
425
|
"zod": "^4.3.6",
|
|
426
|
-
"@agent-native/
|
|
427
|
-
"@agent-native/
|
|
426
|
+
"@agent-native/toolkit": "^0.15.1",
|
|
427
|
+
"@agent-native/recap-cli": "0.5.4"
|
|
428
428
|
},
|
|
429
429
|
"devDependencies": {
|
|
430
430
|
"@ai-sdk/anthropic": "^3.0.71",
|