@ai-matrx/meet 0.7.6 → 0.7.7

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/dist/index.js CHANGED
@@ -192,6 +192,12 @@ var MEET_ROUTES = {
192
192
  meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
193
193
  /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
194
194
  meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
195
+ /**
196
+ * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
197
+ * body from the wrap-up, who it can go to); `POST` = send what the host
198
+ * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
199
+ */
200
+ meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up",
195
201
  /**
196
202
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
197
203
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
@@ -321,10 +327,11 @@ function createTokenClient(options) {
321
327
  throw error;
322
328
  }
323
329
  if (response.status === 401 || response.status === 403) {
330
+ const said = await serverSentence(response);
324
331
  throw new MeetError(
325
332
  "forbidden",
326
- `meet.token: the server refused this join (${response.status})`,
327
- "Authorization is the server's (R5). The joiner is not a member of this meeting's organization, or the meeting is locked \u2014 never mint around it on the client."
333
+ said?.message ?? "You can't join this meeting.",
334
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
328
335
  );
329
336
  }
330
337
  if (response.status === 404) {
@@ -335,12 +342,7 @@ function createTokenClient(options) {
335
342
  );
336
343
  }
337
344
  if (response.status === 410) {
338
- let reason = "This meeting was cancelled.";
339
- try {
340
- const body = await response.json();
341
- if (typeof body?.detail?.message === "string") reason = body.detail.message;
342
- } catch {
343
- }
345
+ const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
344
346
  throw new MeetError(
345
347
  "cancelled",
346
348
  reason,
@@ -348,10 +350,11 @@ function createTokenClient(options) {
348
350
  );
349
351
  }
350
352
  if (response.status === 423) {
353
+ const said = await serverSentence(response);
351
354
  throw new MeetError(
352
355
  "room-locked",
353
- "meet.token: the meeting is locked",
354
- "The host locked the room. Ask them to unlock or admit you; this is a normal state, not a failure."
356
+ said?.message ?? "The meeting is locked.",
357
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
355
358
  );
356
359
  }
357
360
  if (response.status === 503) {
@@ -422,6 +425,19 @@ function placeholder(roomName) {
422
425
  lobby: false
423
426
  };
424
427
  }
428
+ async function serverSentence(response) {
429
+ try {
430
+ const body = await response.json();
431
+ const source = body !== null && typeof body.detail === "object" && body.detail !== null ? body.detail : body;
432
+ if (source === null) return null;
433
+ const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
434
+ if (message === null) return null;
435
+ const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
436
+ return { message, remedy };
437
+ } catch {
438
+ return null;
439
+ }
440
+ }
425
441
 
426
442
  // src/core/ai.ts
427
443
  var MEET_ANSWER_EVENTS = {
@@ -890,7 +906,7 @@ var DEFAULT_RING_TIMEOUT_MS = 45e3;
890
906
  function createCallCenter(options) {
891
907
  const { manager, repository, identity } = options;
892
908
  const timers = options.timers ?? {
893
- setTimeout: (run, ms) => setTimeout(run, ms),
909
+ setTimeout: (run, ms2) => setTimeout(run, ms2),
894
910
  clearTimeout: (handle) => clearTimeout(handle),
895
911
  now: () => Date.now()
896
912
  };
@@ -1174,7 +1190,16 @@ var RPCS = {
1174
1190
  /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
1175
1191
  meetingOccurrences: "meet_meeting_occurrences",
1176
1192
  /** Every occurrence the reader can see between two instants — the Upcoming list. */
1177
- occurrencesBetween: "meet_occurrences_between"
1193
+ occurrencesBetween: "meet_occurrences_between",
1194
+ /*
1195
+ * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
1196
+ * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
1197
+ * ceiling, never the view.
1198
+ */
1199
+ /** The recordings library. */
1200
+ myRecordings: "meet_my_recordings",
1201
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1202
+ search: "meet_search"
1178
1203
  };
1179
1204
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1180
1205
  var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
@@ -1879,12 +1904,117 @@ function createMeetRepository(options) {
1879
1904
  );
1880
1905
  },
1881
1906
  async recordings(meetingId) {
1882
- const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: false });
1907
+ const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("started_at", { ascending: false });
1883
1908
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.recordings");
1884
1909
  return (response.data ?? []).map(projectRecording);
1910
+ },
1911
+ async myRecordings({ includeArchived = false, limit = 200 } = {}) {
1912
+ const rows = await guard(
1913
+ "meet.myRecordings",
1914
+ () => db.rpc(RPCS.myRecordings, { p_include_archived: includeArchived, p_limit: limit })
1915
+ );
1916
+ return rows.map(projectLibraryRecording);
1917
+ },
1918
+ async searchMeetings(query, limit = 60) {
1919
+ if (query.trim().length === 0) return [];
1920
+ const rows = await guard(
1921
+ "meet.searchMeetings",
1922
+ () => db.rpc(RPCS.search, { p_query: query, p_limit: limit })
1923
+ );
1924
+ return rows.map(projectSearchHit);
1925
+ },
1926
+ async renameRecording(recordingId, title) {
1927
+ const clean = title === null ? null : title.trim().slice(0, 200) || null;
1928
+ const response = await db.from(TABLES.recordings).update({ title: clean }).eq("id", recordingId).select("id");
1929
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.renameRecording");
1930
+ if ((response.data ?? []).length === 0) {
1931
+ throw normalizeMeetError(
1932
+ { message: "permission denied: only the meeting's host or an editor can rename it", code: "42501" },
1933
+ "meet.renameRecording"
1934
+ );
1935
+ }
1936
+ },
1937
+ async setRecordingArchived(recordingId, archived) {
1938
+ const response = await db.from(TABLES.recordings).update({ deleted_at: archived ? (/* @__PURE__ */ new Date()).toISOString() : null }).eq("id", recordingId).select("id");
1939
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setRecordingArchived");
1940
+ if ((response.data ?? []).length === 0) {
1941
+ throw normalizeMeetError(
1942
+ { message: "permission denied: only the meeting's host or an editor can archive it", code: "42501" },
1943
+ "meet.setRecordingArchived"
1944
+ );
1945
+ }
1885
1946
  }
1886
1947
  };
1887
1948
  }
1949
+ var LIBRARY_STATES = [
1950
+ "idle",
1951
+ "starting",
1952
+ "recording",
1953
+ "stopping",
1954
+ "processing",
1955
+ "available",
1956
+ "expired",
1957
+ "failed"
1958
+ ];
1959
+ function projectLibraryRecording(row) {
1960
+ const operation = "meet.projectLibraryRecording";
1961
+ const state = text(row, "state");
1962
+ const role = text(row, "my_role");
1963
+ return {
1964
+ id: requireText(row, "recording_id", operation),
1965
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1966
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
1967
+ meetingSlug: requireText(row, "meeting_slug", operation),
1968
+ role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
1969
+ state: LIBRARY_STATES.includes(state) ? state : "failed",
1970
+ title: text(row, "recording_title"),
1971
+ fileId: text(row, "file_id"),
1972
+ startedAt: text(row, "started_at"),
1973
+ endedAt: text(row, "ended_at"),
1974
+ durationMs: num(row, "duration_ms"),
1975
+ sizeBytes: num(row, "size_bytes"),
1976
+ failureReason: text(row, "failure_reason"),
1977
+ archivedAt: text(row, "deleted_at"),
1978
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation))
1979
+ };
1980
+ }
1981
+ var SEARCH_SOURCES = [
1982
+ "title",
1983
+ "summary",
1984
+ "decision",
1985
+ "action_item",
1986
+ "note",
1987
+ "transcript"
1988
+ ];
1989
+ function projectSearchHit(row) {
1990
+ const operation = "meet.projectSearchHit";
1991
+ const source = text(row, "source");
1992
+ return {
1993
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1994
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
1995
+ meetingSlug: requireText(row, "meeting_slug", operation),
1996
+ meetingAt: text(row, "meeting_at"),
1997
+ source: SEARCH_SOURCES.includes(source) ? source : "note",
1998
+ refId: requireText(row, "ref_id", operation),
1999
+ speaker: text(row, "speaker"),
2000
+ snippet: text(row, "snippet") ?? "",
2001
+ at: text(row, "said_at"),
2002
+ rank: num(row, "rank") ?? 0
2003
+ };
2004
+ }
2005
+ function snippetRuns(snippet) {
2006
+ const runs = [];
2007
+ const pattern = /⟦([^⟧]*)⟧/g;
2008
+ let last = 0;
2009
+ for (const match of snippet.matchAll(pattern)) {
2010
+ const start = match.index ?? 0;
2011
+ if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
2012
+ runs.push({ text: match[1] ?? "", hit: true });
2013
+ last = start + match[0].length;
2014
+ }
2015
+ if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2016
+ return runs;
2017
+ }
1888
2018
 
1889
2019
  // src/core/room-events.ts
1890
2020
  var RECORDING_STATES = [
@@ -3318,7 +3448,9 @@ function createMeetingSession(options) {
3318
3448
  return identity.userId;
3319
3449
  }
3320
3450
  function knockIdentity() {
3321
- return asParticipantIdentity(identity.userId ?? `guest:${identity.deviceId}`);
3451
+ return asParticipantIdentity(
3452
+ identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
3453
+ );
3322
3454
  }
3323
3455
  function requireMeeting(operation) {
3324
3456
  const meeting = store.snapshot().meeting;
@@ -3353,8 +3485,9 @@ function createMeetingSession(options) {
3353
3485
  verdict.admitted ? "admitted" : "denied",
3354
3486
  verdict.by === null ? null : verdict.by
3355
3487
  );
3356
- if (verdict.admitted && decided === knockIdentity()) {
3357
- options.onAdmitted?.();
3488
+ if (decided === knockIdentity()) {
3489
+ if (verdict.admitted) options.onAdmitted?.();
3490
+ else options.onDenied?.();
3358
3491
  }
3359
3492
  }
3360
3493
  },
@@ -3726,6 +3859,341 @@ function fileDownloadPath(fileId) {
3726
3859
  return `/files/${encodeURIComponent(fileId)}/download?inline=1`;
3727
3860
  }
3728
3861
 
3862
+ // src/core/after.ts
3863
+ import { formatDurationMs } from "@ai-matrx/kit/format";
3864
+ function kindOf(person) {
3865
+ if (person.isAgent) return "assistant";
3866
+ if (person.role === "host") return "host";
3867
+ if (person.role === "cohost") return "cohost";
3868
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
3869
+ return "member";
3870
+ }
3871
+ function ms(iso) {
3872
+ if (iso === null) return null;
3873
+ const at = new Date(iso).getTime();
3874
+ return Number.isNaN(at) ? null : at;
3875
+ }
3876
+ function attendanceReport(meeting, attendees, names = {}) {
3877
+ const end = ms(meeting.endedAt);
3878
+ const rows = attendees.map((person) => {
3879
+ const joined = ms(person.joinedAt);
3880
+ const left = ms(person.leftAt);
3881
+ const until = left ?? end;
3882
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
3883
+ return {
3884
+ id: person.id,
3885
+ identity: person.identity,
3886
+ name: displayNameFor(names, person.identity, person.displayName),
3887
+ userId: person.userId,
3888
+ kind: kindOf(person),
3889
+ joinedAt: person.joinedAt,
3890
+ leftAt: person.leftAt,
3891
+ durationMs: duration,
3892
+ durationToEnd: duration !== null && left === null,
3893
+ admission: person.admissionState
3894
+ };
3895
+ });
3896
+ const rank = {
3897
+ host: 0,
3898
+ cohost: 1,
3899
+ member: 2,
3900
+ "signed-in guest": 3,
3901
+ guest: 4,
3902
+ assistant: 5
3903
+ };
3904
+ return rows.sort((a, b) => {
3905
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
3906
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
3907
+ });
3908
+ }
3909
+ function attendanceTotals(rows) {
3910
+ const humans = rows.filter((row) => row.kind !== "assistant");
3911
+ const attended = humans.filter((row) => row.joinedAt !== null);
3912
+ const timed = attended.filter((row) => row.durationMs !== null);
3913
+ return {
3914
+ people: attended.length,
3915
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
3916
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
3917
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
3918
+ };
3919
+ }
3920
+ function csvCell(value) {
3921
+ if (value === null) return "";
3922
+ const text3 = String(value);
3923
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
3924
+ }
3925
+ var ATTENDANCE_CSV_HEADER = [
3926
+ "Meeting",
3927
+ "Meeting started",
3928
+ "Name",
3929
+ "Attended as",
3930
+ "Joined",
3931
+ "Left",
3932
+ "Minutes in the meeting",
3933
+ "Admission"
3934
+ ];
3935
+ function attendanceCsv(reports) {
3936
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
3937
+ for (const { meeting, rows } of reports) {
3938
+ for (const row of rows) {
3939
+ lines.push(
3940
+ [
3941
+ meeting.title,
3942
+ meeting.startedAt ?? meeting.scheduledFor,
3943
+ row.name,
3944
+ row.kind,
3945
+ row.joinedAt,
3946
+ row.leftAt,
3947
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
3948
+ row.admission
3949
+ ].map(csvCell).join(",")
3950
+ );
3951
+ }
3952
+ }
3953
+ return `${lines.join("\r\n")}\r
3954
+ `;
3955
+ }
3956
+ function recordingOffsetMs(at, recording) {
3957
+ if (recording === null || recording.state !== "available") return null;
3958
+ const start = ms(recording.startedAt);
3959
+ const moment = ms(at);
3960
+ if (start === null || moment === null || moment < start) return null;
3961
+ const end = ms(recording.endedAt);
3962
+ if (end !== null && moment > end) return null;
3963
+ return moment - start;
3964
+ }
3965
+ function clockPosition(offsetMs) {
3966
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
3967
+ const h = Math.floor(total / 3600);
3968
+ const m = Math.floor(total % 3600 / 60);
3969
+ const s = total % 60;
3970
+ const ss = String(s).padStart(2, "0");
3971
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
3972
+ }
3973
+ function searchTranscript(segments, query, names = {}) {
3974
+ const needle = fold(query.trim());
3975
+ if (needle.length === 0) return [];
3976
+ const out = [];
3977
+ for (const segment of segments) {
3978
+ const hay = fold(segment.text);
3979
+ const ranges = [];
3980
+ let from = 0;
3981
+ for (; ; ) {
3982
+ const at = hay.indexOf(needle, from);
3983
+ if (at < 0) break;
3984
+ ranges.push([at, at + needle.length]);
3985
+ from = at + needle.length;
3986
+ }
3987
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
3988
+ if (ranges.length > 0 || speaker.includes(needle)) {
3989
+ out.push({ segmentId: segment.id, ranges });
3990
+ }
3991
+ }
3992
+ return out;
3993
+ }
3994
+ function fold(value) {
3995
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
3996
+ }
3997
+ function clockOfDay(iso, options) {
3998
+ const at = new Date(iso);
3999
+ if (Number.isNaN(at.getTime())) return "";
4000
+ return at.toLocaleTimeString(options.locale, {
4001
+ hour: "numeric",
4002
+ minute: "2-digit",
4003
+ second: "2-digit",
4004
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
4005
+ });
4006
+ }
4007
+ function vttStamp(offsetMs) {
4008
+ const total = Math.max(0, Math.round(offsetMs));
4009
+ const h = Math.floor(total / 36e5);
4010
+ const m = Math.floor(total % 36e5 / 6e4);
4011
+ const s = Math.floor(total % 6e4 / 1e3);
4012
+ const milli = total % 1e3;
4013
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
4014
+ }
4015
+ function transcriptDocument(bundle, format, options = {}) {
4016
+ const names = options.names ?? bundle.names;
4017
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
4018
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
4019
+ if (format === "vtt") {
4020
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
4021
+ const zero = ms(zeroIso) ?? 0;
4022
+ const cues = ["WEBVTT", ""];
4023
+ lines.forEach((line, index) => {
4024
+ const start = (ms(line.startedAt) ?? zero) - zero;
4025
+ if (start < 0) return;
4026
+ const next = lines[index + 1];
4027
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
4028
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
4029
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
4030
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
4031
+ cues.push("");
4032
+ });
4033
+ return cues.join("\n");
4034
+ }
4035
+ const out = [];
4036
+ const title = bundle.meeting.title;
4037
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
4038
+ out.push("");
4039
+ let last = null;
4040
+ for (const line of lines) {
4041
+ if (line.identity !== last) {
4042
+ if (last !== null) out.push("");
4043
+ const who = speaker(line);
4044
+ const clock = clockOfDay(line.startedAt, options);
4045
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
4046
+ last = line.identity;
4047
+ }
4048
+ out.push(line.text.trim());
4049
+ }
4050
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
4051
+ return `${out.join("\n")}
4052
+ `;
4053
+ }
4054
+ function bullet(note) {
4055
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
4056
+ return `- ${note.text.trim()}${owner}`;
4057
+ }
4058
+ function notesDocument(bundle, options = {}) {
4059
+ const out = [`# ${bundle.meeting.title}`, ""];
4060
+ if (options.when) out.push(`_${options.when}_`, "");
4061
+ const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
4062
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
4063
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
4064
+ out.push("## Decisions", "");
4065
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
4066
+ out.push("## Action items", "");
4067
+ out.push(
4068
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
4069
+ ""
4070
+ );
4071
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
4072
+ return out.join("\n");
4073
+ }
4074
+ function meetingLength(meeting) {
4075
+ const start = ms(meeting.startedAt);
4076
+ const end = ms(meeting.endedAt);
4077
+ if (start === null || end === null || end < start) return null;
4078
+ return formatDurationMs(end - start, { style: "long" });
4079
+ }
4080
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
4081
+ const operation = "meet.loadMeetingChat";
4082
+ let response;
4083
+ try {
4084
+ response = await api.request({
4085
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
4086
+ method: "GET",
4087
+ operation,
4088
+ requireAuth: roomToken === null,
4089
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
4090
+ });
4091
+ } catch (error) {
4092
+ return {
4093
+ messages: [],
4094
+ refusal: {
4095
+ message: `The chat could not be read (${error.message}).`,
4096
+ remedy: error.remedy ?? "Reload the page."
4097
+ }
4098
+ };
4099
+ }
4100
+ if (!response.ok) {
4101
+ let body = null;
4102
+ try {
4103
+ body = await response.json();
4104
+ } catch {
4105
+ body = null;
4106
+ }
4107
+ const said = readServerRefusal(body);
4108
+ return {
4109
+ messages: [],
4110
+ refusal: {
4111
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
4112
+ remedy: said?.remedy || "Reload the page."
4113
+ }
4114
+ };
4115
+ }
4116
+ const payload = await response.json();
4117
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
4118
+ return {
4119
+ messages: rows.map(
4120
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
4121
+ ).filter((message) => message !== null),
4122
+ refusal: null
4123
+ };
4124
+ }
4125
+ async function refusalOf(response, operation) {
4126
+ let body = null;
4127
+ try {
4128
+ body = await response.json();
4129
+ } catch {
4130
+ body = null;
4131
+ }
4132
+ const said = readServerRefusal(body);
4133
+ return new MeetError(
4134
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4135
+ `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
4136
+ said?.remedy || "Nothing was sent. Try again."
4137
+ );
4138
+ }
4139
+ function asRecipient(row) {
4140
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
4141
+ return {
4142
+ key: String(row["key"] ?? ""),
4143
+ name: String(row["name"] ?? ""),
4144
+ email: typeof row["email"] === "string" ? row["email"] : null,
4145
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
4146
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
4147
+ reachable: row["reachable"] !== false
4148
+ };
4149
+ }
4150
+ async function loadFollowUpDraft(api, meetingId) {
4151
+ const operation = "meet.loadFollowUpDraft";
4152
+ const response = await api.request({
4153
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
4154
+ method: "GET",
4155
+ requireAuth: true,
4156
+ operation
4157
+ });
4158
+ if (!response.ok) throw await refusalOf(response, operation);
4159
+ const body = await response.json();
4160
+ const last = body["last_sent"];
4161
+ return {
4162
+ subject: String(body["subject"] ?? ""),
4163
+ body: String(body["body"] ?? ""),
4164
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
4165
+ (row) => asRecipient(row)
4166
+ ),
4167
+ wrapUpWritten: body["wrap_up_written"] === true,
4168
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
4169
+ };
4170
+ }
4171
+ async function sendFollowUp(api, meetingId, input) {
4172
+ const operation = "meet.sendFollowUp";
4173
+ const response = await api.request({
4174
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
4175
+ method: "POST",
4176
+ requireAuth: true,
4177
+ operation,
4178
+ body: {
4179
+ subject: input.subject,
4180
+ body: input.body,
4181
+ recipient_keys: [...input.recipientKeys],
4182
+ dry_run: input.dryRun === true
4183
+ }
4184
+ });
4185
+ if (!response.ok) throw await refusalOf(response, operation);
4186
+ const body = await response.json();
4187
+ return {
4188
+ dryRun: body["dry_run"] === true,
4189
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
4190
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
4191
+ key: String(row["key"] ?? ""),
4192
+ reason: String(row["reason"] ?? "")
4193
+ })) : []
4194
+ };
4195
+ }
4196
+
3729
4197
  // src/core/management.ts
3730
4198
  async function refusal(response, operation) {
3731
4199
  let body = null;
@@ -4482,7 +4950,7 @@ function createInRoomClient(options) {
4482
4950
  }
4483
4951
 
4484
4952
  // src/core/record.ts
4485
- import { formatDurationMs } from "@ai-matrx/kit/format";
4953
+ import { formatDurationMs as formatDurationMs2 } from "@ai-matrx/kit/format";
4486
4954
  function activeWrapUpSummary(notes) {
4487
4955
  return notes.find((note) => note.role === "summary" && note.supersededBy === null) ?? null;
4488
4956
  }
@@ -4565,6 +5033,36 @@ function wasRefused(bundle, part) {
4565
5033
  function absenceSentence(bundle, part, absence) {
4566
5034
  return wasRefused(bundle, part) ? REFUSED_ABSENCE : absence;
4567
5035
  }
5036
+ function noteTakerMetadata(meeting) {
5037
+ const raw = meeting.metadata["note_taker"];
5038
+ return typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
5039
+ }
5040
+ var WRAP_UP_IN_PROGRESS = "The wrap-up is being written\u2026 The summary, the decisions and the action items are produced from the whole transcript once the meeting ends, which takes a moment longer than the meeting does.";
5041
+ var WRAP_UP_ABSENT = "No wrap-up was written for this meeting. A summary is produced from the transcript when the meeting ends and the note-taker was in the room.";
5042
+ function wrapUpStatus(bundle) {
5043
+ if (bundle.summary !== null) return { state: "written" };
5044
+ if (wasRefused(bundle, "notes")) return { state: "refused", message: REFUSED_ABSENCE };
5045
+ const noteTaker = noteTakerMetadata(bundle.meeting);
5046
+ const intelligenceRaw = noteTaker["intelligence"];
5047
+ const intelligence = typeof intelligenceRaw === "object" && intelligenceRaw !== null && !Array.isArray(intelligenceRaw) ? intelligenceRaw : {};
5048
+ const wrapUpRaw = intelligence["wrap_up"];
5049
+ const wrapUp = typeof wrapUpRaw === "object" && wrapUpRaw !== null && !Array.isArray(wrapUpRaw) ? wrapUpRaw : {};
5050
+ if (wrapUp["state"] === "failed") {
5051
+ const reason = wrapUp["reason"];
5052
+ return {
5053
+ state: "failed",
5054
+ // THE SERVER'S OWN SENTENCE, verbatim. It already names the remedy, and a
5055
+ // house sentence over it would hide the one thing worth reading.
5056
+ message: typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : "The wrap-up pass failed and no summary was written. It is not retried automatically; a host can end the meeting again to re-queue it."
5057
+ };
5058
+ }
5059
+ const queued = typeof wrapUp["run_id"] === "string" || typeof intelligence["wrap_up_run_id"] === "string" || wrapUp["state"] === "queued" || wrapUp["state"] === "running";
5060
+ const attended = noteTaker["state"] === "ended" || noteTaker["state"] === "transcribing" || noteTaker["state"] === "handoff_pending";
5061
+ if (hasEnded(bundle.meeting) && (queued || attended)) {
5062
+ return { state: "in-progress", message: WRAP_UP_IN_PROGRESS, canRefresh: true };
5063
+ }
5064
+ return { state: "absent", message: WRAP_UP_ABSENT };
5065
+ }
4568
5066
  function groupTranscript(segments, names = {}) {
4569
5067
  const blocks = [];
4570
5068
  for (const segment of segments) {
@@ -4633,7 +5131,7 @@ function meetingWhen(meeting, locale) {
4633
5131
  const endedAt = new Date(meeting.endedAt);
4634
5132
  if (Number.isNaN(endedAt.getTime())) return when2;
4635
5133
  const elapsedMs = endedAt.getTime() - startedAt.getTime();
4636
- return `${when2} \xB7 ${formatDurationMs(elapsedMs, { style: "long" })}`;
5134
+ return `${when2} \xB7 ${formatDurationMs2(elapsedMs, { style: "long" })}`;
4637
5135
  }
4638
5136
  var GUEST_RECORD_ACCESS_LEVELS = [
4639
5137
  "none",
@@ -4682,6 +5180,37 @@ var NOT_SHARED = {
4682
5180
  recording: "The recording of this meeting is not shared with guests."
4683
5181
  };
4684
5182
  var NOT_SHARED_REMEDY = "The organization that held this meeting decided how much of its record guests can read. If you need more, ask whoever invited you \u2014 they can send it to you, or change the setting for this meeting.";
5183
+ var RESTRICTION_PART_NAME = {
5184
+ attendance: "attendance",
5185
+ notes: "notes and decisions",
5186
+ transcript: "word-for-word transcript",
5187
+ recording: "recording"
5188
+ };
5189
+ function joinPartNames(parts) {
5190
+ const names = parts.map((part) => RESTRICTION_PART_NAME[part]);
5191
+ if (names.length < 2) return names[0] ?? "this part of the record";
5192
+ if (names.length === 2) return `${names[0]} and ${names[1]}`;
5193
+ return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
5194
+ }
5195
+ function summarizeMeetingRecordRestrictions(restrictions) {
5196
+ const grouped = /* @__PURE__ */ new Map();
5197
+ for (const restriction of restrictions) {
5198
+ const rows = grouped.get(restriction.remedy) ?? [];
5199
+ rows.push(restriction);
5200
+ grouped.set(restriction.remedy, rows);
5201
+ }
5202
+ return [...grouped.values()].map((rows) => {
5203
+ if (rows.length === 1) {
5204
+ const row = rows[0];
5205
+ return { parts: [row.part], message: row.message, remedy: row.remedy };
5206
+ }
5207
+ return {
5208
+ parts: rows.map((row) => row.part),
5209
+ message: `Some parts of this meeting record are not available to this reader: ${joinPartNames(rows.map((row) => row.part))}.`,
5210
+ remedy: rows[0].remedy
5211
+ };
5212
+ });
5213
+ }
4685
5214
  async function loadGuestMeetingRecord(args) {
4686
5215
  const { api, repository, meetingId, meeting, roomToken } = args;
4687
5216
  if (roomToken === null) {
@@ -4813,6 +5342,7 @@ async function setGuestRecordAccess(args) {
4813
5342
  }
4814
5343
  var GUEST_RECORD_NOT_SHARED = NOT_SHARED;
4815
5344
  export {
5345
+ ATTENDANCE_CSV_HEADER,
4816
5346
  CALL_INVITE_KIND,
4817
5347
  CALL_INVITE_VERSION,
4818
5348
  CONSENT_NOTICE_ATTRIBUTE,
@@ -4854,6 +5384,7 @@ export {
4854
5384
  TRANSCRIPTION_ATTRIBUTES,
4855
5385
  TRANSCRIPTION_TOPIC,
4856
5386
  absenceSentence,
5387
+ activeWrapUpSummary,
4857
5388
  announceMeeting,
4858
5389
  applyRoomEvent,
4859
5390
  asCallId,
@@ -4862,6 +5393,9 @@ export {
4862
5393
  asParticipantIdentity,
4863
5394
  asRoomName,
4864
5395
  asUserId,
5396
+ attendanceCsv,
5397
+ attendanceReport,
5398
+ attendanceTotals,
4865
5399
  callInviteAction,
4866
5400
  callInviteText,
4867
5401
  callStateLabel,
@@ -4870,6 +5404,7 @@ export {
4870
5404
  canTransition,
4871
5405
  captionSegment,
4872
5406
  changesPatch,
5407
+ clockPosition,
4873
5408
  computeStage,
4874
5409
  countLabel,
4875
5410
  createCallCenter,
@@ -4910,11 +5445,14 @@ export {
4910
5445
  learnName,
4911
5446
  learnNames,
4912
5447
  listenOnlyNotice,
5448
+ loadFollowUpDraft,
4913
5449
  loadGuestMeetingRecord,
5450
+ loadMeetingChat,
4914
5451
  loadMeetingRecord,
4915
5452
  mapConnectionState,
4916
5453
  mapQuality,
4917
5454
  meetingInviteAction,
5455
+ meetingLength,
4918
5456
  meetingLink,
4919
5457
  meetingPath,
4920
5458
  meetingSlug,
@@ -4929,16 +5467,19 @@ export {
4929
5467
  noteTakerLabel,
4930
5468
  noteTakerParticipant,
4931
5469
  noteTakerStatus,
5470
+ notesDocument,
4932
5471
  orderParticipants,
4933
5472
  parseRoomToken,
4934
5473
  participantSummary,
4935
5474
  playableRecordingFileId,
4936
5475
  projectInvitee,
5476
+ projectLibraryRecording,
4937
5477
  projectNote,
4938
5478
  projectOccurrence,
4939
5479
  projectOverride,
4940
5480
  projectParticipant,
4941
5481
  projectRsvpInvitation,
5482
+ projectSearchHit,
4942
5483
  projectSegment,
4943
5484
  projectUpcoming,
4944
5485
  pushMeetingToGoogleCalendar,
@@ -4946,13 +5487,20 @@ export {
4946
5487
  readableTranscript,
4947
5488
  recordingLabel,
4948
5489
  recordingNotice,
5490
+ recordingOffsetMs,
4949
5491
  roleOf,
4950
5492
  roomTopic,
5493
+ searchTranscript,
5494
+ sendFollowUp,
4951
5495
  setGuestRecordAccess,
4952
5496
  settingsPatch,
4953
5497
  snapshotParticipant,
5498
+ snippetRuns,
5499
+ summarizeMeetingRecordRestrictions,
5500
+ transcriptDocument,
4954
5501
  unavailableFrom,
4955
5502
  userTopic,
4956
- wasRefused
5503
+ wasRefused,
5504
+ wrapUpStatus
4957
5505
  };
4958
5506
  //# sourceMappingURL=index.js.map