@wirecat/cli-core 0.388.0 → 0.390.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/CHANGELOG.md +14 -1
- package/dist/cli/context.js +1 -0
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.js +10 -0
- package/dist/meetings/index.d.ts +1 -0
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/stats.d.ts +59 -0
- package/dist/meetings/stats.js +95 -0
- package/dist/runs/recording.d.ts +5 -0
- package/dist/runs/recording.js +6 -3
- package/dist/runs/scope.d.ts +3 -1
- package/dist/runs/scope.js +2 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/meeting-stats.d.ts +13 -0
- package/dist/services/meeting-stats.js +12 -0
- package/dist/store/sqlite/meeting-stats.d.ts +7 -0
- package/dist/store/sqlite/meeting-stats.js +55 -0
- package/dist/store/sqlite/meetings.d.ts +2 -2
- package/dist/store/sqlite/meetings.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,20 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 0.389.0 — 22.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`readMeetingStats`: talk time per speaker and meetings per week.** Over the meetings search would read, with the
|
|
13
|
+
same filters: per speaker (a linked person, else the identity, else the transcript's name) lines, words, talk time,
|
|
14
|
+
meetings and share of talk; per week (Monday in a chosen zone) meetings and talk time. One transcript per meeting,
|
|
15
|
+
so a local transcript beside the provider's does not count twice. `meetings.meetingStats` on the store and
|
|
16
|
+
`aggregateMeetingStats` for rows a host already has.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **A recorded one-shot command's `meta.run_id` names the run it kept**, so `runs show <run_id>` finds it. It named
|
|
21
|
+
the process's own run, which is never kept.
|
|
9
22
|
|
|
10
23
|
## 0.388.0 — 22.10.2026
|
|
11
24
|
|
package/dist/cli/context.js
CHANGED
|
@@ -2,9 +2,9 @@ import { Command } from "commander";
|
|
|
2
2
|
import type { Messenger } from "./context.js";
|
|
3
3
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
4
4
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
5
|
+
error?: undefined;
|
|
5
6
|
path: string;
|
|
6
7
|
exists: boolean;
|
|
7
|
-
error?: undefined;
|
|
8
8
|
} | {
|
|
9
9
|
path: string;
|
|
10
10
|
exists: boolean;
|
package/dist/cli/program.js
CHANGED
|
@@ -10,6 +10,7 @@ import { isCliFailure, isCommanderFailure } from "../errors/failures.js";
|
|
|
10
10
|
import { failureContext } from "../errors/reasons.js";
|
|
11
11
|
import { envName, readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
|
|
12
12
|
import { recorded, wasSettled } from "../runs/recording.js";
|
|
13
|
+
import { processRun, useAsProcessRun } from "../runs/scope.js";
|
|
13
14
|
import { keyForCommand } from "../sends/permissions.js";
|
|
14
15
|
import { logParsedArgv, logVersion } from "./argv-log.js";
|
|
15
16
|
import { commandPathOf } from "./command-contract.js";
|
|
@@ -104,6 +105,15 @@ const RELAY_MARGIN_MS = 10_000;
|
|
|
104
105
|
* message: it tells a script nothing it can branch on, and exits 1 for every kind of failure alike.
|
|
105
106
|
*/
|
|
106
107
|
export const run = async (argv, definition, options = {}) => {
|
|
108
|
+
const outer = processRun();
|
|
109
|
+
try {
|
|
110
|
+
return await runOnce(argv, definition, options);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
useAsProcessRun(outer);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const runOnce = async (argv, definition, options) => {
|
|
107
117
|
const streams = options.streams ?? processStreams;
|
|
108
118
|
if (!options.streams)
|
|
109
119
|
quietBrokenPipes();
|
package/dist/meetings/index.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export { createMeetingSeriesInventoryReader } from "./series-inventory.js";
|
|
|
40
40
|
export type { MeetingSeriesCandidate, MeetingSeriesInventoryPage, MeetingSeriesInventoryPort, MeetingSeriesInventoryRead, MeetingSeriesInventoryRequest, MeetingSeriesInventoryResult, } from "./series-inventory-types.js";
|
|
41
41
|
export { validateMeetingSeriesInventoryCursor } from "./series-inventory-validation.js";
|
|
42
42
|
export type { MeetingSource, SourceParticipant } from "./source.js";
|
|
43
|
+
export { aggregateMeetingStats, type MeetingSpeakerStats, type MeetingStats, type MeetingStatsInput, type MeetingStatsRow, type MeetingStatsSelection, type MeetingStatsStore, type MeetingWeekStats, } from "./stats.js";
|
|
43
44
|
export type { ChatInput, ContractCase, EventCandidateFilter, IdentityInput, MeetingDetails, MeetingFilter, MeetingHitSource, MeetingInput, MeetingSave, MeetingStore, NewRecord, ParticipantInput, SearchHit, TranscriptInput, TranscriptRowInput, } from "./store.js";
|
|
44
45
|
export type { MeetingTranscriptReceiptStore, MeetingTranscriptStore, TranscriptAppend, TranscriptAppendReceipt, UnlinkedTranscriptInput, } from "./transcript-store.js";
|
|
45
46
|
export { parseTranscriptText, TranscriptTextError, type TranscriptTextHeader } from "./transcript-text.js";
|
package/dist/meetings/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export { validateMeetingHitSearch } from "./search-validation.js";
|
|
|
28
28
|
export { validateMeetingSeriesHistory, } from "./series-history.js";
|
|
29
29
|
export { createMeetingSeriesInventoryReader } from "./series-inventory.js";
|
|
30
30
|
export { validateMeetingSeriesInventoryCursor } from "./series-inventory-validation.js";
|
|
31
|
+
export { aggregateMeetingStats, } from "./stats.js";
|
|
31
32
|
export { parseTranscriptText, TranscriptTextError } from "./transcript-text.js";
|
|
32
33
|
export { transcribeMeeting, } from "./transcription.js";
|
|
33
34
|
export { createUpcomingMeetingReader } from "./upcoming.js";
|
|
@@ -38,7 +38,6 @@ export declare const meetingTools: ({
|
|
|
38
38
|
max_read_bytes?: undefined;
|
|
39
39
|
cursor?: undefined;
|
|
40
40
|
} | {
|
|
41
|
-
limit?: undefined;
|
|
42
41
|
event_id?: undefined;
|
|
43
42
|
series_id?: undefined;
|
|
44
43
|
page?: undefined;
|
|
@@ -54,11 +53,11 @@ export declare const meetingTools: ({
|
|
|
54
53
|
format?: undefined;
|
|
55
54
|
transcript_id?: undefined;
|
|
56
55
|
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;
|
|
62
61
|
event_id?: undefined;
|
|
63
62
|
series_id?: undefined;
|
|
64
63
|
page?: undefined;
|
|
@@ -76,11 +75,11 @@ export declare const meetingTools: ({
|
|
|
76
75
|
format?: undefined;
|
|
77
76
|
transcript_id?: undefined;
|
|
78
77
|
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;
|
|
84
83
|
event_id?: undefined;
|
|
85
84
|
series_id?: undefined;
|
|
86
85
|
page?: undefined;
|
|
@@ -102,6 +101,7 @@ export declare const meetingTools: ({
|
|
|
102
101
|
minimum: number;
|
|
103
102
|
};
|
|
104
103
|
query?: undefined;
|
|
104
|
+
limit?: undefined;
|
|
105
105
|
scan_limit?: undefined;
|
|
106
106
|
max_read_bytes?: undefined;
|
|
107
107
|
cursor?: undefined;
|
|
@@ -188,7 +188,6 @@ export declare const meetingTools: ({
|
|
|
188
188
|
max_read_bytes?: undefined;
|
|
189
189
|
cursor?: undefined;
|
|
190
190
|
} | {
|
|
191
|
-
limit?: undefined;
|
|
192
191
|
event_id?: undefined;
|
|
193
192
|
series_id?: undefined;
|
|
194
193
|
page?: undefined;
|
|
@@ -204,11 +203,11 @@ export declare const meetingTools: ({
|
|
|
204
203
|
format?: undefined;
|
|
205
204
|
transcript_id?: undefined;
|
|
206
205
|
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;
|
|
212
211
|
event_id?: undefined;
|
|
213
212
|
series_id?: undefined;
|
|
214
213
|
page?: undefined;
|
|
@@ -226,11 +225,11 @@ export declare const meetingTools: ({
|
|
|
226
225
|
format?: undefined;
|
|
227
226
|
transcript_id?: undefined;
|
|
228
227
|
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;
|
|
234
233
|
event_id?: undefined;
|
|
235
234
|
series_id?: undefined;
|
|
236
235
|
page?: undefined;
|
|
@@ -252,6 +251,7 @@ export declare const meetingTools: ({
|
|
|
252
251
|
minimum: number;
|
|
253
252
|
};
|
|
254
253
|
query?: undefined;
|
|
254
|
+
limit?: undefined;
|
|
255
255
|
scan_limit?: undefined;
|
|
256
256
|
max_read_bytes?: undefined;
|
|
257
257
|
cursor?: undefined;
|
|
@@ -338,7 +338,6 @@ export declare const meetingTools: ({
|
|
|
338
338
|
max_read_bytes?: undefined;
|
|
339
339
|
cursor?: undefined;
|
|
340
340
|
} | {
|
|
341
|
-
limit?: undefined;
|
|
342
341
|
event_id?: undefined;
|
|
343
342
|
series_id?: undefined;
|
|
344
343
|
page?: undefined;
|
|
@@ -354,11 +353,11 @@ export declare const meetingTools: ({
|
|
|
354
353
|
format?: undefined;
|
|
355
354
|
transcript_id?: undefined;
|
|
356
355
|
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;
|
|
362
361
|
event_id?: undefined;
|
|
363
362
|
series_id?: undefined;
|
|
364
363
|
page?: undefined;
|
|
@@ -376,11 +375,11 @@ export declare const meetingTools: ({
|
|
|
376
375
|
format?: undefined;
|
|
377
376
|
transcript_id?: undefined;
|
|
378
377
|
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;
|
|
384
383
|
event_id?: undefined;
|
|
385
384
|
series_id?: undefined;
|
|
386
385
|
page?: undefined;
|
|
@@ -402,6 +401,7 @@ export declare const meetingTools: ({
|
|
|
402
401
|
minimum: number;
|
|
403
402
|
};
|
|
404
403
|
query?: undefined;
|
|
404
|
+
limit?: undefined;
|
|
405
405
|
scan_limit?: undefined;
|
|
406
406
|
max_read_bytes?: undefined;
|
|
407
407
|
cursor?: undefined;
|
|
@@ -488,7 +488,6 @@ export declare const meetingTools: ({
|
|
|
488
488
|
max_read_bytes?: undefined;
|
|
489
489
|
cursor?: undefined;
|
|
490
490
|
} | {
|
|
491
|
-
limit?: undefined;
|
|
492
491
|
event_id?: undefined;
|
|
493
492
|
series_id?: undefined;
|
|
494
493
|
page?: undefined;
|
|
@@ -504,11 +503,11 @@ export declare const meetingTools: ({
|
|
|
504
503
|
format?: undefined;
|
|
505
504
|
transcript_id?: undefined;
|
|
506
505
|
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;
|
|
512
511
|
event_id?: undefined;
|
|
513
512
|
series_id?: undefined;
|
|
514
513
|
page?: undefined;
|
|
@@ -526,11 +525,11 @@ export declare const meetingTools: ({
|
|
|
526
525
|
format?: undefined;
|
|
527
526
|
transcript_id?: undefined;
|
|
528
527
|
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;
|
|
534
533
|
event_id?: undefined;
|
|
535
534
|
series_id?: undefined;
|
|
536
535
|
page?: undefined;
|
|
@@ -552,6 +551,7 @@ export declare const meetingTools: ({
|
|
|
552
551
|
minimum: number;
|
|
553
552
|
};
|
|
554
553
|
query?: undefined;
|
|
554
|
+
limit?: undefined;
|
|
555
555
|
scan_limit?: undefined;
|
|
556
556
|
max_read_bytes?: undefined;
|
|
557
557
|
cursor?: undefined;
|
|
@@ -638,7 +638,6 @@ export declare const meetingTools: ({
|
|
|
638
638
|
max_read_bytes?: undefined;
|
|
639
639
|
cursor?: undefined;
|
|
640
640
|
} | {
|
|
641
|
-
limit?: undefined;
|
|
642
641
|
event_id?: undefined;
|
|
643
642
|
series_id?: undefined;
|
|
644
643
|
page?: undefined;
|
|
@@ -654,11 +653,11 @@ export declare const meetingTools: ({
|
|
|
654
653
|
format?: undefined;
|
|
655
654
|
transcript_id?: undefined;
|
|
656
655
|
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;
|
|
662
661
|
event_id?: undefined;
|
|
663
662
|
series_id?: undefined;
|
|
664
663
|
page?: undefined;
|
|
@@ -676,11 +675,11 @@ export declare const meetingTools: ({
|
|
|
676
675
|
format?: undefined;
|
|
677
676
|
transcript_id?: undefined;
|
|
678
677
|
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;
|
|
684
683
|
event_id?: undefined;
|
|
685
684
|
series_id?: undefined;
|
|
686
685
|
page?: undefined;
|
|
@@ -702,6 +701,7 @@ export declare const meetingTools: ({
|
|
|
702
701
|
minimum: number;
|
|
703
702
|
};
|
|
704
703
|
query?: undefined;
|
|
704
|
+
limit?: undefined;
|
|
705
705
|
scan_limit?: undefined;
|
|
706
706
|
max_read_bytes?: undefined;
|
|
707
707
|
cursor?: undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MeetingSearchOptions } from "./search-store.js";
|
|
2
|
+
/** The meetings the numbers cover: the same filters as meeting search. */
|
|
3
|
+
export type MeetingStatsSelection = Pick<MeetingSearchOptions, "accountId" | "meetingId" | "eventId" | "meetingSeriesId" | "since" | "until" | "participantIdentityId" | "personId" | "projectId" | "tagId" | "signal">;
|
|
4
|
+
export interface MeetingStatsInput extends MeetingStatsSelection {
|
|
5
|
+
/** The zone a week starts on Monday in; UTC when not given. */
|
|
6
|
+
timeZone?: string;
|
|
7
|
+
/** Most transcript lines to read (default 500000); more is an error, never a silent cut. */
|
|
8
|
+
maxRows?: number;
|
|
9
|
+
}
|
|
10
|
+
/** One transcript line as the numbers need it: who said it, when, for how long, in how many words. */
|
|
11
|
+
export interface MeetingStatsRow {
|
|
12
|
+
meetingId: number;
|
|
13
|
+
startedAt: number | null;
|
|
14
|
+
startMs: number;
|
|
15
|
+
endMs: number;
|
|
16
|
+
words: number;
|
|
17
|
+
personId: number | null;
|
|
18
|
+
personName: string | null;
|
|
19
|
+
identityId: number | null;
|
|
20
|
+
identityName: string | null;
|
|
21
|
+
speakerName: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface MeetingSpeakerStats {
|
|
24
|
+
/** `person:<id>`, `identity:<id>`, `name:<lowercased name>` or `unknown`. */
|
|
25
|
+
key: string;
|
|
26
|
+
name: string | null;
|
|
27
|
+
personId: number | null;
|
|
28
|
+
identityId: number | null;
|
|
29
|
+
meetings: number;
|
|
30
|
+
lines: number;
|
|
31
|
+
words: number;
|
|
32
|
+
talkMs: number;
|
|
33
|
+
/** This speaker's part of all talk time in the selection, 0–1. */
|
|
34
|
+
share: number;
|
|
35
|
+
}
|
|
36
|
+
export interface MeetingWeekStats {
|
|
37
|
+
/** The Monday the week starts on, in the chosen zone: `2026-10-19`. */
|
|
38
|
+
weekStart: string;
|
|
39
|
+
meetings: number;
|
|
40
|
+
talkMs: number;
|
|
41
|
+
}
|
|
42
|
+
export interface MeetingStats {
|
|
43
|
+
timeZone: string;
|
|
44
|
+
meetings: number;
|
|
45
|
+
lines: number;
|
|
46
|
+
words: number;
|
|
47
|
+
talkMs: number;
|
|
48
|
+
speakers: MeetingSpeakerStats[];
|
|
49
|
+
weeks: MeetingWeekStats[];
|
|
50
|
+
}
|
|
51
|
+
export interface MeetingStatsStore {
|
|
52
|
+
meetingStats(input: MeetingStatsInput): Promise<MeetingStats>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Talk time, lines and words per speaker, and meetings and talk time per week. A speaker is a linked person when the
|
|
56
|
+
* line's participant has one, else the participant's identity, else the name the transcript gives; lines with none
|
|
57
|
+
* are `unknown`. Talk time is each line's end less its start, so overlapping speech counts for everyone speaking.
|
|
58
|
+
*/
|
|
59
|
+
export declare const aggregateMeetingStats: (rows: readonly MeetingStatsRow[], timeZone?: string) => MeetingStats;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
const zoneOf = (timeZone) => {
|
|
3
|
+
const zone = timeZone ?? "UTC";
|
|
4
|
+
try {
|
|
5
|
+
new Intl.DateTimeFormat("en", { timeZone: zone });
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new CliError("validation_error", "expected an IANA time zone such as Europe/Madrid");
|
|
9
|
+
}
|
|
10
|
+
return zone;
|
|
11
|
+
};
|
|
12
|
+
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
13
|
+
const weekStart = (at, format) => {
|
|
14
|
+
const parts = Object.fromEntries(format.formatToParts(new Date(at)).map((part) => [part.type, part.value]));
|
|
15
|
+
const day = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day));
|
|
16
|
+
return new Date(day - DAYS.indexOf(parts.weekday ?? "Mon") * 86_400_000).toISOString().slice(0, 10);
|
|
17
|
+
};
|
|
18
|
+
/** Lines with no speaker at all; a constant, since a `key: "…"` literal reads as a permission key to the key test. */
|
|
19
|
+
const UNKNOWN_SPEAKER = "unknown";
|
|
20
|
+
const speakerOf = (row) => row.personId !== null
|
|
21
|
+
? { key: `person:${row.personId}`, name: row.personName ?? row.identityName ?? row.speakerName }
|
|
22
|
+
: row.identityId !== null
|
|
23
|
+
? { key: `identity:${row.identityId}`, name: row.identityName ?? row.speakerName }
|
|
24
|
+
: row.speakerName?.trim()
|
|
25
|
+
? { key: `name:${row.speakerName.trim().toLowerCase()}`, name: row.speakerName.trim() }
|
|
26
|
+
: { key: UNKNOWN_SPEAKER, name: null };
|
|
27
|
+
/**
|
|
28
|
+
* Talk time, lines and words per speaker, and meetings and talk time per week. A speaker is a linked person when the
|
|
29
|
+
* line's participant has one, else the participant's identity, else the name the transcript gives; lines with none
|
|
30
|
+
* are `unknown`. Talk time is each line's end less its start, so overlapping speech counts for everyone speaking.
|
|
31
|
+
*/
|
|
32
|
+
export const aggregateMeetingStats = (rows, timeZone) => {
|
|
33
|
+
const zone = zoneOf(timeZone);
|
|
34
|
+
const format = new Intl.DateTimeFormat("en-US", {
|
|
35
|
+
timeZone: zone,
|
|
36
|
+
year: "numeric",
|
|
37
|
+
month: "2-digit",
|
|
38
|
+
day: "2-digit",
|
|
39
|
+
weekday: "short",
|
|
40
|
+
});
|
|
41
|
+
const speakers = new Map();
|
|
42
|
+
const weeks = new Map();
|
|
43
|
+
const meetings = new Set();
|
|
44
|
+
let lines = 0;
|
|
45
|
+
let words = 0;
|
|
46
|
+
let talkMs = 0;
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
const talk = Math.max(0, row.endMs - row.startMs);
|
|
49
|
+
meetings.add(row.meetingId);
|
|
50
|
+
lines += 1;
|
|
51
|
+
words += row.words;
|
|
52
|
+
talkMs += talk;
|
|
53
|
+
const who = speakerOf(row);
|
|
54
|
+
const held = speakers.get(who.key) ?? {
|
|
55
|
+
key: who.key,
|
|
56
|
+
name: who.name ?? null,
|
|
57
|
+
personId: row.personId,
|
|
58
|
+
identityId: row.personId === null ? row.identityId : null,
|
|
59
|
+
meetings: 0,
|
|
60
|
+
lines: 0,
|
|
61
|
+
words: 0,
|
|
62
|
+
talkMs: 0,
|
|
63
|
+
share: 0,
|
|
64
|
+
seen: new Set(),
|
|
65
|
+
};
|
|
66
|
+
held.seen.add(row.meetingId);
|
|
67
|
+
held.lines += 1;
|
|
68
|
+
held.words += row.words;
|
|
69
|
+
held.talkMs += talk;
|
|
70
|
+
speakers.set(who.key, held);
|
|
71
|
+
if (row.startedAt !== null) {
|
|
72
|
+
const week = weeks.get(weekStart(row.startedAt, format)) ?? { meetings: new Set(), talkMs: 0 };
|
|
73
|
+
week.meetings.add(row.meetingId);
|
|
74
|
+
week.talkMs += talk;
|
|
75
|
+
weeks.set(weekStart(row.startedAt, format), week);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
timeZone: zone,
|
|
80
|
+
meetings: meetings.size,
|
|
81
|
+
lines,
|
|
82
|
+
words,
|
|
83
|
+
talkMs,
|
|
84
|
+
speakers: [...speakers.values()]
|
|
85
|
+
.map(({ seen, ...speaker }) => ({
|
|
86
|
+
...speaker,
|
|
87
|
+
meetings: seen.size,
|
|
88
|
+
share: talkMs === 0 ? 0 : Math.round((speaker.talkMs / talkMs) * 1000) / 1000,
|
|
89
|
+
}))
|
|
90
|
+
.sort((a, b) => b.talkMs - a.talkMs || a.key.localeCompare(b.key)),
|
|
91
|
+
weeks: [...weeks.entries()]
|
|
92
|
+
.map(([start, week]) => ({ weekStart: start, meetings: week.meetings.size, talkMs: week.talkMs }))
|
|
93
|
+
.sort((a, b) => a.weekStart.localeCompare(b.weekStart)),
|
|
94
|
+
};
|
|
95
|
+
};
|
package/dist/runs/recording.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export interface RecordingOptions {
|
|
|
19
19
|
now?: () => Date;
|
|
20
20
|
/** The caller's trace; otherwise the enclosing run's, otherwise `TRACEPARENT` from `env`. */
|
|
21
21
|
traceId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* A one-shot command prints its answer after the run ends, outside `inRun`: naming the run as the process's lets
|
|
24
|
+
* that answer's `meta.run_id` name the run `runs show` finds. Never in a server, where runs overlap.
|
|
25
|
+
*/
|
|
26
|
+
processRun?: boolean;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* Runs one command with the diagnostics turned on, and **finalizes on every path**.
|
package/dist/runs/recording.js
CHANGED
|
@@ -4,7 +4,7 @@ import { providerErrorKey, renderEvent } from "./events.js";
|
|
|
4
4
|
import { DIAGNOSTIC_LOG_EVENTS } from "./log-events.js";
|
|
5
5
|
import { logAttributes } from "./log-keys.js";
|
|
6
6
|
import { newRunId, runsDirFor, startRun } from "./run.js";
|
|
7
|
-
import { enclosingTraceId, inRun, traceOf } from "./scope.js";
|
|
7
|
+
import { enclosingTraceId, inRun, traceOf, useAsProcessRun } from "./scope.js";
|
|
8
8
|
/**
|
|
9
9
|
* Runs one command with the diagnostics turned on, and **finalizes on every path**.
|
|
10
10
|
*
|
|
@@ -15,13 +15,16 @@ import { enclosingTraceId, inRun, traceOf } from "./scope.js";
|
|
|
15
15
|
export const recorded = async (options, body) => {
|
|
16
16
|
const recording = startRecording(options);
|
|
17
17
|
try {
|
|
18
|
-
const
|
|
18
|
+
const scope = {
|
|
19
19
|
runId: recording.runId,
|
|
20
20
|
command: options.command,
|
|
21
21
|
startedAt: Date.now(),
|
|
22
22
|
profile: options.profile,
|
|
23
23
|
...(recording.traceId ? { traceId: recording.traceId } : {}),
|
|
24
|
-
}
|
|
24
|
+
};
|
|
25
|
+
if (options.processRun)
|
|
26
|
+
useAsProcessRun(scope);
|
|
27
|
+
const answer = await inRun(scope, () => body(recording.events));
|
|
25
28
|
await recording.succeed(answer);
|
|
26
29
|
return answer;
|
|
27
30
|
}
|
package/dist/runs/scope.d.ts
CHANGED
|
@@ -23,7 +23,9 @@ export declare const typedCommand: (tool: string, words: string, profile?: strin
|
|
|
23
23
|
* For a one-shot command whose action runs outside `inRun` — max's `max bot`, recorded from commander's hooks, which
|
|
24
24
|
* cannot wrap the action — so its answers' `meta.run_id` names the run `runs show` finds. Never in a server.
|
|
25
25
|
*/
|
|
26
|
-
export declare const useAsProcessRun: (scope: RunScope) => void;
|
|
26
|
+
export declare const useAsProcessRun: (scope: RunScope | undefined) => void;
|
|
27
|
+
/** The run `useAsProcessRun` named, if any: a command run in-process puts it back when it ends. */
|
|
28
|
+
export declare const processRun: () => RunScope | undefined;
|
|
27
29
|
/** The trace of the run this one is opened inside — an MCP call's command keeps its caller's trace. */
|
|
28
30
|
export declare const enclosingTraceId: () => string | undefined;
|
|
29
31
|
/**
|
package/dist/runs/scope.js
CHANGED
|
@@ -33,6 +33,8 @@ export const typedCommand = (tool, words, profile = currentRun().profile) => [to
|
|
|
33
33
|
export const useAsProcessRun = (scope) => {
|
|
34
34
|
processScope = scope;
|
|
35
35
|
};
|
|
36
|
+
/** The run `useAsProcessRun` named, if any: a command run in-process puts it back when it ends. */
|
|
37
|
+
export const processRun = () => processScope;
|
|
36
38
|
/** The trace of the run this one is opened inside — an MCP call's command keeps its caller's trace. */
|
|
37
39
|
export const enclosingTraceId = () => scopes.getStore()?.traceId;
|
|
38
40
|
/**
|
package/dist/services/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export { readMail } from "./mail-search.js";
|
|
|
67
67
|
export { type MeetingHitSearchRequest, refreshMeetingIndex, searchMeetingHits } from "./meeting-hit-search.js";
|
|
68
68
|
export { createMeetingProjectCandidateReader } from "./meeting-project-candidates.js";
|
|
69
69
|
export { readMeetingProjectDiscovery } from "./meeting-project-discovery.js";
|
|
70
|
+
export { type MeetingStatsRequest, readMeetingStats } from "./meeting-stats.js";
|
|
70
71
|
export { chatIdIn, storedChatId } from "./message-resolution.js";
|
|
71
72
|
export type { AroundWindow, FoundMessage, ListWindow, MessagesService, MessageTarget, Pinned, Reacted, SearchFound, SearchQuery, SendRequest, Starred, } from "./messages.js";
|
|
72
73
|
export { DELETE_AT_ONCE, messagesService } from "./messages.js";
|
package/dist/services/index.js
CHANGED
|
@@ -48,6 +48,7 @@ export { readMail } from "./mail-search.js";
|
|
|
48
48
|
export { refreshMeetingIndex, searchMeetingHits } from "./meeting-hit-search.js";
|
|
49
49
|
export { createMeetingProjectCandidateReader } from "./meeting-project-candidates.js";
|
|
50
50
|
export { readMeetingProjectDiscovery } from "./meeting-project-discovery.js";
|
|
51
|
+
export { readMeetingStats } from "./meeting-stats.js";
|
|
51
52
|
export { chatIdIn, storedChatId } from "./message-resolution.js";
|
|
52
53
|
export { DELETE_AT_ONCE, messagesService } from "./messages.js";
|
|
53
54
|
export { searchStore } from "./messages-archive-search.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MeetingHitSearchRequest, MeetingStats } from "../meetings/index.js";
|
|
2
|
+
import type { MessageStore } from "../store/store.js";
|
|
3
|
+
import { resolveMeetingSelection } from "./meeting-hit-search.js";
|
|
4
|
+
export type MeetingStatsRequest = Omit<MeetingHitSearchRequest, "limit" | "after" | "maxReadBytes"> & {
|
|
5
|
+
timeZone?: string;
|
|
6
|
+
maxRows?: number;
|
|
7
|
+
};
|
|
8
|
+
/** Talk time and lines per speaker and meetings per week over the meetings search would read, names resolved alike. */
|
|
9
|
+
export declare const readMeetingStats: (store: Pick<MessageStore, "meetings"> & {
|
|
10
|
+
knowledge?: MessageStore["knowledge"];
|
|
11
|
+
}, request: MeetingStatsRequest) => Promise<MeetingStats & {
|
|
12
|
+
selection: Awaited<ReturnType<typeof resolveMeetingSelection>>["selection"];
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolveMeetingSelection } from "./meeting-hit-search.js";
|
|
2
|
+
/** Talk time and lines per speaker and meetings per week over the meetings search would read, names resolved alike. */
|
|
3
|
+
export const readMeetingStats = async (store, request) => {
|
|
4
|
+
const { timeZone, maxRows, ...selecting } = request;
|
|
5
|
+
const { input, selection } = await resolveMeetingSelection(store, selecting);
|
|
6
|
+
const stats = await store.meetings.meetingStats({
|
|
7
|
+
...input,
|
|
8
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
9
|
+
...(maxRows === undefined ? {} : { maxRows }),
|
|
10
|
+
});
|
|
11
|
+
return { ...stats, selection };
|
|
12
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type MeetingStatsStore } from "../../meetings/index.js";
|
|
2
|
+
import type { StoreContext } from "./open.js";
|
|
3
|
+
/**
|
|
4
|
+
* One transcript per meeting — its earliest current one — so a local transcript beside the provider's does not count
|
|
5
|
+
* the talk twice. Words are counted from spaces in SQL, so no transcript text leaves the database.
|
|
6
|
+
*/
|
|
7
|
+
export declare const meetingStatsOver: ({ database }: StoreContext) => MeetingStatsStore;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { aggregateMeetingStats } from "../../meetings/index.js";
|
|
3
|
+
import { checkMeetingCancelled } from "./meeting-operation.js";
|
|
4
|
+
import { positiveReadInteger } from "./meeting-reads.js";
|
|
5
|
+
import { meetingSelectionSql } from "./meeting-selection.js";
|
|
6
|
+
import { read } from "./transaction.js";
|
|
7
|
+
const MOST_ROWS = 500_000;
|
|
8
|
+
/**
|
|
9
|
+
* One transcript per meeting — its earliest current one — so a local transcript beside the provider's does not count
|
|
10
|
+
* the talk twice. Words are counted from spaces in SQL, so no transcript text leaves the database.
|
|
11
|
+
*/
|
|
12
|
+
export const meetingStatsOver = ({ database }) => ({
|
|
13
|
+
async meetingStats(input) {
|
|
14
|
+
checkMeetingCancelled(input.signal);
|
|
15
|
+
positiveReadInteger(input.accountId, "accountId");
|
|
16
|
+
const maxRows = input.maxRows ?? MOST_ROWS;
|
|
17
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MOST_ROWS)
|
|
18
|
+
throw new CliError("validation_error", `meeting stats read 1 through ${MOST_ROWS} transcript lines`);
|
|
19
|
+
return read(database, () => {
|
|
20
|
+
const { clauses, params } = meetingSelectionSql(input);
|
|
21
|
+
const sql = `WITH selected AS (SELECT m.id, m.started_at FROM meetings m WHERE ${clauses.join(" AND ")}),
|
|
22
|
+
chosen AS (SELECT min(t.id) AS id, t.meeting_id FROM meeting_transcripts t JOIN selected s ON s.id = t.meeting_id
|
|
23
|
+
WHERE t.deleted_at IS NULL AND t.superseded_at IS NULL GROUP BY t.meeting_id)
|
|
24
|
+
SELECT c.meeting_id, s.started_at, r.start_ms, r.end_ms,
|
|
25
|
+
length(trim(r.text)) - length(replace(trim(r.text), ' ', '')) + (length(trim(r.text)) > 0) AS words,
|
|
26
|
+
l.person_id, person.name AS person_name, p.identity_id, i.name AS identity_name,
|
|
27
|
+
coalesce(r.speaker_name, p.display_name) AS speaker_name
|
|
28
|
+
FROM chosen c JOIN selected s ON s.id = c.meeting_id
|
|
29
|
+
JOIN meeting_transcript_rows r ON r.meeting_transcript_id = c.id
|
|
30
|
+
LEFT JOIN meeting_participants p ON p.id = r.speaker_participant_id
|
|
31
|
+
LEFT JOIN identities i ON i.id = p.identity_id
|
|
32
|
+
LEFT JOIN identity_links l ON l.identity_id = p.identity_id
|
|
33
|
+
LEFT JOIN persons person ON person.id = l.person_id
|
|
34
|
+
LIMIT ?`;
|
|
35
|
+
const rows = database.prepare(sql).all(...params, maxRows + 1);
|
|
36
|
+
if (rows.length > maxRows)
|
|
37
|
+
throw new CliError("validation_error", `the selection has more than ${maxRows} transcript lines; narrow it`);
|
|
38
|
+
checkMeetingCancelled(input.signal);
|
|
39
|
+
const nullable = (value) => (value === null || value === undefined ? null : Number(value));
|
|
40
|
+
const text = (value) => (value === null || value === undefined ? null : String(value));
|
|
41
|
+
return aggregateMeetingStats(rows.map((row) => ({
|
|
42
|
+
meetingId: Number(row.meeting_id),
|
|
43
|
+
startedAt: nullable(row.started_at),
|
|
44
|
+
startMs: Number(row.start_ms),
|
|
45
|
+
endMs: Number(row.end_ms),
|
|
46
|
+
words: Number(row.words),
|
|
47
|
+
personId: nullable(row.person_id),
|
|
48
|
+
personName: text(row.person_name),
|
|
49
|
+
identityId: nullable(row.identity_id),
|
|
50
|
+
identityName: text(row.identity_name),
|
|
51
|
+
speakerName: text(row.speaker_name),
|
|
52
|
+
})), input.timeZone);
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Attachment, MeetingContextStore, MeetingParticipantSearchStore, MeetingPreparationContextStore, MeetingPreparationKnowledgeStore, MeetingPreparationNotesStore, MeetingRelatedHistoryStore, MeetingSearchIndexStore, MeetingSearchStore, MeetingSeriesHistoryStore, MeetingStore, MeetingTranscriptReceiptStore, MeetingTranscriptStore, NewRecord } from "../../meetings/index.js";
|
|
1
|
+
import type { Attachment, MeetingContextStore, MeetingParticipantSearchStore, MeetingPreparationContextStore, MeetingPreparationKnowledgeStore, MeetingPreparationNotesStore, MeetingRelatedHistoryStore, MeetingSearchIndexStore, MeetingSearchStore, MeetingSeriesHistoryStore, MeetingStatsStore, MeetingStore, MeetingTranscriptReceiptStore, MeetingTranscriptStore, NewRecord } from "../../meetings/index.js";
|
|
2
2
|
import type { CacheDatabase } from "../driver.js";
|
|
3
3
|
import { type MeetingContextReadCapabilities, type MeetingReadCapabilities } from "./meeting-reads.js";
|
|
4
4
|
import type { StoreContext } from "./open.js";
|
|
@@ -11,7 +11,7 @@ export declare const saveAttachment: (database: CacheDatabase, type: string, par
|
|
|
11
11
|
export declare const drainMeetingIndex: (database: CacheDatabase, batch?: number) => number;
|
|
12
12
|
/** Each word of the query, as a prefix, all required. */
|
|
13
13
|
export declare const wordsQuery: (query: string) => string | null;
|
|
14
|
-
export type StoredMeetings = MeetingStore & MeetingPreparationContextStore & MeetingPreparationKnowledgeStore & MeetingPreparationNotesStore & MeetingRelatedHistoryStore & MeetingSeriesHistoryStore & MeetingTranscriptStore & MeetingReadCapabilities & MeetingContextReadCapabilities & MeetingParticipantSearchStore & MeetingSearchStore & MeetingTranscriptReceiptStore & MeetingContextStore & MeetingSearchIndexStore;
|
|
14
|
+
export type StoredMeetings = MeetingStore & MeetingPreparationContextStore & MeetingPreparationKnowledgeStore & MeetingPreparationNotesStore & MeetingRelatedHistoryStore & MeetingSeriesHistoryStore & MeetingTranscriptStore & MeetingReadCapabilities & MeetingContextReadCapabilities & MeetingParticipantSearchStore & MeetingSearchStore & MeetingTranscriptReceiptStore & MeetingContextStore & MeetingSearchIndexStore & MeetingStatsStore;
|
|
15
15
|
/** The `MeetingStore` of `src/meetings/` over the shared store. */
|
|
16
16
|
export declare const meetingStoreOver: (context: StoreContext) => StoredMeetings;
|
|
17
17
|
export {};
|
|
@@ -16,6 +16,7 @@ import { meetingPreparationNotesOver } from "./meeting-preparation-notes.js";
|
|
|
16
16
|
import { meetingReadsOver } from "./meeting-reads.js";
|
|
17
17
|
import { meetingRelatedHistoryOver } from "./meeting-related-history.js";
|
|
18
18
|
import { meetingSeriesHistoryOver } from "./meeting-series-history.js";
|
|
19
|
+
import { meetingStatsOver } from "./meeting-stats.js";
|
|
19
20
|
import { invalidateMeetingStems } from "./meeting-stem-state.js";
|
|
20
21
|
import { meetingOf, transcriptOf, transcriptRowOf } from "./meeting-values.js";
|
|
21
22
|
import { queryFilter } from "./query-filter.js";
|
|
@@ -571,6 +572,7 @@ export const meetingStoreOver = (context) => {
|
|
|
571
572
|
...meetingPreparationNotesOver(context),
|
|
572
573
|
...meetingRelatedHistoryOver(context),
|
|
573
574
|
...meetingHitSearchOver(context),
|
|
575
|
+
...meetingStatsOver(context),
|
|
574
576
|
...meetingParticipantSearchOver(context),
|
|
575
577
|
...meetingHitIndexOver(context),
|
|
576
578
|
...meetingContextLinksOver(context),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.390.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|