@agent-native/core 0.84.22 → 0.84.23
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/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/model-config.ts +1 -1
- package/corpus/core/src/cli/skills.ts +17 -3
- package/corpus/core/src/notifications/index.ts +1 -0
- package/corpus/core/src/notifications/registry.ts +13 -1
- package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +18 -1
- package/corpus/templates/analytics/server/db/schema.ts +1 -1
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -1
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +76 -7
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +30 -1
- package/corpus/templates/clips/actions/finalize-recording.ts +84 -4
- package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +256 -0
- package/corpus/templates/clips/actions/reprocess-recording.ts +160 -0
- package/corpus/templates/clips/changelog/2026-07-01-shared-clips-now-start-playing-and-scrub-instantly-instead-o.md +6 -0
- package/corpus/templates/clips/server/lib/video-remux.ts +222 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +75 -19
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +11 -1
- package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-an-empty-state-selection-chip-in-the-composer.md +6 -0
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +2 -0
- package/corpus/templates/videos/.agents/skills/frontend-design/SKILL.md +2 -0
- package/dist/agent/model-config.js +1 -1
- package/dist/agent/model-config.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +17 -3
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +1 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/registry.d.ts +5 -0
- package/dist/notifications/registry.d.ts.map +1 -1
- package/dist/notifications/registry.js +4 -1
- package/dist/notifications/registry.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
- package/package.json +1 -1
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.23
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fde28eb: Expose notification delivery channel results so callers can distinguish custom-channel delivery from inbox persistence.
|
|
8
|
+
- fde28eb: Re-enable Claude Sonnet 5 as the visible and default Sonnet model now that the Builder gateway supports it.
|
|
9
|
+
- fde28eb: Sharpen the exported Design skill's quality bar to defeat "AI slop": frame the variant flow as separating taste/exploration/spec/code, pair every banned default with a concrete alternative, warn about second-order convergence, and add explicit guidance for building on an existing design system or codebase.
|
|
10
|
+
|
|
3
11
|
## 0.84.22
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.23",
|
|
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": {
|
|
@@ -120,7 +120,7 @@ export function getContextWindowForModel(modelId: string): number {
|
|
|
120
120
|
return DEFAULT_CONTEXT_WINDOW;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const ENABLE_CLAUDE_SONNET_5 =
|
|
123
|
+
const ENABLE_CLAUDE_SONNET_5 = true;
|
|
124
124
|
|
|
125
125
|
export const CLAUDE_SONNET_MODEL_ID = ENABLE_CLAUDE_SONNET_5
|
|
126
126
|
? "claude-sonnet-5"
|
|
@@ -363,14 +363,28 @@ iteration, or a human-in-the-loop choice among design directions.
|
|
|
363
363
|
|
|
364
364
|
## Design Quality Bar
|
|
365
365
|
|
|
366
|
+
Generic "AI slop" comes from letting one prompt set taste, explore, and emit code
|
|
367
|
+
at once — so the model returns the training-average (Inter, an indigo/violet
|
|
368
|
+
gradient, a centered hero, three rounded cards). The variant flow above exists to
|
|
369
|
+
separate those jobs; use it, and hold this bar:
|
|
370
|
+
|
|
366
371
|
- Before generating, name the concrete audience, the screen's primary job, and
|
|
367
372
|
the visual thesis. If the brief is vague, make a reasonable choice and state
|
|
368
373
|
it instead of producing a generic dashboard/landing-page default.
|
|
369
|
-
-
|
|
370
|
-
|
|
374
|
+
- Refuse the defaults, and pair every "don't" with a "do" (banning Inter alone
|
|
375
|
+
just makes you reach for Roboto). Avoid Inter/Roboto/system fonts, the
|
|
376
|
+
indigo/violet slop palette (\`#6366F1\`/\`#8B5CF6\`/\`#A855F7\`) and purple-on-white
|
|
377
|
+
gradients, and centered-hero + three-icon-card layouts; instead pick a
|
|
378
|
+
distinctive font pairing, one non-default palette family with a single decisive
|
|
379
|
+
accent, and an asymmetric layout with a clear focal point.
|
|
371
380
|
- Make each direction distinct in structure and behavior, not just palette.
|
|
372
381
|
Give every variant one memorable signature choice, then keep the surrounding
|
|
373
|
-
chrome disciplined.
|
|
382
|
+
chrome disciplined. Even your creative picks converge (Space Grotesk
|
|
383
|
+
everywhere) — vary deliberately so two directions never share a fingerprint.
|
|
384
|
+
- For existing products, inspect the current screen, design system, tokens, and
|
|
385
|
+
component language before inventing a new direction. Treat any drift back to a
|
|
386
|
+
default as a missing token to pin, and vary layout per screen so on-brand does
|
|
387
|
+
not become same-in-your-colors.
|
|
374
388
|
- Treat copy, data, and imagery as design material. Use realistic domain
|
|
375
389
|
content and first-party/generated assets when images matter; avoid lorem
|
|
376
390
|
ipsum, vague SaaS filler, and decorative placeholder boxes.
|
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
type Notification,
|
|
14
14
|
} from "./types.js";
|
|
15
15
|
|
|
16
|
+
export interface NotificationDeliveryResult {
|
|
17
|
+
notification?: Notification;
|
|
18
|
+
deliveredChannels: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
registerEvent({
|
|
17
22
|
name: "notification.sent",
|
|
18
23
|
description:
|
|
@@ -84,6 +89,13 @@ export async function notify(
|
|
|
84
89
|
input: NotificationInput,
|
|
85
90
|
meta: NotificationMeta,
|
|
86
91
|
): Promise<Notification | undefined> {
|
|
92
|
+
return (await notifyWithDelivery(input, meta)).notification;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function notifyWithDelivery(
|
|
96
|
+
input: NotificationInput,
|
|
97
|
+
meta: NotificationMeta,
|
|
98
|
+
): Promise<NotificationDeliveryResult> {
|
|
87
99
|
if (!meta?.owner) {
|
|
88
100
|
throw new Error("notify: meta.owner is required");
|
|
89
101
|
}
|
|
@@ -166,7 +178,7 @@ export async function notify(
|
|
|
166
178
|
}
|
|
167
179
|
}
|
|
168
180
|
|
|
169
|
-
return stored;
|
|
181
|
+
return { notification: stored, deliveredChannels: delivered };
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
function selectChannels(allowlist?: string[]): NotificationChannel[] {
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -135,7 +135,24 @@ function siteOrigin(request) {
|
|
|
135
135
|
return url.origin;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
async function readScheduledInvocation(request) {
|
|
139
|
+
if (request.method !== "POST") return null;
|
|
140
|
+
let body;
|
|
141
|
+
try {
|
|
142
|
+
body = await request.json();
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const nextRun = typeof body?.next_run === "string" ? body.next_run : "";
|
|
147
|
+
return nextRun && Number.isFinite(Date.parse(nextRun))
|
|
148
|
+
? { nextRun }
|
|
149
|
+
: null;
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
export default async function handler(request) {
|
|
153
|
+
const scheduled = await readScheduledInvocation(request);
|
|
154
|
+
if (!scheduled) return new Response("Not Found", { status: 404 });
|
|
155
|
+
|
|
139
156
|
const url = new URL(WORKER_PATH, siteOrigin(request));
|
|
140
157
|
const response = await fetch(url.toString(), {
|
|
141
158
|
method: "POST",
|
|
@@ -143,7 +160,7 @@ export default async function handler(request) {
|
|
|
143
160
|
"content-type": "application/json",
|
|
144
161
|
"x-agent-native-analytics-alert-cron": CRON_TOKEN,
|
|
145
162
|
},
|
|
146
|
-
body: JSON.stringify({ scheduled: true }),
|
|
163
|
+
body: JSON.stringify({ scheduled: true, next_run: scheduled.nextRun }),
|
|
147
164
|
});
|
|
148
165
|
|
|
149
166
|
if (!response.ok && response.status !== 202) {
|
|
@@ -207,7 +207,7 @@ export const analyticsAlertRules = table("analytics_alert_rules", {
|
|
|
207
207
|
lastEvaluatedAt: text("last_evaluated_at"),
|
|
208
208
|
lastTriggeredAt: text("last_triggered_at"),
|
|
209
209
|
lastStatus: text("last_status", {
|
|
210
|
-
enum: ["ok", "triggered", "cooldown", "error"],
|
|
210
|
+
enum: ["ok", "triggered", "cooldown", "error", "running"],
|
|
211
211
|
}),
|
|
212
212
|
lastError: text("last_error"),
|
|
213
213
|
createdAt: text("created_at").notNull().default(now()),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
claimAnalyticsAlertRuleEvaluation,
|
|
2
3
|
evaluateAndNotifyAnalyticsAlertRule,
|
|
3
4
|
listEnabledAnalyticsAlertRules,
|
|
4
5
|
markAnalyticsAlertRuleError,
|
|
@@ -50,8 +51,10 @@ export async function runAnalyticsAlertsOnce(
|
|
|
50
51
|
remaining = rules.length >= sweepLimit ? 1 : 0;
|
|
51
52
|
|
|
52
53
|
for (const rule of rules) {
|
|
53
|
-
processed++;
|
|
54
54
|
try {
|
|
55
|
+
const claimed = await claimAnalyticsAlertRuleEvaluation(rule);
|
|
56
|
+
if (!claimed) continue;
|
|
57
|
+
processed++;
|
|
55
58
|
const result = await evaluateAndNotifyAnalyticsAlertRule(rule);
|
|
56
59
|
if (result.status === "triggered") triggered++;
|
|
57
60
|
} catch (err) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { notifyWithDelivery } from "@agent-native/core/notifications";
|
|
4
4
|
import { recordChange } from "@agent-native/core/server";
|
|
5
|
-
import { and, asc, desc, eq, gte, isNull, lte, sql } from "drizzle-orm";
|
|
5
|
+
import { and, asc, desc, eq, gte, isNull, lte, or, sql } from "drizzle-orm";
|
|
6
6
|
|
|
7
7
|
import { getDb, schema } from "../db/index.js";
|
|
8
8
|
|
|
@@ -21,7 +21,12 @@ export interface AnalyticsAlertFilter {
|
|
|
21
21
|
|
|
22
22
|
export type AnalyticsAlertThresholdMode = "event_count" | "distinct_count";
|
|
23
23
|
export type AnalyticsAlertSeverity = "warning" | "critical";
|
|
24
|
-
export type AnalyticsAlertStatus =
|
|
24
|
+
export type AnalyticsAlertStatus =
|
|
25
|
+
| "ok"
|
|
26
|
+
| "triggered"
|
|
27
|
+
| "cooldown"
|
|
28
|
+
| "error"
|
|
29
|
+
| "running";
|
|
25
30
|
|
|
26
31
|
export interface AnalyticsAlertRuleInput {
|
|
27
32
|
id?: string;
|
|
@@ -108,6 +113,8 @@ function nowIso(): string {
|
|
|
108
113
|
return new Date().toISOString();
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
const ALERT_RULE_RUNNING_STALE_MS = 15 * 60 * 1000;
|
|
117
|
+
|
|
111
118
|
function safeJsonParse<T>(raw: unknown, fallback: T): T {
|
|
112
119
|
if (typeof raw !== "string" || !raw.trim()) return fallback;
|
|
113
120
|
try {
|
|
@@ -372,6 +379,7 @@ export async function listEnabledAnalyticsAlertRules(options: {
|
|
|
372
379
|
options.orgId ? eq(table.orgId, options.orgId) : isNull(table.orgId),
|
|
373
380
|
);
|
|
374
381
|
}
|
|
382
|
+
clauses.push(alertRuleNotRunningWhere(new Date()));
|
|
375
383
|
const rows = await db
|
|
376
384
|
.select()
|
|
377
385
|
.from(table)
|
|
@@ -385,6 +393,52 @@ export async function listEnabledAnalyticsAlertRules(options: {
|
|
|
385
393
|
return rows.map(rowToRule);
|
|
386
394
|
}
|
|
387
395
|
|
|
396
|
+
function alertRuleNotRunningWhere(now: Date) {
|
|
397
|
+
const table = schema.analyticsAlertRules;
|
|
398
|
+
const staleRunningBefore = new Date(
|
|
399
|
+
now.getTime() - ALERT_RULE_RUNNING_STALE_MS,
|
|
400
|
+
).toISOString();
|
|
401
|
+
return or(
|
|
402
|
+
isNull(table.lastStatus),
|
|
403
|
+
sql`${table.lastStatus} <> 'running'`,
|
|
404
|
+
isNull(table.lastEvaluatedAt),
|
|
405
|
+
lte(table.lastEvaluatedAt, staleRunningBefore),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function alertRulePreviousEvaluationWhere(rule: AnalyticsAlertRule) {
|
|
410
|
+
const table = schema.analyticsAlertRules;
|
|
411
|
+
return rule.lastEvaluatedAt
|
|
412
|
+
? eq(table.lastEvaluatedAt, rule.lastEvaluatedAt)
|
|
413
|
+
: isNull(table.lastEvaluatedAt);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function claimAnalyticsAlertRuleEvaluation(
|
|
417
|
+
rule: AnalyticsAlertRule,
|
|
418
|
+
now: Date = new Date(),
|
|
419
|
+
): Promise<boolean> {
|
|
420
|
+
const db = getDb() as any;
|
|
421
|
+
const table = schema.analyticsAlertRules;
|
|
422
|
+
const claimedAt = now.toISOString();
|
|
423
|
+
const rows = await db
|
|
424
|
+
.update(table)
|
|
425
|
+
.set({
|
|
426
|
+
lastEvaluatedAt: claimedAt,
|
|
427
|
+
lastStatus: "running",
|
|
428
|
+
lastError: null,
|
|
429
|
+
})
|
|
430
|
+
.where(
|
|
431
|
+
and(
|
|
432
|
+
eq(table.id, rule.id),
|
|
433
|
+
eq(table.enabled, true),
|
|
434
|
+
alertRuleNotRunningWhere(now),
|
|
435
|
+
alertRulePreviousEvaluationWhere(rule),
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
.returning({ id: table.id });
|
|
439
|
+
return rows.length > 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
388
442
|
export async function evaluateAndNotifyAnalyticsAlertRule(
|
|
389
443
|
rule: AnalyticsAlertRule,
|
|
390
444
|
now: Date = new Date(),
|
|
@@ -416,12 +470,13 @@ export async function evaluateAndNotifyAnalyticsAlertRule(
|
|
|
416
470
|
}
|
|
417
471
|
|
|
418
472
|
const body = alertBody(rule, evaluation);
|
|
419
|
-
const
|
|
473
|
+
const channels = ensureInboxNotificationChannel(rule.channels);
|
|
474
|
+
const delivery = await notifyWithDelivery(
|
|
420
475
|
{
|
|
421
476
|
severity: rule.severity,
|
|
422
477
|
title: `Analytics alert: ${rule.name}`,
|
|
423
478
|
body,
|
|
424
|
-
channels
|
|
479
|
+
channels,
|
|
425
480
|
metadata: {
|
|
426
481
|
kind: "analytics_alert",
|
|
427
482
|
ruleId: rule.id,
|
|
@@ -438,13 +493,22 @@ export async function evaluateAndNotifyAnalyticsAlertRule(
|
|
|
438
493
|
filters: rule.filters,
|
|
439
494
|
sampleEvents: evaluation.sampleEvents,
|
|
440
495
|
emailRecipients: rule.emailRecipients,
|
|
496
|
+
requestedChannels: rule.channels,
|
|
441
497
|
},
|
|
442
498
|
},
|
|
443
499
|
{ owner: rule.ownerEmail },
|
|
444
500
|
);
|
|
501
|
+
if (delivery.deliveredChannels.length === 0) {
|
|
502
|
+
await markRuleStatus(rule.id, {
|
|
503
|
+
lastEvaluatedAt: evaluatedAt,
|
|
504
|
+
lastStatus: "error",
|
|
505
|
+
lastError: "Analytics alert notification was not delivered.",
|
|
506
|
+
});
|
|
507
|
+
return { ruleId: rule.id, status: "error", ...evaluation };
|
|
508
|
+
}
|
|
445
509
|
|
|
446
510
|
await recordIncident(rule, {
|
|
447
|
-
notificationId:
|
|
511
|
+
notificationId: delivery.notification?.id,
|
|
448
512
|
triggeredAt: evaluatedAt,
|
|
449
513
|
windowStart,
|
|
450
514
|
windowEnd,
|
|
@@ -459,11 +523,16 @@ export async function evaluateAndNotifyAnalyticsAlertRule(
|
|
|
459
523
|
return {
|
|
460
524
|
ruleId: rule.id,
|
|
461
525
|
status: "triggered",
|
|
462
|
-
notificationId:
|
|
526
|
+
notificationId: delivery.notification?.id,
|
|
463
527
|
...evaluation,
|
|
464
528
|
};
|
|
465
529
|
}
|
|
466
530
|
|
|
531
|
+
function ensureInboxNotificationChannel(channels: string[]): string[] {
|
|
532
|
+
const normalized = channels.map((channel) => channel.trim()).filter(Boolean);
|
|
533
|
+
return normalized.includes("inbox") ? normalized : ["inbox", ...normalized];
|
|
534
|
+
}
|
|
535
|
+
|
|
467
536
|
export async function markAnalyticsAlertRuleError(
|
|
468
537
|
id: string,
|
|
469
538
|
error: unknown,
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -53,6 +53,8 @@ Default to Apple/Linear-level restraint: make the primary workflow obvious, then
|
|
|
53
53
|
- **Visual assets**: Websites, games, and object-focused pages need real or generated media when images help users understand the subject.
|
|
54
54
|
- **Responsive fit**: Text must not overflow buttons, cards, tabs, sidebars, or fixed-format tools. Use stable dimensions for boards, grids, toolbars, and counters.
|
|
55
55
|
|
|
56
|
+
**Beat convergence, not just defaults.** You sample toward the "on-distribution" center, so naming what to avoid is not enough: every "don't" needs a "do", or you converge on the next safe option (ban Inter and you reach for Roboto; ban purple gradients and you reach for Space Grotesk + a teal accent on every screen). Commit to one named direction, pair any reference with the reason it fits ("Linear: the quiet confidence of its spacing" — a bare "Linear" collapses back to the average), and match implementation effort to the vision: maximalist wants elaborate motion and effects, minimal wants restraint and precise spacing. When building on an existing app, inspect its tokens/type/components first and treat any drift back to a default as a missing token to pin, not something to re-prompt.
|
|
57
|
+
|
|
56
58
|
## Agent-Native UI Rules
|
|
57
59
|
|
|
58
60
|
- Agent-native apps use React, Vite, Tailwind CSS, shadcn/ui, and `@tabler/icons-react`.
|
|
@@ -31,9 +31,38 @@ Some recordings are linked to a meeting — when `meeting_id` is non-null on the
|
|
|
31
31
|
4. **Record.** Start a `MediaRecorder` with `mimeType: "video/webm;codecs=vp9,opus"` (fallback to vp8, then browser default). Use `timeslice: 2000` so chunks arrive every 2s.
|
|
32
32
|
5. **Upload each chunk.** `ondataavailable` POSTs the chunk bytes to `/api/uploads/chunk` with headers `X-Recording-Id` and `X-Chunk-Index`. Don't retry inline — buffer failed chunks in `IndexedDB` and let a background worker re-send.
|
|
33
33
|
6. **Live transcription.** Alongside the MediaRecorder, `useLiveTranscription` runs the Web Speech API to accumulate transcript text in real time. On stop, the client calls `save-browser-transcript` to persist the result immediately — no API key needed. Desktop recordings use local Whisper/macOS speech first when available, and fall back to Web Speech in the webview on non-mac before relying on upload transcription.
|
|
34
|
-
7. **Finalize.** On stop, send the final chunk to `/api/uploads/:id/chunk?isFinal=1`. The route calls `finalize-recording`, which stitches chunks, uploads the finished media when storage is configured, transitions `status` to `ready`, then kicks off `request-transcript` for higher-quality output (see `ai-video-tools`).
|
|
34
|
+
7. **Finalize.** On stop, send the final chunk to `/api/uploads/:id/chunk?isFinal=1`. The route calls `finalize-recording`, which stitches chunks, makes the media seekable (see below), uploads the finished media when storage is configured, transitions `status` to `ready`, then kicks off `request-transcript` for higher-quality output (see `ai-video-tools`).
|
|
35
35
|
8. **Navigate.** Once the row is `ready` the UI navigates to `/r/:id`.
|
|
36
36
|
|
|
37
|
+
## Seekable playback (don't ship raw MediaRecorder output)
|
|
38
|
+
|
|
39
|
+
Raw `MediaRecorder` files are not friendly to progressive HTTP playback, which
|
|
40
|
+
shows up as "clip takes minutes to load" and "re-buffers every time I seek"
|
|
41
|
+
even though the file downloads fine:
|
|
42
|
+
|
|
43
|
+
- **MP4** is written with the `moov` metadata atom *after* `mdat`, so a player
|
|
44
|
+
must fetch the whole file before it can start or seek.
|
|
45
|
+
- **WebM** is a live stream with no Cues (seek index) and an unknown Segment
|
|
46
|
+
duration, so Chrome won't honor `currentTime = X` and has to scan/download.
|
|
47
|
+
|
|
48
|
+
`finalize-recording` fixes this before upload: MP4 gets pure-TS faststart
|
|
49
|
+
(`server/lib/faststart.ts`), WebM gets a lossless `ffmpeg -c copy` remux that
|
|
50
|
+
writes a SeekHead + Cues + real duration (`server/lib/video-remux.ts`). Both are
|
|
51
|
+
best-effort — on failure we upload the original, never block finalize. Recordings
|
|
52
|
+
above `CLIPS_INLINE_REMUX_MAX_BYTES` (default 200 MB) skip the inline pass and
|
|
53
|
+
are repaired in the background.
|
|
54
|
+
|
|
55
|
+
The **streaming/resumable** upload path forwards raw bytes straight to the
|
|
56
|
+
provider and cannot rewrite them inline, so `finalize-recording` schedules a
|
|
57
|
+
background `ensureRecordingSeekable` pass for those.
|
|
58
|
+
|
|
59
|
+
To repair clips uploaded before this existed (or via streaming), call the
|
|
60
|
+
`reprocess-recording` action: `--id`, `--ids='[...]'`, or `--all --limit=N`. It
|
|
61
|
+
re-fetches provider media, rewrites it, re-uploads, and repoints the row. It's
|
|
62
|
+
idempotent (already-seekable clips are skipped unless `--force`) and only touches
|
|
63
|
+
provider-hosted clips owned by the caller. This is the right tool when a user
|
|
64
|
+
reports a specific slow/buffering clip.
|
|
65
|
+
|
|
37
66
|
## Loom import
|
|
38
67
|
|
|
39
68
|
Use `import-loom-recording` for Loom share or embed URLs. The action validates
|
|
@@ -40,12 +40,27 @@ import {
|
|
|
40
40
|
getResumableSession,
|
|
41
41
|
} from "../server/lib/resumable-session.js";
|
|
42
42
|
import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
|
|
43
|
+
import { remuxWebmToSeekable } from "../server/lib/video-remux.js";
|
|
43
44
|
import {
|
|
44
45
|
requiresConfiguredVideoStorage,
|
|
45
46
|
STORAGE_SETUP_REQUIRED_REASON,
|
|
46
47
|
} from "../server/lib/video-storage.js";
|
|
48
|
+
import {
|
|
49
|
+
ensureRecordingSeekable,
|
|
50
|
+
markRecordingSeekable,
|
|
51
|
+
} from "./lib/ensure-seekable-video.js";
|
|
47
52
|
import requestTranscript from "./request-transcript.js";
|
|
48
53
|
|
|
54
|
+
// Recordings up to this size get their seekable rewrite applied inline during
|
|
55
|
+
// finalize (we already hold the assembled bytes). Larger recordings are handed
|
|
56
|
+
// off to the background/reprocess path so we don't stretch the finalize
|
|
57
|
+
// request or exhaust serverless /tmp. Override with CLIPS_INLINE_REMUX_MAX_BYTES.
|
|
58
|
+
function inlineRemuxMaxBytes(): number {
|
|
59
|
+
const raw = Number(process.env.CLIPS_INLINE_REMUX_MAX_BYTES ?? "");
|
|
60
|
+
if (Number.isFinite(raw) && raw > 0) return Math.floor(raw);
|
|
61
|
+
return 200 * 1024 * 1024;
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
/**
|
|
50
65
|
* Decode a base64 string back into a Uint8Array.
|
|
51
66
|
* We store chunks as base64 in application_state because the SQL JSON
|
|
@@ -118,6 +133,10 @@ async function markRecordingReady(params: {
|
|
|
118
133
|
finalHasAudio: boolean;
|
|
119
134
|
finalHasCamera: boolean;
|
|
120
135
|
existingTitle: string;
|
|
136
|
+
// Whether a seekable rewrite (MP4 faststart / WebM Cues remux) was already
|
|
137
|
+
// applied to the uploaded bytes. When false, a best-effort background repair
|
|
138
|
+
// is triggered so streamed/raw uploads still become seekable.
|
|
139
|
+
seekableApplied: boolean;
|
|
121
140
|
}) {
|
|
122
141
|
const {
|
|
123
142
|
id,
|
|
@@ -131,6 +150,7 @@ async function markRecordingReady(params: {
|
|
|
131
150
|
finalHasAudio,
|
|
132
151
|
finalHasCamera,
|
|
133
152
|
existingTitle,
|
|
153
|
+
seekableApplied,
|
|
134
154
|
} = params;
|
|
135
155
|
const db = getDb();
|
|
136
156
|
const now = new Date().toISOString();
|
|
@@ -181,6 +201,30 @@ async function markRecordingReady(params: {
|
|
|
181
201
|
});
|
|
182
202
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
183
203
|
|
|
204
|
+
if (seekableApplied) {
|
|
205
|
+
// Uploaded bytes are already start-playable and seekable — remember it so
|
|
206
|
+
// later reprocess sweeps skip this clip.
|
|
207
|
+
await markRecordingSeekable(id, videoUrl).catch((err) => {
|
|
208
|
+
console.warn("[finalize] failed to write seekable marker", {
|
|
209
|
+
id,
|
|
210
|
+
error: err instanceof Error ? err.message : String(err),
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
// Streaming/resumable (or oversized) uploads shipped raw MediaRecorder
|
|
215
|
+
// bytes with no seekable rewrite: an MP4 with a trailing moov or a WebM
|
|
216
|
+
// without a Cues index buffers on load and re-buffers on every seek. Fix
|
|
217
|
+
// it in the background so playback is smooth without blocking finalize.
|
|
218
|
+
void Promise.resolve(
|
|
219
|
+
ensureRecordingSeekable({ recordingId: id, ownerEmail }),
|
|
220
|
+
).catch((err: unknown) => {
|
|
221
|
+
console.warn("[finalize] background seekable remux failed", {
|
|
222
|
+
id,
|
|
223
|
+
error: err instanceof Error ? err.message : String(err),
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
184
228
|
// Kick off transcription in the background — fire-and-forget so the chunk
|
|
185
229
|
// endpoint gets a quick response. The request context (user email via
|
|
186
230
|
// AsyncLocalStorage) carries through to async continuations.
|
|
@@ -375,6 +419,10 @@ export default defineAction({
|
|
|
375
419
|
...readyParams,
|
|
376
420
|
videoUrl,
|
|
377
421
|
videoSizeBytes: resumableSession.bytesUploaded,
|
|
422
|
+
// Streaming path forwards raw MediaRecorder bytes straight to the
|
|
423
|
+
// provider — no faststart/Cues rewrite happened. Repair in the
|
|
424
|
+
// background.
|
|
425
|
+
seekableApplied: false,
|
|
378
426
|
});
|
|
379
427
|
// Delete only after durable state is written — so a retry before
|
|
380
428
|
// this point can still find the session and re-enter this path.
|
|
@@ -521,9 +569,14 @@ export default defineAction({
|
|
|
521
569
|
// recordings.
|
|
522
570
|
parts.length = 0;
|
|
523
571
|
|
|
524
|
-
//
|
|
525
|
-
//
|
|
572
|
+
// Make the assembled recording seekable before upload — we already hold
|
|
573
|
+
// the full bytes, so a viewer never has to wait through a non-seekable
|
|
574
|
+
// first play. MP4: relocate moov ahead of mdat (pure TS). WebM: remux to
|
|
575
|
+
// add a Cues index + real duration (ffmpeg -c copy). When neither runs
|
|
576
|
+
// (unknown format, oversized, or ffmpeg unavailable) `seekableApplied`
|
|
577
|
+
// stays false and markRecordingReady schedules a background repair.
|
|
526
578
|
let uploadData = assembled;
|
|
579
|
+
let seekableApplied = false;
|
|
527
580
|
if (videoFormat === "mp4") {
|
|
528
581
|
try {
|
|
529
582
|
uploadData = applyFaststart(assembled);
|
|
@@ -561,6 +614,32 @@ export default defineAction({
|
|
|
561
614
|
}
|
|
562
615
|
throw err;
|
|
563
616
|
}
|
|
617
|
+
// moov is present and validated — the MP4 is start-playable/seekable.
|
|
618
|
+
seekableApplied = true;
|
|
619
|
+
} else if (videoFormat === "webm") {
|
|
620
|
+
// MediaRecorder WebM has no Cues index and an unknown duration, so
|
|
621
|
+
// Chrome buffers on load and re-buffers on every seek. A lossless
|
|
622
|
+
// `ffmpeg -c copy` remux rewrites it with a SeekHead + Cues + real
|
|
623
|
+
// duration. Bounded by size so finalize stays fast; larger clips get a
|
|
624
|
+
// background pass. Best-effort: on any failure we upload the original.
|
|
625
|
+
if (assembled.byteLength <= inlineRemuxMaxBytes()) {
|
|
626
|
+
try {
|
|
627
|
+
const seekable = await remuxWebmToSeekable(uploadData);
|
|
628
|
+
if (seekable.changed) {
|
|
629
|
+
uploadData = seekable.bytes;
|
|
630
|
+
seekableApplied = true;
|
|
631
|
+
debugLog("[finalize] webm remux applied", {
|
|
632
|
+
id,
|
|
633
|
+
bytes: uploadData.byteLength,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
console.warn("[finalize] webm remux failed, uploading as-is", {
|
|
638
|
+
id,
|
|
639
|
+
err: err instanceof Error ? err.message : String(err),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
564
643
|
}
|
|
565
644
|
|
|
566
645
|
let upload: Awaited<ReturnType<typeof uploadFile>>;
|
|
@@ -721,12 +800,13 @@ export default defineAction({
|
|
|
721
800
|
debugLog("[finalize] done", {
|
|
722
801
|
id,
|
|
723
802
|
videoUrl: upload.url,
|
|
724
|
-
bytes:
|
|
803
|
+
bytes: uploadData.byteLength,
|
|
725
804
|
});
|
|
726
805
|
return markRecordingReady({
|
|
727
806
|
...readyParams,
|
|
728
807
|
videoUrl: upload.url,
|
|
729
|
-
videoSizeBytes:
|
|
808
|
+
videoSizeBytes: uploadData.byteLength,
|
|
809
|
+
seekableApplied,
|
|
730
810
|
});
|
|
731
811
|
} finally {
|
|
732
812
|
// Unconditional chunk scratch-space cleanup. Runs on success AND on
|