@chatpanel/events 0.62.0 → 0.64.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.
- package/index.js +1 -0
- package/meeting-shape.js +262 -0
- package/meeting-text.js +5 -46
- package/package.json +8 -5
package/index.js
CHANGED
|
@@ -252,3 +252,4 @@ export { renderMarkdown, defaultLinkPolicy } from './markdown-render.js';
|
|
|
252
252
|
// A meeting read back out of the flat text the warm store holds — the same grammar the
|
|
253
253
|
// extension writes and MCP reads, so every client shows one transcript, not three.
|
|
254
254
|
export { parseMeetingText, speakerStats, densityRibbon } from './meeting-text.js';
|
|
255
|
+
export { speakerBreakdown, speakerTimeline, formatTalkTime, SPEAKER_SLOTS } from './meeting-shape.js';
|
package/meeting-shape.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// THE SHAPE OF A MEETING — who spoke, when, and for how long.
|
|
2
|
+
//
|
|
3
|
+
// A transcript you did not attend is a wall. The shape of it — one band showing who held
|
|
4
|
+
// the floor across the hour, and a breakdown of the minutes — is the fastest way in, and
|
|
5
|
+
// it is derivable from the captions alone. So it lives HERE, not in a client: the
|
|
6
|
+
// extension, the desktop app and anything that reads the warm index all want the same
|
|
7
|
+
// answer, and three implementations of "who talked most" drift into three numbers.
|
|
8
|
+
//
|
|
9
|
+
// TWO BASES, and the module says which one it used.
|
|
10
|
+
//
|
|
11
|
+
// 'clock' — the segments carry real timestamps (the extension's capture stamps every
|
|
12
|
+
// caption with `t`, epoch ms). Buckets are equal slices of WALL TIME, so a
|
|
13
|
+
// five-minute silence is visibly five minutes of nothing, and talk time is
|
|
14
|
+
// real minutes.
|
|
15
|
+
// 'text' — the segments carry no usable clock (a meeting read back out of its flat
|
|
16
|
+
// text form, where the timestamps are display strings like "6:28:00 PM" that
|
|
17
|
+
// cannot be turned into durations without guessing at a date and a timezone).
|
|
18
|
+
// Buckets are equal slices of POSITION and shares are shares of characters.
|
|
19
|
+
//
|
|
20
|
+
// A caller that draws minutes on a 'text' meeting would be inventing them, which is why
|
|
21
|
+
// `basis` is part of the return rather than something to infer from a null.
|
|
22
|
+
//
|
|
23
|
+
// THE TRANSCRIPT IS UNTRUSTED CONTENT — participant-chosen display names, live captions,
|
|
24
|
+
// meeting chat. Everything here treats a speaker as an opaque label and returns it as its
|
|
25
|
+
// own field; nothing is concatenated into a sentence a prompt could read as an instruction.
|
|
26
|
+
|
|
27
|
+
/** Ordinary speech, in characters per second — ~150 words per minute. */
|
|
28
|
+
const CPS = 15;
|
|
29
|
+
/** No caption is shorter than this. Below it, rounding noise becomes "talk time". */
|
|
30
|
+
const MIN_TURN_MS = 800;
|
|
31
|
+
/** Past this, the speaker has stopped and the meeting is simply quiet. A gap longer than
|
|
32
|
+
* half a minute is silence, a screen share or a break — not somebody still holding forth. */
|
|
33
|
+
const MAX_TURN_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
/** How many speakers get their own colour before the rest fold into one bucket. Five is
|
|
36
|
+
* where a categorical palette stops being separable for colour-blind readers; past it,
|
|
37
|
+
* more hues buy nothing and cost legibility. */
|
|
38
|
+
export const SPEAKER_SLOTS = 5;
|
|
39
|
+
|
|
40
|
+
const clean = (v) => String(v ?? '').replace(/\s+/g, ' ').trim();
|
|
41
|
+
|
|
42
|
+
/** Epoch ms from whatever the segment carries, or `null` when it carries nothing usable. */
|
|
43
|
+
function clockOf(seg) {
|
|
44
|
+
const t = seg?.t ?? seg?.ts ?? null;
|
|
45
|
+
if (typeof t === 'number' && Number.isFinite(t) && t > 0) return t;
|
|
46
|
+
// An ISO string round-trips through JSON as a string; a display clock ("6:28:00 PM")
|
|
47
|
+
// does not parse and must not be coerced into today's date.
|
|
48
|
+
if (typeof t === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(t)) {
|
|
49
|
+
const ms = Date.parse(t);
|
|
50
|
+
if (Number.isFinite(ms)) return ms;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The segments as this module needs them: a label, some text, and a clock or `null`. */
|
|
56
|
+
function segmentsOf(meeting) {
|
|
57
|
+
const raw = Array.isArray(meeting) ? meeting : (meeting?.segments || []);
|
|
58
|
+
return raw
|
|
59
|
+
.map((s) => ({ speaker: clean(s?.speaker) || 'Speaker', text: String(s?.text ?? ''), t: clockOf(s) }))
|
|
60
|
+
.filter((s) => s.text.trim() || s.speaker);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** How long a caption plausibly took to say, from its own length. */
|
|
64
|
+
const spoken = (text) => Math.min(MAX_TURN_MS, Math.max(MIN_TURN_MS, (text.length / CPS) * 1000));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Each segment with a start and a duration, sorted, when the meeting has a usable clock.
|
|
68
|
+
* Returns `null` otherwise — the caller falls back to counting characters.
|
|
69
|
+
*
|
|
70
|
+
* A segment runs until the NEXT one starts, capped at `MAX_TURN_MS` so a break does not
|
|
71
|
+
* get billed to whoever spoke last, and floored by its own text so back-to-back interim
|
|
72
|
+
* captions (which can share a millisecond) still register.
|
|
73
|
+
*/
|
|
74
|
+
function timedSegments(meeting) {
|
|
75
|
+
const segs = segmentsOf(meeting).filter((s) => s.t !== null);
|
|
76
|
+
if (segs.length < 2) return null;
|
|
77
|
+
segs.sort((a, b) => a.t - b.t);
|
|
78
|
+
if (segs.at(-1).t - segs[0].t <= 0) return null; // every caption on one millisecond
|
|
79
|
+
return segs.map((s, i) => {
|
|
80
|
+
const next = segs[i + 1];
|
|
81
|
+
const gap = next ? next.t - s.t : 0;
|
|
82
|
+
const ms = gap > 0 ? Math.min(gap, MAX_TURN_MS) : spoken(s.text);
|
|
83
|
+
return { ...s, ms: Math.max(MIN_TURN_MS, ms) };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Who spoke, for how long, in one shape — the input to both the talk-time chart and the
|
|
89
|
+
* colouring of everything else.
|
|
90
|
+
*
|
|
91
|
+
* Speakers are ranked by talk time and given a SLOT in that order, and the slot is what a
|
|
92
|
+
* client turns into a colour. Ranking rather than hashing the name is deliberate: a hash
|
|
93
|
+
* is stable across meetings but collides, so two people in the same meeting can come out
|
|
94
|
+
* the same colour, which is the one thing this chart must never do. The ranking is stable
|
|
95
|
+
* for a given meeting because the speaker set is fixed — no filter removes a series here.
|
|
96
|
+
*
|
|
97
|
+
* Past `slots` speakers, the tail folds into `other` rather than taking a generated hue.
|
|
98
|
+
*/
|
|
99
|
+
export function speakerBreakdown(meeting, { slots = SPEAKER_SLOTS } = {}) {
|
|
100
|
+
const timed = timedSegments(meeting);
|
|
101
|
+
const segs = timed || segmentsOf(meeting);
|
|
102
|
+
const basis = timed ? 'clock' : 'text';
|
|
103
|
+
|
|
104
|
+
const by = new Map();
|
|
105
|
+
for (const s of segs) {
|
|
106
|
+
const prev = by.get(s.speaker) || { speaker: s.speaker, lines: 0, chars: 0, ms: 0 };
|
|
107
|
+
prev.lines += 1;
|
|
108
|
+
prev.chars += s.text.length;
|
|
109
|
+
prev.ms += s.ms || 0;
|
|
110
|
+
by.set(s.speaker, prev);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const weightOf = (s) => (basis === 'clock' ? s.ms : s.chars);
|
|
114
|
+
const total = [...by.values()].reduce((n, s) => n + weightOf(s), 0);
|
|
115
|
+
const ranked = [...by.values()].sort((a, b) => weightOf(b) - weightOf(a) || a.speaker.localeCompare(b.speaker));
|
|
116
|
+
|
|
117
|
+
const speakers = ranked.map((s, i) => ({
|
|
118
|
+
speaker: s.speaker,
|
|
119
|
+
slot: i < slots ? i : -1,
|
|
120
|
+
lines: s.lines,
|
|
121
|
+
chars: s.chars,
|
|
122
|
+
ms: basis === 'clock' ? Math.round(s.ms) : null,
|
|
123
|
+
seconds: basis === 'clock' ? Math.round(s.ms / 1000) : null,
|
|
124
|
+
share: total ? weightOf(s) / total : 0,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const tail = speakers.filter((s) => s.slot === -1);
|
|
128
|
+
return {
|
|
129
|
+
basis,
|
|
130
|
+
totalMs: basis === 'clock' ? Math.round(total) : null,
|
|
131
|
+
lines: segs.length,
|
|
132
|
+
speakers,
|
|
133
|
+
shown: speakers.filter((s) => s.slot >= 0),
|
|
134
|
+
other: tail.length
|
|
135
|
+
? {
|
|
136
|
+
speakers: tail.map((s) => s.speaker),
|
|
137
|
+
lines: tail.reduce((n, s) => n + s.lines, 0),
|
|
138
|
+
ms: basis === 'clock' ? tail.reduce((n, s) => n + s.ms, 0) : null,
|
|
139
|
+
seconds: basis === 'clock' ? Math.round(tail.reduce((n, s) => n + s.ms, 0) / 1000) : null,
|
|
140
|
+
share: tail.reduce((n, s) => n + s.share, 0),
|
|
141
|
+
}
|
|
142
|
+
: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The meeting as a row of buckets — who held the floor, across the meeting.
|
|
148
|
+
*
|
|
149
|
+
* On a clock basis the buckets are equal slices of WALL TIME between the first and last
|
|
150
|
+
* caption, so silence reads as silence and a bucket can be genuinely empty. Position
|
|
151
|
+
* buckets (the fallback) cannot show that: they space the captions evenly whatever the
|
|
152
|
+
* pace, which makes a quiet hour look like a busy one.
|
|
153
|
+
*
|
|
154
|
+
* Each bucket names its DOMINANT speaker and the full split, so a client can paint one
|
|
155
|
+
* bar per bucket and still say in a tooltip who else was in it.
|
|
156
|
+
*/
|
|
157
|
+
export function speakerTimeline(meeting, { buckets = 48 } = {}) {
|
|
158
|
+
const timed = timedSegments(meeting);
|
|
159
|
+
const segs = timed || segmentsOf(meeting);
|
|
160
|
+
if (!segs.length) return { basis: timed ? 'clock' : 'text', from: null, to: null, buckets: [] };
|
|
161
|
+
|
|
162
|
+
const n = Math.max(1, Math.floor(buckets));
|
|
163
|
+
const bins = Array.from({ length: n }, () => new Map());
|
|
164
|
+
const add = (bin, speaker, weight) => bin.set(speaker, (bin.get(speaker) || 0) + weight);
|
|
165
|
+
|
|
166
|
+
let from = null;
|
|
167
|
+
let to = null;
|
|
168
|
+
|
|
169
|
+
if (timed) {
|
|
170
|
+
from = timed[0].t;
|
|
171
|
+
to = Math.max(timed.at(-1).t + timed.at(-1).ms, from + 1);
|
|
172
|
+
const span = to - from;
|
|
173
|
+
for (const s of timed) {
|
|
174
|
+
// A long turn spans buckets; bill each one for the part of the turn inside it, or a
|
|
175
|
+
// ten-minute monologue counts once and the band under-reports a whole stretch.
|
|
176
|
+
const start = s.t;
|
|
177
|
+
const end = Math.min(to, s.t + s.ms);
|
|
178
|
+
const first = Math.min(n - 1, Math.floor(((start - from) / span) * n));
|
|
179
|
+
const last = Math.min(n - 1, Math.floor(((end - from) / span) * n));
|
|
180
|
+
for (let b = first; b <= last; b += 1) {
|
|
181
|
+
const bFrom = from + (span * b) / n;
|
|
182
|
+
const bTo = from + (span * (b + 1)) / n;
|
|
183
|
+
const overlap = Math.min(end, bTo) - Math.max(start, bFrom);
|
|
184
|
+
if (overlap > 0) add(bins[b], s.speaker, overlap);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const per = segs.length / n;
|
|
189
|
+
segs.forEach((s, i) => add(bins[Math.min(n - 1, Math.floor(i / per))], s.speaker, Math.max(1, s.text.length)));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const rows = bins.map((bin, i) => {
|
|
193
|
+
const by = [...bin.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
194
|
+
const weight = by.reduce((sum, [, w]) => sum + w, 0);
|
|
195
|
+
const span = to === null ? null : (to - from) / n;
|
|
196
|
+
return {
|
|
197
|
+
speaker: by[0]?.[0] || '',
|
|
198
|
+
weight,
|
|
199
|
+
by: by.map(([speaker, w]) => ({ speaker, weight: w, share: weight ? w / weight : 0 })),
|
|
200
|
+
from: span === null ? null : Math.round(from + span * i),
|
|
201
|
+
to: span === null ? null : Math.round(from + span * (i + 1)),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const max = Math.max(...rows.map((r) => r.weight), 1);
|
|
206
|
+
return { basis: timed ? 'clock' : 'text', from, to, buckets: rows.map((r) => ({ ...r, weight: r.weight / max })) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Who spoke, and how much — the character-share form kept for callers that only ever had
|
|
211
|
+
* text. `speakerBreakdown` is the fuller answer; this stays because the desktop's meeting
|
|
212
|
+
* pane and `meeting-text.js` are built on it.
|
|
213
|
+
*/
|
|
214
|
+
export function speakerStats(meeting) {
|
|
215
|
+
const by = new Map();
|
|
216
|
+
for (const s of meeting?.segments || []) {
|
|
217
|
+
const prev = by.get(s.speaker) || { speaker: s.speaker, lines: 0, chars: 0 };
|
|
218
|
+
prev.lines += 1;
|
|
219
|
+
prev.chars += s.text.length;
|
|
220
|
+
by.set(s.speaker, prev);
|
|
221
|
+
}
|
|
222
|
+
const total = [...by.values()].reduce((n, s) => n + s.chars, 0) || 1;
|
|
223
|
+
return [...by.values()]
|
|
224
|
+
.map((s) => ({ ...s, share: s.chars / total }))
|
|
225
|
+
.sort((a, b) => b.chars - a.chars);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The meeting as one ribbon of who-spoke-when, bucketed by POSITION.
|
|
230
|
+
*
|
|
231
|
+
* Kept as-is for the desktop pane, which reads meetings back out of their flat text form
|
|
232
|
+
* and so has no clock to bucket by. New callers want `speakerTimeline`, which uses the
|
|
233
|
+
* clock when there is one and falls back to exactly this when there is not.
|
|
234
|
+
*/
|
|
235
|
+
export function densityRibbon(meeting, buckets = 40) {
|
|
236
|
+
const segs = meeting?.segments || [];
|
|
237
|
+
if (!segs.length) return [];
|
|
238
|
+
const out = [];
|
|
239
|
+
const per = segs.length / buckets;
|
|
240
|
+
for (let b = 0; b < buckets; b += 1) {
|
|
241
|
+
const from = Math.floor(b * per);
|
|
242
|
+
const to = Math.max(from + 1, Math.floor((b + 1) * per));
|
|
243
|
+
const slice = segs.slice(from, to);
|
|
244
|
+
if (!slice.length) { out.push({ speaker: '', weight: 0 }); continue; }
|
|
245
|
+
const by = new Map();
|
|
246
|
+
for (const s of slice) by.set(s.speaker, (by.get(s.speaker) || 0) + s.text.length);
|
|
247
|
+
const [speaker, chars] = [...by.entries()].sort((a, b2) => b2[1] - a[1])[0];
|
|
248
|
+
out.push({ speaker, weight: chars });
|
|
249
|
+
}
|
|
250
|
+
const max = Math.max(...out.map((o) => o.weight), 1);
|
|
251
|
+
return out.map((o) => ({ ...o, weight: o.weight / max }));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** `41 min`, `1h 12m`, `48s` — a duration a person reads without converting it. */
|
|
255
|
+
export function formatTalkTime(ms) {
|
|
256
|
+
if (!Number.isFinite(ms) || ms <= 0) return '';
|
|
257
|
+
const total = Math.round(ms / 1000);
|
|
258
|
+
if (total < 60) return `${total}s`;
|
|
259
|
+
const mins = Math.round(total / 60);
|
|
260
|
+
if (mins < 60) return `${mins} min`;
|
|
261
|
+
return `${Math.floor(mins / 60)}h ${String(mins % 60).padStart(2, '0')}m`;
|
|
262
|
+
}
|
package/meeting-text.js
CHANGED
|
@@ -87,49 +87,8 @@ export function parseMeetingText(text) {
|
|
|
87
87
|
return out;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
*/
|
|
96
|
-
export function speakerStats(meeting) {
|
|
97
|
-
const by = new Map();
|
|
98
|
-
for (const s of meeting?.segments || []) {
|
|
99
|
-
const prev = by.get(s.speaker) || { speaker: s.speaker, lines: 0, chars: 0 };
|
|
100
|
-
prev.lines += 1;
|
|
101
|
-
prev.chars += s.text.length;
|
|
102
|
-
by.set(s.speaker, prev);
|
|
103
|
-
}
|
|
104
|
-
const total = [...by.values()].reduce((n, s) => n + s.chars, 0) || 1;
|
|
105
|
-
return [...by.values()]
|
|
106
|
-
.map((s) => ({ ...s, share: s.chars / total }))
|
|
107
|
-
.sort((a, b) => b.chars - a.chars);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The meeting as one ribbon of who-spoke-when.
|
|
112
|
-
*
|
|
113
|
-
* `buckets` slices the transcript by POSITION rather than by clock: the timestamps are
|
|
114
|
-
* display strings from the capture ("6:28:00 PM"), not durations, and parsing them into a
|
|
115
|
-
* timeline would be guessing at a date, a timezone and whether the meeting crossed midnight.
|
|
116
|
-
* Position is honest about being an approximation of pace.
|
|
117
|
-
*/
|
|
118
|
-
export function densityRibbon(meeting, buckets = 40) {
|
|
119
|
-
const segs = meeting?.segments || [];
|
|
120
|
-
if (!segs.length) return [];
|
|
121
|
-
const out = [];
|
|
122
|
-
const per = segs.length / buckets;
|
|
123
|
-
for (let b = 0; b < buckets; b += 1) {
|
|
124
|
-
const from = Math.floor(b * per);
|
|
125
|
-
const to = Math.max(from + 1, Math.floor((b + 1) * per));
|
|
126
|
-
const slice = segs.slice(from, to);
|
|
127
|
-
if (!slice.length) { out.push({ speaker: '', weight: 0 }); continue; }
|
|
128
|
-
const by = new Map();
|
|
129
|
-
for (const s of slice) by.set(s.speaker, (by.get(s.speaker) || 0) + s.text.length);
|
|
130
|
-
const [speaker, chars] = [...by.entries()].sort((a, b2) => b2[1] - a[1])[0];
|
|
131
|
-
out.push({ speaker, weight: chars });
|
|
132
|
-
}
|
|
133
|
-
const max = Math.max(...out.map((o) => o.weight), 1);
|
|
134
|
-
return out.map((o) => ({ ...o, weight: o.weight / max }));
|
|
135
|
-
}
|
|
90
|
+
// The shape analysis — who spoke, when, for how long — lives in `meeting-shape.js` so the
|
|
91
|
+
// clients that never read the text form can have it without the parser. Re-exported here
|
|
92
|
+
// because these two names were part of this module before that split, and the desktop
|
|
93
|
+
// meeting pane imports them from this path.
|
|
94
|
+
export { speakerStats, densityRibbon } from './meeting-shape.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The canonical ChatPanel event-log and capability contracts
|
|
3
|
+
"version": "0.64.0",
|
|
4
|
+
"description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -33,8 +33,10 @@
|
|
|
33
33
|
"./mcp-errors.js": "./mcp-errors.js",
|
|
34
34
|
"./media-transcript.js": "./media-transcript.js",
|
|
35
35
|
"./meeting-analyzers.js": "./meeting-analyzers.js",
|
|
36
|
+
"./meeting-shape.js": "./meeting-shape.js",
|
|
36
37
|
"./meeting-text.js": "./meeting-text.js",
|
|
37
38
|
"./memory.js": "./memory.js",
|
|
39
|
+
"./model-candidates.js": "./model-candidates.js",
|
|
38
40
|
"./model-picker.js": "./model-picker.js",
|
|
39
41
|
"./note-actions.js": "./note-actions.js",
|
|
40
42
|
"./note-graph.js": "./note-graph.js",
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
"./ref.js": "./ref.js",
|
|
54
56
|
"./registry.js": "./registry.js",
|
|
55
57
|
"./route-graph.js": "./route-graph.js",
|
|
58
|
+
"./route-strategies.js": "./route-strategies.js",
|
|
56
59
|
"./router.js": "./router.js",
|
|
57
60
|
"./rrf.js": "./rrf.js",
|
|
58
61
|
"./rules.js": "./rules.js",
|
|
@@ -82,11 +85,10 @@
|
|
|
82
85
|
"./vault.js": "./vault.js",
|
|
83
86
|
"./view.js": "./view.js",
|
|
84
87
|
"./voice-intents.js": "./voice-intents.js",
|
|
88
|
+
"./voice-speaker.js": "./voice-speaker.js",
|
|
85
89
|
"./weather.js": "./weather.js",
|
|
86
90
|
"./web-search.js": "./web-search.js",
|
|
87
|
-
"./widget.js": "./widget.js"
|
|
88
|
-
"./model-candidates.js": "./model-candidates.js",
|
|
89
|
-
"./route-strategies.js": "./route-strategies.js"
|
|
91
|
+
"./widget.js": "./widget.js"
|
|
90
92
|
},
|
|
91
93
|
"files": [
|
|
92
94
|
"LICENSE",
|
|
@@ -118,6 +120,7 @@
|
|
|
118
120
|
"mcp-errors.js",
|
|
119
121
|
"media-transcript.js",
|
|
120
122
|
"meeting-analyzers.js",
|
|
123
|
+
"meeting-shape.js",
|
|
121
124
|
"meeting-text.js",
|
|
122
125
|
"memory.js",
|
|
123
126
|
"model-picker.js",
|