@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/resources/ResourceTree.tsx +11 -5
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +11 -1
- package/corpus/templates/clips/AGENTS.md +14 -4
- package/corpus/templates/clips/actions/update-meeting.ts +13 -3
- package/corpus/templates/clips/actions/view-screen.ts +1 -0
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +96 -4
- package/corpus/templates/clips/app/i18n/en-US.ts +11 -0
- package/corpus/templates/clips/app/lib/public-meeting.ts +85 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +11 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -7
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +241 -135
- package/corpus/templates/clips/changelog/2026-07-20-meeting-share-links-can-include-the-full-transcript-with-an-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-recording-retries-the-default-mac-microphone-when-a-saved-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-shared-clips-now-tell-agents-to-wait-while-uploads-and-trans.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +7 -11
- package/corpus/templates/clips/desktop/src/lib/meeting-join-url.ts +1 -33
- package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +65 -2
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
- package/corpus/templates/clips/server/db/schema.ts +3 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -21
- package/corpus/templates/clips/server/plugins/auth.ts +1 -0
- package/corpus/templates/clips/server/plugins/db.ts +6 -0
- package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +13 -3
- package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +152 -0
- package/corpus/templates/clips/shared/agent-context.ts +60 -0
- package/corpus/templates/clips/shared/meeting-join-url.ts +31 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
- package/corpus/templates/content/AGENTS.md +4 -1
- package/corpus/templates/content/actions/_content-spaces.ts +6 -1
- package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
- package/corpus/templates/content/actions/_database-utils.ts +83 -15
- package/corpus/templates/content/actions/add-database-item.ts +3 -0
- package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
- package/corpus/templates/content/actions/delete-content-database.ts +12 -9
- package/corpus/templates/content/actions/delete-database-items.ts +3 -2
- package/corpus/templates/content/actions/delete-document.ts +233 -6
- package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
- package/corpus/templates/content/actions/get-document.ts +4 -1
- package/corpus/templates/content/actions/list-content-databases.ts +1 -0
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
- package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
- package/corpus/templates/content/actions/pull-document.ts +15 -2
- package/corpus/templates/content/actions/restore-content-database.ts +48 -11
- package/corpus/templates/content/actions/restore-document.ts +28 -0
- package/corpus/templates/content/actions/search-documents.ts +2 -1
- package/corpus/templates/content/actions/view-screen.ts +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
- package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
- package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
- package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
- package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
- package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
- package/corpus/templates/content/app/i18n-data.ts +83 -2
- package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
- package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +3 -0
- package/corpus/templates/content/server/db/schema.ts +2 -0
- package/corpus/templates/content/server/lib/public-documents.ts +8 -2
- package/corpus/templates/content/server/plugins/db.ts +52 -0
- package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
- package/corpus/templates/content/shared/api.ts +10 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/ui/progress.tsx +3 -2
- package/dist/client/resources/ResourceTree.d.ts +3 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +4 -4
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +55 -6
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
- package/src/client/resources/ResourceTree.tsx +11 -5
- 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
|
-
|
|
306
|
-
|
|
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
|
|
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
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
565
|
-
? [
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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: {
|
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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>` |
|
|
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: {
|
|
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
|
-
.
|
|
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
|
|