@agent-native/core 0.68.0 → 0.68.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.
Files changed (152) hide show
  1. package/README.md +4 -4
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +18 -0
  4. package/corpus/core/docs/content/authentication.md +16 -2
  5. package/corpus/core/docs/content/template-analytics.md +14 -9
  6. package/corpus/core/docs/content/template-clips.md +30 -2
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/agent/engine/builder-engine.ts +22 -3
  9. package/corpus/core/src/agent/engine/types.ts +1 -0
  10. package/corpus/core/src/agent/production-agent.ts +2 -0
  11. package/corpus/core/src/agent/types.ts +1 -0
  12. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  13. package/corpus/core/src/client/AssistantChat.tsx +45 -6
  14. package/corpus/core/src/client/CommandMenu.tsx +98 -3
  15. package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
  16. package/corpus/core/src/client/active-run-state.ts +16 -0
  17. package/corpus/core/src/client/guided-questions.tsx +73 -17
  18. package/corpus/core/src/client/index.ts +2 -0
  19. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  20. package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
  21. package/corpus/core/src/provider-api/index.ts +37 -17
  22. package/corpus/core/src/server/auth.ts +6 -8
  23. package/corpus/core/src/server/better-auth-instance.ts +19 -4
  24. package/corpus/core/src/server/core-routes-plugin.ts +157 -5
  25. package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
  26. package/corpus/core/src/server/index.ts +8 -0
  27. package/corpus/core/src/server/onboarding-html.ts +2 -1
  28. package/corpus/core/src/templates/workspace-root/.env.example +4 -0
  29. package/corpus/templates/analytics/app/routes/$.tsx +9 -0
  30. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
  31. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
  32. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
  33. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
  34. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
  35. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
  36. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
  37. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
  38. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
  39. package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
  40. package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
  41. package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
  42. package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
  43. package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
  44. package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
  45. package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
  46. package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
  47. package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
  48. package/corpus/templates/calendar/shared/api.ts +4 -0
  49. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
  50. package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
  51. package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
  52. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
  53. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
  54. package/corpus/templates/clips/app/lib/capture-install-options.ts +13 -0
  55. package/corpus/templates/clips/app/root.tsx +54 -2
  56. package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
  57. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
  58. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
  59. package/corpus/templates/clips/app/routes/download.tsx +49 -0
  60. package/corpus/templates/clips/app/routes/record.tsx +4 -4
  61. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
  62. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +26 -0
  63. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
  64. package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
  65. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
  66. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
  67. package/corpus/templates/design/actions/show-design-questions.ts +15 -1
  68. package/corpus/templates/design/app/hooks/use-agent-generating.ts +47 -8
  69. package/corpus/templates/design/app/pages/DesignEditor.tsx +6 -0
  70. package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
  71. package/corpus/templates/design/app/routes/$.tsx +9 -0
  72. package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
  73. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  74. package/corpus/templates/plan/app/global.css +2 -2
  75. package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
  76. package/corpus/templates/slides/app/routes/$.tsx +9 -0
  77. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
  78. package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
  79. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  80. package/dist/agent/engine/builder-engine.js +21 -3
  81. package/dist/agent/engine/builder-engine.js.map +1 -1
  82. package/dist/agent/engine/types.d.ts +2 -0
  83. package/dist/agent/engine/types.d.ts.map +1 -1
  84. package/dist/agent/engine/types.js.map +1 -1
  85. package/dist/agent/production-agent.d.ts.map +1 -1
  86. package/dist/agent/production-agent.js +3 -0
  87. package/dist/agent/production-agent.js.map +1 -1
  88. package/dist/agent/types.d.ts +2 -0
  89. package/dist/agent/types.d.ts.map +1 -1
  90. package/dist/agent/types.js.map +1 -1
  91. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  92. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  93. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  94. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts.map +1 -1
  96. package/dist/client/AssistantChat.js +42 -7
  97. package/dist/client/AssistantChat.js.map +1 -1
  98. package/dist/client/CommandMenu.d.ts +15 -2
  99. package/dist/client/CommandMenu.d.ts.map +1 -1
  100. package/dist/client/CommandMenu.js +46 -3
  101. package/dist/client/CommandMenu.js.map +1 -1
  102. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  103. package/dist/client/ConnectBuilderCard.js +11 -2
  104. package/dist/client/ConnectBuilderCard.js.map +1 -1
  105. package/dist/client/active-run-state.d.ts +2 -0
  106. package/dist/client/active-run-state.d.ts.map +1 -1
  107. package/dist/client/active-run-state.js +10 -0
  108. package/dist/client/active-run-state.js.map +1 -1
  109. package/dist/client/guided-questions.d.ts +2 -0
  110. package/dist/client/guided-questions.d.ts.map +1 -1
  111. package/dist/client/guided-questions.js +52 -7
  112. package/dist/client/guided-questions.js.map +1 -1
  113. package/dist/client/index.d.ts +1 -1
  114. package/dist/client/index.d.ts.map +1 -1
  115. package/dist/client/index.js.map +1 -1
  116. package/dist/client/sse-event-processor.d.ts +1 -1
  117. package/dist/client/sse-event-processor.d.ts.map +1 -1
  118. package/dist/client/sse-event-processor.js +7 -1
  119. package/dist/client/sse-event-processor.js.map +1 -1
  120. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
  121. package/dist/oauth-tokens/google-refresh.js +29 -17
  122. package/dist/oauth-tokens/google-refresh.js.map +1 -1
  123. package/dist/provider-api/index.d.ts.map +1 -1
  124. package/dist/provider-api/index.js +28 -16
  125. package/dist/provider-api/index.js.map +1 -1
  126. package/dist/server/auth.d.ts.map +1 -1
  127. package/dist/server/auth.js +6 -6
  128. package/dist/server/auth.js.map +1 -1
  129. package/dist/server/better-auth-instance.d.ts.map +1 -1
  130. package/dist/server/better-auth-instance.js +18 -4
  131. package/dist/server/better-auth-instance.js.map +1 -1
  132. package/dist/server/core-routes-plugin.d.ts +5 -0
  133. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  134. package/dist/server/core-routes-plugin.js +107 -5
  135. package/dist/server/core-routes-plugin.js.map +1 -1
  136. package/dist/server/google-oauth-credentials.d.ts +15 -0
  137. package/dist/server/google-oauth-credentials.d.ts.map +1 -0
  138. package/dist/server/google-oauth-credentials.js +34 -0
  139. package/dist/server/google-oauth-credentials.js.map +1 -0
  140. package/dist/server/index.d.ts +1 -0
  141. package/dist/server/index.d.ts.map +1 -1
  142. package/dist/server/index.js +1 -0
  143. package/dist/server/index.js.map +1 -1
  144. package/dist/server/onboarding-html.d.ts.map +1 -1
  145. package/dist/server/onboarding-html.js +2 -1
  146. package/dist/server/onboarding-html.js.map +1 -1
  147. package/dist/templates/workspace-root/.env.example +4 -0
  148. package/docs/content/authentication.md +16 -2
  149. package/docs/content/template-analytics.md +14 -9
  150. package/docs/content/template-clips.md +30 -2
  151. package/package.json +1 -1
  152. package/src/templates/workspace-root/.env.example +4 -0
@@ -19,9 +19,18 @@ import {
19
19
  getThemeInitScript,
20
20
  useCommandMenuShortcut,
21
21
  } from "@agent-native/core/client";
22
- import { IconSun, IconMoon } from "@tabler/icons-react";
22
+ import { IconCheck, IconSun, IconMoon } from "@tabler/icons-react";
23
23
  import { useTheme } from "next-themes";
24
24
  import { Toaster } from "@/components/ui/sonner";
25
+ import { Button } from "@/components/ui/button";
26
+ import {
27
+ Dialog,
28
+ DialogContent,
29
+ DialogDescription,
30
+ DialogFooter,
31
+ DialogHeader,
32
+ DialogTitle,
33
+ } from "@/components/ui/dialog";
25
34
  import type { LinksFunction } from "react-router";
26
35
  import stylesheet from "./global.css?url";
27
36
  import { configureTracking } from "@agent-native/core/client";
@@ -117,8 +126,30 @@ type ExternalChromeRuntime = {
117
126
  ) => void;
118
127
  };
119
128
 
129
+ const CLIPS_COMMAND_DOCS = [
130
+ {
131
+ title: "Use the Chrome extension for browser logs",
132
+ description:
133
+ "Record a browser tab with redacted console logs, JavaScript exceptions, and fetch/XHR diagnostics.",
134
+ href: "https://www.agent-native.com/docs/template-clips#browser-logs-and-developer-diagnostics",
135
+ keywords: [
136
+ "logs",
137
+ "browser logs",
138
+ "developer logs",
139
+ "console logs",
140
+ "network logs",
141
+ "fetch",
142
+ "xhr",
143
+ "diagnostics",
144
+ "chrome extension",
145
+ "recording",
146
+ ],
147
+ },
148
+ ] satisfies React.ComponentProps<typeof CommandMenu.DocsGroup>["docs"];
149
+
120
150
  function ClipsExtensionAuthBridge() {
121
151
  const location = useLocation();
152
+ const [showAuthSuccess, setShowAuthSuccess] = useState(false);
122
153
 
123
154
  useEffect(() => {
124
155
  const params = new URLSearchParams(location.search);
@@ -163,6 +194,7 @@ function ClipsExtensionAuthBridge() {
163
194
  cleaned.searchParams.delete("clipsExtensionAuth");
164
195
  cleaned.searchParams.delete("clipsExtensionId");
165
196
  window.history.replaceState(window.history.state, "", cleaned);
197
+ setShowAuthSuccess(true);
166
198
  },
167
199
  );
168
200
  }
@@ -173,7 +205,26 @@ function ClipsExtensionAuthBridge() {
173
205
  };
174
206
  }, [location.search]);
175
207
 
176
- return null;
208
+ return (
209
+ <Dialog open={showAuthSuccess} onOpenChange={setShowAuthSuccess}>
210
+ <DialogContent className="max-w-sm text-center sm:text-center">
211
+ <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500">
212
+ <IconCheck className="h-9 w-9" strokeWidth={2.5} />
213
+ </div>
214
+ <DialogHeader className="items-center text-center sm:text-center">
215
+ <DialogTitle>Signed in</DialogTitle>
216
+ <DialogDescription className="max-w-xs">
217
+ Open the Clips extension again to start recording.
218
+ </DialogDescription>
219
+ </DialogHeader>
220
+ <DialogFooter className="sm:justify-center">
221
+ <Button type="button" onClick={() => setShowAuthSuccess(false)}>
222
+ Got it
223
+ </Button>
224
+ </DialogFooter>
225
+ </DialogContent>
226
+ </Dialog>
227
+ );
177
228
  }
178
229
 
179
230
  /**
@@ -211,6 +262,7 @@ function AppContent() {
211
262
  <CommandMenu.Group heading="Actions">
212
263
  <CommandMenu.Item onSelect={() => {}}>Search</CommandMenu.Item>
213
264
  </CommandMenu.Group>
265
+ <CommandMenu.DocsGroup docs={CLIPS_COMMAND_DOCS} />
214
266
  <CommandMenu.Group heading="Appearance">
215
267
  <ThemeToggleItem />
216
268
  </CommandMenu.Group>
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Link } from "react-router";
3
2
  import { toast } from "sonner";
4
3
  import {
5
4
  IconArrowsExchange,
@@ -34,6 +33,7 @@ import {
34
33
  TooltipTrigger,
35
34
  } from "@/components/ui/tooltip";
36
35
  import { useDesktopPromo } from "@/hooks/use-desktop-promo";
36
+ import { CaptureInstallButton } from "@/components/capture-install-options";
37
37
 
38
38
  export function meta() {
39
39
  return [{ title: "Dictate · Clips" }];
@@ -609,12 +609,10 @@ function EmptyState({ isDesktopApp }: { isDesktopApp: boolean }) {
609
609
  work in any app — Slack, your editor, anywhere.
610
610
  </p>
611
611
  <div className="mt-5 flex items-center justify-center">
612
- <Button asChild size="sm" className="gap-1.5">
613
- <Link to="/download">
614
- <IconDownload className="h-3.5 w-3.5" />
615
- Download Clips desktop app
616
- </Link>
617
- </Button>
612
+ <CaptureInstallButton size="sm" className="gap-1.5">
613
+ <IconDownload className="h-3.5 w-3.5" />
614
+ Download Clips desktop app
615
+ </CaptureInstallButton>
618
616
  </div>
619
617
  <div className="mt-3 flex items-center justify-center gap-2 text-xs text-muted-foreground">
620
618
  <Kbd>Fn</Kbd>
@@ -646,12 +644,10 @@ function DownloadDesktopAppCard() {
646
644
  desktop app is the way to use this.
647
645
  </p>
648
646
  </div>
649
- <Button asChild size="sm" className="gap-1.5">
650
- <Link to="/download">
651
- <IconDownload className="h-3.5 w-3.5" />
652
- Download
653
- </Link>
654
- </Button>
647
+ <CaptureInstallButton size="sm" className="gap-1.5">
648
+ <IconDownload className="h-3.5 w-3.5" />
649
+ Download
650
+ </CaptureInstallButton>
655
651
  </div>
656
652
  );
657
653
  }
@@ -61,6 +61,7 @@ import {
61
61
  TooltipContent,
62
62
  TooltipTrigger,
63
63
  } from "@/components/ui/tooltip";
64
+ import { CaptureInstallButton } from "@/components/capture-install-options";
64
65
 
65
66
  export function meta() {
66
67
  return [{ title: "Meeting · Clips" }];
@@ -699,17 +700,14 @@ export default function MeetingDetailRoute() {
699
700
  transcript and AI notes will appear here automatically.
700
701
  </span>
701
702
  {!isDesktopApp && (
702
- <Button
703
- asChild
703
+ <CaptureInstallButton
704
704
  size="sm"
705
705
  variant="secondary"
706
706
  className="ml-auto h-8 gap-1.5 cursor-pointer"
707
707
  >
708
- <NavLink to="/download">
709
- <IconExternalLink className="h-3.5 w-3.5" />
710
- Get desktop app
711
- </NavLink>
712
- </Button>
708
+ <IconExternalLink className="h-3.5 w-3.5" />
709
+ Get desktop app
710
+ </CaptureInstallButton>
713
711
  )}
714
712
  </div>
715
713
  )}
@@ -45,6 +45,7 @@ import { Input } from "@/components/ui/input";
45
45
  import { DayHeader, formatDayLabel } from "@/components/meetings/day-header";
46
46
  import type { AttendeeStackParticipant } from "@/components/meetings/attendee-stack";
47
47
  import { PageHeader } from "@/components/library/page-header";
48
+ import { CaptureInstallButton } from "@/components/capture-install-options";
48
49
 
49
50
  export function meta() {
50
51
  return [{ title: "Meetings · Clips" }];
@@ -668,17 +669,14 @@ function MeetingsHeader({
668
669
  </div>
669
670
  {showDesktopCta && (
670
671
  <div className="flex w-fit shrink-0 flex-col items-start gap-1 sm:items-end">
671
- <Button
672
- asChild
672
+ <CaptureInstallButton
673
673
  size="sm"
674
674
  variant="secondary"
675
675
  className="h-8 w-fit gap-1.5 cursor-pointer"
676
676
  >
677
- <NavLink to="/download">
678
- <IconAppWindow className="h-4 w-4" />
679
- Get desktop app
680
- </NavLink>
681
- </Button>
677
+ <IconAppWindow className="h-4 w-4" />
678
+ Get desktop app
679
+ </CaptureInstallButton>
682
680
  <p className="max-w-56 text-[11px] leading-snug text-muted-foreground">
683
681
  Required for meeting reminders and transcription.
684
682
  </p>
@@ -1,12 +1,18 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import {
3
+ IconBrowser,
3
4
  IconBrandApple,
4
5
  IconBrandWindows,
6
+ IconExternalLink,
5
7
  IconPlayerRecord,
6
8
  } from "@tabler/icons-react";
7
9
  import { appBasePath, appPath } from "@agent-native/core/client";
8
10
  import { Button } from "@/components/ui/button";
9
11
  import { Skeleton } from "@/components/ui/skeleton";
12
+ import {
13
+ clipsChromeExtensionEnabled,
14
+ clipsChromeExtensionUrl,
15
+ } from "@/lib/capture-install-options";
10
16
 
11
17
  export function meta() {
12
18
  return [
@@ -237,6 +243,49 @@ export default function DownloadPage() {
237
243
  )}
238
244
  </div>
239
245
  </div>
246
+
247
+ {clipsChromeExtensionEnabled && (
248
+ <section className="mt-10 w-full max-w-xl rounded-2xl border border-border bg-card p-4 text-left shadow-sm">
249
+ <div className="flex items-start gap-3">
250
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
251
+ <IconBrowser className="h-4 w-4" />
252
+ </div>
253
+ <div className="min-w-0 flex-1">
254
+ <h2 className="text-sm font-semibold text-foreground">
255
+ Chrome extension for browser logs
256
+ </h2>
257
+ <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
258
+ Use the extension when you want a recording plus redacted
259
+ console and fetch/XHR diagnostics from the tab you launch
260
+ from. The desktop app is still the smoothest everyday
261
+ recorder.
262
+ </p>
263
+ </div>
264
+ </div>
265
+ <Button
266
+ asChild={Boolean(clipsChromeExtensionUrl)}
267
+ disabled={!clipsChromeExtensionUrl}
268
+ variant="outline"
269
+ className="mt-4 w-full gap-2"
270
+ >
271
+ {clipsChromeExtensionUrl ? (
272
+ <a
273
+ href={clipsChromeExtensionUrl}
274
+ target="_blank"
275
+ rel="noreferrer"
276
+ >
277
+ <IconExternalLink className="h-4 w-4" />
278
+ Install Chrome extension
279
+ </a>
280
+ ) : (
281
+ <>
282
+ <IconExternalLink className="h-4 w-4" />
283
+ Chrome Web Store URL pending
284
+ </>
285
+ )}
286
+ </Button>
287
+ </section>
288
+ )}
240
289
  </div>
241
290
  </main>
242
291
  </div>
@@ -71,6 +71,7 @@ async function writeAppState(key: string, value: unknown): Promise<void> {
71
71
  }
72
72
  import { Button } from "@/components/ui/button";
73
73
  import { Spinner } from "@/components/ui/spinner";
74
+ import { CaptureInstallButton } from "@/components/capture-install-options";
74
75
  import { toast } from "sonner";
75
76
 
76
77
  import { PreRecordPanel } from "@/components/recorder/pre-record-panel";
@@ -542,13 +543,12 @@ function DesktopRecorderCallout() {
542
543
  </p>
543
544
  </div>
544
545
  </div>
545
- <Button
546
- asChild
546
+ <CaptureInstallButton
547
547
  size="sm"
548
548
  className="mt-4 w-full bg-primary text-primary-foreground hover:bg-primary/90"
549
549
  >
550
- <Link to="/download">Download desktop app</Link>
551
- </Button>
550
+ Download desktop app
551
+ </CaptureInstallButton>
552
552
  </aside>
553
553
  );
554
554
  }
@@ -41,6 +41,7 @@ import { DeleteRecordingMenu } from "@/components/player/delete-recording-menu";
41
41
  import { usePlayerShortcuts } from "@/hooks/use-player-shortcuts";
42
42
  import { useViewTracking } from "@/hooks/use-view-tracking";
43
43
  import { Button } from "@/components/ui/button";
44
+ import { CaptureInstallButton } from "@/components/capture-install-options";
44
45
  import {
45
46
  DropdownMenu,
46
47
  DropdownMenuContent,
@@ -1004,12 +1005,10 @@ function PublicAgentEmptyState() {
1004
1005
  Loom alternative
1005
1006
  </p>
1006
1007
  <div className="mt-7 flex w-full max-w-[220px] flex-col gap-2">
1007
- <Button asChild className="w-full gap-2">
1008
- <a href={appPath("/download")}>
1009
- <IconDownload className="h-4 w-4" />
1010
- {downloadLabel}
1011
- </a>
1012
- </Button>
1008
+ <CaptureInstallButton className="w-full gap-2" align="center">
1009
+ <IconDownload className="h-4 w-4" />
1010
+ {downloadLabel}
1011
+ </CaptureInstallButton>
1013
1012
  <Button asChild variant="outline" className="w-full">
1014
1013
  <a href={appPath("/signup")}>Sign up</a>
1015
1014
  </Button>
@@ -39,3 +39,29 @@ Diagnostics are bounded and redacted before they are saved. Clips does not colle
39
39
  ## Review Notes
40
40
 
41
41
  The extension opens `https://clips.agent-native.com/record` by default. Local development can point the settings page to a localhost Clips instance.
42
+
43
+ ## Submission Artifact
44
+
45
+ Build the latest Chrome Web Store ZIP from the repo root:
46
+
47
+ ```bash
48
+ pnpm --filter clips-chrome-extension package
49
+ ```
50
+
51
+ Upload the generated artifact:
52
+
53
+ ```txt
54
+ templates/clips/chrome-extension/releases/clips-chrome-extension-0.1.0.zip
55
+ ```
56
+
57
+ ## Web App Rollout Gate
58
+
59
+ Keep the web app's Chrome extension UI hidden until the Web Store listing is
60
+ approved and there is a stable public URL.
61
+
62
+ - `VITE_CLIPS_CHROME_EXTENSION_ENABLED=1` reveals the Chrome option beside
63
+ Clips desktop prompts.
64
+ - `VITE_CLIPS_CHROME_EXTENSION_URL=<chrome-web-store-url>` powers the install
65
+ links.
66
+ - Leave both unset while submitting the draft so production continues to send
67
+ users directly to the desktop app.
@@ -12,7 +12,8 @@ import {
12
12
  detectPlatform,
13
13
  getEvent,
14
14
  pickJoinUrl,
15
- refreshAccessToken,
15
+ resolveGoogleOAuthCredentialCandidates,
16
+ refreshAccessTokenWithFallback,
16
17
  type CalendarEvent,
17
18
  } from "./google-calendar-client.js";
18
19
 
@@ -77,9 +78,8 @@ export function shouldMarkNeedsReauth(message: string): boolean {
77
78
  export async function resolveCalendarAccessToken(
78
79
  account: CalendarAccountForEvents,
79
80
  ): Promise<string | null> {
80
- const clientId = process.env.GOOGLE_CLIENT_ID;
81
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
82
- if (!clientId || !clientSecret || !account.ownerEmail) return null;
81
+ const credentialCandidates = resolveGoogleOAuthCredentialCandidates();
82
+ if (!credentialCandidates.length || !account.ownerEmail) return null;
83
83
 
84
84
  let bundle: AccessTokenBundle | null = null;
85
85
  if (account.accessTokenSecretRef) {
@@ -112,10 +112,9 @@ export async function resolveCalendarAccessToken(
112
112
 
113
113
  let refreshed;
114
114
  try {
115
- refreshed = await refreshAccessToken({
115
+ refreshed = await refreshAccessTokenWithFallback({
116
116
  refreshToken: refreshSecret.value,
117
- clientId,
118
- clientSecret,
117
+ credentials: credentialCandidates,
119
118
  });
120
119
  } catch {
121
120
  // TODO(needs-reauth classification): a transient refresh failure (network
@@ -82,6 +82,35 @@ export interface ExchangeCodeArgs {
82
82
  redirectUri: string;
83
83
  }
84
84
 
85
+ export interface GoogleOAuthClientCredentials {
86
+ clientId: string;
87
+ clientSecret: string;
88
+ }
89
+
90
+ function readCredentialPair(
91
+ clientIdKey: string,
92
+ clientSecretKey: string,
93
+ ): GoogleOAuthClientCredentials | null {
94
+ const clientId = process.env[clientIdKey];
95
+ const clientSecret = process.env[clientSecretKey];
96
+ if (!clientId || !clientSecret) return null;
97
+ return { clientId, clientSecret };
98
+ }
99
+
100
+ export function resolveGoogleOAuthCredentialCandidates(): GoogleOAuthClientCredentials[] {
101
+ const primary = readCredentialPair(
102
+ "GOOGLE_CLIENT_ID",
103
+ "GOOGLE_CLIENT_SECRET",
104
+ );
105
+ const legacy = readCredentialPair(
106
+ "GOOGLE_LEGACY_CLIENT_ID",
107
+ "GOOGLE_LEGACY_CLIENT_SECRET",
108
+ );
109
+ if (!primary) return legacy ? [legacy] : [];
110
+ if (!legacy || legacy.clientId === primary.clientId) return [primary];
111
+ return [primary, legacy];
112
+ }
113
+
85
114
  /** Exchange an authorization code for tokens. Throws on non-2xx. */
86
115
  export async function exchangeCode(
87
116
  args: ExchangeCodeArgs,
@@ -129,6 +158,38 @@ export async function refreshAccessToken(args: {
129
158
  return (await res.json()) as GoogleTokenResponse;
130
159
  }
131
160
 
161
+ function isPermanentRefreshFailure(error: unknown): boolean {
162
+ const message = error instanceof Error ? error.message : String(error || "");
163
+ const lower = message.toLowerCase();
164
+ return (
165
+ lower.includes("invalid_grant") ||
166
+ lower.includes("unauthorized_client") ||
167
+ lower.includes("invalid_client")
168
+ );
169
+ }
170
+
171
+ export async function refreshAccessTokenWithFallback(args: {
172
+ refreshToken: string;
173
+ credentials: GoogleOAuthClientCredentials[];
174
+ }): Promise<GoogleTokenResponse> {
175
+ let lastError: unknown;
176
+ for (const credentials of args.credentials) {
177
+ try {
178
+ return await refreshAccessToken({
179
+ refreshToken: args.refreshToken,
180
+ clientId: credentials.clientId,
181
+ clientSecret: credentials.clientSecret,
182
+ });
183
+ } catch (error) {
184
+ lastError = error;
185
+ if (!isPermanentRefreshFailure(error)) throw error;
186
+ }
187
+ }
188
+ throw lastError instanceof Error
189
+ ? lastError
190
+ : new Error("Google token refresh failed.");
191
+ }
192
+
132
193
  /** Best-effort revoke. Returns true if Google returned 2xx, false otherwise. */
133
194
  export async function revokeToken(token: string): Promise<boolean> {
134
195
  try {
@@ -8,6 +8,7 @@ import {
8
8
  encodeOAuthState,
9
9
  getSession,
10
10
  isElectron,
11
+ resolveGoogleSignInCredentials,
11
12
  resolveOAuthRedirectUri,
12
13
  safeReturnPath,
13
14
  } from "@agent-native/core/server";
@@ -34,17 +35,6 @@ function oauthRedirectResponse(url: string) {
34
35
  }
35
36
 
36
37
  export default defineEventHandler(async (event: H3Event) => {
37
- const clientId = process.env.GOOGLE_CLIENT_ID;
38
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
39
- if (!clientId || !clientSecret) {
40
- setResponseStatus(event, 422);
41
- return {
42
- error: "missing_credentials",
43
- message:
44
- "Google OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.",
45
- };
46
- }
47
-
48
38
  try {
49
39
  const q = getQuery(event);
50
40
  const redirectUri = resolveOAuthRedirectUri(event);
@@ -67,6 +57,23 @@ export default defineEventHandler(async (event: H3Event) => {
67
57
  const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
68
58
  const calendarConnect =
69
59
  q.calendar === "1" || q.calendar === "true" || q.product === "calendar";
60
+ const credentials = calendarConnect
61
+ ? process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
62
+ ? {
63
+ clientId: process.env.GOOGLE_CLIENT_ID,
64
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
65
+ }
66
+ : null
67
+ : resolveGoogleSignInCredentials();
68
+ if (!credentials) {
69
+ setResponseStatus(event, 422);
70
+ return {
71
+ error: "missing_credentials",
72
+ message: calendarConnect
73
+ ? "Google Calendar OAuth credentials are not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET."
74
+ : "Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
75
+ };
76
+ }
70
77
 
71
78
  if (calendarConnect && !owner) {
72
79
  setResponseStatus(event, 401);
@@ -87,7 +94,7 @@ export default defineEventHandler(async (event: H3Event) => {
87
94
  });
88
95
 
89
96
  const params = new URLSearchParams({
90
- client_id: clientId,
97
+ client_id: credentials.clientId,
91
98
  redirect_uri: redirectUri,
92
99
  response_type: "code",
93
100
  state,
@@ -10,6 +10,7 @@ import {
10
10
  getAppUrl,
11
11
  oauthCallbackResponse,
12
12
  oauthErrorPage,
13
+ resolveGoogleSignInCredentials,
13
14
  resolveOAuthOwner,
14
15
  setDesktopExchange,
15
16
  type OAuthStatePayload,
@@ -51,11 +52,10 @@ async function handleGoogleSignInCallback(
51
52
  return { error: "Missing authorization code" };
52
53
  }
53
54
 
54
- const clientId = process.env.GOOGLE_CLIENT_ID;
55
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
56
- if (!clientId || !clientSecret) {
55
+ const credentials = resolveGoogleSignInCredentials();
56
+ if (!credentials) {
57
57
  return oauthErrorPage(
58
- "Google OAuth is not configured (missing client id/secret).",
58
+ "Google sign-in is not configured (missing client id/secret).",
59
59
  );
60
60
  }
61
61
 
@@ -66,8 +66,8 @@ async function handleGoogleSignInCallback(
66
66
  },
67
67
  body: new URLSearchParams({
68
68
  code,
69
- client_id: clientId,
70
- client_secret: clientSecret,
69
+ client_id: credentials.clientId,
70
+ client_secret: credentials.clientSecret,
71
71
  redirect_uri: state.redirectUri,
72
72
  grant_type: "authorization_code",
73
73
  }),
@@ -47,6 +47,18 @@ const questionSchema = z.object({
47
47
  includeDecide: z.boolean().optional(),
48
48
  });
49
49
 
50
+ function normalizeDesignQuestions(
51
+ questions: z.infer<typeof questionSchema>[],
52
+ ): z.infer<typeof questionSchema>[] {
53
+ return questions.map((question) => ({
54
+ ...question,
55
+ // The agent supplies Explore/Decide choices explicitly when needed.
56
+ // Default injection duplicates cards on every question in the form.
57
+ includeExplore: question.includeExplore ?? false,
58
+ includeDecide: question.includeDecide ?? false,
59
+ }));
60
+ }
61
+
50
62
  export default defineAction({
51
63
  description:
52
64
  "Show a Claude Design-style question form in the Design editor before " +
@@ -95,6 +107,8 @@ export default defineAction({
95
107
  }) => {
96
108
  await assertAccess("design", designId, "editor");
97
109
 
110
+ const normalizedQuestions = normalizeDesignQuestions(questions);
111
+
98
112
  await writeAppState(designQuestionsStateKey(designId), {
99
113
  designId,
100
114
  title: title ?? "Quick questions before I design",
@@ -103,7 +117,7 @@ export default defineAction({
103
117
  "Pick what matters. Use Other for specifics, or let the agent decide.",
104
118
  skipLabel: skipLabel ?? "Decide for me",
105
119
  submitLabel: submitLabel ?? "Continue",
106
- questions,
120
+ questions: normalizedQuestions,
107
121
  });
108
122
 
109
123
  return {