@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
@@ -144,6 +144,70 @@ export interface SessionRecordingSummary {
144
144
  canManage?: boolean;
145
145
  }
146
146
 
147
+ export interface AgentSessionRecordingSummary {
148
+ id: string;
149
+ clientRecordingId: string;
150
+ sessionId: string;
151
+ userId: string | null;
152
+ anonymousId: string | null;
153
+ userKey: string | null;
154
+ startedAt: string;
155
+ endedAt: string | null;
156
+ durationMs: number | null;
157
+ chunkCount: number;
158
+ eventCount: number;
159
+ totalBytes: number;
160
+ pageCount: number;
161
+ errorCount: number;
162
+ rageClickCount: number;
163
+ privacyMode: string;
164
+ firstUrl: string | null;
165
+ lastUrl: string | null;
166
+ path: string | null;
167
+ hostname: string | null;
168
+ referrer: string | null;
169
+ app: string | null;
170
+ template: string | null;
171
+ status: "active" | "completed";
172
+ createdAt: string;
173
+ updatedAt: string;
174
+ lastIngestedAt: string | null;
175
+ }
176
+
177
+ export function compactSessionRecordingSummary(
178
+ recording: SessionRecordingSummary,
179
+ ): AgentSessionRecordingSummary {
180
+ return {
181
+ id: recording.id,
182
+ clientRecordingId: recording.clientRecordingId,
183
+ sessionId: recording.sessionId,
184
+ userId: recording.userId,
185
+ anonymousId: recording.anonymousId,
186
+ userKey: recording.userKey,
187
+ startedAt: recording.startedAt,
188
+ endedAt: recording.endedAt,
189
+ durationMs: recording.durationMs,
190
+ chunkCount: recording.chunkCount,
191
+ eventCount: recording.eventCount,
192
+ totalBytes: recording.totalBytes,
193
+ pageCount: recording.pageCount,
194
+ errorCount: recording.errorCount,
195
+ rageClickCount: recording.rageClickCount,
196
+ privacyMode: recording.privacyMode,
197
+ firstUrl: recording.firstUrl,
198
+ lastUrl: recording.lastUrl,
199
+ path: recording.path,
200
+ hostname: recording.hostname,
201
+ referrer: recording.referrer,
202
+ app: recording.app,
203
+ template: recording.template,
204
+ status: recording.status,
205
+ createdAt: recording.createdAt,
206
+ updatedAt: recording.updatedAt,
207
+ lastIngestedAt: recording.lastIngestedAt,
208
+ };
209
+ }
210
+
147
211
  const MAX_REPLAY_CHUNKS_PER_REQUEST = 20;
148
212
  const MAX_REPLAY_CHUNKS_PER_RECORDING = 2_000;
149
213
  const MAX_INLINE_REPLAY_CHUNK_BYTES = 256 * 1024;
@@ -1524,6 +1588,22 @@ export async function getSessionReplaySummary(
1524
1588
  return rowToSessionRecordingSummary(access.resource, access.role);
1525
1589
  }
1526
1590
 
1591
+ export async function getSessionReplayTokenizedSummary(
1592
+ recordingId: string,
1593
+ ): Promise<SessionRecordingSummary> {
1594
+ const db = getDb() as any;
1595
+ // guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
1596
+ const [row] = await db
1597
+ .select()
1598
+ .from(schema.sessionRecordings)
1599
+ .where(eq(schema.sessionRecordings.id, recordingId))
1600
+ .limit(1);
1601
+ if (!row || !isVisibleSessionRecording(row)) {
1602
+ throw replayError("Session recording not found", 404);
1603
+ }
1604
+ return rowToSessionRecordingSummary(row, "viewer");
1605
+ }
1606
+
1527
1607
  function parseInlineReplayEvents(inlineData: string): unknown[] {
1528
1608
  try {
1529
1609
  const parsed = JSON.parse(inlineData);
@@ -1563,8 +1643,47 @@ export async function getSessionReplayManifest(
1563
1643
  }>;
1564
1644
  }> {
1565
1645
  const recording = await getSessionReplaySummary(recordingId, scope);
1646
+ return getSessionReplayManifestForRecording(recording);
1647
+ }
1648
+
1649
+ export async function getSessionReplayTokenizedManifest(
1650
+ recordingId: string,
1651
+ ): Promise<{
1652
+ recording: AgentSessionRecordingSummary;
1653
+ chunks: Array<{
1654
+ seq: number;
1655
+ checksum: string;
1656
+ byteLength: number;
1657
+ eventCount: number;
1658
+ startedAt: string | null;
1659
+ endedAt: string | null;
1660
+ bytesPath: string;
1661
+ }>;
1662
+ }> {
1663
+ const recording = await getSessionReplayTokenizedSummary(recordingId);
1664
+ const manifest = await getSessionReplayManifestForRecording(recording);
1665
+ return {
1666
+ ...manifest,
1667
+ recording: compactSessionRecordingSummary(manifest.recording),
1668
+ };
1669
+ }
1670
+
1671
+ async function getSessionReplayManifestForRecording(
1672
+ recording: SessionRecordingSummary,
1673
+ ): Promise<{
1674
+ recording: SessionRecordingSummary;
1675
+ chunks: Array<{
1676
+ seq: number;
1677
+ checksum: string;
1678
+ byteLength: number;
1679
+ eventCount: number;
1680
+ startedAt: string | null;
1681
+ endedAt: string | null;
1682
+ bytesPath: string;
1683
+ }>;
1684
+ }> {
1566
1685
  const db = getDb() as any;
1567
- // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) verifies viewer access; chunks are not directly shareable resources.
1686
+ // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) or a scoped agent token verifies access; chunks are not directly shareable resources.
1568
1687
  const rows = await db
1569
1688
  .select()
1570
1689
  .from(schema.sessionReplayChunks)
@@ -1599,8 +1718,39 @@ export async function readSessionReplayChunkBytes(
1599
1718
  json: string;
1600
1719
  }> {
1601
1720
  const recording = await getSessionReplaySummary(recordingId, scope);
1721
+ return readSessionReplayChunkBytesForRecording(recording, seq);
1722
+ }
1723
+
1724
+ export async function readSessionReplayTokenizedChunkBytes(
1725
+ recordingId: string,
1726
+ seq: number,
1727
+ ): Promise<{
1728
+ recording: AgentSessionRecordingSummary;
1729
+ seq: number;
1730
+ checksum: string;
1731
+ /** Decompressed replay-chunk JSON text (a serialized rrweb events array). */
1732
+ json: string;
1733
+ }> {
1734
+ const recording = await getSessionReplayTokenizedSummary(recordingId);
1735
+ const chunk = await readSessionReplayChunkBytesForRecording(recording, seq);
1736
+ return {
1737
+ ...chunk,
1738
+ recording: compactSessionRecordingSummary(chunk.recording),
1739
+ };
1740
+ }
1741
+
1742
+ async function readSessionReplayChunkBytesForRecording(
1743
+ recording: SessionRecordingSummary,
1744
+ seq: number,
1745
+ ): Promise<{
1746
+ recording: SessionRecordingSummary;
1747
+ seq: number;
1748
+ checksum: string;
1749
+ /** Decompressed replay-chunk JSON text (a serialized rrweb events array). */
1750
+ json: string;
1751
+ }> {
1602
1752
  const db = getDb() as any;
1603
- // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) verifies viewer access; chunks are not directly shareable resources.
1753
+ // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) or a scoped agent token verifies access; chunks are not directly shareable resources.
1604
1754
  const [row] = await db
1605
1755
  .select()
1606
1756
  .from(schema.sessionReplayChunks)
@@ -1660,6 +1810,51 @@ export async function getSessionReplayEvents(
1660
1810
  unavailableChunks: number;
1661
1811
  }> {
1662
1812
  const recording = await getSessionReplaySummary(recordingId, scope);
1813
+ return getSessionReplayEventsForRecording(recording, options);
1814
+ }
1815
+
1816
+ export async function getSessionReplayTokenizedEvents(
1817
+ recordingId: string,
1818
+ options: SessionReplayEventReadOptions = {},
1819
+ ): Promise<{
1820
+ recording: AgentSessionRecordingSummary;
1821
+ chunks: Array<{
1822
+ seq: number;
1823
+ checksum: string;
1824
+ byteLength: number;
1825
+ eventCount: number;
1826
+ events: unknown[];
1827
+ unavailable?: boolean;
1828
+ }>;
1829
+ eventCount: number;
1830
+ truncated: boolean;
1831
+ unavailableChunks: number;
1832
+ }> {
1833
+ const recording = await getSessionReplayTokenizedSummary(recordingId);
1834
+ const result = await getSessionReplayEventsForRecording(recording, options);
1835
+ return {
1836
+ ...result,
1837
+ recording: compactSessionRecordingSummary(result.recording),
1838
+ };
1839
+ }
1840
+
1841
+ async function getSessionReplayEventsForRecording(
1842
+ recording: SessionRecordingSummary,
1843
+ options: SessionReplayEventReadOptions = {},
1844
+ ): Promise<{
1845
+ recording: SessionRecordingSummary;
1846
+ chunks: Array<{
1847
+ seq: number;
1848
+ checksum: string;
1849
+ byteLength: number;
1850
+ eventCount: number;
1851
+ events: unknown[];
1852
+ unavailable?: boolean;
1853
+ }>;
1854
+ eventCount: number;
1855
+ truncated: boolean;
1856
+ unavailableChunks: number;
1857
+ }> {
1663
1858
  const maxEvents = Math.min(
1664
1859
  MAX_REPLAY_EVENTS_READ,
1665
1860
  Math.max(1, options.limit ?? MAX_REPLAY_EVENTS_READ),
@@ -1675,7 +1870,7 @@ export async function getSessionReplayEvents(
1675
1870
  }
1676
1871
 
1677
1872
  const db = getDb() as any;
1678
- // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) verifies viewer access; chunks are not directly shareable resources.
1873
+ // guard:allow-unscoped -- chunk rows are loaded only after resolveAccess("session-recording", recordingId) or a scoped agent token verifies access; chunks are not directly shareable resources.
1679
1874
  const rows = await db
1680
1875
  .select()
1681
1876
  .from(schema.sessionReplayChunks)
@@ -1,5 +1,108 @@
1
1
  import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler";
2
+ import {
3
+ defineEventHandler,
4
+ getQuery,
5
+ getRequestURL,
6
+ setResponseHeader,
7
+ } from "h3";
2
8
 
3
- export default createH3SSRHandler(
9
+ import {
10
+ buildSessionReplayAgentContext,
11
+ safeJsonForHtml,
12
+ SESSION_REPLAY_AGENT_ACCESS_PARAM,
13
+ } from "../lib/session-replay-agent-context.js";
14
+
15
+ const ssrHandler = createH3SSRHandler(
4
16
  () => import("virtual:react-router/server-build"),
5
17
  );
18
+
19
+ function configuredAppBasePath(): string {
20
+ const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || "";
21
+ if (!raw || raw === "/") return "";
22
+ const normalized = raw.startsWith("/") ? raw : `/${raw}`;
23
+ return normalized.replace(/\/+$/, "");
24
+ }
25
+
26
+ function stripAppBasePath(pathname: string): string {
27
+ const basePath = configuredAppBasePath();
28
+ if (!basePath) return pathname;
29
+ if (pathname === basePath) return "/";
30
+ if (pathname.startsWith(`${basePath}/`)) {
31
+ return pathname.slice(basePath.length) || "/";
32
+ }
33
+ return pathname;
34
+ }
35
+
36
+ function sessionRecordingIdFromPath(pathname: string): string | null {
37
+ const match = stripAppBasePath(pathname).match(/^\/sessions\/([^/]+)\/?$/);
38
+ if (!match?.[1]) return null;
39
+ try {
40
+ return decodeURIComponent(match[1]);
41
+ } catch {
42
+ return match[1];
43
+ }
44
+ }
45
+
46
+ function queryString(value: unknown): string {
47
+ if (typeof value === "string") return value;
48
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
49
+ return "";
50
+ }
51
+
52
+ async function buildAgentDiscoveryScript(event: any): Promise<string | null> {
53
+ const requestUrl = getRequestURL(event);
54
+ const recordingId = sessionRecordingIdFromPath(requestUrl.pathname);
55
+ if (!recordingId) return null;
56
+
57
+ const query = getQuery(event);
58
+ const token = queryString(query[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
59
+ if (!token) return null;
60
+
61
+ const context = await buildSessionReplayAgentContext({
62
+ recordingId,
63
+ token,
64
+ origin: requestUrl.origin,
65
+ includeTimeline: false,
66
+ }).catch(() => null);
67
+ if (!context) return null;
68
+
69
+ return `<script type="application/agent-native+json" id="analytics-session-replay-agent-context">${safeJsonForHtml(
70
+ context,
71
+ )}</script>`;
72
+ }
73
+
74
+ function injectAgentDiscovery(html: string, script: string): string {
75
+ if (html.includes('id="analytics-session-replay-agent-context"')) {
76
+ return html;
77
+ }
78
+ if (html.includes("</head>")) {
79
+ return html.replace("</head>", `${script}</head>`);
80
+ }
81
+ if (html.includes("</body>")) {
82
+ return html.replace("</body>", `${script}</body>`);
83
+ }
84
+ return `${html}${script}`;
85
+ }
86
+
87
+ export default defineEventHandler(async (event) => {
88
+ const response = (await ssrHandler(event)) as Response;
89
+ const script = await buildAgentDiscoveryScript(event);
90
+ if (!script) return response;
91
+
92
+ const contentType = response.headers.get("content-type") ?? "";
93
+ if (!contentType.includes("text/html")) return response;
94
+
95
+ const html = await response.text();
96
+ const headers = new Headers(response.headers);
97
+ headers.delete("content-length");
98
+ headers.set("Cache-Control", "private, max-age=0, no-store");
99
+ headers.set("Referrer-Policy", "no-referrer");
100
+ setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
101
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
102
+
103
+ return new Response(injectAgentDiscovery(html, script), {
104
+ status: response.status,
105
+ statusText: response.statusText,
106
+ headers,
107
+ });
108
+ });
@@ -0,0 +1,48 @@
1
+ import {
2
+ defineEventHandler,
3
+ getQuery,
4
+ getRequestURL,
5
+ setResponseHeader,
6
+ setResponseStatus,
7
+ } from "h3";
8
+
9
+ import {
10
+ buildSessionReplayAgentContext,
11
+ SESSION_REPLAY_AGENT_ACCESS_PARAM,
12
+ } from "../../../lib/session-replay-agent-context.js";
13
+
14
+ function queryString(value: unknown): string {
15
+ if (typeof value === "string") return value;
16
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
17
+ return "";
18
+ }
19
+
20
+ function applyAgentJsonHeaders(event: any) {
21
+ setResponseHeader(event, "Content-Type", "application/json; charset=utf-8");
22
+ setResponseHeader(event, "X-Content-Type-Options", "nosniff");
23
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
24
+ setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
25
+ }
26
+
27
+ export default defineEventHandler(async (event) => {
28
+ applyAgentJsonHeaders(event);
29
+ const query = getQuery(event);
30
+ const id = queryString(query.id);
31
+ const token = queryString(query[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
32
+
33
+ if (!id || !token) {
34
+ setResponseStatus(event, 400);
35
+ return { error: "id and agent access token are required" };
36
+ }
37
+
38
+ try {
39
+ return await buildSessionReplayAgentContext({
40
+ recordingId: id,
41
+ token,
42
+ origin: getRequestURL(event).origin,
43
+ });
44
+ } catch (error: any) {
45
+ setResponseStatus(event, error?.statusCode ?? 400);
46
+ return { error: error?.message || String(error) };
47
+ }
48
+ });
@@ -0,0 +1,63 @@
1
+ import {
2
+ defineEventHandler,
3
+ getQuery,
4
+ setResponseHeader,
5
+ setResponseStatus,
6
+ } from "h3";
7
+
8
+ import {
9
+ SESSION_REPLAY_AGENT_ACCESS_PARAM,
10
+ verifySessionReplayAgentAccess,
11
+ } from "../../../lib/session-replay-agent-context.js";
12
+ import {
13
+ getSessionReplayTokenizedEvents,
14
+ type SessionReplayEventReadOptions,
15
+ } from "../../../lib/session-replay.js";
16
+
17
+ function queryString(value: unknown): string {
18
+ if (typeof value === "string") return value;
19
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
20
+ return "";
21
+ }
22
+
23
+ function queryInt(value: unknown): number | undefined {
24
+ const raw = queryString(value);
25
+ if (!raw || !/^\d+$/.test(raw)) return undefined;
26
+ return Number(raw);
27
+ }
28
+
29
+ function applyAgentJsonHeaders(event: any) {
30
+ setResponseHeader(event, "Content-Type", "application/json; charset=utf-8");
31
+ setResponseHeader(event, "X-Content-Type-Options", "nosniff");
32
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
33
+ setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
34
+ }
35
+
36
+ export default defineEventHandler(async (event) => {
37
+ applyAgentJsonHeaders(event);
38
+ const query = getQuery(event);
39
+ const id = queryString(query.id);
40
+ const token = queryString(query[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
41
+
42
+ if (!id || !token) {
43
+ setResponseStatus(event, 400);
44
+ return { error: "id and agent access token are required" };
45
+ }
46
+ if (!verifySessionReplayAgentAccess(id, token)) {
47
+ setResponseStatus(event, 401);
48
+ return { error: "Invalid or expired agent access" };
49
+ }
50
+
51
+ const options: SessionReplayEventReadOptions = {
52
+ startSeq: queryInt(query.startSeq),
53
+ endSeq: queryInt(query.endSeq),
54
+ limit: queryInt(query.limit),
55
+ };
56
+
57
+ try {
58
+ return await getSessionReplayTokenizedEvents(id, options);
59
+ } catch (error: any) {
60
+ setResponseStatus(event, error?.statusCode ?? 400);
61
+ return { error: error?.message || String(error) };
62
+ }
63
+ });
@@ -0,0 +1,10 @@
1
+ export const SESSION_REPLAY_AGENT_ACCESS_PARAM = "agent_access";
2
+
3
+ const SESSION_REPLAY_AGENT_ACCESS_TOKEN_PREFIX =
4
+ "analytics-session-replay-agent-context";
5
+
6
+ export function sessionReplayAgentAccessTokenResourceId(
7
+ recordingId: string,
8
+ ): string {
9
+ return `${SESSION_REPLAY_AGENT_ACCESS_TOKEN_PREFIX}:${recordingId}`;
10
+ }
@@ -156,9 +156,10 @@ Required Slack app setup:
156
156
  - Deploy secrets: `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, and
157
157
  `SLACK_SIGNING_SECRET`
158
158
 
159
- ## Agent-readable public clips
159
+ ## Agent-readable clips
160
160
 
161
- Public recordings also expose URLs meant for external agents:
161
+ Recordings can expose URLs meant for external agents without handing over raw
162
+ video bytes:
162
163
 
163
164
  | Endpoint | Meaning |
164
165
  | -------- | ------- |
@@ -166,9 +167,14 @@ Public recordings also expose URLs meant for external agents:
166
167
  | `/api/agent-transcript.json?id=<recordingId>` | Timestamped transcript segments with `startMs`, `endMs`, `timestamp`, `range`, `text`, and optional `source` |
167
168
  | `/api/agent-frame.jpg?id=<recordingId>&atMs=<ms>` | JPEG frame extracted from the video at the requested original-video timestamp |
168
169
 
169
- These endpoints follow the same access model as `/api/public-recording`:
170
+ These endpoints follow the same access model as `/api/public-recording`, plus a
171
+ temporary agent-link path:
170
172
 
171
173
  - Non-public clips return not found to anonymous callers.
174
+ - `create-recording-agent-link` resolves normal recording access, rejects
175
+ archived or trashed recordings, then mints a two-hour `agent_access` URL for
176
+ `/share/:recordingId`. The share page SSR advertises the agent context URL,
177
+ and the JSON endpoints accept the same scoped token.
172
178
  - Expired clips return expired.
173
179
  - Password-protected clips require `password=<pw>` once; successful JSON
174
180
  responses include short-lived tokenized links so the plaintext password is not
@@ -183,11 +189,11 @@ These endpoints follow the same access model as `/api/public-recording`:
183
189
  - Frame extraction must use the checked recording media path and must not expose
184
190
  raw provider URLs.
185
191
 
186
- The share popover's "Share with agents" field should copy the agent context URL,
187
- not raw transcript text. Its "Copy agent prompt" field may wrap that URL with
188
- instructions to fetch transcripts, frames, and browser diagnostics, but it should
189
- still point agents at the context response so they can fetch only the visual
190
- context they need.
192
+ The share popover's "Share with agents" field should copy an agent context URL
193
+ or tokenized share page URL, not raw transcript text. Its "Copy agent prompt"
194
+ field may wrap that URL with instructions to fetch transcripts, frames, and
195
+ browser diagnostics, but it should still point agents at the context response so
196
+ they can fetch only the visual context they need.
191
197
 
192
198
  ## View counting
193
199
 
@@ -132,8 +132,9 @@ Detailed media, meeting, dictation, editing, and sharing rules live in
132
132
  - `navigate` moves the UI to recording/library/meeting/share surfaces.
133
133
  - Use data actions for full transcripts and media metadata.
134
134
  - For the in-app Clips agent, prefer `get-recording-player-data` for full
135
- private/authenticated recording context. Use the public agent-context URLs
136
- when preparing a link for another agent outside Clips.
135
+ private/authenticated recording context. When preparing a link for another
136
+ agent outside Clips, use `create-recording-agent-link`; it mints a two-hour
137
+ `agent_access` share URL without changing recording visibility.
137
138
 
138
139
  ## Skills
139
140
 
@@ -0,0 +1,90 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestContext,
4
+ getRequestUserEmail,
5
+ signShortLivedToken,
6
+ } from "@agent-native/core/server";
7
+ import { ForbiddenError, resolveAccess } from "@agent-native/core/sharing";
8
+ import { z } from "zod";
9
+
10
+ import {
11
+ CLIPS_AGENT_ACCESS_PARAM,
12
+ CLIPS_AGENT_ACCESS_TTL_SECONDS,
13
+ getServerAppBasePath,
14
+ } from "../server/lib/public-agent-context.js";
15
+ import {
16
+ agentAccessTokenResourceId,
17
+ buildAgentApiUrls,
18
+ } from "../shared/agent-context.js";
19
+
20
+ function appOrigin(): string {
21
+ const origin =
22
+ getRequestContext()?.requestOrigin ||
23
+ process.env.APP_URL ||
24
+ process.env.BETTER_AUTH_URL ||
25
+ "http://localhost:3000";
26
+ try {
27
+ return new URL(origin).origin;
28
+ } catch {
29
+ return "http://localhost:3000";
30
+ }
31
+ }
32
+
33
+ function appendAgentAccess(url: string, token: string): string {
34
+ const sep = url.includes("?") ? "&" : "?";
35
+ return `${url}${sep}${CLIPS_AGENT_ACCESS_PARAM}=${encodeURIComponent(token)}`;
36
+ }
37
+
38
+ export default defineAction({
39
+ description:
40
+ "Create a temporary private agent-readable link for one Clips recording. The URL is scoped to that recording and expires after two hours.",
41
+ schema: z.object({
42
+ recordingId: z.string().describe("Recording ID"),
43
+ }),
44
+ readOnly: true,
45
+ run: async (args) => {
46
+ const access = await resolveAccess("recording", args.recordingId);
47
+ if (!access) {
48
+ throw new ForbiddenError(`No access to recording ${args.recordingId}`);
49
+ }
50
+
51
+ const recording = access.resource as {
52
+ id: string;
53
+ archivedAt?: string | null;
54
+ trashedAt?: string | null;
55
+ };
56
+ if (recording.archivedAt || recording.trashedAt) {
57
+ throw new ForbiddenError(
58
+ `Recording ${args.recordingId} is not shareable`,
59
+ );
60
+ }
61
+
62
+ const token = signShortLivedToken({
63
+ resourceId: agentAccessTokenResourceId(recording.id),
64
+ viewerEmail: getRequestUserEmail() || undefined,
65
+ ttlSeconds: CLIPS_AGENT_ACCESS_TTL_SECONDS,
66
+ });
67
+ const origin = appOrigin();
68
+ const basePath = getServerAppBasePath();
69
+ const expiresAt = new Date(
70
+ Date.now() + CLIPS_AGENT_ACCESS_TTL_SECONDS * 1000,
71
+ ).toISOString();
72
+ const pageUrl = appendAgentAccess(
73
+ `${origin}${basePath}/share/${encodeURIComponent(recording.id)}`,
74
+ token,
75
+ );
76
+ const api = buildAgentApiUrls(recording.id, {
77
+ origin,
78
+ basePath,
79
+ token,
80
+ });
81
+
82
+ return {
83
+ recordingId: recording.id,
84
+ url: pageUrl,
85
+ contextUrl: api.contextUrl,
86
+ expiresAt,
87
+ ttlSeconds: CLIPS_AGENT_ACCESS_TTL_SECONDS,
88
+ };
89
+ },
90
+ });