@wirecat/cli-core 0.357.0 → 0.358.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 CHANGED
@@ -5,7 +5,15 @@ 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
- ## Unreleased
8
+ ## 0.358.0 — 19.10.2026
9
+
10
+ ### Added
11
+
12
+ - **Meeting transcripts in bounded context reads:** `ContextSource.kind` accepts `meeting`; the native
13
+ reader returns current transcript cue references with `timeKind: "meeting-start"`. Account/time scope,
14
+ cursor validation, read-only snapshots and pre-hydration byte limits apply. Deleted/superseded and
15
+ queued cues are excluded; missing indexes and unknown archive coverage remain explicit. Hosts must
16
+ opt in to meeting sources; the reader does not fetch recordings, fill indexes or infer project links.
9
17
 
10
18
  ## 0.357.0 — 19.10.2026
11
19
 
@@ -37,7 +37,7 @@ export declare const mapPreparationCommunications: ({ items, ...section }: Meeti
37
37
  title: string | null;
38
38
  url: string | null;
39
39
  text: string;
40
- timeKind: "sent" | "source-modified" | "stored-update";
40
+ timeKind: "sent" | "source-modified" | "stored-update" | "meeting-start";
41
41
  revision: number | null;
42
42
  selectionId: string;
43
43
  textTruncated: boolean;
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { formatLocator, parseLocator } from "../domain/locator.js";
3
+ import { formatMeetingReference, parseMeetingReference } from "../meetings/index.js";
3
4
  import { contextEvidenceRequest, contextSource, } from "./evidence.js";
4
5
  export const contextSourceKey = (source) => JSON.stringify([source.kind, source.accountId, source.provider, source.account]);
5
6
  export const validateContextEvidencePage = (page, request) => {
@@ -49,9 +50,9 @@ export const validateContextEvidencePage = (page, request) => {
49
50
  fail();
50
51
  if (item.revision !== null && (!Number.isSafeInteger(item.revision) || item.revision < 1))
51
52
  fail();
52
- if (!["source-modified", "stored-update", "sent"].includes(item.timeKind) ||
53
- (source.kind !== "document" && item.timeKind !== "sent") ||
54
- (source.kind === "document" && item.timeKind === "sent"))
53
+ if (source.kind === "document"
54
+ ? !["source-modified", "stored-update"].includes(item.timeKind)
55
+ : item.timeKind !== (source.kind === "meeting" ? "meeting-start" : "sent"))
55
56
  fail();
56
57
  const text = [item.text, item.title, item.url, item.provider, item.account, item.timeKind];
57
58
  if (text.some((value) => value !== null && typeof value !== "string"))
@@ -61,6 +62,15 @@ export const validateContextEvidencePage = (page, request) => {
61
62
  if (!Number.isSafeInteger(id) || id < 1 || item.ref !== `document:${id}`)
62
63
  fail();
63
64
  }
65
+ else if (source.kind === "meeting") {
66
+ const reference = parseMeetingReference(item.ref);
67
+ if (reference.accountId !== source.accountId ||
68
+ reference.transcriptId === undefined ||
69
+ reference.cuePosition === undefined ||
70
+ formatMeetingReference(reference) !== item.ref)
71
+ fail();
72
+ text.push(item.ref);
73
+ }
64
74
  else {
65
75
  const locator = parseLocator(item.ref);
66
76
  if (locator.provider !== source.provider ||
@@ -1,4 +1,4 @@
1
- export declare const CONTEXT_SOURCE_KINDS: readonly ["message", "mail", "document"];
1
+ export declare const CONTEXT_SOURCE_KINDS: readonly ["message", "mail", "document", "meeting"];
2
2
  export interface ContextSource {
3
3
  kind: (typeof CONTEXT_SOURCE_KINDS)[number];
4
4
  accountId: number;
@@ -27,7 +27,7 @@ export interface ContextEvidenceItem {
27
27
  url: string | null;
28
28
  text: string;
29
29
  at: number;
30
- timeKind: "sent" | "source-modified" | "stored-update";
30
+ timeKind: "sent" | "source-modified" | "stored-update" | "meeting-start";
31
31
  updatedAt: number;
32
32
  revision: number | null;
33
33
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { CliError } from "@wirecat/cli-base";
3
- export const CONTEXT_SOURCE_KINDS = ["message", "mail", "document"];
3
+ export const CONTEXT_SOURCE_KINDS = ["message", "mail", "document", "meeting"];
4
4
  export const contextCursorScope = (request) => createHash("sha256")
5
5
  .update(JSON.stringify([
6
6
  "context-evidence-v1",
@@ -3,6 +3,7 @@ import { contextCursorScope, contextEvidenceRequest, } from "../../context/evide
3
3
  import { formatLocator } from "../../domain/locator.js";
4
4
  import { NORMALIZER_VERSION, normalize } from "../normalize.js";
5
5
  import { contextDocumentTime, contextDocumentTimeKind, contextDocumentUrl } from "./context-document-source.js";
6
+ import { readMeetingContextEvidence } from "./context-meeting-source.js";
6
7
  import { drainEmailIndex } from "./email-index.js";
7
8
  import { drainNoteIndex } from "./note-index.js";
8
9
  import { read } from "./transaction.js";
@@ -52,10 +53,12 @@ export const contextEvidenceOver = ({ database }) => ({
52
53
  async read(original) {
53
54
  const request = contextEvidenceRequest(original);
54
55
  const { source, words, after, before, limit, maxReadBytes, cursor, signal } = request;
56
+ if (source.kind === "meeting")
57
+ return readMeetingContextEvidence(database, request);
58
+ const corpus = corpora[source.kind];
55
59
  const scope = contextCursorScope({ source, query: request.query, after, before });
56
60
  signal?.throwIfAborted();
57
61
  return read(database, () => {
58
- const corpus = corpora[source.kind];
59
62
  const coverage = {
60
63
  available: false,
61
64
  index: "unknown",
@@ -0,0 +1,6 @@
1
+ import { type ContextEvidencePage, type contextEvidenceRequest } from "../../context/evidence.js";
2
+ import type { CacheDatabase } from "../driver.js";
3
+ type Request = ReturnType<typeof contextEvidenceRequest>;
4
+ /** Transcript cues keep their retained revision reference; a meeting title alone is not a transcript match. */
5
+ export declare const readMeetingContextEvidence: (database: CacheDatabase, request: Request) => ContextEvidencePage;
6
+ export {};
@@ -0,0 +1,97 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { contextCursorScope } from "../../context/evidence.js";
3
+ import { normalize } from "../normalize.js";
4
+ import { read } from "./transaction.js";
5
+ /** Transcript cues keep their retained revision reference; a meeting title alone is not a transcript match. */
6
+ export const readMeetingContextEvidence = (database, request) => {
7
+ const { source, words, after, before, limit, maxReadBytes, cursor, signal } = request;
8
+ signal?.throwIfAborted();
9
+ return read(database, () => {
10
+ const coverage = {
11
+ available: false,
12
+ index: "unknown",
13
+ hasMore: false,
14
+ hasUndated: false,
15
+ completeness: "unknown",
16
+ matching: "literal-words",
17
+ consistency: "snapshot-per-page",
18
+ };
19
+ const empty = () => ({ source, items: [], next: null, readBytes: 0, coverage });
20
+ const scope = [source.accountId, source.provider, source.account];
21
+ if (!database.prepare("SELECT 1 FROM accounts WHERE id=? AND provider=? AND external_id=?").get(...scope))
22
+ return empty();
23
+ coverage.available = true;
24
+ const base = `FROM meeting_transcript_rows r
25
+ JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id
26
+ JOIN meetings m ON m.id=t.meeting_id
27
+ JOIN accounts a ON a.id=m.account_id
28
+ WHERE m.account_id=? AND a.provider=? AND a.external_id=?
29
+ AND m.deleted_at IS NULL AND t.deleted_at IS NULL AND t.superseded_at IS NULL`;
30
+ coverage.hasUndated = Boolean(database.prepare(`SELECT 1 ${base} AND m.started_at IS NULL LIMIT 1`).get(...scope));
31
+ if (!database.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='meeting_words'").get()) {
32
+ coverage.index = "missing";
33
+ return empty();
34
+ }
35
+ coverage.index = database
36
+ .prepare(`SELECT 1 ${base} AND m.started_at>=? AND m.started_at<?
37
+ AND EXISTS(SELECT 1 FROM meeting_index_pending p
38
+ WHERE p.indexable_type='meeting_transcript_row' AND p.id=r.id) LIMIT 1`)
39
+ .get(...scope, after, before)
40
+ ? "pending"
41
+ : "ready";
42
+ const reference = "'meeting:'||m.account_id||'/'||m.id||'/'||t.id||'/'||r.position";
43
+ const selection = `SELECT r.id,m.title,r.text,a.provider,a.external_id AS account,
44
+ m.started_at AS at,max(m.updated_at,t.updated_at,r.created_at) AS updated_at,
45
+ ${reference} AS ref ${base} AND m.started_at>=? AND m.started_at<?
46
+ AND (r.id*4+1) IN (SELECT rowid FROM meeting_words WHERE meeting_words MATCH ?)
47
+ AND NOT EXISTS(SELECT 1 FROM meeting_index_pending p
48
+ WHERE p.indexable_type='meeting_transcript_row' AND p.id=r.id)
49
+ ${cursor ? "AND (m.started_at<? OR (m.started_at=? AND r.id<?))" : ""}
50
+ ORDER BY m.started_at DESC,r.id DESC LIMIT ?`;
51
+ const params = [
52
+ ...scope,
53
+ after,
54
+ before,
55
+ words.map((word) => `normalized_text:"${normalize(word)}"`).join(" AND "),
56
+ ];
57
+ if (cursor)
58
+ params.push(cursor.at, cursor.at, cursor.id);
59
+ params.push(limit + 1);
60
+ const probe = database
61
+ .prepare(`SELECT coalesce(sum(length(CAST(coalesce(title,'') AS BLOB))+
62
+ length(CAST(text AS BLOB))+length(CAST(provider AS BLOB))+
63
+ length(CAST(account AS BLOB))+length(CAST(ref AS BLOB))+length('meeting-start')),0) AS bytes FROM (${selection})`)
64
+ .get(...params);
65
+ const readBytes = Number(probe?.bytes);
66
+ if (!Number.isSafeInteger(readBytes) || readBytes < 0 || readBytes > maxReadBytes)
67
+ throw new CliError("validation_error", "meeting context evidence exceeds maxReadBytes before hydration", {
68
+ reason: "context.read-budget",
69
+ });
70
+ signal?.throwIfAborted();
71
+ const rows = database.prepare(selection).all(...params);
72
+ signal?.throwIfAborted();
73
+ coverage.hasMore = rows.length > limit;
74
+ const selected = rows.slice(0, limit);
75
+ const last = selected.at(-1);
76
+ return {
77
+ source,
78
+ readBytes,
79
+ coverage,
80
+ next: coverage.hasMore && last
81
+ ? { at: Number(last.at), id: Number(last.id), scope: contextCursorScope(request) }
82
+ : null,
83
+ items: selected.map((row) => ({
84
+ ref: String(row.ref),
85
+ provider: String(row.provider),
86
+ account: String(row.account),
87
+ title: row.title === null ? null : String(row.title),
88
+ url: null,
89
+ text: String(row.text),
90
+ at: Number(row.at),
91
+ timeKind: "meeting-start",
92
+ updatedAt: Number(row.updated_at),
93
+ revision: null,
94
+ })),
95
+ };
96
+ });
97
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.357.0",
3
+ "version": "0.358.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",