@agent-native/core 0.84.55 → 0.84.57

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 (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-clips.mdx +2 -0
  5. package/corpus/core/docs/content/locales/de-DE/template-analytics.mdx +2 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-clips.mdx +2 -0
  7. package/corpus/core/docs/content/locales/es-ES/template-analytics.mdx +2 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-clips.mdx +2 -0
  9. package/corpus/core/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-clips.mdx +2 -0
  11. package/corpus/core/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-clips.mdx +2 -0
  13. package/corpus/core/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-clips.mdx +2 -0
  15. package/corpus/core/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-clips.mdx +2 -0
  17. package/corpus/core/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-clips.mdx +2 -0
  19. package/corpus/core/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-clips.mdx +2 -0
  21. package/corpus/core/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-clips.mdx +2 -0
  23. package/corpus/core/docs/content/template-analytics.mdx +2 -0
  24. package/corpus/core/docs/content/template-clips.mdx +22 -10
  25. package/corpus/core/docs/content/tracking.mdx +2 -0
  26. package/corpus/core/package.json +1 -1
  27. package/corpus/core/src/cli/recap.ts +146 -5
  28. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +8 -0
  29. package/corpus/templates/analytics/AGENTS.md +5 -0
  30. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +31 -0
  31. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  32. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  33. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +65 -3
  34. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-can-be-copied-as-temporary-private-links-for-agents.md +6 -0
  35. package/corpus/templates/analytics/server/handlers/session-replay.ts +78 -0
  36. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +313 -0
  37. package/corpus/templates/analytics/server/lib/session-replay.ts +198 -3
  38. package/corpus/templates/analytics/server/routes/[...page].get.ts +104 -1
  39. package/corpus/templates/analytics/server/routes/api/session-replay/agent-context.json.get.ts +48 -0
  40. package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +63 -0
  41. package/corpus/templates/analytics/shared/session-replay-agent-access.ts +10 -0
  42. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +14 -8
  43. package/corpus/templates/clips/AGENTS.md +3 -2
  44. package/corpus/templates/clips/actions/create-recording-agent-link.ts +90 -0
  45. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -46
  46. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -3
  47. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -1
  48. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -1
  49. package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
  50. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -1
  51. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -1
  52. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -1
  53. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -1
  54. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -1
  55. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -1
  56. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -1
  57. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -1
  58. package/corpus/templates/clips/app/routes/share.$shareId.tsx +100 -46
  59. package/corpus/templates/clips/changelog/2026-07-02-private-recordings-can-be-shared-with-agents-through-temporary-links-without-making-them-public.md +6 -0
  60. package/corpus/templates/clips/server/lib/public-agent-context.ts +26 -13
  61. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +2 -1
  62. package/corpus/templates/clips/server/routes/api/agent-frame.jpg.get.ts +2 -1
  63. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +2 -1
  64. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -2
  65. package/corpus/templates/clips/shared/agent-context.ts +5 -0
  66. package/corpus/templates/plan/actions/create-visual-recap.ts +39 -0
  67. package/corpus/templates/plan/actions/list-visual-plans.ts +2 -0
  68. package/corpus/templates/plan/actions/view-screen.ts +2 -0
  69. package/corpus/templates/plan/app/pages/PlansPage.tsx +279 -114
  70. package/corpus/templates/plan/changelog/2026-07-02-plan-comment-shortcuts-now-open-comment-mode-from-the-keyboa.md +6 -0
  71. package/corpus/templates/plan/server/db/schema.ts +3 -0
  72. package/corpus/templates/plan/server/lib/comment-notifications.ts +21 -2
  73. package/corpus/templates/plan/server/plans.ts +6 -0
  74. package/corpus/templates/plan/server/plugins/db.ts +11 -0
  75. package/corpus/templates/plan/shared/comment-context.ts +1 -0
  76. package/corpus/templates/plan/shared/types.ts +4 -0
  77. package/dist/cli/recap.d.ts +14 -0
  78. package/dist/cli/recap.d.ts.map +1 -1
  79. package/dist/cli/recap.js +88 -5
  80. package/dist/cli/recap.js.map +1 -1
  81. package/dist/collab/awareness.d.ts +2 -2
  82. package/dist/collab/awareness.d.ts.map +1 -1
  83. package/dist/collab/routes.d.ts +1 -1
  84. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  85. package/dist/notifications/routes.d.ts +3 -3
  86. package/dist/observability/routes.d.ts +4 -4
  87. package/dist/server/transcribe-voice.d.ts +1 -1
  88. package/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
  89. package/docs/content/locales/ar-SA/template-clips.mdx +2 -0
  90. package/docs/content/locales/de-DE/template-analytics.mdx +2 -0
  91. package/docs/content/locales/de-DE/template-clips.mdx +2 -0
  92. package/docs/content/locales/es-ES/template-analytics.mdx +2 -0
  93. package/docs/content/locales/es-ES/template-clips.mdx +2 -0
  94. package/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
  95. package/docs/content/locales/fr-FR/template-clips.mdx +2 -0
  96. package/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
  97. package/docs/content/locales/hi-IN/template-clips.mdx +2 -0
  98. package/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
  99. package/docs/content/locales/ja-JP/template-clips.mdx +2 -0
  100. package/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
  101. package/docs/content/locales/ko-KR/template-clips.mdx +2 -0
  102. package/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
  103. package/docs/content/locales/pt-BR/template-clips.mdx +2 -0
  104. package/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
  105. package/docs/content/locales/zh-CN/template-clips.mdx +2 -0
  106. package/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
  107. package/docs/content/locales/zh-TW/template-clips.mdx +2 -0
  108. package/docs/content/template-analytics.mdx +2 -0
  109. package/docs/content/template-clips.mdx +22 -10
  110. package/docs/content/tracking.mdx +2 -0
  111. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
- appBasePath,
3
2
  appPath,
3
+ useActionMutation,
4
4
  useActionQuery,
5
5
  useSession,
6
6
  useT,
@@ -38,7 +38,6 @@ import {
38
38
  import { Switch } from "@/components/ui/switch";
39
39
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
40
40
 
41
- import { buildAgentApiUrls } from "../../../shared/agent-context";
42
41
  import { isLoomEmbedUrl } from "../../../shared/loom";
43
42
  import { withShareAttribution } from "../../../shared/share-attribution";
44
43
 
@@ -284,46 +283,27 @@ function LinkTab({
284
283
  (data?.visibility as Visibility | null) ?? "private";
285
284
  const isPublic = visibility === "public";
286
285
  const isLoomRecording = isLoomRecordingProp || isLoomEmbedUrl(videoUrl);
287
- const publicAgentContextUrl =
288
- typeof window === "undefined"
289
- ? ""
290
- : buildAgentApiUrls(recordingId, {
291
- origin: window.location.origin,
292
- basePath: appBasePath(),
293
- }).contextUrl;
294
- const [tokenizedAgentContextUrl, setTokenizedAgentContextUrl] = useState("");
286
+ const createAgentLink = useActionMutation(
287
+ "create-recording-agent-link" as any,
288
+ );
289
+ const [agentContextUrl, setAgentContextUrl] = useState("");
295
290
 
296
291
  useEffect(() => {
297
- if (!isPublic || !hasPassword || typeof window === "undefined") {
298
- setTokenizedAgentContextUrl("");
299
- return;
300
- }
301
-
302
- let cancelled = false;
303
- async function loadTokenizedAgentContextUrl() {
304
- setTokenizedAgentContextUrl("");
305
- const res = await fetch(publicAgentContextUrl, {
306
- credentials: "include",
307
- }).catch(() => null);
308
- if (!res?.ok) return;
309
- const payload = await res.json().catch(() => null);
310
- const contextUrl =
311
- typeof payload?.apis?.context?.url === "string"
312
- ? payload.apis.context.url
313
- : "";
314
- if (!cancelled) setTokenizedAgentContextUrl(contextUrl);
315
- }
316
-
317
- void loadTokenizedAgentContextUrl();
318
- return () => {
319
- cancelled = true;
320
- };
321
- }, [hasPassword, isPublic, publicAgentContextUrl]);
322
-
323
- const agentContextUrl = hasPassword
324
- ? tokenizedAgentContextUrl
325
- : publicAgentContextUrl;
326
- const agentShareDisabled = isPending || !isPublic || !agentContextUrl;
292
+ setAgentContextUrl("");
293
+ }, [recordingId, visibility]);
294
+
295
+ async function handleCreateAgentLink() {
296
+ if (createAgentLink.isPending) return;
297
+ const result = (await createAgentLink.mutateAsync({
298
+ recordingId,
299
+ })) as { url?: string };
300
+ if (!result?.url) return;
301
+ setAgentContextUrl(result.url);
302
+ copyToClipboard(result.url);
303
+ }
304
+
305
+ const agentShareDisabled =
306
+ isPending || createAgentLink.isPending || !agentContextUrl;
327
307
  const agentPrompt = agentContextUrl
328
308
  ? t("shareDialog.agentPrompt", { agentContextUrl })
329
309
  : "";
@@ -348,11 +328,28 @@ function LinkTab({
348
328
  (and a connect link) instead of leaving it buried in Settings. */}
349
329
  {isPublic ? <SlackShareHint canManage={canManage} /> : null}
350
330
 
351
- <CopyField
352
- label={t("shareDialog.shareWithAgents")}
353
- value={agentContextUrl}
354
- disabled={agentShareDisabled}
355
- />
331
+ <div className="space-y-2">
332
+ <div className="flex items-end justify-between gap-2">
333
+ <div className="text-xs font-medium text-muted-foreground">
334
+ {t("shareDialog.shareWithAgents")}
335
+ </div>
336
+ <Button
337
+ type="button"
338
+ variant="outline"
339
+ size="sm"
340
+ className="h-7"
341
+ onClick={() => void handleCreateAgentLink()}
342
+ disabled={isPending || createAgentLink.isPending}
343
+ >
344
+ {t("shareUi.copy")}
345
+ </Button>
346
+ </div>
347
+ <CopyField
348
+ label=""
349
+ value={agentContextUrl}
350
+ disabled={agentShareDisabled}
351
+ />
352
+ </div>
356
353
 
357
354
  <CopyField
358
355
  label={t("shareDialog.copyAgentPrompt")}
@@ -360,7 +357,7 @@ function LinkTab({
360
357
  disabled={agentShareDisabled}
361
358
  />
362
359
 
363
- {isPublic && hasPassword ? (
360
+ {agentContextUrl || hasPassword || !isPublic ? (
364
361
  <p className="text-xs text-muted-foreground">
365
362
  {t("shareDialog.agentTokenDescription")}
366
363
  </p>
@@ -270,9 +270,11 @@ export function CopyField({
270
270
  };
271
271
  return (
272
272
  <div>
273
- <div className="mb-1 text-xs font-medium text-muted-foreground">
274
- {label}
275
- </div>
273
+ {label ? (
274
+ <div className="mb-1 text-xs font-medium text-muted-foreground">
275
+ {label}
276
+ </div>
277
+ ) : null}
276
278
  <div className="flex items-stretch gap-2">
277
279
  <Input
278
280
  readOnly
@@ -419,7 +419,7 @@ const messages = {
419
419
  agentPrompt:
420
420
  "اجلب عنوان URL لسياق وكيل Clips هذا: {{agentContextUrl}}. استخدم transcript.segments للسياق المنطوق، واجلب recommendedFrames أو عناوين URL الخاصة بواجهة API للإطارات لرؤية الشاشة، وتحقق من browserDiagnostics إن وجدت لسجلات وحدة التحكم المنقحة وبيانات طلبات fetch/XHR الوصفية.",
421
421
  agentTokenDescription:
422
- "يستخدم هذا الوكيل URL رمزًا مميزًا قصير العمر، بحيث يمكن للعملاء قراءة المقطع دون الكشف عن كلمة المرور.",
422
+ "يسمح URL المؤقت هذا للوكلاء بقراءة المقطع دون جعله عاما. تنتهي صلاحيته بعد ساعتين.",
423
423
  gifPreview: "معاينة GIF",
424
424
  openPlayer: "مشغل مفتوح",
425
425
  downloadMp4: "تحميل MP4",
@@ -436,7 +436,7 @@ const messages = {
436
436
  agentPrompt:
437
437
  "Rufe diese Clips-Agent-Kontext-URL ab: {{agentContextUrl}}. Verwende transcript.segments fuer den gesprochenen Kontext, rufe recommendedFrames oder die Frame-API-URLs ab, um den Bildschirm zu sehen, und pruefe browserDiagnostics, falls vorhanden, fuer redigierte Konsolenprotokolle und fetch/XHR-Anfragemetadaten.",
438
438
  agentTokenDescription:
439
- "Dieser Agent URL verwendet ein kurzlebiges Token, sodass Agenten den Clip lesen können, ohne das Passwort preiszugeben.",
439
+ "Diese temporare Agent-URL lasst Agenten den Clip lesen, ohne ihn offentlich zu machen. Sie lauft nach zwei Stunden ab.",
440
440
  gifPreview: "GIF-Vorschau",
441
441
  openPlayer: "Spieler öffnen",
442
442
  downloadMp4: "Laden Sie MP4 herunter",
@@ -419,7 +419,7 @@ const messages = {
419
419
  agentPrompt:
420
420
  "Fetch this Clips agent context URL: {{agentContextUrl}}. Use transcript.segments for spoken context, fetch recommendedFrames or the frame API URLs to see the screen, and check browserDiagnostics if present for redacted console logs and fetch/XHR request metadata.",
421
421
  agentTokenDescription:
422
- "This agent URL uses a short-lived token, so agents can read the clip without exposing the password.",
422
+ "This temporary agent URL lets agents read the clip without making it public. It expires after two hours.",
423
423
  gifPreview: "GIF preview",
424
424
  openPlayer: "Open player",
425
425
  downloadMp4: "Download MP4",
@@ -431,7 +431,7 @@ const messages = {
431
431
  agentPrompt:
432
432
  "Obtén esta URL de contexto para agentes de Clips: {{agentContextUrl}}. Usa transcript.segments para el contexto hablado, obtén recommendedFrames o las URLs de la API de fotogramas para ver la pantalla y revisa browserDiagnostics si está presente para ver registros de consola redactados y metadatos de solicitudes fetch/XHR.",
433
433
  agentTokenDescription:
434
- "Este agente URL utiliza un token de corta duración, por lo que los agentes pueden leer el clip sin exponer la contraseña.",
434
+ "Esta URL temporal para agentes permite leer el clip sin hacerlo publico. Caduca en dos horas.",
435
435
  gifPreview: "vista previa de GIF",
436
436
  openPlayer: "jugador abierto",
437
437
  downloadMp4: "Descargar MP4",
@@ -431,7 +431,7 @@ const messages = {
431
431
  agentPrompt:
432
432
  "Récupère cette URL de contexte Clips pour agent : {{agentContextUrl}}. Utilise transcript.segments pour le contexte parlé, récupère recommendedFrames ou les URLs de l'API d'images pour voir l'écran, et consulte browserDiagnostics s'il est présent pour les journaux de console expurgés et les métadonnées de requêtes fetch/XHR.",
433
433
  agentTokenDescription:
434
- "Cet agent URL utilise un jeton de courte durée, afin que les agents puissent lire le clip sans exposer le mot de passe.",
434
+ "Cette URL temporaire pour agents permet de lire le clip sans le rendre public. Elle expire dans deux heures.",
435
435
  gifPreview: "aperçu de GIF",
436
436
  openPlayer: "Joueur ouvert",
437
437
  downloadMp4: "Télécharger MP4",
@@ -416,7 +416,7 @@ const messages = {
416
416
  agentPrompt:
417
417
  "यह Clips एजेंट संदर्भ URL प्राप्त करें: {{agentContextUrl}}। बोले गए संदर्भ के लिए transcript.segments का उपयोग करें, स्क्रीन देखने के लिए recommendedFrames या फ्रेम API URL प्राप्त करें, और यदि browserDiagnostics मौजूद हो तो संशोधित कंसोल लॉग और fetch/XHR अनुरोध मेटाडेटा जांचें।",
418
418
  agentTokenDescription:
419
- "यह एजेंट URL एक अल्पकालिक टोकन का उपयोग करता है, ताकि एजेंट पासवर्ड उजागर किए बिना क्लिप पढ़ सकें।",
419
+ "यह अस्थायी एजेंट URL क्लिप को सार्वजनिक किए बिना एजेंटों को इसे पढ़ने देता है। यह दो घंटे बाद समाप्त हो जाता है।",
420
420
  gifPreview: "GIF पूर्वावलोकन",
421
421
  openPlayer: "खुला खिलाड़ी",
422
422
  downloadMp4: "MP4 डाउनलोड करें",
@@ -428,7 +428,7 @@ const messages = {
428
428
  agentPrompt:
429
429
  "この Clips エージェントコンテキスト URL を取得してください: {{agentContextUrl}}。音声の文脈には transcript.segments を使い、画面を見るために recommendedFrames またはフレーム API URL を取得し、browserDiagnostics がある場合は、編集済みのコンソールログと fetch/XHR リクエストのメタデータを確認してください。",
430
430
  agentTokenDescription:
431
- "このエージェント URL は有効期間の短いトークンを使用するため、エージェントはパスワードを公開せずにクリップを読み取ることができます。",
431
+ "この一時的なエージェント URL により、クリップを公開せずにエージェントが読み取れます。2 時間後に期限切れになります。",
432
432
  gifPreview: "GIF プレビュー",
433
433
  openPlayer: "プレーヤーを開く",
434
434
  downloadMp4: "ダウンロード",
@@ -421,7 +421,7 @@ const messages = {
421
421
  agentPrompt:
422
422
  "이 Clips 에이전트 컨텍스트 URL을 가져오세요: {{agentContextUrl}}. 말한 내용의 맥락은 transcript.segments를 사용하고, 화면을 보기 위해 recommendedFrames 또는 프레임 API URL을 가져오며, browserDiagnostics가 있으면 수정된 콘솔 로그와 fetch/XHR 요청 메타데이터를 확인하세요.",
423
423
  agentTokenDescription:
424
- "이 에이전트 URL 수명이 짧은 토큰을 사용하므로 에이전트는 비밀번호를 노출하지 않고도 클립을 읽을 수 있습니다.",
424
+ "이 임시 에이전트 URL 사용하면 클립을 공개하지 않고도 에이전트가 읽을 수 있습니다. 2시간 후 만료됩니다.",
425
425
  gifPreview: "GIF 미리보기",
426
426
  openPlayer: "플레이어 열기",
427
427
  downloadMp4: "MP4 다운로드",
@@ -429,7 +429,7 @@ const messages = {
429
429
  agentPrompt:
430
430
  "Busque esta URL de contexto para agentes do Clips: {{agentContextUrl}}. Use transcript.segments para o contexto falado, busque recommendedFrames ou as URLs da API de quadros para ver a tela e confira browserDiagnostics, se presente, para logs de console redigidos e metadados de solicitações fetch/XHR.",
431
431
  agentTokenDescription:
432
- "Este agente URL usa um token de curta duração, para que os agentes possam ler o clipe sem expor a senha.",
432
+ "Esta URL temporaria para agentes permite ler o clipe sem torna-lo publico. Ela expira em duas horas.",
433
433
  gifPreview: "visualização de GIF",
434
434
  openPlayer: "Jogador aberto",
435
435
  downloadMp4: "Baixar MP4",
@@ -403,7 +403,7 @@ const messages = {
403
403
  agentPrompt:
404
404
  "获取这个 Clips 代理上下文 URL:{{agentContextUrl}}。使用 transcript.segments 读取语音上下文,获取 recommendedFrames 或帧 API URL 来查看屏幕,并在 browserDiagnostics 存在时检查经过脱敏的控制台日志和 fetch/XHR 请求元数据。",
405
405
  agentTokenDescription:
406
- "该代理 URL 使用短期令牌,因此代理可以在不暴露密码的情况下读取剪辑。",
406
+ "这个临时代理 URL 可让代理读取剪辑,而无需将其设为公开。它会在两小时后过期。",
407
407
  gifPreview: "GIF 预览",
408
408
  openPlayer: "开放玩家",
409
409
  downloadMp4: "下载MP4",
@@ -403,7 +403,7 @@ const messages = {
403
403
  agentPrompt:
404
404
  "取得這個 Clips Agent 脈絡 URL:{{agentContextUrl}}。使用 transcript.segments 讀取語音脈絡,取得 recommendedFrames 或影格 API URL 來查看螢幕,並在 browserDiagnostics 存在時檢查已遮蔽的主控台記錄和 fetch/XHR 請求中繼資料。",
405
405
  agentTokenDescription:
406
- " Agent URL 使用短期權杖,因此 Agent 可以在不暴露密碼的情況下讀取剪輯。",
406
+ "這個臨時 Agent URL 可讓 Agent 讀取剪輯,而不必將其設為公開。它會在兩小時後過期。",
407
407
  gifPreview: "GIF 預覽",
408
408
  openPlayer: "開啟播放器",
409
409
  downloadMp4: "下載 MP4",
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  getRequestUserEmail,
13
13
  signShortLivedToken,
14
+ verifyShortLivedToken,
14
15
  } from "@agent-native/core/server";
15
16
  import { resolveAccess } from "@agent-native/core/sharing";
16
17
  import {
@@ -32,7 +33,12 @@ import {
32
33
  useState,
33
34
  type ReactNode,
34
35
  } from "react";
35
- import type { LoaderFunctionArgs, MetaFunction } from "react-router";
36
+ import {
37
+ data,
38
+ type HeadersArgs,
39
+ type LoaderFunctionArgs,
40
+ type MetaFunction,
41
+ } from "react-router";
36
42
  import { useLoaderData, useNavigate, useParams } from "react-router";
37
43
 
38
44
  import { CaptureInstallButton } from "@/components/capture-install-options";
@@ -65,7 +71,12 @@ import { parsePlaybackSpeed } from "@/lib/playback-speed";
65
71
  import { isStorageSetupFailureReason } from "@/lib/storage-failures";
66
72
 
67
73
  import { getDb, schema } from "../../server/db";
68
- import { buildAgentApiUrls, safeJsonForHtml } from "../../shared/agent-context";
74
+ import { CLIPS_AGENT_ACCESS_PARAM } from "../../server/lib/public-agent-context";
75
+ import {
76
+ agentAccessTokenResourceId,
77
+ buildAgentApiUrls,
78
+ safeJsonForHtml,
79
+ } from "../../shared/agent-context";
69
80
  import {
70
81
  isLoomEmbedBackedRecording,
71
82
  isLoomRecordingSource,
@@ -99,6 +110,34 @@ type SharePageLoaderData = {
99
110
  shareUrl: string | null;
100
111
  };
101
112
 
113
+ const PRIVATE_AGENT_SHARE_HEADERS = {
114
+ "Cache-Control": "private, max-age=0, no-store",
115
+ "Referrer-Policy": "no-referrer",
116
+ };
117
+
118
+ function emptyLoaderData(url: URL): SharePageLoaderData {
119
+ return {
120
+ recording: null,
121
+ agentContextUrl: null,
122
+ origin: url.origin,
123
+ shareUrl: null,
124
+ };
125
+ }
126
+
127
+ function shareLoaderData(
128
+ payload: SharePageLoaderData,
129
+ privateAgentAccess = false,
130
+ ) {
131
+ if (!privateAgentAccess) return payload;
132
+ return data(payload, {
133
+ headers: PRIVATE_AGENT_SHARE_HEADERS,
134
+ });
135
+ }
136
+
137
+ export function headers({ loaderHeaders }: HeadersArgs) {
138
+ return loaderHeaders;
139
+ }
140
+
102
141
  function failureDetail(reason: string | null | undefined): string | null {
103
142
  const trimmed = reason?.trim();
104
143
  if (!trimmed) return null;
@@ -132,13 +171,7 @@ function shouldShowGeneratedTitleSkeleton(
132
171
 
133
172
  export async function loader({ params, url }: LoaderFunctionArgs) {
134
173
  const id = params.shareId;
135
- if (!id)
136
- return {
137
- recording: null,
138
- agentContextUrl: null,
139
- origin: url.origin,
140
- shareUrl: null,
141
- };
174
+ if (!id) return emptyLoaderData(url);
142
175
 
143
176
  const [rec] = await getDb()
144
177
  .select({
@@ -151,6 +184,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
151
184
  status: schema.recordings.status,
152
185
  ownerEmail: schema.recordings.ownerEmail,
153
186
  password: schema.recordings.password,
187
+ expiresAt: schema.recordings.expiresAt,
154
188
  archivedAt: schema.recordings.archivedAt,
155
189
  trashedAt: schema.recordings.trashedAt,
156
190
  })
@@ -158,24 +192,25 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
158
192
  .where(eq(schema.recordings.id, id))
159
193
  .limit(1);
160
194
 
161
- if (!rec)
162
- return {
163
- recording: null,
164
- agentContextUrl: null,
165
- origin: url.origin,
166
- shareUrl: null,
167
- };
195
+ const agentAccessToken = url.searchParams.get(CLIPS_AGENT_ACCESS_PARAM) ?? "";
196
+ const hasAgentAccessToken = Boolean(agentAccessToken);
197
+ const tokenGrantsAgentAccess = agentAccessToken
198
+ ? verifyShortLivedToken(agentAccessToken, agentAccessTokenResourceId(id)).ok
199
+ : false;
200
+
201
+ if (!rec) return shareLoaderData(emptyLoaderData(url), hasAgentAccessToken);
202
+
203
+ if (rec.expiresAt) {
204
+ const expires = new Date(rec.expiresAt).getTime();
205
+ if (Number.isFinite(expires) && expires < Date.now()) {
206
+ return shareLoaderData(emptyLoaderData(url), hasAgentAccessToken);
207
+ }
208
+ }
168
209
 
169
- if (rec.visibility !== "public") {
210
+ if (rec.visibility !== "public" && !tokenGrantsAgentAccess) {
170
211
  const userEmail = getRequestUserEmail();
171
212
  const access = userEmail ? await resolveAccess("recording", id) : null;
172
- if (!access)
173
- return {
174
- recording: null,
175
- agentContextUrl: null,
176
- origin: url.origin,
177
- shareUrl: null,
178
- };
213
+ if (!access) return emptyLoaderData(url);
179
214
  }
180
215
 
181
216
  const recording: SharePageMetaRecording = {
@@ -190,29 +225,37 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
190
225
  trashedAt: rec.trashedAt,
191
226
  };
192
227
  const canExposeAgentContext =
193
- rec.visibility === "public" && !rec.archivedAt && !rec.trashedAt;
194
- const token =
195
- canExposeAgentContext &&
196
- rec.password &&
197
- getRequestUserEmail() === rec.ownerEmail
198
- ? signShortLivedToken({ resourceId: id })
228
+ (rec.visibility === "public" || tokenGrantsAgentAccess) &&
229
+ !rec.archivedAt &&
230
+ !rec.trashedAt;
231
+ const token = tokenGrantsAgentAccess
232
+ ? agentAccessToken
233
+ : canExposeAgentContext &&
234
+ rec.password &&
235
+ getRequestUserEmail() === rec.ownerEmail
236
+ ? signShortLivedToken({ resourceId: agentAccessTokenResourceId(id) })
199
237
  : undefined;
200
238
  const canExposeAnonymousAgentContext = canExposeAgentContext && !rec.password;
201
239
  const canExposeOwnerAgentContext = canExposeAgentContext && Boolean(token);
202
- return {
203
- recording,
204
- origin: url.origin,
205
- shareUrl: `${url.origin}${url.pathname}`,
206
- agentContextUrl:
207
- canExposeAnonymousAgentContext || canExposeOwnerAgentContext
208
- ? buildAgentApiUrls(id, {
209
- origin: url.origin,
210
- basePath:
211
- process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || "",
212
- token,
213
- }).contextUrl
214
- : null,
215
- };
240
+ return shareLoaderData(
241
+ {
242
+ recording,
243
+ origin: url.origin,
244
+ shareUrl: `${url.origin}${url.pathname}`,
245
+ agentContextUrl:
246
+ canExposeAnonymousAgentContext || canExposeOwnerAgentContext
247
+ ? buildAgentApiUrls(id, {
248
+ origin: url.origin,
249
+ basePath:
250
+ process.env.VITE_APP_BASE_PATH ||
251
+ process.env.APP_BASE_PATH ||
252
+ "",
253
+ token,
254
+ }).contextUrl
255
+ : null,
256
+ },
257
+ hasAgentAccessToken || canExposeOwnerAgentContext,
258
+ );
216
259
  }
217
260
 
218
261
  export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
@@ -273,7 +316,7 @@ function AgentDiscovery({
273
316
  {t("sharePage.agentReadableContext")}
274
317
  </a>
275
318
  <script
276
- type="application/json"
319
+ type="application/agent-native+json"
277
320
  id="clips-agent-context"
278
321
  dangerouslySetInnerHTML={{ __html: safeJsonForHtml(payload) }}
279
322
  />
@@ -362,9 +405,17 @@ export default function ShareRoute() {
362
405
  [],
363
406
  );
364
407
  const [downloading, setDownloading] = useState(false);
408
+ const agentAccessToken = useMemo(() => {
409
+ if (typeof window === "undefined") return "";
410
+ return (
411
+ new URLSearchParams(window.location.search).get(
412
+ CLIPS_AGENT_ACCESS_PARAM,
413
+ ) ?? ""
414
+ );
415
+ }, []);
365
416
 
366
417
  const dataQ = useQuery({
367
- queryKey: ["public-recording", shareId, password],
418
+ queryKey: ["public-recording", shareId, password, agentAccessToken],
368
419
  queryFn: async () => {
369
420
  const url = new URL(
370
421
  `${appBasePath()}/api/public-recording`,
@@ -372,6 +423,9 @@ export default function ShareRoute() {
372
423
  );
373
424
  url.searchParams.set("id", shareId ?? "");
374
425
  if (password) url.searchParams.set("password", password);
426
+ if (agentAccessToken) {
427
+ url.searchParams.set(CLIPS_AGENT_ACCESS_PARAM, agentAccessToken);
428
+ }
375
429
  const res = await fetch(url.toString());
376
430
  const data = await res.json().catch(() => ({}));
377
431
  return { ok: res.ok, status: res.status, data };
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-02
4
+ ---
5
+
6
+ Private recordings can be shared with agents through temporary links without making them public.
@@ -9,6 +9,7 @@ import { asc, eq } from "drizzle-orm";
9
9
  import { getRequestURL, setResponseHeader, type H3Event } from "h3";
10
10
 
11
11
  import {
12
+ agentAccessTokenResourceId,
12
13
  buildAgentApiUrls,
13
14
  buildRecommendedFrames,
14
15
  CLIP_AGENT_CONTEXT_VERSION,
@@ -53,6 +54,8 @@ export type PublicAgentAccessResult =
53
54
  | { ok: false; failure: PublicAgentFailure };
54
55
 
55
56
  const DEFAULT_MAX_AGENT_FRAME_MEDIA_BYTES = 200 * 1024 * 1024;
57
+ export const CLIPS_AGENT_ACCESS_PARAM = "agent_access";
58
+ export const CLIPS_AGENT_ACCESS_TTL_SECONDS = 2 * 60 * 60;
56
59
 
57
60
  function sameOwnerEmail(
58
61
  a: string | null | undefined,
@@ -207,8 +210,20 @@ export async function loadPublicAgentAccess(
207
210
  const viewerIsOwner = Boolean(
208
211
  session?.email && sameOwnerEmail(session.email, recording.ownerEmail),
209
212
  );
213
+ const suppliedToken = options.token ?? "";
214
+ const tokenAccess = suppliedToken
215
+ ? verifyShortLivedToken(
216
+ suppliedToken,
217
+ agentAccessTokenResourceId(recording.id),
218
+ )
219
+ : null;
220
+ const tokenAllowsAgentAccess = Boolean(tokenAccess?.ok);
210
221
 
211
- if (recording.visibility !== "public" && !viewerIsOwner) {
222
+ if (
223
+ recording.visibility !== "public" &&
224
+ !viewerIsOwner &&
225
+ !tokenAllowsAgentAccess
226
+ ) {
212
227
  return {
213
228
  ok: false,
214
229
  failure: { status: 404, body: { error: "Not found" } },
@@ -229,26 +244,22 @@ export async function loadPublicAgentAccess(
229
244
  }
230
245
 
231
246
  let apiToken: string | null = null;
247
+ if (tokenAllowsAgentAccess) {
248
+ apiToken = suppliedToken;
249
+ }
232
250
  if (
233
251
  recording.password &&
234
252
  recording.visibility === "public" &&
235
253
  viewerIsOwner
236
254
  ) {
237
- apiToken = signShortLivedToken({ resourceId: recording.id });
255
+ apiToken = signShortLivedToken({
256
+ resourceId: agentAccessTokenResourceId(recording.id),
257
+ });
238
258
  }
239
259
 
240
260
  if (recording.password && !viewerIsOwner) {
241
- const suppliedToken = options.token ?? "";
242
261
  const suppliedPassword = options.password ?? "";
243
- let allowed = false;
244
-
245
- if (suppliedToken) {
246
- const result = verifyShortLivedToken(suppliedToken, recording.id);
247
- if (result.ok) {
248
- allowed = true;
249
- apiToken = suppliedToken;
250
- }
251
- }
262
+ let allowed = tokenAllowsAgentAccess;
252
263
 
253
264
  if (
254
265
  !allowed &&
@@ -256,7 +267,9 @@ export async function loadPublicAgentAccess(
256
267
  verifySharePassword(suppliedPassword, recording.password)
257
268
  ) {
258
269
  allowed = true;
259
- apiToken = signShortLivedToken({ resourceId: recording.id });
270
+ apiToken = signShortLivedToken({
271
+ resourceId: agentAccessTokenResourceId(recording.id),
272
+ });
260
273
  }
261
274
 
262
275
  if (!allowed) {
@@ -23,6 +23,7 @@ import {
23
23
  loadPublicAgentAccess,
24
24
  parseAgentChapters,
25
25
  queryString,
26
+ CLIPS_AGENT_ACCESS_PARAM,
26
27
  } from "../../lib/public-agent-context.js";
27
28
 
28
29
  export default defineEventHandler(async (event: H3Event) => {
@@ -32,7 +33,7 @@ export default defineEventHandler(async (event: H3Event) => {
32
33
  const id = queryString(query.id);
33
34
  const accessResult = await loadPublicAgentAccess(event, id, {
34
35
  password: queryString(query.password),
35
- token: queryString(query.t),
36
+ token: queryString(query[CLIPS_AGENT_ACCESS_PARAM]) || queryString(query.t),
36
37
  });
37
38
 
38
39
  if (!accessResult.ok) {
@@ -13,6 +13,7 @@ import {
13
13
  } from "h3";
14
14
 
15
15
  import {
16
+ CLIPS_AGENT_ACCESS_PARAM,
16
17
  loadPublicAgentAccess,
17
18
  loadRecordingMediaBytes,
18
19
  queryString,
@@ -95,7 +96,7 @@ export default defineEventHandler(async (event: H3Event) => {
95
96
  const id = queryString(query.id);
96
97
  const accessResult = await loadPublicAgentAccess(event, id, {
97
98
  password: queryString(query.password),
98
- token: queryString(query.t),
99
+ token: queryString(query[CLIPS_AGENT_ACCESS_PARAM]) || queryString(query.t),
99
100
  });
100
101
 
101
102
  if (!accessResult.ok) {
@@ -16,6 +16,7 @@ import { buildAgentApiUrls } from "../../../shared/agent-context.js";
16
16
  import { isLoomEmbedBackedRecording } from "../../../shared/loom.js";
17
17
  import {
18
18
  applyAgentJsonHeaders,
19
+ CLIPS_AGENT_ACCESS_PARAM,
19
20
  getServerAppBasePath,
20
21
  loadAgentTranscript,
21
22
  loadPublicAgentAccess,
@@ -30,7 +31,7 @@ export default defineEventHandler(async (event: H3Event) => {
30
31
  const id = queryString(query.id);
31
32
  const accessResult = await loadPublicAgentAccess(event, id, {
32
33
  password: queryString(query.password),
33
- token: queryString(query.t),
34
+ token: queryString(query[CLIPS_AGENT_ACCESS_PARAM]) || queryString(query.t),
34
35
  });
35
36
 
36
37
  if (!accessResult.ok) {
@@ -26,7 +26,10 @@ import {
26
26
  type H3Event,
27
27
  } from "h3";
28
28
 
29
- import { buildAgentApiUrls } from "../../../shared/agent-context.js";
29
+ import {
30
+ agentAccessTokenResourceId,
31
+ buildAgentApiUrls,
32
+ } from "../../../shared/agent-context.js";
30
33
  import {
31
34
  normalizeTranscriptSegments,
32
35
  parseTranscriptSegments,
@@ -248,7 +251,9 @@ export default defineEventHandler(async (event) => {
248
251
  rec.visibility === "public" && !rec.archivedAt && !rec.trashedAt;
249
252
  const agentToken =
250
253
  canExposeAgentContext && rec.password
251
- ? signShortLivedToken({ resourceId: recordingId })
254
+ ? signShortLivedToken({
255
+ resourceId: agentAccessTokenResourceId(recordingId),
256
+ })
252
257
  : undefined;
253
258
  const agentContextUrl = canExposeAgentContext
254
259
  ? buildAgentApiUrls(recordingId, {