@agent-native/core 0.114.4 → 0.114.6

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 (95) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/resources/ResourceTree.tsx +11 -5
  5. package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
  6. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +11 -1
  7. package/corpus/templates/clips/AGENTS.md +14 -4
  8. package/corpus/templates/clips/actions/update-meeting.ts +13 -3
  9. package/corpus/templates/clips/actions/view-screen.ts +1 -0
  10. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +96 -4
  11. package/corpus/templates/clips/app/i18n/en-US.ts +11 -0
  12. package/corpus/templates/clips/app/lib/public-meeting.ts +85 -0
  13. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +11 -1
  14. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -7
  15. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +241 -135
  16. package/corpus/templates/clips/changelog/2026-07-20-meeting-share-links-can-include-the-full-transcript-with-an-.md +6 -0
  17. package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
  18. package/corpus/templates/clips/changelog/2026-07-20-recording-retries-the-default-mac-microphone-when-a-saved-in.md +6 -0
  19. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-now-tell-agents-to-wait-while-uploads-and-trans.md +6 -0
  20. package/corpus/templates/clips/desktop/src/app.tsx +7 -11
  21. package/corpus/templates/clips/desktop/src/lib/meeting-join-url.ts +1 -33
  22. package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
  23. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +65 -2
  24. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
  25. package/corpus/templates/clips/server/db/schema.ts +3 -0
  26. package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -21
  27. package/corpus/templates/clips/server/plugins/auth.ts +1 -0
  28. package/corpus/templates/clips/server/plugins/db.ts +6 -0
  29. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +13 -3
  30. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +152 -0
  31. package/corpus/templates/clips/shared/agent-context.ts +60 -0
  32. package/corpus/templates/clips/shared/meeting-join-url.ts +31 -0
  33. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
  34. package/corpus/templates/content/AGENTS.md +4 -1
  35. package/corpus/templates/content/actions/_content-spaces.ts +6 -1
  36. package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
  37. package/corpus/templates/content/actions/_database-utils.ts +83 -15
  38. package/corpus/templates/content/actions/add-database-item.ts +3 -0
  39. package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
  40. package/corpus/templates/content/actions/delete-content-database.ts +12 -9
  41. package/corpus/templates/content/actions/delete-database-items.ts +3 -2
  42. package/corpus/templates/content/actions/delete-document.ts +233 -6
  43. package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
  44. package/corpus/templates/content/actions/get-document.ts +4 -1
  45. package/corpus/templates/content/actions/list-content-databases.ts +1 -0
  46. package/corpus/templates/content/actions/list-documents.ts +1 -0
  47. package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
  48. package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
  49. package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
  50. package/corpus/templates/content/actions/pull-document.ts +15 -2
  51. package/corpus/templates/content/actions/restore-content-database.ts +48 -11
  52. package/corpus/templates/content/actions/restore-document.ts +28 -0
  53. package/corpus/templates/content/actions/search-documents.ts +2 -1
  54. package/corpus/templates/content/actions/view-screen.ts +2 -1
  55. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
  56. package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
  57. package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
  58. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
  59. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
  60. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
  61. package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
  62. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
  63. package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
  64. package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
  65. package/corpus/templates/content/app/i18n-data.ts +83 -2
  66. package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
  67. package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
  68. package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
  69. package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
  70. package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
  71. package/corpus/templates/content/parity/matrix.md +1 -1
  72. package/corpus/templates/content/parity/matrix.ts +3 -0
  73. package/corpus/templates/content/server/db/schema.ts +2 -0
  74. package/corpus/templates/content/server/lib/public-documents.ts +8 -2
  75. package/corpus/templates/content/server/plugins/db.ts +52 -0
  76. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
  77. package/corpus/templates/content/shared/api.ts +10 -0
  78. package/corpus/toolkit/CHANGELOG.md +6 -0
  79. package/corpus/toolkit/package.json +1 -1
  80. package/corpus/toolkit/src/ui/progress.tsx +3 -2
  81. package/dist/client/resources/ResourceTree.d.ts +3 -1
  82. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  83. package/dist/client/resources/ResourceTree.js +4 -4
  84. package/dist/client/resources/ResourceTree.js.map +1 -1
  85. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  86. package/dist/client/resources/ResourcesPanel.js +55 -6
  87. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  88. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  89. package/dist/notifications/routes.d.ts +3 -3
  90. package/dist/observability/routes.d.ts +5 -5
  91. package/dist/secrets/routes.d.ts +9 -9
  92. package/dist/server/transcribe-voice.d.ts +1 -1
  93. package/package.json +3 -3
  94. package/src/client/resources/ResourceTree.tsx +11 -5
  95. package/src/client/resources/ResourcesPanel.tsx +141 -18
@@ -0,0 +1,22 @@
1
+ import { open as openExternal } from "@tauri-apps/plugin-shell";
2
+
3
+ import { resolveDesktopMeetingJoinUrl } from "./meeting-join-url";
4
+
5
+ type OpenExternal = (url: string) => Promise<void>;
6
+
7
+ export async function openMeetingJoinUrl(
8
+ joinUrl: string,
9
+ open: OpenExternal = openExternal,
10
+ ): Promise<void> {
11
+ const nativeJoinUrl = resolveDesktopMeetingJoinUrl(joinUrl);
12
+ if (nativeJoinUrl === joinUrl) {
13
+ await open(joinUrl);
14
+ return;
15
+ }
16
+
17
+ try {
18
+ await open(nativeJoinUrl);
19
+ } catch {
20
+ await open(joinUrl);
21
+ }
22
+ }
@@ -224,6 +224,35 @@ function browserLocale(): string {
224
224
  return navigator.language || "en-US";
225
225
  }
226
226
 
227
+ function isUnavailableSelectedMicrophoneError(error: unknown): boolean {
228
+ const message = error instanceof Error ? error.message : String(error);
229
+ return /selected microphone .+ is not available/i.test(message);
230
+ }
231
+
232
+ function transcriptionStartError(
233
+ error: unknown,
234
+ selectedMicrophoneUnavailable: boolean,
235
+ ): Error {
236
+ if (selectedMicrophoneUnavailable) {
237
+ return new Error(
238
+ "Your selected microphone is no longer available. Clips tried your Mac's default microphone, but notes still could not start. Choose an available microphone in Clips settings, then try again.",
239
+ );
240
+ }
241
+
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ if (
244
+ /screencapturekit|voiceprocessingi|microphone|audio capture|local .*capture/i.test(
245
+ message,
246
+ )
247
+ ) {
248
+ return new Error(
249
+ "Clips could not start local audio capture. Check that Clips has Microphone and Screen Recording access in System Settings, then try again.",
250
+ );
251
+ }
252
+
253
+ return new Error("Clips could not start local transcription. Try again.");
254
+ }
255
+
227
256
  export function recordingTranscriptionLanguage(): string | null {
228
257
  return null;
229
258
  }
@@ -298,12 +327,46 @@ export async function startTranscriptionEngine(opts: {
298
327
  );
299
328
  return "whisper";
300
329
  } catch (err) {
330
+ let fallbackMic = opts.mic;
331
+ const selectedMicrophoneUnavailable =
332
+ Boolean(opts.mic) && isUnavailableSelectedMicrophoneError(err);
301
333
  console.warn(
302
334
  "[transcription] whisper mic+system failed, falling back to mic-only:",
303
335
  err,
304
336
  );
305
- await restartTranscriptionEngine("macos-native", opts.mic);
306
- return "macos-native";
337
+ if (selectedMicrophoneUnavailable) {
338
+ console.warn(
339
+ "[transcription] selected microphone is unavailable; retrying with the macOS default input:",
340
+ err,
341
+ );
342
+ try {
343
+ await restartTranscriptionEngine(
344
+ "whisper",
345
+ undefined,
346
+ captureSystem,
347
+ voiceProcessing,
348
+ emitPartials,
349
+ );
350
+ return "whisper";
351
+ } catch (defaultMicErr) {
352
+ console.warn(
353
+ "[transcription] default mic+system capture failed, falling back to default mic-only:",
354
+ defaultMicErr,
355
+ );
356
+ fallbackMic = undefined;
357
+ }
358
+ }
359
+ try {
360
+ await restartTranscriptionEngine("macos-native", fallbackMic);
361
+ return "macos-native";
362
+ } catch (fallbackErr) {
363
+ throw transcriptionStartError(
364
+ fallbackErr,
365
+ selectedMicrophoneUnavailable ||
366
+ (Boolean(opts.mic) &&
367
+ isUnavailableSelectedMicrophoneError(fallbackErr)),
368
+ );
369
+ }
307
370
  }
308
371
  }
309
372
 
@@ -9,16 +9,15 @@ import { invoke } from "@tauri-apps/api/core";
9
9
  import { LogicalSize } from "@tauri-apps/api/dpi";
10
10
  import { emit, listen } from "@tauri-apps/api/event";
11
11
  import { getCurrentWindow } from "@tauri-apps/api/window";
12
- import { open as openExternal } from "@tauri-apps/plugin-shell";
13
12
  import { useEffect, useRef, useState } from "react";
14
13
 
15
- import { resolveDesktopMeetingJoinUrl } from "../lib/meeting-join-url";
16
14
  import {
17
15
  detectMeetingJoinProvider,
18
16
  joinProviderLabel,
19
17
  meetingNotificationAutoHideMs,
20
18
  type MeetingJoinProvider,
21
19
  } from "../lib/meeting-notification-timing";
20
+ import { openMeetingJoinUrl } from "../lib/open-meeting-join-url";
22
21
 
23
22
  interface NotificationData {
24
23
  type: "calendar" | "adhoc";
@@ -51,7 +50,7 @@ const NOTIFICATION_MENU_HEIGHT = 224;
51
50
  async function openJoinUrl(url: string | null | undefined): Promise<void> {
52
51
  if (!url) return;
53
52
  try {
54
- await openExternal(resolveDesktopMeetingJoinUrl(url));
53
+ await openMeetingJoinUrl(url);
55
54
  } catch (err) {
56
55
  console.error("[clips-tray] openJoinUrl failed:", err);
57
56
  }
@@ -425,6 +425,9 @@ export const meetings = table("clips_meetings", {
425
425
  })
426
426
  .notNull()
427
427
  .default("idle"),
428
+ shareTranscript: integer("share_transcript", { mode: "boolean" })
429
+ .notNull()
430
+ .default(false),
428
431
  summaryMd: text("summary_md").notNull().default(""),
429
432
  // JSON array of `{ text }` bullets.
430
433
  bulletsJson: text("bullets_json").notNull().default("[]"),
@@ -11,6 +11,7 @@ import { getRequestURL, setResponseHeader, type H3Event } from "h3";
11
11
  import {
12
12
  buildAgentApiUrls,
13
13
  buildRecommendedFrames,
14
+ getAgentClipReadiness,
14
15
  CLIP_AGENT_ACCESS_TOKEN_PREFIX,
15
16
  CLIPS_AGENT_ACCESS_PARAM,
16
17
  CLIP_AGENT_CONTEXT_VERSION,
@@ -538,18 +539,24 @@ export function buildPublicAgentContext({
538
539
  const publicPageUrl = `${requestUrl.origin}${getServerAppBasePath()}/share/${encodeURIComponent(recording.id)}`;
539
540
  const isLoomSource = isLoomRecordingSource(recording);
540
541
  const isLoomEmbedBacked = isLoomEmbedBackedRecording(recording);
541
- const suggestedFrames = isLoomEmbedBacked
542
- ? []
543
- : buildRecommendedFrames({
544
- durationMs: recording.durationMs,
545
- chapters,
546
- segments: agentSegments,
547
- }).map((frame) => ({
548
- ...frame,
549
- url: api.frameUrl(frame.atMs),
550
- }));
542
+ const agentReadiness = getAgentClipReadiness(recording.status);
543
+ const clipIsReady = agentReadiness.state === "ready";
544
+ const suggestedFrames =
545
+ !clipIsReady || isLoomEmbedBacked
546
+ ? []
547
+ : buildRecommendedFrames({
548
+ durationMs: recording.durationMs,
549
+ chapters,
550
+ segments: agentSegments,
551
+ }).map((frame) => ({
552
+ ...frame,
553
+ url: api.frameUrl(frame.atMs),
554
+ }));
551
555
  const instructions = [
552
- "Use transcript.segments for timestamped spoken context.",
556
+ ...(agentReadiness.instruction ? [agentReadiness.instruction] : []),
557
+ ...(clipIsReady
558
+ ? ["Use transcript.segments for timestamped spoken context."]
559
+ : []),
553
560
  ...transcriptStatusInstructions(transcript),
554
561
  ...(bugReport
555
562
  ? [
@@ -561,15 +568,17 @@ export function buildPublicAgentContext({
561
568
  "Use browserDiagnostics.consoleLogs for the redacted console stream (all levels: debug/log/info/warn/error) and browserDiagnostics.networkRequests for the fetch/XHR requests (method, sanitized URL, status, duration) captured during the recording. browserDiagnostics.consoleIssues highlights just the warnings/errors, and browserDiagnostics.failedNetworkRequests highlights failed requests.",
562
569
  ]
563
570
  : []),
564
- ...(isLoomEmbedBacked
565
- ? [
566
- "This clip is a legacy Loom embed import; frame extraction is not available through Clips until it is reimported as a Clips-hosted video.",
567
- ]
568
- : [
569
- "This clip is readable as both text (transcript) and images (JPEG frames) — you can hear AND see it.",
570
- "To SEE the screen, GET apis.frame.urlTemplate with atMs (returns image/jpeg). Start with recommendedFrames, then fetch additional frames around transcript timestamps that matter for the task.",
571
- "If you cannot load an image from a URL, you will only have the transcript tell the user to open the clip in an image-capable agent (ChatGPT, Claude Code, Cursor, Codex) or to upload a frame image directly so you can see it.",
572
- ]),
571
+ ...(!clipIsReady
572
+ ? []
573
+ : isLoomEmbedBacked
574
+ ? [
575
+ "This clip is a legacy Loom embed import; frame extraction is not available through Clips until it is reimported as a Clips-hosted video.",
576
+ ]
577
+ : [
578
+ "This clip is readable as both text (transcript) and images (JPEG frames) you can hear AND see it.",
579
+ "To SEE the screen, GET apis.frame.urlTemplate with atMs (returns image/jpeg). Start with recommendedFrames, then fetch additional frames around transcript timestamps that matter for the task.",
580
+ "If you cannot load an image from a URL, you will only have the transcript — tell the user to open the clip in an image-capable agent (ChatGPT, Claude Code, Cursor, Codex) or to upload a frame image directly so you can see it.",
581
+ ]),
573
582
  ];
574
583
 
575
584
  return {
@@ -593,13 +602,14 @@ export function buildPublicAgentContext({
593
602
  hasAudio: Boolean(recording.hasAudio),
594
603
  hasCamera: Boolean(recording.hasCamera),
595
604
  status: recording.status,
605
+ agentReadiness,
596
606
  createdAt: recording.createdAt,
597
607
  updatedAt: recording.updatedAt,
598
608
  },
599
609
  apis: {
600
610
  context: { method: "GET", url: api.contextUrl },
601
611
  transcript: { method: "GET", url: api.transcriptUrl },
602
- ...(isLoomEmbedBacked
612
+ ...(!clipIsReady || isLoomEmbedBacked
603
613
  ? {}
604
614
  : {
605
615
  frame: {
@@ -30,6 +30,7 @@ export default createAuthPlugin({
30
30
  "/__manifest",
31
31
  "/api/view-event",
32
32
  "/api/public-recording",
33
+ "/api/public-meeting",
33
34
  "/api/slack",
34
35
  "/api/agent-context.json",
35
36
  "/api/agent-transcript.json",
@@ -64,6 +64,7 @@ async function retypeBooleanColumnsOnPostgres(): Promise<void> {
64
64
  ["recording_viewers", "counted_view", false],
65
65
  ["recording_viewers", "cta_clicked", false],
66
66
  ["meeting_participants", "is_organizer", false],
67
+ ["clips_meetings", "share_transcript", false],
67
68
  ];
68
69
  for (const [table, column, defaultTrue] of alters) {
69
70
  try {
@@ -833,6 +834,11 @@ const migrations = runMigrations(
833
834
  `CREATE UNIQUE INDEX IF NOT EXISTS recording_viewers_recording_viewer_key_unique_idx ON recording_viewers (recording_id, viewer_key)`,
834
835
  ].join("; "),
835
836
  },
837
+ {
838
+ version: 49,
839
+ name: "clips-meetings-share-transcript",
840
+ sql: `ALTER TABLE clips_meetings ADD COLUMN IF NOT EXISTS share_transcript INTEGER NOT NULL DEFAULT 0`,
841
+ },
836
842
  ],
837
843
  { table: "clips_migrations" },
838
844
  );
@@ -12,7 +12,10 @@ import {
12
12
  type H3Event,
13
13
  } from "h3";
14
14
 
15
- import { buildAgentApiUrls } from "../../../shared/agent-context.js";
15
+ import {
16
+ buildAgentApiUrls,
17
+ getAgentClipReadiness,
18
+ } from "../../../shared/agent-context.js";
16
19
  import { isLoomEmbedBackedRecording } from "../../../shared/loom.js";
17
20
  import {
18
21
  applyAgentJsonHeaders,
@@ -50,6 +53,8 @@ export default defineEventHandler(async (event: H3Event) => {
50
53
  token: accessResult.access.apiToken,
51
54
  });
52
55
  const isLoomEmbedBacked = isLoomEmbedBackedRecording(recording);
56
+ const agentReadiness = getAgentClipReadiness(recording.status);
57
+ const clipIsReady = agentReadiness.state === "ready";
53
58
 
54
59
  return {
55
60
  type: "agent-native.clip.transcript",
@@ -57,11 +62,13 @@ export default defineEventHandler(async (event: H3Event) => {
57
62
  id: recording.id,
58
63
  title: recording.title,
59
64
  durationMs: recording.durationMs,
65
+ status: recording.status,
66
+ agentReadiness,
60
67
  },
61
68
  apis: {
62
69
  context: { method: "GET", url: api.contextUrl },
63
70
  transcript: { method: "GET", url: api.transcriptUrl },
64
- ...(isLoomEmbedBacked
71
+ ...(!clipIsReady || isLoomEmbedBacked
65
72
  ? {}
66
73
  : {
67
74
  frame: {
@@ -79,6 +86,9 @@ export default defineEventHandler(async (event: H3Event) => {
79
86
  segments: agentSegments,
80
87
  segmentCount: agentSegments.length,
81
88
  },
82
- instructions: transcriptStatusInstructions(transcript),
89
+ instructions: [
90
+ ...(agentReadiness.instruction ? [agentReadiness.instruction] : []),
91
+ ...transcriptStatusInstructions(transcript),
92
+ ],
83
93
  };
84
94
  });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * GET /api/public-meeting?id=<meetingId>
3
+ *
4
+ * Access-checked meeting notes for the anonymous share surface. Meeting access
5
+ * governs the payload; the linked transcript is an explicit, default-off part
6
+ * of that share and is omitted unless the meeting owner enables it.
7
+ */
8
+
9
+ import { getSession, runWithRequestContext } from "@agent-native/core/server";
10
+ import { resolveAccess } from "@agent-native/core/sharing";
11
+ import { eq } from "drizzle-orm";
12
+ import {
13
+ defineEventHandler,
14
+ getQuery,
15
+ setResponseHeader,
16
+ setResponseStatus,
17
+ } from "h3";
18
+
19
+ import {
20
+ normalizeTranscriptSegments,
21
+ parseTranscriptSegments,
22
+ } from "../../../shared/transcript-segments.js";
23
+ import { resolveTranscriptPresentation } from "../../../shared/transcript-status.js";
24
+ import { getDb, schema } from "../../db/index.js";
25
+
26
+ interface Bullet {
27
+ text: string;
28
+ }
29
+
30
+ function parseBullets(raw: string | null | undefined): Bullet[] {
31
+ if (!raw) return [];
32
+ try {
33
+ const parsed = JSON.parse(raw);
34
+ if (!Array.isArray(parsed)) return [];
35
+ return parsed.filter(
36
+ (bullet): bullet is Bullet =>
37
+ typeof bullet === "object" &&
38
+ bullet !== null &&
39
+ typeof bullet.text === "string",
40
+ );
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ export default defineEventHandler(async (event) => {
47
+ setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
48
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
49
+
50
+ const query = getQuery(event);
51
+ const meetingId = typeof query.id === "string" ? query.id : "";
52
+ if (!meetingId) {
53
+ setResponseStatus(event, 400);
54
+ return { error: "id is required" };
55
+ }
56
+
57
+ const session = await getSession(event).catch(() => null);
58
+ const accessContext = {
59
+ userEmail: session?.email,
60
+ orgId: session?.orgId,
61
+ };
62
+
63
+ return runWithRequestContext(accessContext, async () => {
64
+ const access = await resolveAccess("meeting", meetingId, accessContext);
65
+ const meeting = access?.resource;
66
+ if (!meeting || meeting.trashedAt) {
67
+ setResponseStatus(event, 404);
68
+ return { error: "Not found" };
69
+ }
70
+
71
+ const db = getDb();
72
+ const [participants, actionItems, transcriptRows] = await Promise.all([
73
+ db
74
+ .select({
75
+ email: schema.meetingParticipants.email,
76
+ name: schema.meetingParticipants.name,
77
+ isOrganizer: schema.meetingParticipants.isOrganizer,
78
+ })
79
+ .from(schema.meetingParticipants)
80
+ .where(eq(schema.meetingParticipants.meetingId, meetingId)),
81
+ db
82
+ .select({
83
+ id: schema.meetingActionItems.id,
84
+ text: schema.meetingActionItems.text,
85
+ assigneeEmail: schema.meetingActionItems.assigneeEmail,
86
+ completedAt: schema.meetingActionItems.completedAt,
87
+ })
88
+ .from(schema.meetingActionItems)
89
+ .where(eq(schema.meetingActionItems.meetingId, meetingId)),
90
+ meeting.shareTranscript && meeting.recordingId
91
+ ? db
92
+ .select({
93
+ status: schema.recordingTranscripts.status,
94
+ language: schema.recordingTranscripts.language,
95
+ fullText: schema.recordingTranscripts.fullText,
96
+ failureReason: schema.recordingTranscripts.failureReason,
97
+ segmentsJson: schema.recordingTranscripts.segmentsJson,
98
+ updatedAt: schema.recordingTranscripts.updatedAt,
99
+ })
100
+ .from(schema.recordingTranscripts)
101
+ .where(
102
+ eq(schema.recordingTranscripts.recordingId, meeting.recordingId),
103
+ )
104
+ .limit(1)
105
+ : Promise.resolve([]),
106
+ ]);
107
+
108
+ const transcript = transcriptRows[0] ?? null;
109
+ const transcriptPresentation = resolveTranscriptPresentation(transcript);
110
+ const transcriptSegments = transcript
111
+ ? normalizeTranscriptSegments({
112
+ segments: parseTranscriptSegments(transcript.segmentsJson),
113
+ fullText: transcript.fullText,
114
+ })
115
+ : [];
116
+ const role = access.role;
117
+
118
+ return {
119
+ meeting: {
120
+ id: meeting.id,
121
+ title: meeting.title,
122
+ scheduledStart: meeting.scheduledStart,
123
+ actualStart: meeting.actualStart,
124
+ actualEnd: meeting.actualEnd,
125
+ transcriptStatus: meeting.transcriptStatus,
126
+ summaryMd: meeting.summaryMd,
127
+ bullets: parseBullets(meeting.bulletsJson),
128
+ participants,
129
+ actionItems,
130
+ ...(meeting.shareTranscript
131
+ ? {
132
+ transcript: transcript
133
+ ? {
134
+ status: transcriptPresentation.status,
135
+ language: transcript.language,
136
+ fullText: transcript.fullText,
137
+ segments: transcriptSegments,
138
+ }
139
+ : null,
140
+ }
141
+ : {}),
142
+ },
143
+ viewer: session?.email
144
+ ? {
145
+ role,
146
+ canEdit: role === "owner" || role === "admin" || role === "editor",
147
+ isOwner: role === "owner",
148
+ }
149
+ : null,
150
+ };
151
+ });
152
+ });
@@ -13,6 +13,66 @@ export const AGENT_FRAME_ENDPOINT = "/api/agent-frame.jpg";
13
13
  export const CLIP_AGENT_ACCESS_TOKEN_PREFIX = "clip-agent-context";
14
14
  export const CLIPS_AGENT_ACCESS_PARAM = AGENT_ACCESS_PARAM || "agent_access";
15
15
 
16
+ export type AgentClipReadiness = {
17
+ state: "preparing" | "ready" | "failed";
18
+ retryAfterSeconds: number | null;
19
+ instruction: string | null;
20
+ };
21
+
22
+ export function getAgentClipReadiness(
23
+ status: string | null | undefined,
24
+ ): AgentClipReadiness {
25
+ if (status === "uploading") {
26
+ return {
27
+ state: "preparing",
28
+ retryAfterSeconds: 15,
29
+ instruction:
30
+ "This clip is still uploading and is not ready to inspect. Wait 15 seconds, then fetch agentContextUrl again. Do not open the share page, request frames, or draw conclusions until clip.status is ready.",
31
+ };
32
+ }
33
+
34
+ if (status === "processing") {
35
+ return {
36
+ state: "preparing",
37
+ retryAfterSeconds: 15,
38
+ instruction:
39
+ "This clip is still processing and may still be transcoding or transcribing. Wait 15 seconds, then fetch agentContextUrl again. Do not open the share page, request frames, or draw conclusions until clip.status is ready.",
40
+ };
41
+ }
42
+
43
+ return {
44
+ state: status === "failed" ? "failed" : "ready",
45
+ retryAfterSeconds: null,
46
+ instruction: null,
47
+ };
48
+ }
49
+
50
+ export function buildAgentDiscoveryPayload({
51
+ recordingId,
52
+ title,
53
+ status,
54
+ agentContextUrl,
55
+ }: {
56
+ recordingId: string;
57
+ title: string;
58
+ status: string | null | undefined;
59
+ agentContextUrl: string;
60
+ }) {
61
+ const readiness = getAgentClipReadiness(status);
62
+ return {
63
+ type: "agent-native.clip.discovery",
64
+ version: CLIP_AGENT_CONTEXT_VERSION,
65
+ clipId: recordingId,
66
+ title,
67
+ recordingStatus: status ?? "unknown",
68
+ agentReadiness: readiness,
69
+ agentContextUrl,
70
+ instructions:
71
+ readiness.instruction ??
72
+ "Fetch agentContextUrl for the transcript and JPEG frame URLs. Fetch the frame URLs to SEE the screen, not just read the transcript.",
73
+ };
74
+ }
75
+
16
76
  export function agentAccessTokenResourceId(recordingId: string): string {
17
77
  if (typeof scopedAgentAccessResourceId !== "function") {
18
78
  return `${CLIP_AGENT_ACCESS_TOKEN_PREFIX}:${recordingId}`;
@@ -0,0 +1,31 @@
1
+ const ZOOM_JOIN_PATH = /^\/j\/(\d+)\/?$/;
2
+ const ZOOM_WEB_CLIENT_JOIN_PATH = /^\/wc\/(\d+)\/join\/?$/;
3
+
4
+ function isZoomMeetingHost(hostname: string): boolean {
5
+ return hostname === "zoom.us" || hostname.endsWith(".zoom.us");
6
+ }
7
+
8
+ export function resolveNativeMeetingJoinUrl(joinUrl: string): string {
9
+ try {
10
+ const url = new URL(joinUrl);
11
+ if (url.protocol !== "https:" || !isZoomMeetingHost(url.hostname)) {
12
+ return joinUrl;
13
+ }
14
+
15
+ const meetingNumber =
16
+ ZOOM_JOIN_PATH.exec(url.pathname)?.[1] ??
17
+ ZOOM_WEB_CLIENT_JOIN_PATH.exec(url.pathname)?.[1];
18
+ if (!meetingNumber) return joinUrl;
19
+
20
+ const params = new URLSearchParams({
21
+ action: "join",
22
+ confno: meetingNumber,
23
+ });
24
+ const passcode = url.searchParams.get("pwd");
25
+ if (passcode) params.set("pwd", passcode);
26
+
27
+ return `zoommtg://${url.hostname}/join?${params.toString()}`;
28
+ } catch {
29
+ return joinUrl;
30
+ }
31
+ }
@@ -82,12 +82,20 @@ pnpm action update-document --id abc123 --description "Stable guidance for what
82
82
 
83
83
  ### delete-document
84
84
 
85
- Delete a document and all its children recursively.
85
+ Move a document and all its children to Trash. IDs, bodies, hierarchy, and
86
+ database membership remain intact so the subtree can be restored.
86
87
 
87
88
  ```bash
88
89
  pnpm action delete-document --id abc123
89
90
  ```
90
91
 
92
+ Restore the root subtree, or permanently delete it only after it is in Trash:
93
+
94
+ ```bash
95
+ pnpm action restore-document --id abc123
96
+ pnpm action permanently-delete-document --id abc123
97
+ ```
98
+
91
99
  ## Comments
92
100
 
93
101
  Comments are Notion/Google-Docs-style **inline comments**. Selecting text and commenting leaves the passage **highlighted inline** via a ProseMirror decoration overlay — nothing is written into the markdown body, so the document round-trips unchanged. Each thread stores the quoted text plus surrounding context (`anchorPrefix`/`anchorSuffix`) and an approximate `anchorStartOffset`, so the highlight follows the text as the document is edited, disambiguates repeated text, and degrades gracefully (the thread stays in the sidebar) when its text is deleted.
@@ -146,6 +146,9 @@ cd templates/content && pnpm action <name> [args]
146
146
  | `get-content-database` | `--databaseId <id>` or `--documentId <id>` | Get a database with its description, property/option schema guidance, item pages, and computed ancestry context |
147
147
  | `list-trashed-content-databases` | | List soft-deleted databases visible in the sidebar Trash surface |
148
148
  | `restore-content-database` | `--databaseId <id>` | Restore a soft-deleted database from the sidebar Trash surface |
149
+ | `list-trashed-documents` | | List access-filtered page roots visible in the sidebar Trash surface |
150
+ | `restore-document` | `--id <id>` | Restore a page and the subtree moved to Trash with it |
151
+ | `permanently-delete-document` | `--id <id>` | Permanently destroy a document subtree already in Trash |
149
152
  | `get-content-database-source` | `--databaseId <id>` or `--documentId <id>` | Inspect local/no-source or source-backed status, mappings, row identity, freshness, and change sets |
150
153
  | `attach-content-database-source` | `--databaseId <id>` or `--documentId <id> [--sourceType mock-local\|builder-cms\|local-table\|notion-database] [--sourceName] [--sourceTable] [--relationshipMode items\|details] [--join <json>]` | Attach a source binding; use `items` to add more Builder rows and `details` to match a read-only local/Notion source onto existing rows |
151
154
  | `list-notion-database-sources` | `[--query <text>] [--limit <1-100>]` | List Notion data sources visible through the current user's OAuth connection; returns IDs/names only, never tokens |
@@ -175,7 +178,7 @@ cd templates/content && pnpm action <name> [args]
175
178
  | `reorder-document-property` | `--documentId <id> --propertyId <propertyId> --targetPropertyId <id> [--position before\|after]` | Reorder a property definition within its database (used to reorder Blocks fields on the page) |
176
179
  | `set-document-discoverability` | `--id <id> --hideFromSearch true\|false [--includeChildren true\|false]` | Hide/show an org-accessible document in Organization/search while keeping link access |
177
180
  | `move-document` | `--id <id> [--parentId] [--position]` | Move or reorder a document in the page tree |
178
- | `delete-document` | `--id <id>` | Delete with recursive children |
181
+ | `delete-document` | `--id <id>` | Move a document and its recursive children to Trash |
179
182
 
180
183
  Database views follow Notion-style tab labels. When creating or duplicating
181
184
  views in `viewConfig`, use unique default names (`Table 2`, `SEO copy 2`, etc.)
@@ -806,7 +806,11 @@ async function provisionOwnedContentSpace(
806
806
  export async function provisionSourceBackedContentSpace(
807
807
  db: Db,
808
808
  userEmail: string,
809
- input: { connectionId: string; name: string },
809
+ input: {
810
+ connectionId: string;
811
+ name: string;
812
+ propertyValues?: Record<string, unknown>;
813
+ },
810
814
  ) {
811
815
  const connectionId = input.connectionId.trim();
812
816
  if (!connectionId) throw new Error("Local folder connection ID is required");
@@ -814,6 +818,7 @@ export async function provisionSourceBackedContentSpace(
814
818
  spaceId: sourceBackedContentSpaceId(userEmail, connectionId),
815
819
  name: input.name.trim() || "Local folder",
816
820
  kind: "source_backed",
821
+ propertyValues: input.propertyValues,
817
822
  });
818
823
  }
819
824
 
@@ -151,6 +151,7 @@ export async function resolveDatabaseRowsForBatch(
151
151
  eq(schema.contentDatabaseItems.databaseId, database.id),
152
152
  rowPredicates.length === 1 ? rowPredicates[0] : or(...rowPredicates),
153
153
  isNull(schema.contentDatabases.deletedAt),
154
+ isNull(schema.documents.trashedAt),
154
155
  ),
155
156
  )
156
157
  .orderBy(asc(schema.contentDatabaseItems.position));
@@ -185,9 +186,21 @@ export async function renumberDatabaseRows(
185
186
  now: string,
186
187
  ) {
187
188
  const rows = await db
188
- .select()
189
+ .select({
190
+ id: schema.contentDatabaseItems.id,
191
+ documentId: schema.contentDatabaseItems.documentId,
192
+ })
189
193
  .from(schema.contentDatabaseItems)
190
- .where(eq(schema.contentDatabaseItems.databaseId, database.id))
194
+ .innerJoin(
195
+ schema.documents,
196
+ eq(schema.documents.id, schema.contentDatabaseItems.documentId),
197
+ )
198
+ .where(
199
+ and(
200
+ eq(schema.contentDatabaseItems.databaseId, database.id),
201
+ isNull(schema.documents.trashedAt),
202
+ ),
203
+ )
191
204
  .orderBy(asc(schema.contentDatabaseItems.position));
192
205
  if (rows.length === 0) return;
193
206