@agent-native/core 0.191.0 → 0.192.0-nightly-20260926053942

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +4 -0
  3. package/corpus/templates/analytics/app/components/layout/Layout.tsx +41 -12
  4. package/corpus/templates/analytics/app/lib/chat-handoff.ts +62 -0
  5. package/corpus/templates/analytics/changelog/2026-09-25-chat-sidebar-stays-closed-in-settings.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-09-25-show-one-recent-prompt-per-chat-turn.md +6 -0
  7. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +74 -11
  8. package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
  9. package/corpus/templates/clips/changelog/2026-09-25-one-click-builder-storage.md +6 -0
  10. package/corpus/templates/design/actions/get-design.schema.ts +19 -0
  11. package/corpus/templates/design/actions/get-design.ts +16 -19
  12. package/corpus/templates/mail/actions/apply-ai-filter.ts +8 -0
  13. package/corpus/templates/mail/actions/create-automation.ts +7 -1
  14. package/corpus/templates/mail/actions/manage-ai-filter-rule-undo.ts +34 -0
  15. package/corpus/templates/mail/actions/manage-automations.ts +21 -44
  16. package/corpus/templates/mail/app/components/email/EmailList.tsx +35 -0
  17. package/corpus/templates/mail/app/components/onboarding/AiInboxSetup.tsx +116 -34
  18. package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +108 -70
  19. package/corpus/templates/mail/app/components/settings/AiRulePromptField.tsx +8 -1
  20. package/corpus/templates/mail/app/components/settings/JevConnectionPrompt.tsx +171 -0
  21. package/corpus/templates/mail/app/hooks/use-automations.ts +24 -0
  22. package/corpus/templates/mail/app/i18n/en-US.ts +8 -2
  23. package/corpus/templates/mail/app/pages/InboxPage.tsx +12 -2
  24. package/corpus/templates/mail/changelog/2026-09-25-mail-now-guides-users-to-connect-jev-before-setting-up-triag.md +6 -0
  25. package/corpus/templates/mail/server/db/schema.ts +17 -0
  26. package/corpus/templates/mail/server/lib/ai-filter-rule-undo.ts +151 -0
  27. package/corpus/templates/mail/server/lib/automations.ts +34 -0
  28. package/corpus/templates/mail/server/plugins/db.ts +18 -0
  29. package/corpus/templates/mail/server/plugins/mail-jobs.ts +6 -0
  30. package/dist/automation/index.d.ts +1 -1
  31. package/dist/client/AssistantChat.js +20 -3
  32. package/dist/client/BuilderReferralInviteRow.d.ts +3 -0
  33. package/dist/client/BuilderReferralInviteRow.js +16 -0
  34. package/dist/client/agent-chat-adapter.js +6 -5
  35. package/dist/client/chat/run-recovery.js +6 -5
  36. package/dist/client/settings/UsageSection.js +2 -1
  37. package/dist/client/settings/index.d.ts +1 -1
  38. package/dist/client/settings/index.js +1 -1
  39. package/dist/client/settings/useBuilderStatus.d.ts +4 -1
  40. package/dist/client/settings/useBuilderStatus.js +32 -16
  41. package/dist/client/use-agent-chat-running-threads.d.ts +2 -0
  42. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  43. package/dist/framework-tools.d.ts +3 -1
  44. package/dist/framework-tools.js +2 -0
  45. package/dist/localization/core-messages/ar-SA.js +4 -0
  46. package/dist/localization/core-messages/de-DE.js +4 -0
  47. package/dist/localization/core-messages/en-US.d.ts +4 -0
  48. package/dist/localization/core-messages/en-US.js +4 -0
  49. package/dist/localization/core-messages/es-ES.js +4 -0
  50. package/dist/localization/core-messages/fr-FR.js +4 -0
  51. package/dist/localization/core-messages/hi-IN.js +4 -0
  52. package/dist/localization/core-messages/ja-JP.js +4 -0
  53. package/dist/localization/core-messages/ko-KR.js +4 -0
  54. package/dist/localization/core-messages/pt-BR.js +4 -0
  55. package/dist/localization/core-messages/zh-CN.js +4 -0
  56. package/dist/localization/core-messages/zh-TW.js +4 -0
  57. package/dist/localization/core-messages.d.ts +4 -0
  58. package/dist/notifications/routes.d.ts +6 -6
  59. package/dist/observability/routes.d.ts +5 -5
  60. package/dist/progress/routes.d.ts +1 -1
  61. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  62. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  63. package/dist/server/action-discovery.js +4 -0
  64. package/dist/server/agent-chat/plugin-options.d.ts +1 -1
  65. package/dist/server/agent-chat-plugin.js +6 -4
  66. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  67. package/dist/server/fusion-app.d.ts +3 -0
  68. package/dist/server/fusion-app.js +17 -0
  69. package/dist/server/transcribe-voice.d.ts +1 -1
  70. package/dist/shared/builder-referrals.d.ts +10 -0
  71. package/dist/shared/builder-referrals.js +29 -0
  72. package/dist/usage/actions/get-builder-referral-info.d.ts +9 -0
  73. package/dist/usage/actions/get-builder-referral-info.js +26 -0
  74. package/dist/usage/metrics-store.js +142 -30
  75. package/dist/vite/action-types-plugin.js +4 -0
  76. package/docs/content/actions-agent-tools.mdx +2 -1
  77. package/package.json +4 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 10028
34
+ - template files: 10036
@@ -16,6 +16,10 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
16
16
  - Human review previews Analytics dashboards as real charts
17
17
  - Relevant metric definitions and saved dashboard examples are available with the first response so Analytics queries can reuse proven definitions and query shapes.
18
18
 
19
+ ### Fixed
20
+
21
+ - Opening Settings no longer opens the chat sidebar.
22
+
19
23
  ## 2026-09-24
20
24
 
21
25
  ### Improved
@@ -18,13 +18,19 @@ import {
18
18
  CreativeContextComposerChip,
19
19
  useCreativeContextLab,
20
20
  } from "@agent-native/creative-context/client";
21
- import { useEffect, useMemo } from "react";
21
+ import { useEffect, useMemo, useRef } from "react";
22
22
  import { useLocation, useNavigate } from "react-router";
23
23
 
24
24
  import { useNavigationState } from "@/hooks/use-navigation-state";
25
25
  import {
26
26
  ANALYTICS_CHAT_STORAGE_KEY,
27
+ ANALYTICS_RECENT_CHAT_HANDOFF_TTL_MS,
28
+ discardAnalyticsChatHandoffOnSettings,
29
+ hasTrackedAnalyticsChatRun,
30
+ isAnalyticsSettingsPath,
27
31
  markAnalyticsChatActivity,
32
+ updateAnalyticsChatHandoffForRun,
33
+ type AnalyticsChatRunningRuns,
28
34
  } from "@/lib/chat-handoff";
29
35
  import { TAB_ID } from "@/lib/tab-id";
30
36
 
@@ -164,35 +170,58 @@ function InteractiveLayout({ children }: LayoutProps) {
164
170
  location.pathname === "/monitoring" ||
165
171
  location.pathname.startsWith("/monitoring/");
166
172
  const isAskRoute = location.pathname === "/ask";
173
+ const isSettingsRoute = isAnalyticsSettingsPath(location.pathname);
174
+ const runningRuns = useRef<AnalyticsChatRunningRuns>(new Map());
167
175
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
168
176
  storageKey: ANALYTICS_CHAT_STORAGE_KEY,
169
177
  activePath: location.pathname,
170
- enabled: !isAskRoute,
178
+ ttlMs: ANALYTICS_RECENT_CHAT_HANDOFF_TTL_MS,
179
+ enabled: !isAskRoute && !isSettingsRoute,
171
180
  });
172
- const chatHomeHandoffPending = isAgentChatHomeHandoffActive(
173
- ANALYTICS_CHAT_STORAGE_KEY,
174
- );
181
+ const chatHomeHandoffPending =
182
+ !isSettingsRoute &&
183
+ isAgentChatHomeHandoffActive(ANALYTICS_CHAT_STORAGE_KEY, {
184
+ ttlMs: ANALYTICS_RECENT_CHAT_HANDOFF_TTL_MS,
185
+ });
175
186
  useAgentChatHomeHandoffLinks({
176
187
  storageKey: ANALYTICS_CHAT_STORAGE_KEY,
177
188
  chatPath: "/ask",
189
+ ttlMs: ANALYTICS_RECENT_CHAT_HANDOFF_TTL_MS,
178
190
  enabled: true,
179
191
  requireActiveHandoff: true,
180
192
  });
193
+ useEffect(() => {
194
+ discardAnalyticsChatHandoffOnSettings(location.pathname);
195
+ }, [location.pathname]);
196
+ useEffect(() => {
197
+ if (!isAskRoute) return;
198
+ const refreshHandoff = () => {
199
+ if (!hasTrackedAnalyticsChatRun(runningRuns.current)) return;
200
+ markAgentChatHomeHandoff(ANALYTICS_CHAT_STORAGE_KEY);
201
+ };
202
+ refreshHandoff();
203
+ const interval = window.setInterval(
204
+ refreshHandoff,
205
+ ANALYTICS_RECENT_CHAT_HANDOFF_TTL_MS / 2,
206
+ );
207
+ return () => window.clearInterval(interval);
208
+ }, [isAskRoute]);
181
209
  useEffect(() => {
182
210
  function handleChatRunning(event: Event) {
183
211
  const detail = (event as CustomEvent).detail;
184
- if (isAskRoute && typeof detail?.isRunning === "boolean") {
185
- markAnalyticsChatActivity();
186
- if (detail.isRunning === true) {
187
- markAgentChatHomeHandoff(ANALYTICS_CHAT_STORAGE_KEY);
188
- }
189
- }
212
+ if (typeof detail?.isRunning !== "boolean") return;
213
+ if (location.pathname === "/ask") markAnalyticsChatActivity();
214
+ updateAnalyticsChatHandoffForRun(
215
+ runningRuns.current,
216
+ detail,
217
+ location.pathname,
218
+ );
190
219
  }
191
220
 
192
221
  window.addEventListener("agentNative.chatRunning", handleChatRunning);
193
222
  return () =>
194
223
  window.removeEventListener("agentNative.chatRunning", handleChatRunning);
195
- }, [isAskRoute]);
224
+ }, [location.pathname]);
196
225
 
197
226
  function openAskAgentFullscreen() {
198
227
  focusAgentChat();
@@ -1,10 +1,72 @@
1
+ import {
2
+ consumeAgentChatHomeHandoff,
3
+ markAgentChatHomeHandoff,
4
+ resolveAgentChatRunningThreadId,
5
+ type AgentChatRunningEventDetail,
6
+ } from "@agent-native/core/client/agent-chat";
7
+
1
8
  export const ANALYTICS_CHAT_STORAGE_KEY = "analytics";
2
9
 
3
10
  export const ANALYTICS_RECENT_CHAT_HANDOFF_TTL_MS = 5 * 60 * 1000;
4
11
 
12
+ export type AnalyticsChatRunningRuns = Map<string, Set<string>>;
13
+
5
14
  const ANALYTICS_LAST_CHAT_ACTIVITY_KEY =
6
15
  "agent-native.analytics.last-chat-activity-at";
7
16
 
17
+ export function isAnalyticsSettingsPath(pathname: string): boolean {
18
+ return pathname === "/settings" || pathname.startsWith("/settings/");
19
+ }
20
+
21
+ export function discardAnalyticsChatHandoffOnSettings(pathname: string): void {
22
+ if (!isAnalyticsSettingsPath(pathname)) return;
23
+ consumeAgentChatHomeHandoff(ANALYTICS_CHAT_STORAGE_KEY, {
24
+ ttlMs: ANALYTICS_RECENT_CHAT_HANDOFF_TTL_MS,
25
+ });
26
+ }
27
+
28
+ export function hasTrackedAnalyticsChatRun(
29
+ runningRuns: AnalyticsChatRunningRuns,
30
+ ): boolean {
31
+ return Array.from(runningRuns.values()).some((runIds) => runIds.size > 0);
32
+ }
33
+
34
+ export function updateAnalyticsChatHandoffForRun(
35
+ runningRuns: AnalyticsChatRunningRuns,
36
+ detail: unknown,
37
+ pathname: string,
38
+ ): void {
39
+ if (!detail || typeof detail !== "object") return;
40
+ const run = detail as Partial<AgentChatRunningEventDetail>;
41
+ if (typeof run.isRunning !== "boolean") return;
42
+ const tabId = resolveAgentChatRunningThreadId(run);
43
+ if (!tabId) return;
44
+ const runId =
45
+ (typeof run.runId === "string" && run.runId) ||
46
+ (typeof run.turnId === "string" && run.turnId) ||
47
+ null;
48
+ const state = runningRuns.get(tabId);
49
+
50
+ if (run.isRunning) {
51
+ if (pathname !== "/ask") return;
52
+ if (runId) {
53
+ const next = state ?? new Set<string>();
54
+ next.add(runId);
55
+ runningRuns.set(tabId, next);
56
+ }
57
+ markAgentChatHomeHandoff(ANALYTICS_CHAT_STORAGE_KEY);
58
+ return;
59
+ }
60
+
61
+ if (!runId || !state) return;
62
+ const hadActiveRun = state.delete(runId);
63
+
64
+ if (state.size === 0) runningRuns.delete(tabId);
65
+ if (hadActiveRun && pathname === "/ask") {
66
+ markAgentChatHomeHandoff(ANALYTICS_CHAT_STORAGE_KEY);
67
+ }
68
+ }
69
+
8
70
  function readAnalyticsLastChatActivityAt(): number {
9
71
  if (typeof window === "undefined") return 0;
10
72
  try {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Opening Settings no longer opens the chat sidebar.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Show one recent prompt per chat turn.
@@ -1,7 +1,7 @@
1
1
  import { agentNativePath, appPath } from "@agent-native/core/client/api-path";
2
2
  import { useT } from "@agent-native/core/client/i18n";
3
3
  import {
4
- BuilderConnectPopover,
4
+ hasBuilderOAuthCredential,
5
5
  useBuilderConnectFlow,
6
6
  } from "@agent-native/core/client/settings";
7
7
  import {
@@ -12,6 +12,7 @@ import {
12
12
  } from "@tabler/icons-react";
13
13
  import { useCallback, useEffect, useRef, useState } from "react";
14
14
 
15
+ import { Button } from "@/components/ui/button";
15
16
  import {
16
17
  Tooltip,
17
18
  TooltipContent,
@@ -168,6 +169,18 @@ export function StorageSetupCard({
168
169
  },
169
170
  [builderConnect.start],
170
171
  );
172
+ const handleBuilderCancel = useCallback(() => {
173
+ connectRequestedRef.current = false;
174
+ builderConnect.cancel();
175
+ }, [builderConnect.cancel]);
176
+ const hasBuilderAccount =
177
+ builderConnect.accountExists || hasBuilderOAuthCredential(builderConnect);
178
+ const provisionAccount =
179
+ !hasBuilderAccount &&
180
+ builderConnect.statusResolved &&
181
+ builderConnect.agentNativeProvisioningEnabled;
182
+ const builderConnecting = builderConnect.connecting;
183
+ const actionConnecting = connecting || builderConnecting;
171
184
 
172
185
  useEffect(() => {
173
186
  mountedRef.current = true;
@@ -191,14 +204,13 @@ export function StorageSetupCard({
191
204
  <p className="text-sm text-muted-foreground">{description}</p>
192
205
  </div>
193
206
 
194
- {/* Builder.io — primary option, one-click Connect flow. */}
195
- <BuilderConnectPopover
196
- flow={builderConnect}
197
- onConnect={handleBuilderConnect}
198
- >
207
+ {/* Builder.io — primary option. */}
208
+ <div className="space-y-2">
199
209
  <button
200
210
  type="button"
201
- disabled={connecting || connected}
211
+ disabled={actionConnecting || connected}
212
+ data-testid="storage-setup-builder-primary"
213
+ onClick={() => handleBuilderConnect(provisionAccount)}
202
214
  className={
203
215
  "flex items-start gap-3 rounded-xl border px-4 py-3.5 text-start transition-colors " +
204
216
  (connected
@@ -216,7 +228,7 @@ export function StorageSetupCard({
216
228
  >
217
229
  {connected ? (
218
230
  <IconCheck className="h-5 w-5" />
219
- ) : connecting ? (
231
+ ) : actionConnecting ? (
220
232
  <IconLoader2 className="h-5 w-5 animate-spin" />
221
233
  ) : (
222
234
  <BuilderBMark className="h-5 w-5" />
@@ -227,9 +239,13 @@ export function StorageSetupCard({
227
239
  <span className="text-sm font-medium">
228
240
  {connected
229
241
  ? t("storageSetup.builderConnected")
230
- : connecting
242
+ : actionConnecting
231
243
  ? t("storageSetup.waitingForBuilder")
232
- : t("storageSetup.connectBuilder")}
244
+ : provisionAccount
245
+ ? t("storageSetup.createBuilderAccount")
246
+ : hasBuilderAccount
247
+ ? t("storageSetup.signInWithBuilderAccount")
248
+ : t("storageSetup.connectBuilder")}
233
249
  </span>
234
250
  </div>
235
251
  <span
@@ -244,7 +260,54 @@ export function StorageSetupCard({
244
260
  </span>
245
261
  </div>
246
262
  </button>
247
- </BuilderConnectPopover>
263
+ {provisionAccount && (
264
+ <button
265
+ type="button"
266
+ disabled={actionConnecting || connected}
267
+ data-testid="storage-setup-builder-sign-in"
268
+ onClick={() => handleBuilderConnect(false)}
269
+ className="w-full text-center text-xs text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60"
270
+ >
271
+ {t("storageSetup.signInWithBuilderAccount")}
272
+ </button>
273
+ )}
274
+ </div>
275
+ {builderConnecting && (
276
+ <Button
277
+ type="button"
278
+ variant="ghost"
279
+ size="sm"
280
+ data-testid="storage-setup-builder-cancel"
281
+ className="self-end text-xs font-normal text-muted-foreground"
282
+ onClick={handleBuilderCancel}
283
+ >
284
+ {t("common.cancel")}
285
+ </Button>
286
+ )}
287
+
288
+ {provisionAccount && (
289
+ <p className="text-center text-xs leading-5 text-muted-foreground">
290
+ {t("storageSetup.builderConsentPrefix")}{" "}
291
+ <a
292
+ href="https://www.builder.io/legal/terms"
293
+ target="_blank"
294
+ rel="noreferrer"
295
+ className="text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
296
+ >
297
+ {t("storageSetup.builderTerms")}
298
+ </a>{" "}
299
+ {t("storageSetup.builderConsentAnd")}{" "}
300
+ <a
301
+ href="https://www.builder.io/legal/privacy"
302
+ target="_blank"
303
+ rel="noreferrer"
304
+ className="text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
305
+ >
306
+ {t("storageSetup.builderPrivacy")}
307
+ </a>
308
+ .
309
+ </p>
310
+ )}
248
311
 
249
312
  {err && <p className="text-xs text-muted-foreground">{err}</p>}
250
313
 
@@ -1357,6 +1357,12 @@ const messages = {
1357
1357
  builderConnected: "Builder.io connected",
1358
1358
  waitingForBuilder: "Waiting for Builder...",
1359
1359
  connectBuilder: "Use Builder.io",
1360
+ createBuilderAccount: "Create Builder.io account",
1361
+ signInWithBuilderAccount: "Sign in with Builder.io account",
1362
+ builderConsentPrefix: "By creating a Builder.io account, you agree to our",
1363
+ builderTerms: "Terms of Service",
1364
+ builderConsentAnd: "and",
1365
+ builderPrivacy: "Privacy Policy",
1360
1366
  free: "Free",
1361
1367
  configureS3: "configure S3-compatible storage",
1362
1368
  whyPrompt: "Why am I seeing this?",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Connect Builder storage by creating an account in one click.
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+
3
+ const queryBoolean = z.union([
4
+ z.boolean(),
5
+ z.enum(["true", "false", "1", "0"]),
6
+ ]);
7
+
8
+ export const getDesignSchema = z.object({
9
+ id: z.string().describe("Design ID"),
10
+ fileId: z.string().min(1).optional().describe("Read one design file by ID"),
11
+ includeFileContent: queryBoolean
12
+ .optional()
13
+ .transform((value) =>
14
+ value === undefined
15
+ ? undefined
16
+ : value === true || value === "true" || value === "1",
17
+ )
18
+ .describe("Set false to return file metadata without HTML contents"),
19
+ });
@@ -3,12 +3,12 @@ import { loadAgentDesignSystemContext } from "@agent-native/core/shared";
3
3
  import { resolveAccess } from "@agent-native/core/sharing";
4
4
  import { track } from "@agent-native/core/tracking";
5
5
  import { and, asc, eq } from "drizzle-orm";
6
- import { z } from "zod";
7
6
 
8
7
  import { getDb, schema } from "../server/db/index.js";
9
8
  import { designDataForAccessRole } from "../server/lib/design-data-access.js";
10
9
  import "../server/db/index.js"; // ensure registerShareableResource runs
11
10
  import getDesignSystem from "./get-design-system.js";
11
+ import { getDesignSchema } from "./get-design.schema.js";
12
12
 
13
13
  // The editor re-reads get-design after saves, on sync events, and every second
14
14
  // while a generation runs. Count a signed-in viewer's view once per window,
@@ -40,14 +40,7 @@ function shouldTrackDesignView(
40
40
  export default defineAction({
41
41
  description:
42
42
  "Get a design project by ID. Returns the full design data and linked `designSystem.agentContext` when readable. By default, returns all associated files; pass `includeFileContent=false` for file metadata only, then pass a `fileId` to read just one file. Treat design-system context as authoritative before authoring or restyling.",
43
- schema: z.object({
44
- id: z.string().describe("Design ID"),
45
- fileId: z.string().min(1).optional().describe("Read one design file by ID"),
46
- includeFileContent: z
47
- .boolean()
48
- .optional()
49
- .describe("Set false to return file metadata without HTML contents"),
50
- }),
43
+ schema: getDesignSchema,
51
44
  readOnly: true,
52
45
  requiresAuth: false,
53
46
  publicAgent: { expose: true, readOnly: true, requiresAuth: false },
@@ -134,16 +127,20 @@ export default defineAction({
134
127
  createdAt: row.createdAt,
135
128
  updatedAt: row.updatedAt,
136
129
  accessRole: access.role,
137
- files: files.map((f) => ({
138
- id: f.id,
139
- filename: f.filename,
140
- fileType: f.fileType,
141
- ...(includeFileContent === false || !("content" in f)
142
- ? {}
143
- : { content: f.content }),
144
- createdAt: f.createdAt,
145
- updatedAt: f.updatedAt,
146
- })),
130
+ files: files.map((f) => {
131
+ const metadata = {
132
+ id: f.id,
133
+ filename: f.filename,
134
+ fileType: f.fileType,
135
+ createdAt: f.createdAt,
136
+ updatedAt: f.updatedAt,
137
+ };
138
+ if (includeFileContent === false) return metadata;
139
+ if (!("content" in f)) {
140
+ throw new Error("File content was requested but not selected");
141
+ }
142
+ return { ...metadata, content: f.content };
143
+ }),
147
144
  };
148
145
  },
149
146
  });
@@ -22,6 +22,7 @@ import {
22
22
  ensureGmailLabel,
23
23
  } from "../server/lib/automation-actions.js";
24
24
  import {
25
+ assertMailJevEnabled,
25
26
  createAutomationRule,
26
27
  listAutomationRules,
27
28
  } from "../server/lib/automations.js";
@@ -144,6 +145,12 @@ export default defineAction({
144
145
  if (!ownerEmail) throw new Error("no authenticated user");
145
146
 
146
147
  if (args.mode === "settings") {
148
+ const settingKeys = Object.keys(args.settings ?? {});
149
+ const disableOnly =
150
+ settingKeys.length === 1 && args.settings?.enabled === false;
151
+ if (settingKeys.length > 0 && !disableOnly) {
152
+ await assertMailJevEnabled(ownerEmail);
153
+ }
147
154
  const state = await getAiFilterState(ownerEmail);
148
155
  const next = {
149
156
  ...state,
@@ -154,6 +161,7 @@ export default defineAction({
154
161
  return { changed: 0, failures: [], state: next };
155
162
  }
156
163
 
164
+ await assertMailJevEnabled(ownerEmail);
157
165
  const targets = args.targets ?? [];
158
166
  if (targets.length === 0) throw new Error("targets are required");
159
167
 
@@ -3,7 +3,10 @@ import { getRequestUserEmail } from "@agent-native/core/server";
3
3
  import { track } from "@agent-native/core/tracking";
4
4
  import { z } from "zod";
5
5
 
6
- import { createAutomationRule } from "../server/lib/automations.js";
6
+ import {
7
+ assertMailJevEnabled,
8
+ createAutomationRule,
9
+ } from "../server/lib/automations.js";
7
10
  import { automationActionSchema } from "../shared/automation-schema.js";
8
11
 
9
12
  export default defineAction({
@@ -25,6 +28,9 @@ export default defineAction({
25
28
  if (!args.name || !args.condition || !args.actions) {
26
29
  throw new Error("name, condition, and actions are required");
27
30
  }
31
+ if ((args.domain ?? "mail") === "mail" && args.kind === "ai-filter") {
32
+ await assertMailJevEnabled(ownerEmail);
33
+ }
28
34
  const result = await createAutomationRule(ownerEmail, args);
29
35
  track(
30
36
  "automation_created",
@@ -0,0 +1,34 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { getRequestUserEmail } from "@agent-native/core/server";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ clearMailAiFilterRules,
7
+ restoreMailAiFilterRules,
8
+ } from "../server/lib/ai-filter-rule-undo.js";
9
+
10
+ export default defineAction({
11
+ description:
12
+ "Clear active Mail AI-filter rules or restore them from a short-lived undo token.",
13
+ schema: z.discriminatedUnion("operation", [
14
+ z.object({
15
+ operation: z.literal("clear"),
16
+ ids: z.array(z.string().min(1)).min(1).max(32),
17
+ }),
18
+ z.object({
19
+ operation: z.literal("undo"),
20
+ undoId: z.string().min(1),
21
+ }),
22
+ ]),
23
+ agentTool: false,
24
+ run: async (args) => {
25
+ const ownerEmail = getRequestUserEmail();
26
+ if (!ownerEmail) throw new Error("Unauthenticated");
27
+ if (args.operation === "clear") {
28
+ const undoId = await clearMailAiFilterRules(ownerEmail, args.ids);
29
+ return { undoId };
30
+ }
31
+ await restoreMailAiFilterRules(ownerEmail, args.undoId);
32
+ return { restored: true };
33
+ },
34
+ });
@@ -4,6 +4,8 @@ import { eq, and } from "drizzle-orm";
4
4
  import { nanoid } from "nanoid";
5
5
  import { z } from "zod";
6
6
 
7
+ import type { AutomationAction } from "../shared/types.js";
8
+
7
9
  export const createManageEmailRulesAction = (agentTool: boolean) =>
8
10
  defineAction({
9
11
  description:
@@ -103,31 +105,26 @@ export const createManageEmailRulesAction = (agentTool: boolean) =>
103
105
  case "update": {
104
106
  if (!args.id) throw new Error("--id is required for update");
105
107
 
106
- const updates: Record<string, any> = {
107
- updatedAt: Math.floor(Date.now() / 1_000),
108
- };
109
- if (args.name !== undefined) updates.name = args.name;
110
- if (args.condition !== undefined) updates.condition = args.condition;
108
+ const patch: {
109
+ name?: string;
110
+ condition?: string;
111
+ actions?: AutomationAction[];
112
+ enabled?: boolean;
113
+ } = {};
114
+ if (args.name !== undefined) patch.name = args.name;
115
+ if (args.condition !== undefined) patch.condition = args.condition;
111
116
  if (args.actions !== undefined) {
112
117
  try {
113
- JSON.parse(args.actions);
114
- updates.actions = args.actions;
118
+ patch.actions = JSON.parse(args.actions);
115
119
  } catch {
116
120
  throw new Error("--actions must be valid JSON array");
117
121
  }
118
122
  }
119
- if (args.enabled !== undefined)
120
- updates.enabled = args.enabled ? 1 : 0;
123
+ if (args.enabled !== undefined) patch.enabled = args.enabled;
121
124
 
122
- await db
123
- .update(schema.automationRules)
124
- .set(updates)
125
- .where(
126
- and(
127
- eq(schema.automationRules.id, args.id),
128
- eq(schema.automationRules.ownerEmail, ownerEmail),
129
- ),
130
- );
125
+ const { updateAutomationRule } =
126
+ await import("../server/lib/automations.js");
127
+ await updateAutomationRule(ownerEmail, args.id, patch);
131
128
 
132
129
  return `Updated automation rule ${args.id}`;
133
130
  }
@@ -149,38 +146,18 @@ export const createManageEmailRulesAction = (agentTool: boolean) =>
149
146
 
150
147
  case "enable": {
151
148
  if (!args.id) throw new Error("--id is required for enable");
152
-
153
- await db
154
- .update(schema.automationRules)
155
- .set({
156
- enabled: 1,
157
- updatedAt: Math.floor(Date.now() / 1_000),
158
- } as any)
159
- .where(
160
- and(
161
- eq(schema.automationRules.id, args.id),
162
- eq(schema.automationRules.ownerEmail, ownerEmail),
163
- ),
164
- );
149
+ const { updateAutomationRule } =
150
+ await import("../server/lib/automations.js");
151
+ await updateAutomationRule(ownerEmail, args.id, { enabled: true });
165
152
 
166
153
  return `Enabled automation rule ${args.id}`;
167
154
  }
168
155
 
169
156
  case "disable": {
170
157
  if (!args.id) throw new Error("--id is required for disable");
171
-
172
- await db
173
- .update(schema.automationRules)
174
- .set({
175
- enabled: 0,
176
- updatedAt: Math.floor(Date.now() / 1_000),
177
- } as any)
178
- .where(
179
- and(
180
- eq(schema.automationRules.id, args.id),
181
- eq(schema.automationRules.ownerEmail, ownerEmail),
182
- ),
183
- );
158
+ const { updateAutomationRule } =
159
+ await import("../server/lib/automations.js");
160
+ await updateAutomationRule(ownerEmail, args.id, { enabled: false });
184
161
 
185
162
  return `Disabled automation rule ${args.id}`;
186
163
  }