@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/react.cjs CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  // src/react.ts
33
33
  var react_exports = {};
34
34
  __export(react_exports, {
35
+ ATTENDANCE_CSV_HEADER: () => ATTENDANCE_CSV_HEADER,
35
36
  AiPanel: () => AiPanel,
36
37
  AttendanceNotice: () => AttendanceNotice,
37
38
  Avatar: () => Avatar,
@@ -135,6 +136,7 @@ __export(react_exports, {
135
136
  TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
136
137
  WifiLowIcon: () => WifiLowIcon,
137
138
  absenceSentence: () => absenceSentence,
139
+ activeWrapUpSummary: () => activeWrapUpSummary,
138
140
  announceMeeting: () => announceMeeting,
139
141
  applyRoomEvent: () => applyRoomEvent,
140
142
  asCallId: () => asCallId,
@@ -143,6 +145,9 @@ __export(react_exports, {
143
145
  asParticipantIdentity: () => asParticipantIdentity,
144
146
  asRoomName: () => asRoomName,
145
147
  asUserId: () => asUserId,
148
+ attendanceCsv: () => attendanceCsv,
149
+ attendanceReport: () => attendanceReport,
150
+ attendanceTotals: () => attendanceTotals,
146
151
  callInviteAction: () => callInviteAction,
147
152
  callInviteText: () => callInviteText,
148
153
  callStateLabel: () => callStateLabel,
@@ -151,6 +156,7 @@ __export(react_exports, {
151
156
  canTransition: () => canTransition,
152
157
  captionSegment: () => captionSegment,
153
158
  changesPatch: () => changesPatch,
159
+ clockPosition: () => clockPosition,
154
160
  computeStage: () => computeStage,
155
161
  countLabel: () => countLabel,
156
162
  createCallCenter: () => createCallCenter,
@@ -191,11 +197,14 @@ __export(react_exports, {
191
197
  learnName: () => learnName,
192
198
  learnNames: () => learnNames,
193
199
  listenOnlyNotice: () => listenOnlyNotice,
200
+ loadFollowUpDraft: () => loadFollowUpDraft,
194
201
  loadGuestMeetingRecord: () => loadGuestMeetingRecord,
202
+ loadMeetingChat: () => loadMeetingChat,
195
203
  loadMeetingRecord: () => loadMeetingRecord,
196
204
  mapConnectionState: () => mapConnectionState,
197
205
  mapQuality: () => mapQuality,
198
206
  meetingInviteAction: () => meetingInviteAction,
207
+ meetingLength: () => meetingLength,
199
208
  meetingLink: () => meetingLink,
200
209
  meetingPath: () => meetingPath,
201
210
  meetingSlug: () => meetingSlug,
@@ -210,16 +219,19 @@ __export(react_exports, {
210
219
  noteTakerLabel: () => noteTakerLabel,
211
220
  noteTakerParticipant: () => noteTakerParticipant,
212
221
  noteTakerStatus: () => noteTakerStatus,
222
+ notesDocument: () => notesDocument,
213
223
  orderParticipants: () => orderParticipants,
214
224
  parseRoomToken: () => parseRoomToken,
215
225
  participantSummary: () => participantSummary,
216
226
  playableRecordingFileId: () => playableRecordingFileId,
217
227
  projectInvitee: () => projectInvitee,
228
+ projectLibraryRecording: () => projectLibraryRecording,
218
229
  projectNote: () => projectNote,
219
230
  projectOccurrence: () => projectOccurrence,
220
231
  projectOverride: () => projectOverride,
221
232
  projectParticipant: () => projectParticipant,
222
233
  projectRsvpInvitation: () => projectRsvpInvitation,
234
+ projectSearchHit: () => projectSearchHit,
223
235
  projectSegment: () => projectSegment,
224
236
  projectUpcoming: () => projectUpcoming,
225
237
  pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
@@ -227,11 +239,17 @@ __export(react_exports, {
227
239
  readableTranscript: () => readableTranscript,
228
240
  recordingLabel: () => recordingLabel,
229
241
  recordingNotice: () => recordingNotice,
242
+ recordingOffsetMs: () => recordingOffsetMs,
230
243
  roleOf: () => roleOf,
231
244
  roomTopic: () => roomTopic,
245
+ searchTranscript: () => searchTranscript,
246
+ sendFollowUp: () => sendFollowUp,
232
247
  setGuestRecordAccess: () => setGuestRecordAccess,
233
248
  settingsPatch: () => settingsPatch,
234
249
  snapshotParticipant: () => snapshotParticipant,
250
+ snippetRuns: () => snippetRuns,
251
+ summarizeMeetingRecordRestrictions: () => summarizeMeetingRecordRestrictions,
252
+ transcriptDocument: () => transcriptDocument,
235
253
  unavailableFrom: () => unavailableFrom,
236
254
  useAudioLevel: () => useAudioLevel,
237
255
  useCalls: () => useCalls,
@@ -267,7 +285,8 @@ __export(react_exports, {
267
285
  useStage: () => useStage,
268
286
  useTrack: () => useTrack,
269
287
  userTopic: () => userTopic,
270
- wasRefused: () => wasRefused
288
+ wasRefused: () => wasRefused,
289
+ wrapUpStatus: () => wrapUpStatus
271
290
  });
272
291
  module.exports = __toCommonJS(react_exports);
273
292
 
@@ -469,6 +488,12 @@ var MEET_ROUTES = {
469
488
  meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
470
489
  /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
471
490
  meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
491
+ /**
492
+ * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
493
+ * body from the wrap-up, who it can go to); `POST` = send what the host
494
+ * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
495
+ */
496
+ meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up",
472
497
  /**
473
498
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
474
499
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
@@ -598,10 +623,11 @@ function createTokenClient(options) {
598
623
  throw error;
599
624
  }
600
625
  if (response.status === 401 || response.status === 403) {
626
+ const said = await serverSentence(response);
601
627
  throw new MeetError(
602
628
  "forbidden",
603
- `meet.token: the server refused this join (${response.status})`,
604
- "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."
629
+ said?.message ?? "You can't join this meeting.",
630
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
605
631
  );
606
632
  }
607
633
  if (response.status === 404) {
@@ -612,12 +638,7 @@ function createTokenClient(options) {
612
638
  );
613
639
  }
614
640
  if (response.status === 410) {
615
- let reason = "This meeting was cancelled.";
616
- try {
617
- const body = await response.json();
618
- if (typeof body?.detail?.message === "string") reason = body.detail.message;
619
- } catch {
620
- }
641
+ const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
621
642
  throw new MeetError(
622
643
  "cancelled",
623
644
  reason,
@@ -625,10 +646,11 @@ function createTokenClient(options) {
625
646
  );
626
647
  }
627
648
  if (response.status === 423) {
649
+ const said = await serverSentence(response);
628
650
  throw new MeetError(
629
651
  "room-locked",
630
- "meet.token: the meeting is locked",
631
- "The host locked the room. Ask them to unlock or admit you; this is a normal state, not a failure."
652
+ said?.message ?? "The meeting is locked.",
653
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
632
654
  );
633
655
  }
634
656
  if (response.status === 503) {
@@ -699,6 +721,19 @@ function placeholder(roomName) {
699
721
  lobby: false
700
722
  };
701
723
  }
724
+ async function serverSentence(response) {
725
+ try {
726
+ const body = await response.json();
727
+ const source = body !== null && typeof body.detail === "object" && body.detail !== null ? body.detail : body;
728
+ if (source === null) return null;
729
+ const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
730
+ if (message === null) return null;
731
+ const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
732
+ return { message, remedy };
733
+ } catch {
734
+ return null;
735
+ }
736
+ }
702
737
 
703
738
  // src/core/ai.ts
704
739
  var MEET_ANSWER_EVENTS = {
@@ -1142,7 +1177,7 @@ var DEFAULT_RING_TIMEOUT_MS = 45e3;
1142
1177
  function createCallCenter(options) {
1143
1178
  const { manager, repository, identity } = options;
1144
1179
  const timers = options.timers ?? {
1145
- setTimeout: (run, ms) => setTimeout(run, ms),
1180
+ setTimeout: (run, ms2) => setTimeout(run, ms2),
1146
1181
  clearTimeout: (handle) => clearTimeout(handle),
1147
1182
  now: () => Date.now()
1148
1183
  };
@@ -2080,7 +2115,9 @@ function createMeetingSession(options) {
2080
2115
  return identity.userId;
2081
2116
  }
2082
2117
  function knockIdentity() {
2083
- return asParticipantIdentity(identity.userId ?? `guest:${identity.deviceId}`);
2118
+ return asParticipantIdentity(
2119
+ identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
2120
+ );
2084
2121
  }
2085
2122
  function requireMeeting(operation) {
2086
2123
  const meeting = store.snapshot().meeting;
@@ -2115,8 +2152,9 @@ function createMeetingSession(options) {
2115
2152
  verdict.admitted ? "admitted" : "denied",
2116
2153
  verdict.by === null ? null : verdict.by
2117
2154
  );
2118
- if (verdict.admitted && decided === knockIdentity()) {
2119
- options.onAdmitted?.();
2155
+ if (decided === knockIdentity()) {
2156
+ if (verdict.admitted) options.onAdmitted?.();
2157
+ else options.onDenied?.();
2120
2158
  }
2121
2159
  }
2122
2160
  },
@@ -2453,7 +2491,16 @@ var RPCS = {
2453
2491
  /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
2454
2492
  meetingOccurrences: "meet_meeting_occurrences",
2455
2493
  /** Every occurrence the reader can see between two instants — the Upcoming list. */
2456
- occurrencesBetween: "meet_occurrences_between"
2494
+ occurrencesBetween: "meet_occurrences_between",
2495
+ /*
2496
+ * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
2497
+ * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
2498
+ * ceiling, never the view.
2499
+ */
2500
+ /** The recordings library. */
2501
+ myRecordings: "meet_my_recordings",
2502
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
2503
+ search: "meet_search"
2457
2504
  };
2458
2505
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
2459
2506
  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";
@@ -3158,12 +3205,117 @@ function createMeetRepository(options) {
3158
3205
  );
3159
3206
  },
3160
3207
  async recordings(meetingId) {
3161
- const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: false });
3208
+ const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("started_at", { ascending: false });
3162
3209
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.recordings");
3163
3210
  return (response.data ?? []).map(projectRecording);
3211
+ },
3212
+ async myRecordings({ includeArchived = false, limit = 200 } = {}) {
3213
+ const rows = await guard(
3214
+ "meet.myRecordings",
3215
+ () => db.rpc(RPCS.myRecordings, { p_include_archived: includeArchived, p_limit: limit })
3216
+ );
3217
+ return rows.map(projectLibraryRecording);
3218
+ },
3219
+ async searchMeetings(query, limit = 60) {
3220
+ if (query.trim().length === 0) return [];
3221
+ const rows = await guard(
3222
+ "meet.searchMeetings",
3223
+ () => db.rpc(RPCS.search, { p_query: query, p_limit: limit })
3224
+ );
3225
+ return rows.map(projectSearchHit);
3226
+ },
3227
+ async renameRecording(recordingId, title) {
3228
+ const clean = title === null ? null : title.trim().slice(0, 200) || null;
3229
+ const response = await db.from(TABLES.recordings).update({ title: clean }).eq("id", recordingId).select("id");
3230
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.renameRecording");
3231
+ if ((response.data ?? []).length === 0) {
3232
+ throw normalizeMeetError(
3233
+ { message: "permission denied: only the meeting's host or an editor can rename it", code: "42501" },
3234
+ "meet.renameRecording"
3235
+ );
3236
+ }
3237
+ },
3238
+ async setRecordingArchived(recordingId, archived) {
3239
+ const response = await db.from(TABLES.recordings).update({ deleted_at: archived ? (/* @__PURE__ */ new Date()).toISOString() : null }).eq("id", recordingId).select("id");
3240
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setRecordingArchived");
3241
+ if ((response.data ?? []).length === 0) {
3242
+ throw normalizeMeetError(
3243
+ { message: "permission denied: only the meeting's host or an editor can archive it", code: "42501" },
3244
+ "meet.setRecordingArchived"
3245
+ );
3246
+ }
3164
3247
  }
3165
3248
  };
3166
3249
  }
3250
+ var LIBRARY_STATES = [
3251
+ "idle",
3252
+ "starting",
3253
+ "recording",
3254
+ "stopping",
3255
+ "processing",
3256
+ "available",
3257
+ "expired",
3258
+ "failed"
3259
+ ];
3260
+ function projectLibraryRecording(row) {
3261
+ const operation = "meet.projectLibraryRecording";
3262
+ const state = text(row, "state");
3263
+ const role = text(row, "my_role");
3264
+ return {
3265
+ id: requireText(row, "recording_id", operation),
3266
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
3267
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
3268
+ meetingSlug: requireText(row, "meeting_slug", operation),
3269
+ role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
3270
+ state: LIBRARY_STATES.includes(state) ? state : "failed",
3271
+ title: text(row, "recording_title"),
3272
+ fileId: text(row, "file_id"),
3273
+ startedAt: text(row, "started_at"),
3274
+ endedAt: text(row, "ended_at"),
3275
+ durationMs: num(row, "duration_ms"),
3276
+ sizeBytes: num(row, "size_bytes"),
3277
+ failureReason: text(row, "failure_reason"),
3278
+ archivedAt: text(row, "deleted_at"),
3279
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation))
3280
+ };
3281
+ }
3282
+ var SEARCH_SOURCES = [
3283
+ "title",
3284
+ "summary",
3285
+ "decision",
3286
+ "action_item",
3287
+ "note",
3288
+ "transcript"
3289
+ ];
3290
+ function projectSearchHit(row) {
3291
+ const operation = "meet.projectSearchHit";
3292
+ const source = text(row, "source");
3293
+ return {
3294
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
3295
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
3296
+ meetingSlug: requireText(row, "meeting_slug", operation),
3297
+ meetingAt: text(row, "meeting_at"),
3298
+ source: SEARCH_SOURCES.includes(source) ? source : "note",
3299
+ refId: requireText(row, "ref_id", operation),
3300
+ speaker: text(row, "speaker"),
3301
+ snippet: text(row, "snippet") ?? "",
3302
+ at: text(row, "said_at"),
3303
+ rank: num(row, "rank") ?? 0
3304
+ };
3305
+ }
3306
+ function snippetRuns(snippet) {
3307
+ const runs = [];
3308
+ const pattern = /⟦([^⟧]*)⟧/g;
3309
+ let last = 0;
3310
+ for (const match of snippet.matchAll(pattern)) {
3311
+ const start = match.index ?? 0;
3312
+ if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
3313
+ runs.push({ text: match[1] ?? "", hit: true });
3314
+ last = start + match[0].length;
3315
+ }
3316
+ if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
3317
+ return runs;
3318
+ }
3167
3319
 
3168
3320
  // src/core/session-identity.ts
3169
3321
  var RESOLVING = Object.freeze({
@@ -4794,6 +4946,11 @@ function MeetRuntime(props) {
4794
4946
  // publish nor subscribe, so the host saying yes has to mint a new one.
4795
4947
  onAdmitted: () => {
4796
4948
  void engine.rejoin();
4949
+ },
4950
+ // A "no" is ALSO a mint: the server refuses it with its own sentence, which
4951
+ // turns the waiting room into the pre-join screen showing that sentence.
4952
+ onDenied: () => {
4953
+ void engine.rejoin().catch(() => void 0);
4797
4954
  }
4798
4955
  });
4799
4956
  const resolveRoomToken = () => {
@@ -6919,22 +7076,27 @@ function MeetingOverlays() {
6919
7076
  const room = useRoom();
6920
7077
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
6921
7078
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReactionOverlay, { reactions }),
6922
- notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6923
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
6924
- notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6925
- "button",
6926
- {
6927
- type: "button",
6928
- className: "mx-meet__primary mx-meet__primary--small",
6929
- onClick: () => {
6930
- dismiss();
6931
- void room.toggleMicrophone();
6932
- },
6933
- children: "Unmute"
6934
- }
6935
- ) : null,
6936
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
6937
- ] }) : null
7079
+ notice !== null && notice.kind !== "removed" ? (
7080
+ // A host's request ("the host is asking you to unmute"), not an error:
7081
+ // an assertive status, so it is announced at once without being filed
7082
+ // as a failure (design-system error-box census).
7083
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "aria-live": "assertive", children: [
7084
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
7085
+ notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7086
+ "button",
7087
+ {
7088
+ type: "button",
7089
+ className: "mx-meet__primary mx-meet__primary--small",
7090
+ onClick: () => {
7091
+ dismiss();
7092
+ void room.toggleMicrophone();
7093
+ },
7094
+ children: "Unmute"
7095
+ }
7096
+ ) : null,
7097
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
7098
+ ] })
7099
+ ) : null
6938
7100
  ] });
6939
7101
  }
6940
7102
  function ShortcutsSheet({ onClose }) {
@@ -8867,6 +9029,341 @@ function createMeetingInviteHandler(options) {
8867
9029
  };
8868
9030
  }
8869
9031
 
9032
+ // src/core/after.ts
9033
+ var import_format4 = require("@ai-matrx/kit/format");
9034
+ function kindOf(person) {
9035
+ if (person.isAgent) return "assistant";
9036
+ if (person.role === "host") return "host";
9037
+ if (person.role === "cohost") return "cohost";
9038
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
9039
+ return "member";
9040
+ }
9041
+ function ms(iso) {
9042
+ if (iso === null) return null;
9043
+ const at = new Date(iso).getTime();
9044
+ return Number.isNaN(at) ? null : at;
9045
+ }
9046
+ function attendanceReport(meeting, attendees, names = {}) {
9047
+ const end = ms(meeting.endedAt);
9048
+ const rows = attendees.map((person) => {
9049
+ const joined = ms(person.joinedAt);
9050
+ const left = ms(person.leftAt);
9051
+ const until = left ?? end;
9052
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
9053
+ return {
9054
+ id: person.id,
9055
+ identity: person.identity,
9056
+ name: displayNameFor(names, person.identity, person.displayName),
9057
+ userId: person.userId,
9058
+ kind: kindOf(person),
9059
+ joinedAt: person.joinedAt,
9060
+ leftAt: person.leftAt,
9061
+ durationMs: duration,
9062
+ durationToEnd: duration !== null && left === null,
9063
+ admission: person.admissionState
9064
+ };
9065
+ });
9066
+ const rank = {
9067
+ host: 0,
9068
+ cohost: 1,
9069
+ member: 2,
9070
+ "signed-in guest": 3,
9071
+ guest: 4,
9072
+ assistant: 5
9073
+ };
9074
+ return rows.sort((a, b) => {
9075
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
9076
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
9077
+ });
9078
+ }
9079
+ function attendanceTotals(rows) {
9080
+ const humans = rows.filter((row) => row.kind !== "assistant");
9081
+ const attended = humans.filter((row) => row.joinedAt !== null);
9082
+ const timed = attended.filter((row) => row.durationMs !== null);
9083
+ return {
9084
+ people: attended.length,
9085
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
9086
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
9087
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
9088
+ };
9089
+ }
9090
+ function csvCell(value) {
9091
+ if (value === null) return "";
9092
+ const text3 = String(value);
9093
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
9094
+ }
9095
+ var ATTENDANCE_CSV_HEADER = [
9096
+ "Meeting",
9097
+ "Meeting started",
9098
+ "Name",
9099
+ "Attended as",
9100
+ "Joined",
9101
+ "Left",
9102
+ "Minutes in the meeting",
9103
+ "Admission"
9104
+ ];
9105
+ function attendanceCsv(reports) {
9106
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
9107
+ for (const { meeting, rows } of reports) {
9108
+ for (const row of rows) {
9109
+ lines.push(
9110
+ [
9111
+ meeting.title,
9112
+ meeting.startedAt ?? meeting.scheduledFor,
9113
+ row.name,
9114
+ row.kind,
9115
+ row.joinedAt,
9116
+ row.leftAt,
9117
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
9118
+ row.admission
9119
+ ].map(csvCell).join(",")
9120
+ );
9121
+ }
9122
+ }
9123
+ return `${lines.join("\r\n")}\r
9124
+ `;
9125
+ }
9126
+ function recordingOffsetMs(at, recording) {
9127
+ if (recording === null || recording.state !== "available") return null;
9128
+ const start = ms(recording.startedAt);
9129
+ const moment = ms(at);
9130
+ if (start === null || moment === null || moment < start) return null;
9131
+ const end = ms(recording.endedAt);
9132
+ if (end !== null && moment > end) return null;
9133
+ return moment - start;
9134
+ }
9135
+ function clockPosition(offsetMs) {
9136
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
9137
+ const h = Math.floor(total / 3600);
9138
+ const m = Math.floor(total % 3600 / 60);
9139
+ const s = total % 60;
9140
+ const ss = String(s).padStart(2, "0");
9141
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
9142
+ }
9143
+ function searchTranscript(segments, query, names = {}) {
9144
+ const needle = fold(query.trim());
9145
+ if (needle.length === 0) return [];
9146
+ const out = [];
9147
+ for (const segment of segments) {
9148
+ const hay = fold(segment.text);
9149
+ const ranges = [];
9150
+ let from = 0;
9151
+ for (; ; ) {
9152
+ const at = hay.indexOf(needle, from);
9153
+ if (at < 0) break;
9154
+ ranges.push([at, at + needle.length]);
9155
+ from = at + needle.length;
9156
+ }
9157
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
9158
+ if (ranges.length > 0 || speaker.includes(needle)) {
9159
+ out.push({ segmentId: segment.id, ranges });
9160
+ }
9161
+ }
9162
+ return out;
9163
+ }
9164
+ function fold(value) {
9165
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
9166
+ }
9167
+ function clockOfDay(iso, options) {
9168
+ const at = new Date(iso);
9169
+ if (Number.isNaN(at.getTime())) return "";
9170
+ return at.toLocaleTimeString(options.locale, {
9171
+ hour: "numeric",
9172
+ minute: "2-digit",
9173
+ second: "2-digit",
9174
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
9175
+ });
9176
+ }
9177
+ function vttStamp(offsetMs) {
9178
+ const total = Math.max(0, Math.round(offsetMs));
9179
+ const h = Math.floor(total / 36e5);
9180
+ const m = Math.floor(total % 36e5 / 6e4);
9181
+ const s = Math.floor(total % 6e4 / 1e3);
9182
+ const milli = total % 1e3;
9183
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
9184
+ }
9185
+ function transcriptDocument(bundle, format, options = {}) {
9186
+ const names = options.names ?? bundle.names;
9187
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
9188
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
9189
+ if (format === "vtt") {
9190
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
9191
+ const zero = ms(zeroIso) ?? 0;
9192
+ const cues = ["WEBVTT", ""];
9193
+ lines.forEach((line, index) => {
9194
+ const start = (ms(line.startedAt) ?? zero) - zero;
9195
+ if (start < 0) return;
9196
+ const next = lines[index + 1];
9197
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
9198
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
9199
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
9200
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
9201
+ cues.push("");
9202
+ });
9203
+ return cues.join("\n");
9204
+ }
9205
+ const out = [];
9206
+ const title = bundle.meeting.title;
9207
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
9208
+ out.push("");
9209
+ let last = null;
9210
+ for (const line of lines) {
9211
+ if (line.identity !== last) {
9212
+ if (last !== null) out.push("");
9213
+ const who = speaker(line);
9214
+ const clock = clockOfDay(line.startedAt, options);
9215
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
9216
+ last = line.identity;
9217
+ }
9218
+ out.push(line.text.trim());
9219
+ }
9220
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
9221
+ return `${out.join("\n")}
9222
+ `;
9223
+ }
9224
+ function bullet(note) {
9225
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
9226
+ return `- ${note.text.trim()}${owner}`;
9227
+ }
9228
+ function notesDocument(bundle, options = {}) {
9229
+ const out = [`# ${bundle.meeting.title}`, ""];
9230
+ if (options.when) out.push(`_${options.when}_`, "");
9231
+ const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
9232
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
9233
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
9234
+ out.push("## Decisions", "");
9235
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
9236
+ out.push("## Action items", "");
9237
+ out.push(
9238
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
9239
+ ""
9240
+ );
9241
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
9242
+ return out.join("\n");
9243
+ }
9244
+ function meetingLength(meeting) {
9245
+ const start = ms(meeting.startedAt);
9246
+ const end = ms(meeting.endedAt);
9247
+ if (start === null || end === null || end < start) return null;
9248
+ return (0, import_format4.formatDurationMs)(end - start, { style: "long" });
9249
+ }
9250
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
9251
+ const operation = "meet.loadMeetingChat";
9252
+ let response;
9253
+ try {
9254
+ response = await api.request({
9255
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
9256
+ method: "GET",
9257
+ operation,
9258
+ requireAuth: roomToken === null,
9259
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
9260
+ });
9261
+ } catch (error) {
9262
+ return {
9263
+ messages: [],
9264
+ refusal: {
9265
+ message: `The chat could not be read (${error.message}).`,
9266
+ remedy: error.remedy ?? "Reload the page."
9267
+ }
9268
+ };
9269
+ }
9270
+ if (!response.ok) {
9271
+ let body = null;
9272
+ try {
9273
+ body = await response.json();
9274
+ } catch {
9275
+ body = null;
9276
+ }
9277
+ const said = readServerRefusal(body);
9278
+ return {
9279
+ messages: [],
9280
+ refusal: {
9281
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
9282
+ remedy: said?.remedy || "Reload the page."
9283
+ }
9284
+ };
9285
+ }
9286
+ const payload = await response.json();
9287
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
9288
+ return {
9289
+ messages: rows.map(
9290
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
9291
+ ).filter((message) => message !== null),
9292
+ refusal: null
9293
+ };
9294
+ }
9295
+ async function refusalOf(response, operation) {
9296
+ let body = null;
9297
+ try {
9298
+ body = await response.json();
9299
+ } catch {
9300
+ body = null;
9301
+ }
9302
+ const said = readServerRefusal(body);
9303
+ return new MeetError(
9304
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
9305
+ `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
9306
+ said?.remedy || "Nothing was sent. Try again."
9307
+ );
9308
+ }
9309
+ function asRecipient(row) {
9310
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
9311
+ return {
9312
+ key: String(row["key"] ?? ""),
9313
+ name: String(row["name"] ?? ""),
9314
+ email: typeof row["email"] === "string" ? row["email"] : null,
9315
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
9316
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
9317
+ reachable: row["reachable"] !== false
9318
+ };
9319
+ }
9320
+ async function loadFollowUpDraft(api, meetingId) {
9321
+ const operation = "meet.loadFollowUpDraft";
9322
+ const response = await api.request({
9323
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9324
+ method: "GET",
9325
+ requireAuth: true,
9326
+ operation
9327
+ });
9328
+ if (!response.ok) throw await refusalOf(response, operation);
9329
+ const body = await response.json();
9330
+ const last = body["last_sent"];
9331
+ return {
9332
+ subject: String(body["subject"] ?? ""),
9333
+ body: String(body["body"] ?? ""),
9334
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
9335
+ (row) => asRecipient(row)
9336
+ ),
9337
+ wrapUpWritten: body["wrap_up_written"] === true,
9338
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
9339
+ };
9340
+ }
9341
+ async function sendFollowUp(api, meetingId, input) {
9342
+ const operation = "meet.sendFollowUp";
9343
+ const response = await api.request({
9344
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9345
+ method: "POST",
9346
+ requireAuth: true,
9347
+ operation,
9348
+ body: {
9349
+ subject: input.subject,
9350
+ body: input.body,
9351
+ recipient_keys: [...input.recipientKeys],
9352
+ dry_run: input.dryRun === true
9353
+ }
9354
+ });
9355
+ if (!response.ok) throw await refusalOf(response, operation);
9356
+ const body = await response.json();
9357
+ return {
9358
+ dryRun: body["dry_run"] === true,
9359
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
9360
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
9361
+ key: String(row["key"] ?? ""),
9362
+ reason: String(row["reason"] ?? "")
9363
+ })) : []
9364
+ };
9365
+ }
9366
+
8870
9367
  // src/core/management.ts
8871
9368
  async function refusal(response, operation) {
8872
9369
  let body = null;