@wirecat/cli-core 0.550.0 → 0.552.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 (190) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/README.md +1 -1
  3. package/THIRD_PARTY_NOTICES +41 -0
  4. package/dist/background/schedules.d.ts +12 -1
  5. package/dist/background/schedules.js +65 -24
  6. package/dist/background/units.d.ts +16 -9
  7. package/dist/background/units.js +26 -11
  8. package/dist/calendar/ics.d.ts +1 -1
  9. package/dist/calendar/ics.js +5 -2
  10. package/dist/calendar/windows-zones.generated.d.ts +1 -0
  11. package/dist/calendar/windows-zones.generated.js +146 -0
  12. package/dist/cli/compact.js +2 -0
  13. package/dist/cli/context.d.ts +2 -0
  14. package/dist/cli/index.d.ts +3 -0
  15. package/dist/cli/index.js +3 -0
  16. package/dist/cli/meeting-findings-input.js +4 -3
  17. package/dist/cli/meeting-preparation-answer.d.ts +3 -0
  18. package/dist/cli/meeting-preparation-projects.d.ts +3 -0
  19. package/dist/cli/meeting-preparation-read.d.ts +3 -0
  20. package/dist/cli/messenger/backfill-command.js +28 -1
  21. package/dist/cli/messenger/context.js +1 -0
  22. package/dist/cli/messenger/history-gaps.js +4 -4
  23. package/dist/cli/messenger/mcp-command.js +1 -0
  24. package/dist/cli/messenger/messages-delete-command.js +7 -8
  25. package/dist/cli/messenger/projects-command.js +47 -2
  26. package/dist/cli/messenger/search-answers.js +8 -1
  27. package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
  28. package/dist/cli/messenger/serve-catch-up.js +90 -0
  29. package/dist/cli/messenger/serve-command.js +54 -1
  30. package/dist/cli/messenger/server-command.d.ts +4 -3
  31. package/dist/cli/messenger/server-command.js +30 -12
  32. package/dist/cli/payment-query.d.ts +8 -0
  33. package/dist/cli/payment-query.js +27 -0
  34. package/dist/cli/program.js +9 -4
  35. package/dist/cli/project-status-rows.d.ts +8 -0
  36. package/dist/cli/project-status-rows.js +102 -0
  37. package/dist/cli/project-status.d.ts +132 -0
  38. package/dist/cli/project-status.js +248 -0
  39. package/dist/cli/schedule/schedule-command.js +5 -1
  40. package/dist/cli/steps.js +4 -1
  41. package/dist/domain/knowledge.d.ts +1 -1
  42. package/dist/domain/knowledge.js +1 -1
  43. package/dist/domain/status-words.d.ts +3 -0
  44. package/dist/domain/status-words.js +44 -0
  45. package/dist/entities/fact.d.ts +12 -1
  46. package/dist/entities/fact.js +13 -0
  47. package/dist/entities/hit.d.ts +5 -0
  48. package/dist/entities/hit.js +5 -0
  49. package/dist/entities/index.d.ts +45 -2
  50. package/dist/entities/listener.d.ts +6 -0
  51. package/dist/entities/listener.js +1 -0
  52. package/dist/entities/meeting-findings.d.ts +1 -1
  53. package/dist/entities/meeting-preparation.d.ts +3 -0
  54. package/dist/entities/message-transfer.d.ts +16 -0
  55. package/dist/entities/message-transfer.js +6 -0
  56. package/dist/errors/reasons.js +1 -0
  57. package/dist/mcp/app-server.js +1 -0
  58. package/dist/mcp/http/oauth.d.ts +2 -1
  59. package/dist/mcp/http/oauth.js +2 -2
  60. package/dist/mcp/http/serve.d.ts +2 -0
  61. package/dist/mcp/http/serve.js +2 -1
  62. package/dist/mcp/meeting-preparation.d.ts +3 -0
  63. package/dist/mcp/server.js +1 -0
  64. package/dist/mcp/surface.js +1 -1
  65. package/dist/meetings/answers.d.ts +5 -0
  66. package/dist/meetings/answers.js +8 -0
  67. package/dist/meetings/cli/index.js +14 -0
  68. package/dist/meetings/findings-rules.d.ts +14 -3
  69. package/dist/meetings/findings-rules.js +16 -2
  70. package/dist/meetings/index.d.ts +5 -3
  71. package/dist/meetings/index.js +3 -1
  72. package/dist/meetings/ingest.js +2 -1
  73. package/dist/meetings/language.d.ts +5 -0
  74. package/dist/meetings/language.js +57 -0
  75. package/dist/meetings/mcp/search.d.ts +3 -0
  76. package/dist/meetings/meeting-context.d.ts +2 -3
  77. package/dist/meetings/meeting-context.js +3 -4
  78. package/dist/meetings/payment.d.ts +27 -0
  79. package/dist/meetings/payment.js +81 -0
  80. package/dist/meetings/project-evidence-answer.d.ts +3 -0
  81. package/dist/meetings/result-items.d.ts +9 -0
  82. package/dist/meetings/result-items.js +7 -0
  83. package/dist/meetings/store.d.ts +22 -0
  84. package/dist/meetings/testing/memory.js +3 -0
  85. package/dist/meetings/transcription.d.ts +2 -0
  86. package/dist/meetings/transcription.js +5 -1
  87. package/dist/search/lucene/types.d.ts +7 -0
  88. package/dist/search/lucene/types.js +11 -1
  89. package/dist/services/archive.d.ts +13 -0
  90. package/dist/services/archive.js +56 -5
  91. package/dist/services/deps.d.ts +3 -0
  92. package/dist/services/history-limits.d.ts +2 -0
  93. package/dist/services/index.d.ts +4 -1
  94. package/dist/services/index.js +4 -1
  95. package/dist/services/job-query.d.ts +5 -2
  96. package/dist/services/job-query.js +5 -2
  97. package/dist/services/link-scorers/alias-hits.d.ts +7 -0
  98. package/dist/services/link-scorers/alias-hits.js +54 -0
  99. package/dist/services/link-scorers/chat-senders.d.ts +6 -0
  100. package/dist/services/link-scorers/chat-senders.js +56 -0
  101. package/dist/services/link-scorers/chat-title.d.ts +6 -0
  102. package/dist/services/link-scorers/chat-title.js +44 -0
  103. package/dist/services/link-scorers/email-domain.d.ts +6 -0
  104. package/dist/services/link-scorers/email-domain.js +64 -0
  105. package/dist/services/link-scorers/index.d.ts +9 -0
  106. package/dist/services/link-scorers/index.js +8 -0
  107. package/dist/services/link-scorers/mail-thread.d.ts +3 -0
  108. package/dist/services/link-scorers/mail-thread.js +74 -0
  109. package/dist/services/link-scorers/member-chats.d.ts +3 -0
  110. package/dist/services/link-scorers/member-chats.js +40 -0
  111. package/dist/services/link-scorers/member-dialog.d.ts +3 -0
  112. package/dist/services/link-scorers/member-dialog.js +34 -0
  113. package/dist/services/link-scorers/scan.d.ts +41 -0
  114. package/dist/services/link-scorers/scan.js +244 -0
  115. package/dist/services/link-scorers/shared.d.ts +26 -0
  116. package/dist/services/link-scorers/shared.js +48 -0
  117. package/dist/services/link-scorers/types.d.ts +77 -0
  118. package/dist/services/link-scorers/types.js +1 -0
  119. package/dist/services/messages-archive-search.js +3 -2
  120. package/dist/services/messages-combined.js +2 -2
  121. package/dist/services/messages-discovery.js +2 -2
  122. package/dist/services/notes-search.js +4 -5
  123. package/dist/services/project-status/index.d.ts +71 -0
  124. package/dist/services/project-status/index.js +196 -0
  125. package/dist/services/project-status/items.d.ts +10 -0
  126. package/dist/services/project-status/items.js +23 -0
  127. package/dist/services/project-status/signals/amounts.d.ts +20 -0
  128. package/dist/services/project-status/signals/amounts.js +91 -0
  129. package/dist/services/project-status/signals/children.d.ts +13 -0
  130. package/dist/services/project-status/signals/children.js +28 -0
  131. package/dist/services/project-status/signals/coverage.d.ts +13 -0
  132. package/dist/services/project-status/signals/coverage.js +30 -0
  133. package/dist/services/project-status/signals/index.d.ts +3 -0
  134. package/dist/services/project-status/signals/index.js +21 -0
  135. package/dist/services/project-status/signals/last-message.d.ts +4 -0
  136. package/dist/services/project-status/signals/last-message.js +22 -0
  137. package/dist/services/project-status/signals/payments.d.ts +9 -0
  138. package/dist/services/project-status/signals/payments.js +24 -0
  139. package/dist/services/project-status/signals/promises.d.ts +17 -0
  140. package/dist/services/project-status/signals/promises.js +109 -0
  141. package/dist/services/project-status/signals/questions.d.ts +8 -0
  142. package/dist/services/project-status/signals/questions.js +38 -0
  143. package/dist/services/project-status/signals/reply-next.d.ts +19 -0
  144. package/dist/services/project-status/signals/reply-next.js +75 -0
  145. package/dist/services/project-status/signals/topics.d.ts +10 -0
  146. package/dist/services/project-status/signals/topics.js +50 -0
  147. package/dist/services/project-status/types.d.ts +84 -0
  148. package/dist/services/project-status/types.js +1 -0
  149. package/dist/services/proposals/index.d.ts +1 -0
  150. package/dist/services/proposals/index.js +1 -0
  151. package/dist/services/proposals/kinds/findings.d.ts +3 -0
  152. package/dist/services/proposals/kinds/findings.js +8 -1
  153. package/dist/services/search-all-meetings.d.ts +3 -0
  154. package/dist/services/search-all-meetings.js +6 -1
  155. package/dist/speech/local.d.ts +2 -0
  156. package/dist/speech/local.js +8 -1
  157. package/dist/speech/models.d.ts +2 -0
  158. package/dist/speech/models.js +2 -0
  159. package/dist/store/decisions.d.ts +5 -1
  160. package/dist/store/decisions.js +5 -1
  161. package/dist/store/driver.d.ts +11 -0
  162. package/dist/store/driver.js +13 -0
  163. package/dist/store/drivers/node-sqlite.d.ts +1 -1
  164. package/dist/store/drivers/node-sqlite.js +3 -0
  165. package/dist/store/evidence-findings.js +1 -0
  166. package/dist/store/index.d.ts +2 -1
  167. package/dist/store/index.js +1 -0
  168. package/dist/store/project-transfer/request.d.ts +1 -1
  169. package/dist/store/project-transfer/schema.d.ts +2 -2
  170. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  171. package/dist/store/sqlite/evidence-findings.js +5 -1
  172. package/dist/store/sqlite/facts.d.ts +13 -0
  173. package/dist/store/sqlite/facts.js +64 -6
  174. package/dist/store/sqlite/findings-core.js +14 -1
  175. package/dist/store/sqlite/knowledge.d.ts +16 -2
  176. package/dist/store/sqlite/knowledge.js +1 -0
  177. package/dist/store/sqlite/lucene.d.ts +1 -1
  178. package/dist/store/sqlite/lucene.js +33 -7
  179. package/dist/store/sqlite/meeting-hit-search.js +10 -3
  180. package/dist/store/sqlite/meeting-languages.d.ts +4 -0
  181. package/dist/store/sqlite/meeting-languages.js +36 -0
  182. package/dist/store/sqlite/meetings.d.ts +2 -1
  183. package/dist/store/sqlite/meetings.js +9 -3
  184. package/dist/store/sqlite/note-search.js +2 -2
  185. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  186. package/dist/store/sqlite/relation-proposals.d.ts +29 -2
  187. package/dist/store/sqlite/relation-proposals.js +75 -4
  188. package/dist/store/store.d.ts +1 -0
  189. package/package.json +3 -3
  190. package/parity.json +11 -0
@@ -0,0 +1,30 @@
1
+ /** What the store holds of a chat it does not hold whole; the answer adds the line that syncs it. */
2
+ export const coverage = {
3
+ id: "coverage",
4
+ version: 1,
5
+ scope: "chat",
6
+ read: async (context, chat) => {
7
+ if (!chat || chat.coverage.completeness.state === "complete")
8
+ return [];
9
+ const { completeness, held } = chat.coverage;
10
+ const oldest = held.oldestAt === null ? null : Date.parse(held.oldestAt);
11
+ return [
12
+ {
13
+ signal: "coverage",
14
+ chat: chat.ref,
15
+ at: completeness.fetchedAt,
16
+ who: null,
17
+ evidence: [],
18
+ basis: "store",
19
+ data: {
20
+ state: completeness.state,
21
+ held,
22
+ beforeHeld: completeness.state !== "complete" && (oldest === null || context.since < oldest),
23
+ },
24
+ },
25
+ ];
26
+ },
27
+ attention: (items) => items.flatMap((item) => item.chat !== null && item.data.state !== "complete" && item.data.beforeHeld
28
+ ? [{ chat: item.chat, weight: 3, why: "held in part" }]
29
+ : []),
30
+ };
@@ -0,0 +1,3 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ /** The registry; its order is the answer's order. A new signal is one file here and its row in the CLI. */
3
+ export declare const STATUS_SIGNALS: readonly StatusSignal[];
@@ -0,0 +1,21 @@
1
+ import { amounts } from "./amounts.js";
2
+ import { children } from "./children.js";
3
+ import { coverage } from "./coverage.js";
4
+ import { lastMessage } from "./last-message.js";
5
+ import { payments } from "./payments.js";
6
+ import { promises } from "./promises.js";
7
+ import { questions } from "./questions.js";
8
+ import { replyNext } from "./reply-next.js";
9
+ import { topics } from "./topics.js";
10
+ /** The registry; its order is the answer's order. A new signal is one file here and its row in the CLI. */
11
+ export const STATUS_SIGNALS = [
12
+ lastMessage,
13
+ replyNext,
14
+ questions,
15
+ promises,
16
+ amounts,
17
+ payments,
18
+ topics,
19
+ coverage,
20
+ children,
21
+ ];
@@ -0,0 +1,4 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export type LastMessageData = Record<string, never>;
3
+ /** The newest stored message in the window: when, and who wrote it. A chat held in part says so through `coverage`. */
4
+ export declare const lastMessage: StatusSignal<LastMessageData>;
@@ -0,0 +1,22 @@
1
+ import { evidenceOf, whoOf } from "../items.js";
2
+ /** The newest stored message in the window: when, and who wrote it. A chat held in part says so through `coverage`. */
3
+ export const lastMessage = {
4
+ id: "last-message",
5
+ version: 1,
6
+ scope: "chat",
7
+ read: async (_context, chat) => {
8
+ const last = chat?.messages.at(-1);
9
+ if (!chat || !last)
10
+ return [];
11
+ const item = {
12
+ signal: "last-message",
13
+ chat: chat.ref,
14
+ at: last.timestamp,
15
+ who: whoOf(last),
16
+ evidence: evidenceOf(chat, last),
17
+ basis: "store",
18
+ data: {},
19
+ };
20
+ return [item];
21
+ },
22
+ };
@@ -0,0 +1,9 @@
1
+ import { type Payment } from "../../../meetings/index.js";
2
+ import type { StatusSignal } from "../types.js";
3
+ export interface PaymentsData extends Payment {
4
+ title: string;
5
+ /** The fact's review status: proposed or accepted. */
6
+ status: string;
7
+ }
8
+ /** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
9
+ export declare const payments: StatusSignal<PaymentsData>;
@@ -0,0 +1,24 @@
1
+ import { paymentTitle } from "../../../meetings/index.js";
2
+ const HOLDING = new Set(["proposed", "accepted"]);
3
+ /** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
4
+ export const payments = {
5
+ id: "payments",
6
+ version: 2,
7
+ scope: "project",
8
+ read: async (context) => {
9
+ const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
10
+ const facts = (await Promise.all(projects.map((project) => context.store.facts.list({ project, type: "payment", limit: 500 })))).flat();
11
+ return facts
12
+ .filter((fact) => fact.payment !== null && HOLDING.has(fact.status))
13
+ .map((fact) => ({
14
+ signal: "payments",
15
+ chat: null,
16
+ at: fact.occurredAt ?? fact.createdAt,
17
+ who: null,
18
+ // The fact first, then what it cites, so an amount read from a cited message is not counted twice.
19
+ evidence: [fact.ref, ...fact.evidence].map((ref) => ({ ref, fingerprint: null })),
20
+ basis: "fact",
21
+ data: { ...fact.payment, title: paymentTitle(fact), status: fact.status },
22
+ }));
23
+ },
24
+ };
@@ -0,0 +1,17 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export interface PromisesData {
3
+ /** Where it came from: a stored task, a recorded fact, or a word match in a message (a guess). */
4
+ source: "task" | "fact" | "pattern";
5
+ /** `promise` or `request` for a task; a fact's and a pattern's are promises. */
6
+ kind: string;
7
+ title: string;
8
+ /** Who promised, for a pattern: the owner (`yours`) or the other side. */
9
+ side: "yours" | "theirs" | null;
10
+ due: string | null;
11
+ status: string;
12
+ }
13
+ /**
14
+ * Open promise and request tasks of the project or from its chats, promise facts proposed or accepted, and up to
15
+ * three promises per chat found by first person future words ("сделаю", "I'll send"), marked as patterns.
16
+ */
17
+ export declare const promises: StatusSignal<PromisesData>;
@@ -0,0 +1,109 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { isLocator, parseLocator } from "../../../domain/locator.js";
3
+ import { PROMISE_FORMS } from "../../../domain/status-words.js";
4
+ import { dueDayOf } from "../../../search/task-signature.js";
5
+ import { normalize } from "../../../store/normalize.js";
6
+ import { inboxKey } from "../../../store/store.js";
7
+ import { taskAccount } from "../../task-rules.js";
8
+ import { day, evidenceOf, excerpt, fromOwner, whoOf } from "../items.js";
9
+ const PATTERNS_PER_CHAT = 3;
10
+ const DAY = 86_400_000;
11
+ const KINDS = new Set(["promise", "request"]);
12
+ const OPEN = new Set(["open", "todo", "in_progress"]);
13
+ const chatOfSource = (context, source) => {
14
+ if (source === null || !isLocator(source))
15
+ return undefined;
16
+ const { provider, account, chat } = parseLocator(source);
17
+ return context.chats.find((one) => one.account.provider === provider && one.account.account === account && one.chatId === chat);
18
+ };
19
+ // An account that never had a task has no inbox project yet.
20
+ const orNone = (error) => {
21
+ if (error instanceof CliError && error.code === "not_found")
22
+ return [];
23
+ throw error;
24
+ };
25
+ const tasksOf = async (context) => {
26
+ const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
27
+ const accounts = new Map(context.chats.map((chat) => [taskAccount(chat.account), chat.account]));
28
+ const inboxes = [...accounts.keys()].map(inboxKey);
29
+ const found = await Promise.all([...projects, ...inboxes].map((project) => context.store.tasks.find({ project, limit: 500 }).catch(orNone)));
30
+ const projectRefs = new Set(projects);
31
+ return found
32
+ .flat()
33
+ .filter((task) => KINDS.has(task.type) && OPEN.has(task.status))
34
+ .filter((task) => projectRefs.has(task.project) || chatOfSource(context, task.source) !== undefined);
35
+ };
36
+ const taskItem = (context, task) => ({
37
+ signal: "promises",
38
+ chat: chatOfSource(context, task.source)?.ref ?? null,
39
+ at: task.createdAt,
40
+ who: null,
41
+ evidence: [{ ref: task.ref, fingerprint: null }, ...(task.source ? [{ ref: task.source, fingerprint: null }] : [])],
42
+ basis: "task",
43
+ data: {
44
+ source: "task",
45
+ kind: task.type,
46
+ title: excerpt(task.title),
47
+ side: null,
48
+ due: task.dueAt === null ? null : day(task.dueAt),
49
+ status: task.status,
50
+ },
51
+ });
52
+ const patternItems = (chat) => chat.messages
53
+ .filter((message) => !message.text.includes("?") && PROMISE_FORMS.test(message.text))
54
+ .slice(-PATTERNS_PER_CHAT)
55
+ .reverse()
56
+ .map((message) => {
57
+ const due = dueDayOf(normalize(message.text), Date.parse(message.timestamp));
58
+ return {
59
+ signal: "promises",
60
+ chat: chat.ref,
61
+ at: message.timestamp,
62
+ who: whoOf(message),
63
+ evidence: evidenceOf(chat, message),
64
+ basis: "pattern",
65
+ data: {
66
+ source: "pattern",
67
+ kind: "promise",
68
+ title: excerpt(message.text),
69
+ side: fromOwner(message) ? "yours" : "theirs",
70
+ due: due === null ? null : day(due * DAY),
71
+ status: "open",
72
+ },
73
+ };
74
+ });
75
+ /**
76
+ * Open promise and request tasks of the project or from its chats, promise facts proposed or accepted, and up to
77
+ * three promises per chat found by first person future words ("сделаю", "I'll send"), marked as patterns.
78
+ */
79
+ export const promises = {
80
+ id: "promises",
81
+ version: 1,
82
+ scope: "project",
83
+ read: async (context) => {
84
+ const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
85
+ const facts = (await Promise.all(projects.map((project) => context.store.facts.list({ type: "promise", project }))))
86
+ .flat()
87
+ .filter((fact) => fact.status === "proposed" || fact.status === "accepted");
88
+ return [
89
+ ...(await tasksOf(context)).map((task) => taskItem(context, task)),
90
+ ...facts.map((fact) => ({
91
+ signal: "promises",
92
+ chat: null,
93
+ at: fact.occurredAt ?? fact.createdAt,
94
+ who: null,
95
+ evidence: [{ ref: fact.ref, fingerprint: null }],
96
+ basis: "fact",
97
+ data: {
98
+ source: "fact",
99
+ kind: "promise",
100
+ title: excerpt(fact.statement),
101
+ side: null,
102
+ due: null,
103
+ status: fact.status,
104
+ },
105
+ })),
106
+ ...context.chats.flatMap(patternItems),
107
+ ];
108
+ },
109
+ };
@@ -0,0 +1,8 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export interface QuestionsData {
3
+ /** `theirs`: someone asked and the owner has not answered; `yours`: the owner asked and nobody answered. */
4
+ side: "theirs" | "yours";
5
+ excerpt: string;
6
+ }
7
+ /** Open questions both ways, newest first, at most five a side. A question answered in another chat stays open. */
8
+ export declare const questions: StatusSignal<QuestionsData>;
@@ -0,0 +1,38 @@
1
+ import { questions as questionsOf } from "../../questions.js";
2
+ import { evidenceOf, excerpt, whoOf } from "../items.js";
3
+ const PER_SIDE = 5;
4
+ const WEEK = 7 * 86_400_000;
5
+ const NOBODY = new Set();
6
+ /** Open questions both ways, newest first, at most five a side. A question answered in another chat stays open. */
7
+ export const questions = {
8
+ id: "questions",
9
+ version: 1,
10
+ scope: "chat",
11
+ read: async (_context, chat) => {
12
+ if (!chat)
13
+ return [];
14
+ const open = (asker) => questionsOf(chat.messages, { answerers: NOBODY, asker })
15
+ .filter(({ answer }) => answer === undefined)
16
+ .map(({ question }) => question)
17
+ .reverse()
18
+ .slice(0, PER_SIDE);
19
+ return [
20
+ ["theirs", open("others")],
21
+ ["yours", open("owner")],
22
+ ].flatMap(([side, asked]) => asked.map((question) => ({
23
+ signal: "questions",
24
+ chat: chat.ref,
25
+ at: question.timestamp,
26
+ who: whoOf(question),
27
+ evidence: evidenceOf(chat, question),
28
+ basis: "store",
29
+ data: { side, excerpt: excerpt(question.text) },
30
+ })));
31
+ },
32
+ attention: (items, context) => {
33
+ const stale = new Set(items.flatMap((item) => item.chat !== null && item.data.side === "yours" && item.at && context.now - Date.parse(item.at) > WEEK
34
+ ? [item.chat]
35
+ : []));
36
+ return [...stale].map((chat) => ({ chat, weight: 1, why: "your question unanswered for a week" }));
37
+ },
38
+ };
@@ -0,0 +1,19 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export interface ReplyNextData {
3
+ next: "owner" | "them" | "named" | "nobody";
4
+ /** Since when the one named has been the one to answer. */
5
+ waitingSince: string | null;
6
+ /** In a group: the person the newest message replies to or mentions. */
7
+ addressed: {
8
+ ref: string | null;
9
+ name: string | null;
10
+ } | null;
11
+ /** Waited longer than `tasks agenda` does before it lists the chat (a day for you, a week for them). */
12
+ overdue: boolean;
13
+ }
14
+ /**
15
+ * Whose move it is, by five rules in order: the owner wrote last → them; a dialog → the owner; in a group a message
16
+ * that replies to or mentions the owner, or a question right after the owner spoke → the owner; one that names
17
+ * another member → that member; else nobody. A name typed without a mention is not seen.
18
+ */
19
+ export declare const replyNext: StatusSignal<ReplyNextData>;
@@ -0,0 +1,75 @@
1
+ import { AGENDA_DEFAULTS } from "../../agenda.js";
2
+ import { evidenceOf, fromOwner, whoOf } from "../items.js";
3
+ /** Whom a message answers or names: the quoted message's sender first, then the first mention. */
4
+ const targetOf = (message, chat, owners) => {
5
+ const byId = (id) => chat.messages.find((one) => one.id === id);
6
+ const quoted = message.replyTo ?? (message.replyToId === undefined ? undefined : byId(message.replyToId));
7
+ if (quoted)
8
+ return {
9
+ owner: quoted.outgoing === true || (quoted.senderId !== null && owners.has(quoted.senderId)),
10
+ id: quoted.senderId,
11
+ name: quoted.senderName,
12
+ };
13
+ const [mentioned] = message.mentions ?? [];
14
+ if (mentioned === undefined)
15
+ return undefined;
16
+ if (owners.has(mentioned))
17
+ return { owner: true, id: mentioned, name: null };
18
+ const named = chat.messages.find((one) => one.senderId === mentioned);
19
+ return { owner: false, id: mentioned, name: named?.senderName ?? null };
20
+ };
21
+ const asksSomething = (message) => message.text.includes("?");
22
+ /**
23
+ * Whose move it is, by five rules in order: the owner wrote last → them; a dialog → the owner; in a group a message
24
+ * that replies to or mentions the owner, or a question right after the owner spoke → the owner; one that names
25
+ * another member → that member; else nobody. A name typed without a mention is not seen.
26
+ */
27
+ export const replyNext = {
28
+ id: "reply-next",
29
+ version: 1,
30
+ scope: "chat",
31
+ read: async (context, chat) => {
32
+ const last = chat?.messages.at(-1);
33
+ if (!chat || !last)
34
+ return [];
35
+ const { messages } = chat;
36
+ const owners = new Set(messages.flatMap((one) => (fromOwner(one) && one.senderId !== null ? [one.senderId] : [])));
37
+ const lastOwn = messages.findLastIndex(fromOwner);
38
+ const firstAfterOwn = messages[lastOwn + 1] ?? messages[0];
39
+ const target = targetOf(last, chat, owners);
40
+ const decide = () => {
41
+ if (last.outgoing === null)
42
+ return { next: "nobody", addressed: null, since: null };
43
+ if (fromOwner(last))
44
+ return {
45
+ next: "them",
46
+ addressed: chat.kind !== "dialog" && target && !target.owner ? { ref: null, name: target.name } : null,
47
+ since: last.timestamp,
48
+ };
49
+ const since = firstAfterOwn?.timestamp ?? last.timestamp;
50
+ if (chat.kind === "dialog")
51
+ return { next: "owner", addressed: null, since };
52
+ if (target?.owner || (asksSomething(last) && lastOwn === messages.length - 2))
53
+ return { next: "owner", addressed: null, since };
54
+ if (target)
55
+ return { next: "named", addressed: { ref: null, name: target.name }, since };
56
+ return { next: "nobody", addressed: null, since };
57
+ };
58
+ const { next, addressed, since } = decide();
59
+ const waited = since === null ? 0 : context.now - Date.parse(since);
60
+ const overdue = (next === "owner" && waited >= AGENDA_DEFAULTS.unanswered) ||
61
+ (next === "them" && waited >= AGENDA_DEFAULTS.waiting);
62
+ return [
63
+ {
64
+ signal: "reply-next",
65
+ chat: chat.ref,
66
+ at: last.timestamp,
67
+ who: whoOf(last),
68
+ evidence: evidenceOf(chat, last),
69
+ basis: "store",
70
+ data: { next, waitingSince: since, addressed, overdue },
71
+ },
72
+ ];
73
+ },
74
+ attention: (items) => items.flatMap((item) => item.chat !== null && item.data.next === "owner" ? [{ chat: item.chat, weight: 2, why: "your move" }] : []),
75
+ };
@@ -0,0 +1,10 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export interface TopicsData {
3
+ query: string;
4
+ yoursLast: string | null;
5
+ theirsLast: string | null;
6
+ /** Their newest mention, when the owner spoke of it later: the other side went quiet on it. */
7
+ silentSince: string | null;
8
+ }
9
+ /** Per `--context-query`, the newest message of each side that holds every one of its words, in the window. */
10
+ export declare const topics: StatusSignal<TopicsData>;
@@ -0,0 +1,50 @@
1
+ import { normalize } from "../../../store/normalize.js";
2
+ import { evidenceOf, fromOwner } from "../items.js";
3
+ const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
+ const wholeWord = (word) => new RegExp(`(?<![\\p{L}\\p{N}])${escaped(word)}(?![\\p{L}\\p{N}])`, "u");
5
+ /** Every word of the query, folded, as a whole word somewhere in the text: `projects evidence`'s reading. */
6
+ const matcherOf = (query) => {
7
+ const words = normalize(query).split(" ").filter(Boolean).map(wholeWord);
8
+ return (text) => {
9
+ const folded = normalize(text);
10
+ return words.length > 0 && words.every((word) => word.test(folded));
11
+ };
12
+ };
13
+ /** Per `--context-query`, the newest message of each side that holds every one of its words, in the window. */
14
+ export const topics = {
15
+ id: "topics",
16
+ version: 1,
17
+ scope: "chat",
18
+ read: async (context, chat) => {
19
+ if (!chat)
20
+ return [];
21
+ return context.contextQueries.flatMap((query) => {
22
+ const matches = matcherOf(query);
23
+ const naming = chat.messages.filter((message) => matches(message.text));
24
+ const yours = naming.findLast(fromOwner);
25
+ const theirs = naming.findLast((message) => !fromOwner(message));
26
+ if (!yours && !theirs)
27
+ return [];
28
+ const silent = theirs && yours && Date.parse(yours.timestamp) > Date.parse(theirs.timestamp);
29
+ const newest = [yours, theirs]
30
+ .filter((one) => one !== undefined)
31
+ .sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
32
+ return [
33
+ {
34
+ signal: "topics",
35
+ chat: chat.ref,
36
+ at: newest[0]?.timestamp ?? null,
37
+ who: null,
38
+ evidence: evidenceOf(chat, ...newest),
39
+ basis: "store",
40
+ data: {
41
+ query,
42
+ yoursLast: yours?.timestamp ?? null,
43
+ theirsLast: theirs?.timestamp ?? null,
44
+ silentSince: silent ? theirs.timestamp : null,
45
+ },
46
+ },
47
+ ];
48
+ });
49
+ },
50
+ };
@@ -0,0 +1,84 @@
1
+ import type { ChatKind, Id, Message } from "../../domain/models.js";
2
+ import type { AccountKey, MessageStore } from "../../store/store.js";
3
+ import type { ChatCoverage } from "../chat-coverage.js";
4
+ /** What every chat signal reads; loaded once per chat by the pipeline. Never printed. */
5
+ export interface StatusChat {
6
+ /** `chat:<provider>/<account>/<chat>` */
7
+ ref: string;
8
+ account: AccountKey;
9
+ chatId: Id;
10
+ title: string | null;
11
+ kind: ChatKind;
12
+ /** The link's role: "client", "contractor", or null. */
13
+ role: string | null;
14
+ /** The chat's purpose in the project; `work` until links carry one. */
15
+ purpose: string;
16
+ /** `project:<id>`; a child's id when rolled up. */
17
+ project: string;
18
+ coverage: ChatCoverage;
19
+ /** Oldest first, the window, at most `STATUS_WINDOW`. */
20
+ messages: Message[];
21
+ }
22
+ export interface StatusProject {
23
+ ref: string;
24
+ key: string | null;
25
+ name: string;
26
+ }
27
+ export interface StatusChild extends StatusProject {
28
+ parent: string;
29
+ depth: number;
30
+ }
31
+ export interface StatusContext {
32
+ now: number;
33
+ /** Window start, ms. */
34
+ since: number;
35
+ chats: StatusChat[];
36
+ project: StatusProject & {
37
+ children: StatusChild[];
38
+ /** Children not read: too deep, seen twice, or past the limit. */
39
+ cut: {
40
+ ref: string;
41
+ reason: "depth" | "cycle" | "limit";
42
+ }[];
43
+ };
44
+ /** Read only: facts, tasks, chats. */
45
+ store: MessageStore;
46
+ /** `--context-query` texts: every word of one must appear in one message. */
47
+ contextQueries: string[];
48
+ }
49
+ export interface StatusWho {
50
+ ref: string | null;
51
+ name: string | null;
52
+ owner: boolean;
53
+ }
54
+ /** One finding of a signal. Stable keys; the signal adds its own under `data`. */
55
+ export interface StatusItem<D = Record<string, unknown>> {
56
+ signal: string;
57
+ /** Null for a project level finding. */
58
+ chat: string | null;
59
+ /** ISO, when it happened. */
60
+ at: string | null;
61
+ who: StatusWho | null;
62
+ /** `msg:`, `fact:`, `task:` refs. */
63
+ evidence: {
64
+ ref: string;
65
+ fingerprint: string | null;
66
+ }[];
67
+ /** How sure: `pattern` is a word match, to check before it is trusted. */
68
+ basis: "store" | "fact" | "task" | "pattern";
69
+ data: D;
70
+ }
71
+ export interface StatusAttention {
72
+ chat: string;
73
+ weight: number;
74
+ why: string;
75
+ }
76
+ export interface StatusSignal<D = Record<string, unknown>> {
77
+ id: string;
78
+ /** Bumped when the logic changes, so a saved answer says which rules made it. */
79
+ version: number;
80
+ scope: "chat" | "project";
81
+ read(context: StatusContext, chat?: StatusChat): Promise<StatusItem<D>[]>;
82
+ /** Why a chat should be read next, weight 0 to 3; the pipeline sums them per chat. */
83
+ attention?(items: StatusItem<D>[], context: StatusContext): StatusAttention[];
84
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
1
  export { parseProposalIds } from "./ids.js";
2
+ export { FACT_SUMMARIES } from "./kinds/findings.js";
2
3
  export { emptyProposalRegistry, proposalRegistry } from "./registry.js";
3
4
  export type { Decided, Decision, ProposalFilter, ProposalKind, ProposalRegistry, ProposalRow } from "./types.js";
@@ -1,2 +1,3 @@
1
1
  export { parseProposalIds } from "./ids.js";
2
+ export { FACT_SUMMARIES } from "./kinds/findings.js";
2
3
  export { emptyProposalRegistry, proposalRegistry } from "./registry.js";
@@ -1,5 +1,8 @@
1
+ import type { Finding } from "../../../meetings/index.js";
1
2
  import { type MessageStore } from "../../../store/index.js";
2
3
  import type { ProposalKind } from "../types.js";
4
+ /** A finding's one line in the inbox, per kind; a new fact kind adds its row here. */
5
+ export declare const FACT_SUMMARIES: Record<string, (finding: Finding) => string>;
3
6
  /**
4
7
  * An agent's batch of cited facts and tasks from stored messages, mail and documents. Confirming accepts the parts
5
8
  * named, or every finding not rejected before; the rest are rejected. Meeting findings stay with their meeting.
@@ -1,5 +1,12 @@
1
+ import { paymentTitle } from "../../../meetings/index.js";
1
2
  import { EVIDENCE_FINDINGS_KIND } from "../../../store/index.js";
2
3
  import { kept, proposerOfAuthor } from "./shared.js";
4
+ /** A finding's one line in the inbox, per kind; a new fact kind adds its row here. */
5
+ export const FACT_SUMMARIES = {
6
+ fact: (finding) => finding.statement,
7
+ task: (finding) => finding.title,
8
+ payment: (finding) => paymentTitle(finding),
9
+ };
3
10
  const statusOf = (batch) => batch.status === "proposed" ? "proposed" : batch.receipt?.accepted.length ? "confirmed" : "rejected";
4
11
  const rowOf = (batch, createdAt) => {
5
12
  const findings = batch.input.findings;
@@ -20,7 +27,7 @@ const rowOf = (batch, createdAt) => {
20
27
  parts: findings.map((finding) => ({
21
28
  id: finding.id,
22
29
  kind: finding.kind,
23
- summary: finding.kind === "fact" ? finding.statement : finding.title,
30
+ summary: FACT_SUMMARIES[finding.kind]?.(finding) ?? finding.kind,
24
31
  outcome: outcome(finding.id),
25
32
  })),
26
33
  createdAt,
@@ -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
  });
@@ -5,6 +5,8 @@ export interface LocalSpeechRecognitionModel {
5
5
  engine: "sherpa-onnx";
6
6
  model: string;
7
7
  version: string;
8
+ /** The one language the model writes; absent for a multilingual model. */
9
+ language?: string;
8
10
  sampleRate: 16000;
9
11
  maxChunkSamples: 480000;
10
12
  }
@@ -23,7 +23,14 @@ export const localSpeechRecognitionModel = (id) => {
23
23
  featureDim: model.featureDim,
24
24
  }))
25
25
  .digest("hex");
26
- return Object.freeze({ engine: "sherpa-onnx", model: model.id, version, sampleRate: 16000, maxChunkSamples: 480000 });
26
+ return Object.freeze({
27
+ engine: "sherpa-onnx",
28
+ model: model.id,
29
+ version,
30
+ ...(model.language === undefined ? {} : { language: model.language }),
31
+ sampleRate: 16000,
32
+ maxChunkSamples: 480000,
33
+ });
27
34
  };
28
35
  const notInstalled = (id) => new CliError("not_found", `speech model ${id} is not installed; download it explicitly with a WireCat messenger's models download command`, { reason: "model.not-downloaded", model: id });
29
36
  /**
@@ -18,6 +18,8 @@ export interface SpeechModel {
18
18
  id: string;
19
19
  title: string;
20
20
  languages: string;
21
+ /** The one language the model writes, so its transcripts need no guessing; absent for multilingual models. */
22
+ language?: string;
21
23
  files: ModelFile[];
22
24
  /** The part of sherpa-onnx's `modelConfig` that names the files, given where they are. */
23
25
  config: (path: (name: string) => string) => Record<string, unknown>;