@wirecat/cli-core 0.551.0 → 0.553.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 (94) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +1 -1
  3. package/dist/background/schedules.d.ts +12 -1
  4. package/dist/background/schedules.js +65 -24
  5. package/dist/background/units.d.ts +16 -9
  6. package/dist/background/units.js +26 -11
  7. package/dist/cli/compact.d.ts +4 -0
  8. package/dist/cli/compact.js +65 -7
  9. package/dist/cli/context.d.ts +2 -0
  10. package/dist/cli/meeting-preparation-answer.d.ts +3 -0
  11. package/dist/cli/meeting-preparation-projects.d.ts +3 -0
  12. package/dist/cli/meeting-preparation-read.d.ts +3 -0
  13. package/dist/cli/messenger/context.d.ts +3 -0
  14. package/dist/cli/messenger/projects-command.js +38 -11
  15. package/dist/cli/messenger/search-answers.js +8 -1
  16. package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
  17. package/dist/cli/messenger/serve-catch-up.js +90 -0
  18. package/dist/cli/messenger/serve-command.js +54 -1
  19. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  20. package/dist/cli/messenger/server-command.d.ts +4 -3
  21. package/dist/cli/messenger/server-command.js +30 -12
  22. package/dist/cli/program.js +9 -4
  23. package/dist/cli/project-evidence.d.ts +33 -4
  24. package/dist/cli/project-evidence.js +38 -4
  25. package/dist/cli/project-status-rows.js +11 -1
  26. package/dist/cli/project-status.d.ts +24 -0
  27. package/dist/cli/project-status.js +52 -9
  28. package/dist/cli/schedule/schedule-command.js +5 -1
  29. package/dist/entities/hit.d.ts +5 -0
  30. package/dist/entities/hit.js +5 -0
  31. package/dist/entities/index.d.ts +27 -0
  32. package/dist/entities/listener.d.ts +6 -0
  33. package/dist/entities/listener.js +1 -0
  34. package/dist/entities/meeting-preparation.d.ts +3 -0
  35. package/dist/errors/reasons.js +1 -0
  36. package/dist/mcp/meeting-preparation.d.ts +3 -0
  37. package/dist/meetings/answers.d.ts +5 -0
  38. package/dist/meetings/answers.js +8 -0
  39. package/dist/meetings/cli/index.js +14 -0
  40. package/dist/meetings/index.d.ts +2 -1
  41. package/dist/meetings/index.js +1 -0
  42. package/dist/meetings/ingest.js +2 -1
  43. package/dist/meetings/language.d.ts +5 -0
  44. package/dist/meetings/language.js +57 -0
  45. package/dist/meetings/mcp/search.d.ts +3 -0
  46. package/dist/meetings/project-evidence-answer.d.ts +3 -0
  47. package/dist/meetings/result-items.d.ts +9 -0
  48. package/dist/meetings/result-items.js +7 -0
  49. package/dist/meetings/store.d.ts +22 -0
  50. package/dist/meetings/testing/memory.js +3 -0
  51. package/dist/meetings/transcription.d.ts +2 -0
  52. package/dist/meetings/transcription.js +5 -1
  53. package/dist/search/lucene/types.d.ts +7 -0
  54. package/dist/search/lucene/types.js +11 -1
  55. package/dist/services/index.d.ts +4 -3
  56. package/dist/services/index.js +4 -3
  57. package/dist/services/link-scorers/index.d.ts +1 -1
  58. package/dist/services/link-scorers/index.js +1 -1
  59. package/dist/services/link-scorers/scan.d.ts +0 -1
  60. package/dist/services/link-scorers/scan.js +1 -1
  61. package/dist/services/messages-archive-search.js +3 -2
  62. package/dist/services/messages-combined.js +2 -2
  63. package/dist/services/messages-discovery.js +2 -2
  64. package/dist/services/notes-search.js +4 -5
  65. package/dist/services/project-purposes.d.ts +28 -0
  66. package/dist/services/project-purposes.js +41 -0
  67. package/dist/services/project-status/index.d.ts +22 -8
  68. package/dist/services/project-status/index.js +17 -9
  69. package/dist/services/search-all-meetings.d.ts +3 -0
  70. package/dist/services/search-all-meetings.js +6 -1
  71. package/dist/services/stored-project-gathering.d.ts +79 -4
  72. package/dist/services/stored-project-gathering.js +171 -22
  73. package/dist/speech/local.d.ts +2 -0
  74. package/dist/speech/local.js +8 -1
  75. package/dist/speech/models.d.ts +2 -0
  76. package/dist/speech/models.js +2 -0
  77. package/dist/store/driver.d.ts +11 -0
  78. package/dist/store/driver.js +13 -0
  79. package/dist/store/drivers/node-sqlite.d.ts +1 -1
  80. package/dist/store/drivers/node-sqlite.js +3 -0
  81. package/dist/store/messages.d.ts +2 -1
  82. package/dist/store/messages.js +2 -2
  83. package/dist/store/sqlite/lucene.d.ts +1 -1
  84. package/dist/store/sqlite/lucene.js +33 -7
  85. package/dist/store/sqlite/meeting-hit-search.js +10 -3
  86. package/dist/store/sqlite/meeting-languages.d.ts +4 -0
  87. package/dist/store/sqlite/meeting-languages.js +36 -0
  88. package/dist/store/sqlite/meetings.d.ts +2 -1
  89. package/dist/store/sqlite/meetings.js +9 -3
  90. package/dist/store/sqlite/note-search.js +2 -2
  91. package/dist/store/sqlite/reads.d.ts +1 -1
  92. package/dist/store/sqlite/reads.js +2 -2
  93. package/package.json +1 -1
  94. package/parity.json +10 -0
@@ -139,6 +139,9 @@ export declare const callMeetingSearchTool: (search: MeetingHitSearch, input: un
139
139
  id: string;
140
140
  excerpt: string;
141
141
  offset_ms: number | null;
142
+ language?: string | null | undefined;
143
+ line?: number | undefined;
144
+ lines_total?: number | undefined;
142
145
  match?: "corrected" | "words" | undefined;
143
146
  corrections?: {
144
147
  from: string;
@@ -24,6 +24,9 @@ export declare const meetingProjectEvidence: v.StrictObjectSchema<{
24
24
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
25
25
  readonly excerpt: v.StringSchema<undefined>;
26
26
  readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
27
+ readonly language: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
28
+ readonly line: v.OptionalSchema<v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, v.MinValueAction<number, 1, undefined>]>, undefined>;
29
+ readonly lines_total: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
27
30
  readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
28
31
  readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
29
32
  readonly from: v.StringSchema<undefined>;
@@ -124,4 +124,13 @@ export declare const MEETING_RESULT_ENTITIES: {
124
124
  readonly next_transcript_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
125
125
  }, undefined>;
126
126
  }, undefined>], undefined>;
127
+ readonly meeting_repair: v.StrictObjectSchema<{
128
+ readonly account_id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
129
+ readonly applied: v.BooleanSchema<undefined>;
130
+ readonly transcripts: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
131
+ readonly languages: v.ArraySchema<v.StrictObjectSchema<{
132
+ readonly language: v.NullableSchema<v.StringSchema<undefined>, undefined>;
133
+ readonly transcripts: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
134
+ }, undefined>, undefined>;
135
+ }, undefined>;
127
136
  };
@@ -116,10 +116,17 @@ const embedding = v.union([
116
116
  }),
117
117
  }),
118
118
  ]);
119
+ const repair = v.strictObject({
120
+ account_id: identifier,
121
+ applied: flag,
122
+ transcripts: count,
123
+ languages: v.array(v.strictObject({ language: nullableText, transcripts: count })),
124
+ });
119
125
  export const MEETING_RESULT_ENTITIES = {
120
126
  meeting_ingestion: run,
121
127
  meeting_export: exported,
122
128
  meeting_association: meetingChange,
123
129
  meeting_transcription: transcription,
124
130
  meeting_embedding: embedding,
131
+ meeting_repair: repair,
125
132
  };
@@ -90,6 +90,11 @@ export interface SearchHit {
90
90
  startMs: number | null;
91
91
  /** Who spoke a transcript hit's cue, as the transcript names them; absent for chat and summary hits. */
92
92
  speakerName?: string | null;
93
+ /** A transcript hit's transcript language (`ru`, `en`…), `null` when unknown; absent for chat and summary hits. */
94
+ language?: string | null;
95
+ /** A transcript hit's line, from 1, of `linesTotal` lines in its transcript: where in the meeting it was said. */
96
+ line?: number;
97
+ linesTotal?: number;
93
98
  /** `corrected` when a query word matched only as a stored word recognition may have garbled. */
94
99
  match?: "words" | "corrected";
95
100
  /** For a `corrected` hit, the stored words each such query word matched in this passage. */
@@ -126,4 +131,21 @@ export interface MeetingStore {
126
131
  reviewMeetingLink?(meetingId: number, eventIds: number[], now: number): Promise<void>;
127
132
  cursor(accountId: number): Promise<string | null>;
128
133
  setCursor(accountId: number, value: string, now: number): Promise<void>;
134
+ /** Detects the language of the account's transcripts that have none; stores it only with `apply`. */
135
+ fillTranscriptLanguages?(input: {
136
+ accountId: number;
137
+ apply: boolean;
138
+ now: number;
139
+ }): Promise<TranscriptLanguageFill>;
140
+ }
141
+ export interface TranscriptLanguageFill {
142
+ accountId: number;
143
+ applied: boolean;
144
+ /** Transcripts that had no language. */
145
+ transcripts: number;
146
+ /** How many of them each language was found in; `null` where the text did not show one. */
147
+ languages: {
148
+ language: string | null;
149
+ transcripts: number;
150
+ }[];
129
151
  }
@@ -121,6 +121,9 @@ export const memoryMeetingStore = () => {
121
121
  text: r.text,
122
122
  startMs: r.startMs,
123
123
  speakerName: r.speakerName,
124
+ language: t.transcript.language,
125
+ line: r.position + 1,
126
+ linesTotal: t.rows.length,
124
127
  })));
125
128
  rows.push(...d.chat.map((r) => ({
126
129
  meetingId: d.meeting.id,
@@ -13,6 +13,8 @@ export interface MeetingMediaDescriptor {
13
13
  model: string;
14
14
  version?: string;
15
15
  timing?: "fixed-windows" | "recognition-segments";
16
+ /** The one language the model writes (`ru`); without it the language is read from the recognized text. */
17
+ language?: string;
16
18
  };
17
19
  }
18
20
  export interface MeetingRecognitionSegment {
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { contentHash } from "./ingest.js";
3
+ import { transcriptLanguage } from "./language.js";
3
4
  import { dispatchMeetingMutation } from "./meeting-management.js";
4
5
  const bounded = (value, max, name) => {
5
6
  if (!Number.isSafeInteger(value) || value < 1 || value > max)
@@ -46,6 +47,9 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
46
47
  const timing = recognition.timing ?? "recognition-segments";
47
48
  if (timing !== "fixed-windows" && timing !== "recognition-segments")
48
49
  throw new CliError("invalid_response", "unsupported recognition timing");
50
+ const modelLanguage = recognition.language;
51
+ if (modelLanguage !== undefined && (typeof modelLanguage !== "string" || !/^[a-z]{2,3}$/.test(modelLanguage)))
52
+ throw new CliError("invalid_response", "recognition language must be a two- or three-letter code");
49
53
  const media = Object.freeze({
50
54
  id: identifier(raw.id),
51
55
  contentHash: raw.contentHash,
@@ -137,7 +141,7 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
137
141
  source: `local/${encodeURIComponent(media.recognition.engine)}/${encodeURIComponent(media.recognition.model)}`,
138
142
  format: "local-transcription",
139
143
  }),
140
- language: null,
144
+ language: modelLanguage ?? transcriptLanguage(rows.map((row) => row.text)),
141
145
  contentHash: hash,
142
146
  externalCreatedAt: null,
143
147
  metadata: provenance,
@@ -22,4 +22,11 @@ export declare const MESSAGE_PARSING: {
22
22
  /** Further apart than this, two words are rarely about one thing, even in a long message. */
23
23
  export declare const MOST_WORDS_APART = 50;
24
24
  export declare function searchError(reason: string, span: Span, alternative?: string): never;
25
+ /**
26
+ * The search ran out of time, not the query out of bounds: a valid query over a large store. `timeout`, so a caller
27
+ * does not tell the person to fix what they typed.
28
+ */
29
+ export declare function outOfTime(limitMs?: number): never;
30
+ /** A search limit that leaves the answer without a part, such as its total, rather than failing it. */
31
+ export declare const isQueryLimit: (error: unknown) => boolean;
25
32
  export declare function exhausted(budget: string): never;
@@ -1,4 +1,4 @@
1
- import { CliError } from "@wirecat/cli-base";
1
+ import { CliError, isCliError } from "@wirecat/cli-base";
2
2
  import { QUERY_LIMITS as PARSER_LIMITS, queryError } from "@wirecat/cli-base/query";
3
3
  export const QUERY_VERSION = 1;
4
4
  export const QUERY_LIMITS = {
@@ -22,6 +22,16 @@ export const MOST_WORDS_APART = 50;
22
22
  export function searchError(reason, span, alternative) {
23
23
  queryError(reason, span, alternative, SEARCH_QUERY);
24
24
  }
25
+ /**
26
+ * The search ran out of time, not the query out of bounds: a valid query over a large store. `timeout`, so a caller
27
+ * does not tell the person to fix what they typed.
28
+ */
29
+ export function outOfTime(limitMs = QUERY_LIMITS.milliseconds) {
30
+ throw new CliError("timeout", `search stopped at its time limit of ${limitMs / 1000}s — the query is valid; ` +
31
+ "add chat: or date: to search less of the store", { reason: "query.time-limit", budget: "time", limit_ms: limitMs, complete: false });
32
+ }
33
+ /** A search limit that leaves the answer without a part, such as its total, rather than failing it. */
34
+ export const isQueryLimit = (error) => isCliError(error) && (error.details.reason === "query.too-large" || error.details.reason === "query.time-limit");
25
35
  export function exhausted(budget) {
26
36
  throw new CliError("validation_error", `search exceeded its ${budget} budget — narrow the chat or date range`, {
27
37
  reason: "query.too-large",
@@ -6,7 +6,7 @@ export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
6
6
  export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
7
7
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
8
8
  export { excerpt } from "./excerpt.js";
9
- export { aliasHits, BUILT_IN_PURPOSES, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
9
+ export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
10
10
  export type { MediaArchiveOptions, MediaArchivePorts } from "./media-archive.js";
11
11
  export { archiveMedia, checkMediaArchive } from "./media-archive.js";
12
12
  export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.js";
@@ -21,9 +21,10 @@ export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./pr
21
21
  export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
22
22
  export { findEvidenceMentions } from "./project-gathering-mentions.js";
23
23
  export type { ProjectChatFetched, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
24
- export { DEFAULT_PURPOSES, type MoneyTotal, type ProjectStatus, type ProjectStatusInput, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, type StatusAttention, type StatusChat, type StatusChatSummary, type StatusContext, type StatusItem, type StatusSignal, } from "./project-status/index.js";
24
+ export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, type PurposeChoice, type PurposeConfig, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
25
+ export { type MoneyTotal, type OutOfScope, type ProjectStatus, type ProjectStatusInput, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, type StatusAttention, type StatusChat, type StatusChatSummary, type StatusContext, type StatusItem, type StatusSignal, } from "./project-status/index.js";
25
26
  export { questions } from "./questions.js";
26
- export { canonicalProjectRef, gatherStoredProject, type LinkedChat, linkedChatsOf, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
27
+ export { type ChatCount, type ChatCounter, canonicalProjectRef, gatherStoredProject, type LeftOutChat, type LinkedChat, linkedChatsOf, type ProjectMember, type StoredProjectGathering, type StoredProjectInput, storedChatCount, } from "./stored-project-gathering.js";
27
28
  import { type AccountService } from "./account.js";
28
29
  import { type AdminService } from "./admin.js";
29
30
  import { type AdminStatisticsService } from "./admin-statistics.js";
@@ -5,7 +5,7 @@ export { AGENDA_DEFAULTS } from "./agenda.js";
5
5
  export { chatCoverage } from "./chat-coverage.js";
6
6
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
7
7
  export { excerpt } from "./excerpt.js";
8
- export { aliasHits, BUILT_IN_PURPOSES, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
8
+ export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
9
9
  export { archiveMedia, checkMediaArchive } from "./media-archive.js";
10
10
  export { moveMediaArchive } from "./media-archive-move.js";
11
11
  export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
@@ -13,9 +13,10 @@ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, moneyClassesOf, PROMISE } from
13
13
  export { gatherProjectEvidence } from "./project-gathering.js";
14
14
  export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
15
15
  export { findEvidenceMentions } from "./project-gathering-mentions.js";
16
- export { DEFAULT_PURPOSES, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, } from "./project-status/index.js";
16
+ export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
17
+ export { projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, } from "./project-status/index.js";
17
18
  export { questions } from "./questions.js";
18
- export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, } from "./stored-project-gathering.js";
19
+ export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, storedChatCount, } from "./stored-project-gathering.js";
19
20
  import { accountService } from "./account.js";
20
21
  import { adminService } from "./admin.js";
21
22
  import { adminStatisticsService } from "./admin-statistics.js";
@@ -5,5 +5,5 @@ export { emailDomain } from "./email-domain.js";
5
5
  export { mailThread } from "./mail-thread.js";
6
6
  export { memberChats } from "./member-chats.js";
7
7
  export { memberDialog } from "./member-dialog.js";
8
- export { BUILT_IN_PURPOSES, combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput, scanProjectLinks, } from "./scan.js";
8
+ export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput, scanProjectLinks, } from "./scan.js";
9
9
  export type { LinkCandidate, LinkScorer, ScannedProject, ScannedProposal, ScanResult, ScorerInput, Signal, } from "./types.js";
@@ -5,4 +5,4 @@ export { emailDomain } from "./email-domain.js";
5
5
  export { mailThread } from "./mail-thread.js";
6
6
  export { memberChats } from "./member-chats.js";
7
7
  export { memberDialog } from "./member-dialog.js";
8
- export { BUILT_IN_PURPOSES, combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./scan.js";
8
+ export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./scan.js";
@@ -10,7 +10,6 @@ export declare const SCAN_BOT: {
10
10
  readonly bot: "memo-scan";
11
11
  readonly kind: "rule";
12
12
  };
13
- export declare const BUILT_IN_PURPOSES: readonly ["work", "sales", "support", "other"];
14
13
  export declare const DEFAULT_THRESHOLD = 0.5;
15
14
  /** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
16
15
  export declare const PERSON_THRESHOLD = 0.3;
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { parseReference } from "../../domain/references.js";
3
+ import { BUILT_IN_PURPOSES } from "../project-purposes.js";
3
4
  import { canonicalProjectRef } from "../stored-project-gathering.js";
4
5
  import { aliasHits } from "./alias-hits.js";
5
6
  import { chatSenders } from "./chat-senders.js";
@@ -15,7 +16,6 @@ export const SCORERS = [chatTitle, aliasHits, memberChats, memberDialog, chatSen
15
16
  export const PERSON_SCORERS = [emailDomain];
16
17
  /** The rule bot every scan proposal is written by: only the owner decides its proposals. */
17
18
  export const SCAN_BOT = { bot: "memo-scan", kind: "rule" };
18
- export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
19
19
  export const DEFAULT_THRESHOLD = 0.5;
20
20
  /** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
21
21
  export const PERSON_THRESHOLD = 0.3;
@@ -1,5 +1,6 @@
1
- import { CliError, isCliError } from "@wirecat/cli-base";
1
+ import { CliError } from "@wirecat/cli-base";
2
2
  import { parseLocator } from "../domain/locator.js";
3
+ import { isQueryLimit } from "../search/lucene/types.js";
3
4
  import { TOTAL_CAP, totalOf } from "../search/total.js";
4
5
  import { discoveryRequest, searchDiscovery } from "./messages-discovery.js";
5
6
  import { countLucene, searchLucene, statsLucene } from "./messages-search.js";
@@ -53,7 +54,7 @@ export const searchStore = async (store, account, request, messenger = {}, meani
53
54
  const total = discovery
54
55
  ? totalOf(discovery.candidates, found.items.length, found.hasMore, !discovery.truncated)
55
56
  : totalOf(await countLucene(store, account, { ...(request.discover ? discoveryRequest(request) : request), limit: request.limit + offset }, messenger, TOTAL_CAP).catch((error) => {
56
- if (isCliError(error) && error.details.reason === "query.too-large")
57
+ if (isQueryLimit(error))
57
58
  return undefined;
58
59
  throw error;
59
60
  }), found.items.length, found.hasMore);
@@ -4,7 +4,7 @@ import { parseLocator } from "../domain/locator.js";
4
4
  import { correctWords } from "../search/correct.js";
5
5
  import { PRESET_VERSION } from "../search/lucene/presets.js";
6
6
  import { FIELD_VERSION } from "../search/lucene/registry.js";
7
- import { exhausted, QUERY_LIMITS } from "../search/lucene/types.js";
7
+ import { exhausted, outOfTime, QUERY_LIMITS } from "../search/lucene/types.js";
8
8
  import { normalize } from "../store/normalize.js";
9
9
  import { withExcerpt } from "./excerpt.js";
10
10
  import { coverageOf, prepareLucene } from "./messages-search.js";
@@ -31,7 +31,7 @@ export const searchCombined = async (store, account, request, messenger = {}, op
31
31
  if (request.signal?.aborted)
32
32
  throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
33
33
  if (performance.now() - started > QUERY_LIMITS.milliseconds)
34
- exhausted("milliseconds");
34
+ outOfTime();
35
35
  };
36
36
  check();
37
37
  const spans = request.ast === undefined && !request.exact && request.text !== undefined
@@ -3,7 +3,7 @@ import { implicitTextTerms, parseLucene, walkQuery } from "@wirecat/cli-base/que
3
3
  import { parseLocator } from "../domain/locator.js";
4
4
  import { fieldsOfQuery, meaningWords } from "../search/lucene/meaning-parts.js";
5
5
  import { validateFields } from "../search/lucene/registry.js";
6
- import { exhausted, MESSAGE_PARSING, QUERY_LIMITS } from "../search/lucene/types.js";
6
+ import { exhausted, MESSAGE_PARSING, outOfTime, QUERY_LIMITS } from "../search/lucene/types.js";
7
7
  import { planQuestion, words } from "../search/question-plan.js";
8
8
  import { searchCombined } from "./messages-combined.js";
9
9
  import { prepareLucene, searchLucene } from "./messages-search.js";
@@ -101,7 +101,7 @@ export const searchDiscovery = async (store, account, asked, messenger = {}, mea
101
101
  if (request.signal?.aborted)
102
102
  throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
103
103
  if (performance.now() - started > QUERY_LIMITS.milliseconds)
104
- exhausted("milliseconds");
104
+ outOfTime();
105
105
  };
106
106
  const union = new Map();
107
107
  let truncated = false;
@@ -1,4 +1,4 @@
1
- import { CliError, isCliError } from "@wirecat/cli-base";
1
+ import { CliError } from "@wirecat/cli-base";
2
2
  import { dateRange, parseLucene, timezoneOf } from "@wirecat/cli-base/query";
3
3
  import { INTERNAL_NOTES } from "../domain/providers.js";
4
4
  import { normalizeTag } from "../domain/tags.js";
@@ -9,7 +9,7 @@ import { entriesOfList, fitting } from "../search/account-list.js";
9
9
  import { fieldsOfQuery as fieldsOf, meaningWords } from "../search/lucene/meaning-parts.js";
10
10
  import { validateFields } from "../search/lucene/registry.js";
11
11
  import { hasStems } from "../search/lucene/resolved.js";
12
- import { SEARCH_QUERY } from "../search/lucene/types.js";
12
+ import { isQueryLimit, SEARCH_QUERY } from "../search/lucene/types.js";
13
13
  import { createStemmer, DEFAULT_STEMMERS } from "../search/stem.js";
14
14
  import { TOTAL_CAP, totalOf } from "../search/total.js";
15
15
  import { openPictureEmbedderFor } from "./document-pictures.js";
@@ -286,12 +286,11 @@ const filterRefs = async (store, narrowing, scope) => {
286
286
  return found.hasMore ? undefined : found.refs;
287
287
  }
288
288
  catch (error) {
289
- if (tooLarge(error))
289
+ if (isQueryLimit(error))
290
290
  return undefined;
291
291
  throw error;
292
292
  }
293
293
  };
294
- const tooLarge = (error) => isCliError(error) && error.details.reason === "query.too-large";
295
294
  /**
296
295
  * The notes the words find, with those found by meaning: every note the search would list, paged to the end. Never
297
296
  * below what the page already showed; a count the store refuses as too costly is that lower bound.
@@ -313,7 +312,7 @@ const notesTotal = async (store, text, { exact, scope, shown, hasMore, }, others
313
312
  });
314
313
  }
315
314
  catch (error) {
316
- if (tooLarge(error))
315
+ if (isQueryLimit(error))
317
316
  return totalOf(undefined, shown, hasMore);
318
317
  throw error;
319
318
  }
@@ -0,0 +1,28 @@
1
+ /** What a chat can be for in a project, when the host's config names none. */
2
+ export declare const BUILT_IN_PURPOSES: readonly string[];
3
+ /** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
4
+ export declare const DEFAULT_PURPOSES: readonly string[];
5
+ /** A link with no purpose is a work chat. */
6
+ export declare const LINK_PURPOSE = "work";
7
+ /** The purposes a host's config lists; core knows only the built-in ones and takes the rest as given. */
8
+ export interface PurposeConfig {
9
+ purposes: readonly string[];
10
+ /** Read when no `--purpose` is given. */
11
+ defaults: readonly string[];
12
+ /** A project's own defaults, by its key or `project:<id>`. */
13
+ projects?: Readonly<Record<string, readonly string[]>>;
14
+ }
15
+ export declare const BUILT_IN_PURPOSE_CONFIG: PurposeConfig;
16
+ /** The purposes one run reads: `all`, or a set. */
17
+ export type PurposeChoice = "all" | ReadonlySet<string>;
18
+ /**
19
+ * The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
20
+ * else the config's. `all` reads every purpose, a link's unlisted one too.
21
+ */
22
+ export declare const purposesOf: (asked: readonly string[] | undefined, config?: PurposeConfig, project?: {
23
+ ref: string | null;
24
+ key: string | null;
25
+ }) => PurposeChoice;
26
+ export declare const purposeRead: (choice: PurposeChoice, purpose: string | null) => boolean;
27
+ /** `a,b` and repeated flags as one list. */
28
+ export declare const purposeList: (value: string, previous?: string[]) => string[];
@@ -0,0 +1,41 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ /** What a chat can be for in a project, when the host's config names none. */
3
+ export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
4
+ /** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
5
+ export const DEFAULT_PURPOSES = ["work"];
6
+ /** A link with no purpose is a work chat. */
7
+ export const LINK_PURPOSE = "work";
8
+ export const BUILT_IN_PURPOSE_CONFIG = { purposes: BUILT_IN_PURPOSES, defaults: DEFAULT_PURPOSES };
9
+ const unknown = (asked, config) => {
10
+ const wrong = asked.filter((one) => !config.purposes.includes(one));
11
+ if (wrong.length)
12
+ throw new CliError("validation_error", `unknown purpose ${wrong.join(", ")}; the configured ones are ${config.purposes.join(", ")}, or all`, { param: "purpose", purposes: [...config.purposes] });
13
+ };
14
+ /**
15
+ * The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
16
+ * else the config's. `all` reads every purpose, a link's unlisted one too.
17
+ */
18
+ export const purposesOf = (asked, config = BUILT_IN_PURPOSE_CONFIG, project) => {
19
+ const words = (asked ?? []).map((one) => one.trim()).filter(Boolean);
20
+ if (words.includes("all")) {
21
+ if (words.length > 1)
22
+ throw new CliError("validation_error", "--purpose all reads every purpose; give it alone");
23
+ return "all";
24
+ }
25
+ if (words.length) {
26
+ unknown(words, config);
27
+ return new Set(words);
28
+ }
29
+ const own = (project?.key ? config.projects?.[project.key] : undefined) ??
30
+ (project?.ref ? config.projects?.[project.ref] : undefined);
31
+ return new Set(own ?? config.defaults);
32
+ };
33
+ export const purposeRead = (choice, purpose) => choice === "all" || choice.has(purpose ?? LINK_PURPOSE);
34
+ /** `a,b` and repeated flags as one list. */
35
+ export const purposeList = (value, previous = []) => [
36
+ ...previous,
37
+ ...value
38
+ .split(",")
39
+ .map((one) => one.trim())
40
+ .filter(Boolean),
41
+ ];
@@ -1,4 +1,7 @@
1
+ import type { Id } from "../../domain/models.js";
1
2
  import type { AccountKey, MessageStore } from "../../store/store.js";
3
+ import { type PurposeConfig } from "../project-purposes.js";
4
+ import { type ChatCount, type ChatCounter } from "../stored-project-gathering.js";
2
5
  import { STATUS_SIGNALS } from "./signals/index.js";
3
6
  import type { StatusAttention, StatusChat, StatusItem, StatusProject, StatusSignal } from "./types.js";
4
7
  export type * from "./types.js";
@@ -7,8 +10,6 @@ export { STATUS_SIGNALS };
7
10
  export declare const STATUS_WINDOW = 300;
8
11
  /** Linked chats read at most, as `projects evidence` reads. */
9
12
  export declare const STATUS_CHATS = 16;
10
- /** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
11
- export declare const DEFAULT_PURPOSES: readonly string[];
12
13
  export interface ProjectStatusInput {
13
14
  /** An id, key, name or alias, as `canonicalProjectRef` takes it. */
14
15
  project: string;
@@ -18,12 +19,27 @@ export interface ProjectStatusInput {
18
19
  after?: number;
19
20
  asOf?: number;
20
21
  contextQueries?: string[];
21
- /** The chat purposes read; `all` reads every one. Default `DEFAULT_PURPOSES`. */
22
+ /** The chat purposes read; `["all"]` reads every one. Left out, the config's defaults. */
22
23
  purposes?: readonly string[];
24
+ /** The host's purposes; core's built-in ones when left out. */
25
+ purposeConfig?: PurposeConfig;
26
+ /** How many messages a chat left out by purpose holds in the window; the store's own count when left out. */
27
+ count?: ChatCounter;
23
28
  /** Default `STATUS_SIGNALS`; tests pass their own. */
24
29
  signals?: readonly StatusSignal[];
25
30
  }
26
31
  export type StatusChatSummary = Omit<StatusChat, "messages" | "account">;
32
+ /** A linked chat not read; one left out by its purpose says how much it holds in the window. */
33
+ export type OutOfScope = {
34
+ chat: string;
35
+ reason: "other-account" | "linked-chat-limit";
36
+ } | ({
37
+ chat: string;
38
+ reason: "purpose";
39
+ purpose: string;
40
+ account: AccountKey;
41
+ chatId: Id;
42
+ } & ChatCount);
27
43
  export interface MoneyTotal {
28
44
  direction: string;
29
45
  currency: string | null;
@@ -48,11 +64,9 @@ export interface ProjectStatus {
48
64
  money: {
49
65
  open: MoneyTotal[];
50
66
  };
51
- outOfScope: {
52
- chat: string;
53
- reason: "other-account" | "linked-chat-limit" | "purpose";
54
- purpose?: string;
55
- }[];
67
+ outOfScope: OutOfScope[];
68
+ /** The purposes read: `all`, or the list. */
69
+ purposes: "all" | string[];
56
70
  failed: {
57
71
  signal: string;
58
72
  why: string;
@@ -1,7 +1,8 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { formatReference } from "../../domain/references.js";
3
3
  import { chatCoverage } from "../chat-coverage.js";
4
- import { canonicalProjectRef, linkedChatsOf } from "../stored-project-gathering.js";
4
+ import { LINK_PURPOSE, purposeRead, purposesOf } from "../project-purposes.js";
5
+ import { canonicalProjectRef, linkedChatsOf, storedChatCount, } from "../stored-project-gathering.js";
5
6
  import { centsText } from "./signals/amounts.js";
6
7
  import { STATUS_SIGNALS } from "./signals/index.js";
7
8
  export { STATUS_SIGNALS };
@@ -12,8 +13,6 @@ export const STATUS_WINDOW = 300;
12
13
  export const STATUS_CHATS = 16;
13
14
  const CHILD_DEPTH = 2;
14
15
  const CHILDREN = 8;
15
- /** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
16
- export const DEFAULT_PURPOSES = ["work"];
17
16
  const projectsCatalog = async (store) => new Map((await store.knowledge.vocabulary({ kind: "project" })).entries.map((entry) => [`project:${entry.id}`, entry]));
18
17
  /** Confirmed `member-of` links from another project; depth two, eight at most, a project seen twice skipped. */
19
18
  const childrenOf = async (store, root, catalog) => {
@@ -92,13 +91,14 @@ export const projectStatus = async (store, input) => {
92
91
  if (!(since < now))
93
92
  throw new CliError("validation_error", "the window must start before it ends");
94
93
  const signals = input.signals ?? STATUS_SIGNALS;
95
- const purposes = new Set(input.purposes ?? DEFAULT_PURPOSES);
96
94
  const catalog = await projectsCatalog(store);
97
95
  const ref = await canonicalProjectRef(store, input.project);
98
96
  const entry = catalog.get(ref);
99
97
  if (!entry)
100
98
  throw new CliError("not_found", "the owner project is unavailable");
101
99
  const root = { ref, key: entry.key, name: entry.name };
100
+ const purposes = purposesOf(input.purposes, input.purposeConfig, root);
101
+ const count = input.count ?? storedChatCount(store);
102
102
  const { children, cut } = await childrenOf(store, root, catalog);
103
103
  const outOfScope = [];
104
104
  const linked = new Map();
@@ -110,10 +110,17 @@ export const projectStatus = async (store, input) => {
110
110
  }
111
111
  const chosen = [];
112
112
  for (const [chat, link] of linked) {
113
- // Links carry no purpose yet; every one is `work`, as a link without one will be.
114
- const purpose = "work";
115
- if (!purposes.has("all") && !purposes.has(purpose))
116
- outOfScope.push({ chat, reason: "purpose", purpose });
113
+ if (!purposeRead(purposes, link.purpose)) {
114
+ const account = { provider: link.provider, account: link.account };
115
+ outOfScope.push({
116
+ chat,
117
+ reason: "purpose",
118
+ purpose: link.purpose ?? LINK_PURPOSE,
119
+ account,
120
+ chatId: link.chat,
121
+ ...(await count(account, link.chat, { since, until: now })),
122
+ });
123
+ }
117
124
  else if (!inAccounts(input.accounts, link.provider, link.account))
118
125
  outOfScope.push({ chat, reason: "other-account" });
119
126
  else if (chosen.length >= STATUS_CHATS)
@@ -134,7 +141,7 @@ export const projectStatus = async (store, input) => {
134
141
  title: coverage?.title ?? null,
135
142
  kind: (await store.chats.chatKind(account, link.chat)) ?? "unknown",
136
143
  role: link.role,
137
- purpose: "work",
144
+ purpose: link.purpose ?? LINK_PURPOSE,
138
145
  project: link.project,
139
146
  coverage: coverage,
140
147
  messages: page.items.filter((message) => Date.parse(message.timestamp) <= now),
@@ -189,6 +196,7 @@ export const projectStatus = async (store, input) => {
189
196
  items,
190
197
  money: { open: totalsOf(items) },
191
198
  outOfScope,
199
+ purposes: purposes === "all" ? "all" : [...purposes],
192
200
  failed,
193
201
  attention,
194
202
  signals: signals.map(({ id, version }) => ({ id, version })),
@@ -22,6 +22,9 @@ export interface MeetingSearchItem {
22
22
  /** A transcript hit's cue, `meeting:<account>/<meeting>/<transcript>/<cue>`, which `meetings passage` opens. */
23
23
  cueRef?: string;
24
24
  speakerName?: string | null;
25
+ language?: string | null;
26
+ line?: number;
27
+ linesTotal?: number;
25
28
  }
26
29
  export interface MeetingSearchCursor {
27
30
  accountId: number;
@@ -105,7 +105,12 @@ const collectMeetings = async (store, account, request, query) => {
105
105
  id: hit.id,
106
106
  startMs: hit.startMs,
107
107
  ...(hit.scope === "transcript" && hit.ref !== undefined
108
- ? { cueRef: hit.ref, speakerName: hit.speakerName ?? null }
108
+ ? {
109
+ cueRef: hit.ref,
110
+ speakerName: hit.speakerName ?? null,
111
+ language: hit.language ?? null,
112
+ ...(hit.line === undefined ? {} : { line: hit.line, linesTotal: hit.linesTotal }),
113
+ }
109
114
  : {}),
110
115
  },
111
116
  });