@wirecat/cli-core 0.419.0 → 0.421.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli/bot/context.d.ts +3 -2
  3. package/dist/cli/bot/context.js +15 -0
  4. package/dist/cli/config-command.js +1 -2
  5. package/dist/cli/index.d.ts +0 -1
  6. package/dist/cli/index.js +0 -1
  7. package/dist/cli/messenger/context.js +3 -6
  8. package/dist/cli/permission-keys.d.ts +2 -0
  9. package/dist/cli/permission-keys.js +10 -3
  10. package/dist/cli/preview.d.ts +2 -2
  11. package/dist/cli/preview.js +4 -3
  12. package/dist/cli/program.js +4 -8
  13. package/dist/cli/settings.d.ts +0 -2
  14. package/dist/cli/settings.js +1 -5
  15. package/dist/domain/providers.d.ts +20 -2
  16. package/dist/domain/providers.js +23 -3
  17. package/dist/domain/tag-consolidation.d.ts +30 -1
  18. package/dist/entities/index.d.ts +4 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/mcp/surface.js +0 -4
  22. package/dist/mcp/tool.d.ts +1 -3
  23. package/dist/mcp/tool.js +1 -2
  24. package/dist/meetings/calendar-text.d.ts +13 -0
  25. package/dist/meetings/calendar-text.js +57 -0
  26. package/dist/meetings/event-store.d.ts +202 -0
  27. package/dist/meetings/event-store.js +1 -0
  28. package/dist/meetings/events.js +5 -1
  29. package/dist/meetings/index.d.ts +3 -0
  30. package/dist/meetings/index.js +2 -0
  31. package/dist/meetings/join-key.d.ts +7 -0
  32. package/dist/meetings/join-key.js +38 -0
  33. package/dist/meetings/mcp/index.d.ts +15 -15
  34. package/dist/meetings/model.d.ts +8 -2
  35. package/dist/meetings/read-answers.d.ts +6 -2
  36. package/dist/meetings/read-answers.js +9 -2
  37. package/dist/meetings/store.d.ts +2 -0
  38. package/dist/meetings/testing/event-store-contract.d.ts +10 -0
  39. package/dist/meetings/testing/event-store-contract.js +161 -0
  40. package/dist/meetings/testing/index.d.ts +1 -0
  41. package/dist/meetings/testing/index.js +1 -0
  42. package/dist/parity/manifest.js +0 -4
  43. package/dist/search/account-list.js +39 -7
  44. package/dist/sends/index.d.ts +1 -1
  45. package/dist/sends/index.js +1 -1
  46. package/dist/sends/permissions.d.ts +0 -13
  47. package/dist/sends/permissions.js +15 -142
  48. package/dist/services/agenda.js +5 -5
  49. package/dist/services/document-source.d.ts +7 -3
  50. package/dist/services/document-source.js +24 -12
  51. package/dist/services/documents/ingestion.d.ts +6 -1
  52. package/dist/services/documents/ingestion.js +4 -0
  53. package/dist/services/notes-search.js +3 -2
  54. package/dist/services/search-all.js +1 -0
  55. package/dist/services/task-tag-filters.d.ts +3 -0
  56. package/dist/services/task-tag-filters.js +8 -0
  57. package/dist/services/tasks.js +5 -9
  58. package/dist/store/document-transfer/index.js +12 -2
  59. package/dist/store/index.d.ts +2 -1
  60. package/dist/store/sqlite/accounts.js +1 -0
  61. package/dist/store/sqlite/calendar.d.ts +3 -0
  62. package/dist/store/sqlite/calendar.js +593 -0
  63. package/dist/store/sqlite/catalog.js +16 -5
  64. package/dist/store/sqlite/chat-rekey.js +1 -1
  65. package/dist/store/sqlite/chats.js +2 -2
  66. package/dist/store/sqlite/context-document-source.js +8 -4
  67. package/dist/store/sqlite/document-transfer/apply.js +24 -2
  68. package/dist/store/sqlite/document-transfer/collection.d.ts +19 -0
  69. package/dist/store/sqlite/document-transfer/collection.js +66 -0
  70. package/dist/store/sqlite/document-transfer/plan.js +15 -8
  71. package/dist/store/sqlite/document-transfer/snapshot.js +12 -7
  72. package/dist/store/sqlite/entity-types.d.ts +5 -1
  73. package/dist/store/sqlite/entity-types.js +5 -1
  74. package/dist/store/sqlite/events.d.ts +8 -2
  75. package/dist/store/sqlite/events.js +36 -12
  76. package/dist/store/sqlite/files.d.ts +93 -0
  77. package/dist/store/sqlite/files.js +293 -0
  78. package/dist/store/sqlite/folded.d.ts +3 -0
  79. package/dist/store/sqlite/folded.js +1 -0
  80. package/dist/store/sqlite/imported-documents.js +1 -1
  81. package/dist/store/sqlite/join-keys.d.ts +8 -0
  82. package/dist/store/sqlite/join-keys.js +27 -0
  83. package/dist/store/sqlite/knowledge.js +6 -6
  84. package/dist/store/sqlite/link-reviews.d.ts +9 -0
  85. package/dist/store/sqlite/link-reviews.js +12 -0
  86. package/dist/store/sqlite/lucene-fields.js +8 -6
  87. package/dist/store/sqlite/manifest.js +11 -0
  88. package/dist/store/sqlite/meeting-selection.js +1 -1
  89. package/dist/store/sqlite/meetings.js +6 -1
  90. package/dist/store/sqlite/migrations.generated.js +143 -0
  91. package/dist/store/sqlite/note-index.d.ts +8 -1
  92. package/dist/store/sqlite/note-index.js +14 -0
  93. package/dist/store/sqlite/note-search.js +6 -5
  94. package/dist/store/sqlite/notes.d.ts +18 -1
  95. package/dist/store/sqlite/notes.js +237 -40
  96. package/dist/store/sqlite/open-store.js +4 -0
  97. package/dist/store/sqlite/person-links.js +7 -2
  98. package/dist/store/sqlite/schema/canonical-tags.d.ts +87 -0
  99. package/dist/store/sqlite/schema/canonical-tags.js +8 -0
  100. package/dist/store/sqlite/schema/tag-consolidations.d.ts +270 -0
  101. package/dist/store/sqlite/schema/tag-consolidations.js +34 -0
  102. package/dist/store/sqlite/schema.d.ts +3589 -1634
  103. package/dist/store/sqlite/schema.js +191 -13
  104. package/dist/store/sqlite/tag-consolidation-apply.d.ts +3 -0
  105. package/dist/store/sqlite/tag-consolidation-apply.js +88 -0
  106. package/dist/store/sqlite/tag-consolidation-input.d.ts +16 -0
  107. package/dist/store/sqlite/tag-consolidation-input.js +52 -0
  108. package/dist/store/sqlite/tag-consolidation.js +51 -66
  109. package/dist/store/sqlite/tag-creation.js +17 -8
  110. package/dist/store/sqlite/tag-resolution.d.ts +5 -0
  111. package/dist/store/sqlite/tag-resolution.js +33 -0
  112. package/dist/store/sqlite/tags.d.ts +3 -1
  113. package/dist/store/sqlite/tags.js +70 -50
  114. package/dist/store/sqlite/tasks.d.ts +2 -0
  115. package/dist/store/sqlite/tasks.js +6 -1
  116. package/dist/store/sqlite/things.js +2 -1
  117. package/dist/store/sqlite/vocabulary.d.ts +1 -0
  118. package/dist/store/sqlite/vocabulary.js +30 -8
  119. package/dist/store/store.d.ts +6 -0
  120. package/dist/store/store.js +10 -0
  121. package/dist/store/tags.js +2 -2
  122. package/package.json +2 -1
  123. package/dist/cli/moved.d.ts +0 -18
  124. package/dist/cli/moved.js +0 -72
  125. package/dist/sends/permission-aliases.d.ts +0 -5
  126. package/dist/sends/permission-aliases.js +0 -50
@@ -0,0 +1,57 @@
1
+ import { joinUrlWithoutQuery } from "./join-key.js";
2
+ const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'", apos: "'", nbsp: " " };
3
+ /**
4
+ * A line that gives a way into a meeting: a passcode, a password, a PIN or an access code. Bounded by letters and
5
+ * digits, not `\b`, which knows only ASCII and would never match a Cyrillic word.
6
+ */
7
+ const SECRET_LINE = /(?<![\p{L}\p{N}])(pass ?codes?|passwords?|pwd|pins?|access ?codes?|meeting ?codes?|kennwort|пароль|пин-?код|код доступа)(?![\p{L}\p{N}])|pwd=/iu;
8
+ const htmlToText = (html) => html
9
+ .replace(/<\s*br\s*\/?>/giu, "\n")
10
+ .replace(/<\/\s*(p|div|li|tr|h[1-6])\s*>/giu, "\n")
11
+ .replace(/<a\s[^>]*href\s*=\s*"([^"]*)"[^>]*>/giu, " $1 ")
12
+ .replace(/<[^>]+>/gu, "")
13
+ .replace(/&(amp|lt|gt|quot|#39|apos|nbsp);/gu, (_, name) => ENTITIES[name] ?? "");
14
+ /** Every web link without its query string or fragment, which is where passcodes ride (`pwd=`). */
15
+ export const withoutLinkQueries = (text) => text.replace(/https?:\/\/[^\s<>"')\]]+/giu, (url) => joinUrlWithoutQuery(url) ?? "");
16
+ /**
17
+ * A calendar's text as the store keeps it: HTML reduced to text, links without their query strings, and each line
18
+ * naming a passcode, password or PIN dropped — except from a title, where "PIN reset review" is a meeting's name.
19
+ * `null` when nothing is left.
20
+ */
21
+ export const calendarText = (text, { dropSecretLines = true } = {}) => {
22
+ if (!text)
23
+ return null;
24
+ const kept = htmlToText(text)
25
+ .split(/\r?\n/u)
26
+ .map((line) => withoutLinkQueries(line)
27
+ .replace(/[ \t]+/gu, " ")
28
+ .trim())
29
+ .filter((line) => !dropSecretLines || !SECRET_LINE.test(line));
30
+ const joined = kept
31
+ .join("\n")
32
+ .replace(/\n{3,}/gu, "\n\n")
33
+ .trim();
34
+ return joined || null;
35
+ };
36
+ /** The provider fields a calendar entry may keep in `metadata`: none of them can open a meeting. */
37
+ export const ENTRY_METADATA_KEYS = [
38
+ "colorId",
39
+ "guestsCanInviteOthers",
40
+ "guestsCanModify",
41
+ "guestsCanSeeOtherGuests",
42
+ "hangoutLink",
43
+ "locked",
44
+ "privateCopy",
45
+ "recurring_event_id",
46
+ ];
47
+ export const entryMetadata = (metadata) => {
48
+ const kept = {};
49
+ for (const key of ENTRY_METADATA_KEYS) {
50
+ const value = metadata?.[key];
51
+ if (typeof value === "string")
52
+ kept[key] = withoutLinkQueries(value);
53
+ else if (typeof value === "number" || typeof value === "boolean")
54
+ kept[key] = value;
55
+ }
56
+ return kept;
57
+ };
@@ -0,0 +1,202 @@
1
+ import type { Event } from "./model.js";
2
+ /** One calendar as a provider lists it. */
3
+ export interface CalendarInput {
4
+ externalId: string;
5
+ title: string | null;
6
+ timezone: string | null;
7
+ /** owner, writer, reader, freeBusyReader. A calendar the owner owns syncs by default. */
8
+ accessRole: string | null;
9
+ primary: boolean;
10
+ }
11
+ export interface StoredCalendar {
12
+ id: number;
13
+ accountId: number;
14
+ externalId: string;
15
+ title: string | null;
16
+ timezone: string | null;
17
+ accessRole: string | null;
18
+ primary: boolean;
19
+ synced: boolean;
20
+ seenAt: number | null;
21
+ deletedAt: number | null;
22
+ }
23
+ export interface EntryParticipantInput {
24
+ email: string;
25
+ name?: string | null;
26
+ response?: string | null;
27
+ optional?: boolean;
28
+ organizer?: boolean;
29
+ self?: boolean;
30
+ /** A room or a piece of equipment: listed, never a contact. */
31
+ resource?: boolean;
32
+ }
33
+ export type EntryKind = "single" | "series" | "occurrence";
34
+ export type EntryStatus = "confirmed" | "tentative" | "cancelled";
35
+ /**
36
+ * One calendar entry as the provider returns it. Times are epoch milliseconds, `endsAt` exclusive. Text is cleaned
37
+ * before it is stored; there is no field for a conference's entry points, so no passcode or PIN can be given.
38
+ */
39
+ export interface EntryInput {
40
+ externalId: string;
41
+ kind: EntryKind;
42
+ /** An occurrence's series, by the series entry's `externalId` (Google's `recurringEventId`). */
43
+ seriesExternalId?: string | null;
44
+ icalUid?: string | null;
45
+ /** An occurrence's slot in its series (Google's `originalStartTime`). */
46
+ originalStartsAt?: number | null;
47
+ exception?: boolean;
48
+ status: EntryStatus;
49
+ title?: string | null;
50
+ description?: string | null;
51
+ location?: string | null;
52
+ startsAt?: number | null;
53
+ endsAt?: number | null;
54
+ allDay?: boolean;
55
+ startDate?: string | null;
56
+ endDate?: string | null;
57
+ timezone?: string | null;
58
+ recurrence?: string[] | null;
59
+ /** A series' last slot; `null` repeats indefinitely. */
60
+ recurrenceEndsAt?: number | null;
61
+ eventType?: string | null;
62
+ transparency?: string | null;
63
+ visibility?: string | null;
64
+ ownResponse?: string | null;
65
+ organizer?: {
66
+ email: string;
67
+ name?: string | null;
68
+ } | null;
69
+ participants?: EntryParticipantInput[];
70
+ joinUrl?: string | null;
71
+ conference?: string | null;
72
+ url?: string | null;
73
+ sequence?: number | null;
74
+ etag?: string | null;
75
+ externalUpdatedAt?: number | null;
76
+ /** Only the keys `ENTRY_METADATA_KEYS` names are kept. */
77
+ metadata?: Record<string, unknown> | null;
78
+ }
79
+ export interface StoredEntry {
80
+ id: number;
81
+ calendarId: number;
82
+ kind: EntryKind;
83
+ seriesEntryId: number | null;
84
+ eventId: number | null;
85
+ eventSeriesId: number | null;
86
+ externalId: string;
87
+ icalUid: string | null;
88
+ originalStartsAt: number | null;
89
+ exception: boolean;
90
+ status: EntryStatus;
91
+ title: string | null;
92
+ description: string | null;
93
+ location: string | null;
94
+ startsAt: number | null;
95
+ endsAt: number | null;
96
+ allDay: boolean;
97
+ startDate: string | null;
98
+ endDate: string | null;
99
+ timezone: string | null;
100
+ recurrence: string[] | null;
101
+ recurrenceEndsAt: number | null;
102
+ eventType: string | null;
103
+ ownResponse: string | null;
104
+ organizerIdentityId: number | null;
105
+ joinUrl: string | null;
106
+ joinKey: string | null;
107
+ url: string | null;
108
+ externalUpdatedAt: number | null;
109
+ seenAt: number | null;
110
+ deletedAt: number | null;
111
+ }
112
+ export interface StoredParticipant {
113
+ entryId: number;
114
+ identityId: number | null;
115
+ email: string;
116
+ name: string | null;
117
+ response: string | null;
118
+ optional: boolean;
119
+ organizer: boolean;
120
+ self: boolean;
121
+ resource: boolean;
122
+ }
123
+ /** An entry's tracked fields as they were before one change. */
124
+ export interface EntryRevision {
125
+ entryId: number;
126
+ title: string | null;
127
+ startsAt: number | null;
128
+ endsAt: number | null;
129
+ location: string | null;
130
+ status: string | null;
131
+ recurrence: string[] | null;
132
+ recurrenceEndsAt: number | null;
133
+ externalUpdatedAt: number | null;
134
+ createdAt: number;
135
+ }
136
+ export interface EventDetails {
137
+ event: Event;
138
+ entries: StoredEntry[];
139
+ participants: StoredParticipant[];
140
+ revisions: EntryRevision[];
141
+ }
142
+ export interface EventFilter {
143
+ /** Events that end after this instant (exclusive end). */
144
+ from?: number;
145
+ /** Events that start before this instant. */
146
+ to?: number;
147
+ accountId?: number;
148
+ calendarId?: number;
149
+ /** Events whose entries list one of this person's contacts. */
150
+ personId?: number;
151
+ limit?: number;
152
+ offset?: number;
153
+ }
154
+ /** Pairs the rules left unlinked because two or more entities fit equally well (requirements 3.6). */
155
+ export interface LinkReview {
156
+ id: number;
157
+ linkableType: string;
158
+ linkableId: number;
159
+ entityType: string;
160
+ candidates: number[];
161
+ reason: string;
162
+ status: "open" | "linked" | "apart";
163
+ chosenId: number | null;
164
+ decidedBy: string | null;
165
+ decidedAt: number | null;
166
+ createdAt: number;
167
+ }
168
+ export type ReviewDecision = {
169
+ choose: number;
170
+ } | {
171
+ apart: true;
172
+ };
173
+ /**
174
+ * Calendars and their entries, the source records of events (requirements 3.4). Apart from `MeetingStore`: a
175
+ * calendar CLI implements this one, a meeting app the other. Every write is one transaction; an owner's link is never
176
+ * replaced by a rule, and two equally good candidates make no link and one review entry.
177
+ */
178
+ export interface EventStore {
179
+ saveCalendars(accountId: number, calendars: CalendarInput[], now: number): Promise<StoredCalendar[]>;
180
+ calendars(accountId: number): Promise<StoredCalendar[]>;
181
+ setCalendarSynced(calendarId: number, synced: boolean, now: number): Promise<void>;
182
+ /** One sync page, matched by rules 1–5, in one transaction. */
183
+ saveEntries(calendarId: number, entries: EntryInput[], now: number): Promise<StoredEntry[]>;
184
+ /**
185
+ * After a full list (`410 Gone`): entries not seen since `seenBefore` get `deleted_at`. Plain occurrences come only
186
+ * from the window fetched, so only those inside it count. Events, series and links stay.
187
+ */
188
+ markUnseen(calendarId: number, seenBefore: number, window: {
189
+ from: number;
190
+ to: number;
191
+ }, now: number): Promise<number>;
192
+ events(filter?: EventFilter): Promise<Event[]>;
193
+ event(id: number): Promise<EventDetails | null>;
194
+ /** The owner's link of an entry to an event; no rule replaces it. */
195
+ linkEntry(entryId: number, eventId: number, now: number): Promise<void>;
196
+ /** The owner's unlink: the rules leave the entry alone after it. */
197
+ unlinkEntry(entryId: number, now: number): Promise<void>;
198
+ reviews(filter?: {
199
+ status?: LinkReview["status"];
200
+ }): Promise<LinkReview[]>;
201
+ resolveReview(id: number, decision: ReviewDecision, now: number): Promise<LinkReview>;
202
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -13,7 +13,11 @@ export const linkEvent = async (store, details, now = Date.now()) => {
13
13
  startsAt: meeting.startedAt,
14
14
  endsAt: meeting.endedAt ?? meeting.startedAt,
15
15
  });
16
- let event = candidates.length === 1 ? candidates[0] : undefined;
16
+ if (candidates.length > 1) {
17
+ await store.reviewMeetingLink?.(meeting.id, candidates.map(({ id }) => id), now);
18
+ return null;
19
+ }
20
+ let event = candidates[0];
17
21
  if (!event) {
18
22
  let eventSeriesId = series?.eventSeriesId ?? null;
19
23
  if (series?.kind === "recurring" && eventSeriesId === null) {
@@ -1,8 +1,10 @@
1
1
  export { MEETING_STEPS, type MeetingDetailsItem, type MeetingHitItem, type MeetingItemBody, type MeetingListBody, type MeetingSummaryItem, type MeetingTranscriptItem, meetingDetails, meetingHit, meetingHitsList, meetingSummary, meetingTranscript, } from "./answers.js";
2
2
  export type { MeetingBatchReader } from "./batch.js";
3
+ export { calendarText, ENTRY_METADATA_KEYS, entryMetadata, withoutLinkQueries } from "./calendar-text.js";
3
4
  export type { MeetingContextStore, MeetingEventLinkInput, MeetingEventLinkReceipt, ParticipantPerson, ParticipantPersonInput, ParticipantPersonLinkInput, ParticipantPersonLinkReceipt, } from "./context-store.js";
4
5
  export { type MeetingCueIdentity, meetingCueFingerprint } from "./cue-fingerprint.js";
5
6
  export { EVENT_QUERY } from "./event-query.js";
7
+ export type { CalendarInput, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EventDetails, EventFilter, EventStore, LinkReview, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "./event-store.js";
6
8
  export { linkEvent } from "./events.js";
7
9
  export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
8
10
  export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
@@ -10,6 +12,7 @@ export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRu
10
12
  export { liftMeetingFields, MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
11
13
  export { type ImportFile, importFiles } from "./import.js";
12
14
  export { MEETING_ISSUES, MEETING_QUERY, type MeetingIssue, type MeetingItem, type MeetingOwner, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
15
+ export { joinKeyOf, joinUrlWithoutQuery } from "./join-key.js";
13
16
  export { CHAT_MESSAGES_READ, type MeetingChatLine, type MeetingChatLinesInput, type MeetingChatLinesStore, type MeetingLink, } from "./links.js";
14
17
  export { createMeetingContext, MEETING_FACT_TYPES, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
15
18
  export { createMeetingManagement, dispatchMeetingMutation, type ManagedMeeting, type ManagedMeetingChange, type MeetingManagement, type MeetingManagementPorts, type MeetingMutationOptions, type MeetingMutationProposal, type MeetingMutationReceipt, type MeetingOperationOptions, type MeetingProviderContext, validateMeetingChange, } from "./meeting-management.js";
@@ -1,4 +1,5 @@
1
1
  export { MEETING_STEPS, meetingDetails, meetingHit, meetingHitsList, meetingSummary, meetingTranscript, } from "./answers.js";
2
+ export { calendarText, ENTRY_METADATA_KEYS, entryMetadata, withoutLinkQueries } from "./calendar-text.js";
2
3
  export { meetingCueFingerprint } from "./cue-fingerprint.js";
3
4
  export { EVENT_QUERY } from "./event-query.js";
4
5
  export { linkEvent } from "./events.js";
@@ -8,6 +9,7 @@ export { findingFingerprint, validateFindings, } from "./findings-rules.js";
8
9
  export { liftMeetingFields, MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
9
10
  export { importFiles } from "./import.js";
10
11
  export { MEETING_ISSUES, MEETING_QUERY, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
12
+ export { joinKeyOf, joinUrlWithoutQuery } from "./join-key.js";
11
13
  export { CHAT_MESSAGES_READ, } from "./links.js";
12
14
  export { createMeetingContext, MEETING_FACT_TYPES, validateMeetingContextChange, } from "./meeting-context.js";
13
15
  export { createMeetingManagement, dispatchMeetingMutation, validateMeetingChange, } from "./meeting-management.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * What one meeting room is, whatever link it was shared by: `zoom:<number>`, `meet:<code>`, else the host and path.
3
+ * A query string carries passcodes (`pwd=`), so it never reaches the key or a stored link.
4
+ */
5
+ export declare const joinKeyOf: (url: string | null | undefined) => string | null;
6
+ /** The link without its query string or fragment, which may hold a passcode. */
7
+ export declare const joinUrlWithoutQuery: (url: string | null | undefined) => string | null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What one meeting room is, whatever link it was shared by: `zoom:<number>`, `meet:<code>`, else the host and path.
3
+ * A query string carries passcodes (`pwd=`), so it never reaches the key or a stored link.
4
+ */
5
+ export const joinKeyOf = (url) => {
6
+ const parsed = urlOf(url);
7
+ if (!parsed)
8
+ return null;
9
+ const host = parsed.hostname.toLowerCase().replace(/^www\./u, "");
10
+ const path = parsed.pathname.replace(/\/+$/u, "");
11
+ if (host === "zoom.us" || host.endsWith(".zoom.us") || host === "zoomgov.com" || host.endsWith(".zoomgov.com")) {
12
+ const number = /^\/(?:j|w|s|wc\/join|wc)\/(\d{9,12})(?:\/|$)/u.exec(path)?.[1];
13
+ if (number)
14
+ return `zoom:${number}`;
15
+ }
16
+ if (host === "meet.google.com") {
17
+ const code = /^\/([a-z]{3}-[a-z]{4}-[a-z]{3})$/iu.exec(path)?.[1];
18
+ if (code)
19
+ return `meet:${code.toLowerCase()}`;
20
+ }
21
+ return `${host}${path}`;
22
+ };
23
+ /** The link without its query string or fragment, which may hold a passcode. */
24
+ export const joinUrlWithoutQuery = (url) => {
25
+ const parsed = urlOf(url);
26
+ return parsed ? `${parsed.origin}${parsed.pathname}` : null;
27
+ };
28
+ const urlOf = (url) => {
29
+ if (!url?.trim())
30
+ return null;
31
+ try {
32
+ const parsed = new URL(url.trim());
33
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed : null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ };
@@ -38,6 +38,7 @@ export declare const meetingTools: ({
38
38
  max_read_bytes?: undefined;
39
39
  cursor?: undefined;
40
40
  } | {
41
+ limit?: undefined;
41
42
  event_id?: undefined;
42
43
  series_id?: undefined;
43
44
  page?: undefined;
@@ -53,11 +54,11 @@ export declare const meetingTools: ({
53
54
  format?: undefined;
54
55
  transcript_id?: undefined;
55
56
  query?: undefined;
56
- limit?: undefined;
57
57
  scan_limit?: undefined;
58
58
  max_read_bytes?: undefined;
59
59
  cursor?: undefined;
60
60
  } | {
61
+ limit?: undefined;
61
62
  event_id?: undefined;
62
63
  series_id?: undefined;
63
64
  page?: undefined;
@@ -75,11 +76,11 @@ export declare const meetingTools: ({
75
76
  format?: undefined;
76
77
  transcript_id?: undefined;
77
78
  query?: undefined;
78
- limit?: undefined;
79
79
  scan_limit?: undefined;
80
80
  max_read_bytes?: undefined;
81
81
  cursor?: undefined;
82
82
  } | {
83
+ limit?: undefined;
83
84
  event_id?: undefined;
84
85
  series_id?: undefined;
85
86
  page?: undefined;
@@ -101,7 +102,6 @@ export declare const meetingTools: ({
101
102
  minimum: number;
102
103
  };
103
104
  query?: undefined;
104
- limit?: undefined;
105
105
  scan_limit?: undefined;
106
106
  max_read_bytes?: undefined;
107
107
  cursor?: undefined;
@@ -188,6 +188,7 @@ export declare const meetingTools: ({
188
188
  max_read_bytes?: undefined;
189
189
  cursor?: undefined;
190
190
  } | {
191
+ limit?: undefined;
191
192
  event_id?: undefined;
192
193
  series_id?: undefined;
193
194
  page?: undefined;
@@ -203,11 +204,11 @@ export declare const meetingTools: ({
203
204
  format?: undefined;
204
205
  transcript_id?: undefined;
205
206
  query?: undefined;
206
- limit?: undefined;
207
207
  scan_limit?: undefined;
208
208
  max_read_bytes?: undefined;
209
209
  cursor?: undefined;
210
210
  } | {
211
+ limit?: undefined;
211
212
  event_id?: undefined;
212
213
  series_id?: undefined;
213
214
  page?: undefined;
@@ -225,11 +226,11 @@ export declare const meetingTools: ({
225
226
  format?: undefined;
226
227
  transcript_id?: undefined;
227
228
  query?: undefined;
228
- limit?: undefined;
229
229
  scan_limit?: undefined;
230
230
  max_read_bytes?: undefined;
231
231
  cursor?: undefined;
232
232
  } | {
233
+ limit?: undefined;
233
234
  event_id?: undefined;
234
235
  series_id?: undefined;
235
236
  page?: undefined;
@@ -251,7 +252,6 @@ export declare const meetingTools: ({
251
252
  minimum: number;
252
253
  };
253
254
  query?: undefined;
254
- limit?: undefined;
255
255
  scan_limit?: undefined;
256
256
  max_read_bytes?: undefined;
257
257
  cursor?: undefined;
@@ -338,6 +338,7 @@ export declare const meetingTools: ({
338
338
  max_read_bytes?: undefined;
339
339
  cursor?: undefined;
340
340
  } | {
341
+ limit?: undefined;
341
342
  event_id?: undefined;
342
343
  series_id?: undefined;
343
344
  page?: undefined;
@@ -353,11 +354,11 @@ export declare const meetingTools: ({
353
354
  format?: undefined;
354
355
  transcript_id?: undefined;
355
356
  query?: undefined;
356
- limit?: undefined;
357
357
  scan_limit?: undefined;
358
358
  max_read_bytes?: undefined;
359
359
  cursor?: undefined;
360
360
  } | {
361
+ limit?: undefined;
361
362
  event_id?: undefined;
362
363
  series_id?: undefined;
363
364
  page?: undefined;
@@ -375,11 +376,11 @@ export declare const meetingTools: ({
375
376
  format?: undefined;
376
377
  transcript_id?: undefined;
377
378
  query?: undefined;
378
- limit?: undefined;
379
379
  scan_limit?: undefined;
380
380
  max_read_bytes?: undefined;
381
381
  cursor?: undefined;
382
382
  } | {
383
+ limit?: undefined;
383
384
  event_id?: undefined;
384
385
  series_id?: undefined;
385
386
  page?: undefined;
@@ -401,7 +402,6 @@ export declare const meetingTools: ({
401
402
  minimum: number;
402
403
  };
403
404
  query?: undefined;
404
- limit?: undefined;
405
405
  scan_limit?: undefined;
406
406
  max_read_bytes?: undefined;
407
407
  cursor?: undefined;
@@ -488,6 +488,7 @@ export declare const meetingTools: ({
488
488
  max_read_bytes?: undefined;
489
489
  cursor?: undefined;
490
490
  } | {
491
+ limit?: undefined;
491
492
  event_id?: undefined;
492
493
  series_id?: undefined;
493
494
  page?: undefined;
@@ -503,11 +504,11 @@ export declare const meetingTools: ({
503
504
  format?: undefined;
504
505
  transcript_id?: undefined;
505
506
  query?: undefined;
506
- limit?: undefined;
507
507
  scan_limit?: undefined;
508
508
  max_read_bytes?: undefined;
509
509
  cursor?: undefined;
510
510
  } | {
511
+ limit?: undefined;
511
512
  event_id?: undefined;
512
513
  series_id?: undefined;
513
514
  page?: undefined;
@@ -525,11 +526,11 @@ export declare const meetingTools: ({
525
526
  format?: undefined;
526
527
  transcript_id?: undefined;
527
528
  query?: undefined;
528
- limit?: undefined;
529
529
  scan_limit?: undefined;
530
530
  max_read_bytes?: undefined;
531
531
  cursor?: undefined;
532
532
  } | {
533
+ limit?: undefined;
533
534
  event_id?: undefined;
534
535
  series_id?: undefined;
535
536
  page?: undefined;
@@ -551,7 +552,6 @@ export declare const meetingTools: ({
551
552
  minimum: number;
552
553
  };
553
554
  query?: undefined;
554
- limit?: undefined;
555
555
  scan_limit?: undefined;
556
556
  max_read_bytes?: undefined;
557
557
  cursor?: undefined;
@@ -638,6 +638,7 @@ export declare const meetingTools: ({
638
638
  max_read_bytes?: undefined;
639
639
  cursor?: undefined;
640
640
  } | {
641
+ limit?: undefined;
641
642
  event_id?: undefined;
642
643
  series_id?: undefined;
643
644
  page?: undefined;
@@ -653,11 +654,11 @@ export declare const meetingTools: ({
653
654
  format?: undefined;
654
655
  transcript_id?: undefined;
655
656
  query?: undefined;
656
- limit?: undefined;
657
657
  scan_limit?: undefined;
658
658
  max_read_bytes?: undefined;
659
659
  cursor?: undefined;
660
660
  } | {
661
+ limit?: undefined;
661
662
  event_id?: undefined;
662
663
  series_id?: undefined;
663
664
  page?: undefined;
@@ -675,11 +676,11 @@ export declare const meetingTools: ({
675
676
  format?: undefined;
676
677
  transcript_id?: undefined;
677
678
  query?: undefined;
678
- limit?: undefined;
679
679
  scan_limit?: undefined;
680
680
  max_read_bytes?: undefined;
681
681
  cursor?: undefined;
682
682
  } | {
683
+ limit?: undefined;
683
684
  event_id?: undefined;
684
685
  series_id?: undefined;
685
686
  page?: undefined;
@@ -701,7 +702,6 @@ export declare const meetingTools: ({
701
702
  minimum: number;
702
703
  };
703
704
  query?: undefined;
704
- limit?: undefined;
705
705
  scan_limit?: undefined;
706
706
  max_read_bytes?: undefined;
707
707
  cursor?: undefined;
@@ -13,7 +13,8 @@ export interface EventSeries extends RecordTimes {
13
13
  id: number;
14
14
  title: string | null;
15
15
  recurrence: string | null;
16
- origin: "auto" | "owner";
16
+ /** `auto` from a meeting, `owner`, or `calendar` from a calendar's entries. */
17
+ origin: "auto" | "owner" | "calendar";
17
18
  }
18
19
  export interface Event extends RecordTimes {
19
20
  id: number;
@@ -24,8 +25,13 @@ export interface Event extends RecordTimes {
24
25
  startsAt: number | null;
25
26
  endsAt: number | null;
26
27
  timezone: string | null;
27
- origin: "auto" | "owner";
28
+ origin: "auto" | "owner" | "calendar";
28
29
  deletedAt: number | null;
30
+ /** confirmed, tentative, cancelled: cancelled only when every live calendar entry of it is. */
31
+ status?: string | null;
32
+ allDay?: boolean | null;
33
+ /** When its calendar deleted it; a past event stays as a meeting that happened. */
34
+ sourceDeletedAt?: number | null;
29
35
  }
30
36
  export interface MeetingSeries extends RecordTimes {
31
37
  id: number;
@@ -176,8 +176,12 @@ export declare const event: v.StrictObjectSchema<{
176
176
  readonly starts_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
177
177
  readonly ends_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
178
178
  readonly timezone: v.NullableSchema<v.StringSchema<undefined>, undefined>;
179
- /** `auto`: made from a meeting; `owner`: made by hand. */
180
- readonly origin: v.PicklistSchema<["auto", "owner"], undefined>;
179
+ /** `auto`: made from a meeting; `owner`: made by hand; `calendar`: from a calendar's entries. */
180
+ readonly origin: v.PicklistSchema<["auto", "owner", "calendar"], undefined>;
181
+ readonly status: v.NullableSchema<v.StringSchema<undefined>, undefined>;
182
+ readonly all_day: v.BooleanSchema<undefined>;
183
+ /** When its calendar deleted it: shown as "deleted in Google", kept as history. */
184
+ readonly source_deleted_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
181
185
  readonly deleted_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
182
186
  readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
183
187
  readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
@@ -191,8 +191,12 @@ export const event = v.strictObject({
191
191
  starts_at: v.nullable(isoTime),
192
192
  ends_at: v.nullable(isoTime),
193
193
  timezone: v.nullable(text),
194
- /** `auto`: made from a meeting; `owner`: made by hand. */
195
- origin: v.picklist(["auto", "owner"]),
194
+ /** `auto`: made from a meeting; `owner`: made by hand; `calendar`: from a calendar's entries. */
195
+ origin: v.picklist(["auto", "owner", "calendar"]),
196
+ status: v.nullable(text),
197
+ all_day: v.boolean(),
198
+ /** When its calendar deleted it: shown as "deleted in Google", kept as history. */
199
+ source_deleted_at: v.nullable(isoTime),
196
200
  deleted_at: v.nullable(isoTime),
197
201
  created_at: isoTime,
198
202
  updated_at: isoTime,
@@ -207,6 +211,9 @@ export const eventItem = (one) => ({
207
211
  ends_at: iso(one.endsAt),
208
212
  timezone: one.timezone,
209
213
  origin: one.origin,
214
+ status: one.status ?? null,
215
+ all_day: one.allDay === true,
216
+ source_deleted_at: iso(one.sourceDeletedAt ?? null),
210
217
  deleted_at: iso(one.deletedAt),
211
218
  created_at: new Date(one.createdAt).toISOString(),
212
219
  updated_at: new Date(one.updatedAt).toISOString(),
@@ -120,6 +120,8 @@ export interface MeetingStore {
120
120
  setEventSeries(meetingSeriesId: number, eventSeriesId: number, now: number): Promise<void>;
121
121
  /** Automatic linking must not replace an existing link. */
122
122
  linkMeeting(id: number, eventId: number, now: number, mode?: "auto" | "owner"): Promise<void>;
123
+ /** Two or more events fit a meeting equally: no link, and the pair goes to the owner's review list. */
124
+ reviewMeetingLink?(meetingId: number, eventIds: number[], now: number): Promise<void>;
123
125
  cursor(accountId: number): Promise<string | null>;
124
126
  setCursor(accountId: number, value: string, now: number): Promise<void>;
125
127
  }
@@ -0,0 +1,10 @@
1
+ import type { EventStore } from "../event-store.js";
2
+ import type { ContractCase } from "../store.js";
3
+ /** A fresh, empty store and one calendar account in it. */
4
+ export interface EventStoreFixture {
5
+ store: EventStore;
6
+ accountId: number;
7
+ }
8
+ /** "Now" in every case: entries before it have happened. */
9
+ export declare const CONTRACT_NOW: number;
10
+ export declare const eventStoreContract: (make: () => EventStoreFixture | Promise<EventStoreFixture>) => ContractCase[];