@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,77 @@
1
+ import type { MessageStore } from "../../store/store.js";
2
+ import type { AccountKey } from "../../store/types.js";
3
+ import type { ProposalRow } from "../proposals/types.js";
4
+ /** One scorer's reason to link a chat, a mail thread or a person to a project, or a person to a person. */
5
+ export interface LinkCandidate {
6
+ /** `chat:…` (a mail thread is `chat:email/…`) or `person:…`. */
7
+ from: string;
8
+ /** `project:…`, or `person:…` for a person ↔ person rule. */
9
+ to: string;
10
+ kind: "member-of" | "related-to";
11
+ /** 0..1. */
12
+ weight: number;
13
+ /** Ids and counts only, never message text. */
14
+ why: string;
15
+ evidence: string[];
16
+ /**
17
+ * The `contact:<provider>/<id>` a person candidate was found by. A person candidate without one, or with one that
18
+ * is not that person's, is refused: people are matched by identity, never by name.
19
+ */
20
+ identity?: string;
21
+ /** What the chat is for in the project, one of the configured purposes; absent means the default, `work`. */
22
+ purpose?: string;
23
+ }
24
+ export interface ScannedProject {
25
+ ref: string;
26
+ name: string;
27
+ key: string | null;
28
+ aliases: string[];
29
+ }
30
+ export interface ScorerInput {
31
+ store: MessageStore;
32
+ /** `null` for a scan with no project, which only person ↔ person rules answer. */
33
+ project: ScannedProject | null;
34
+ confirmed: {
35
+ chats: string[];
36
+ people: string[];
37
+ };
38
+ /** Explicit; never a default account. */
39
+ sources: AccountKey[];
40
+ /** ISO 8601: the start of the window message signals count in. */
41
+ since: string;
42
+ /** The purposes the host's config lists; a candidate's purpose outside them is dropped. */
43
+ purposes: readonly string[];
44
+ /** The owner's `person:` reference, never a candidate. */
45
+ owner: string | null;
46
+ signal?: AbortSignal;
47
+ }
48
+ export interface LinkScorer {
49
+ readonly id: string;
50
+ readonly version: number;
51
+ candidates(input: ScorerInput): Promise<LinkCandidate[]>;
52
+ }
53
+ export type Signal = NonNullable<ProposalRow["signals"]>[number];
54
+ export interface ScannedProposal extends LinkCandidate {
55
+ score: number;
56
+ signals: Signal[];
57
+ /** The stored proposal; `null` in a dry run for a pair nothing holds yet. */
58
+ id: string | null;
59
+ state: "new" | "updated" | "unchanged" | "dry-run";
60
+ }
61
+ export interface ScanResult {
62
+ project: string | null;
63
+ proposals: ScannedProposal[];
64
+ skipped: {
65
+ /** A link already joins the pair. */
66
+ confirmed: number;
67
+ /** The owner (or an agent for the owner) rejected the pair once; it is never proposed again. */
68
+ rejected: number;
69
+ belowThreshold: number;
70
+ /** Over the limit of new proposals for one run, lowest scores first. */
71
+ overLimit: number;
72
+ /** A person candidate with no identity behind it. */
73
+ nameOnly: number;
74
+ /** An end the store does not hold, or a chat outside the sources. */
75
+ unresolved: number;
76
+ };
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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,71 @@
1
+ import type { AccountKey, MessageStore } from "../../store/store.js";
2
+ import { STATUS_SIGNALS } from "./signals/index.js";
3
+ import type { StatusAttention, StatusChat, StatusItem, StatusProject, StatusSignal } from "./types.js";
4
+ export type * from "./types.js";
5
+ export { STATUS_SIGNALS };
6
+ /** Messages read per chat; the newest of the window. */
7
+ export declare const STATUS_WINDOW = 300;
8
+ /** Linked chats read at most, as `projects evidence` reads. */
9
+ 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
+ export interface ProjectStatusInput {
13
+ /** An id, key, name or alias, as `canonicalProjectRef` takes it. */
14
+ project: string;
15
+ /** tg and max: the profile's own; left out, every account the links name. */
16
+ accounts?: AccountKey[];
17
+ days?: number;
18
+ after?: number;
19
+ asOf?: number;
20
+ contextQueries?: string[];
21
+ /** The chat purposes read; `all` reads every one. Default `DEFAULT_PURPOSES`. */
22
+ purposes?: readonly string[];
23
+ /** Default `STATUS_SIGNALS`; tests pass their own. */
24
+ signals?: readonly StatusSignal[];
25
+ }
26
+ export type StatusChatSummary = Omit<StatusChat, "messages" | "account">;
27
+ export interface MoneyTotal {
28
+ direction: string;
29
+ currency: string | null;
30
+ amount: string;
31
+ rows: number;
32
+ }
33
+ export interface ProjectStatus {
34
+ project: StatusProject;
35
+ children: (StatusProject & {
36
+ chats: number;
37
+ })[];
38
+ window: {
39
+ since: string;
40
+ until: string;
41
+ };
42
+ /** Every account a read chat is in, `provider/account`. */
43
+ accounts: string[];
44
+ chats: StatusChatSummary[];
45
+ /** Every signal's items, in signal order, then chat order. */
46
+ items: StatusItem[];
47
+ /** Open amounts and expected payment facts, summed per direction and currency. */
48
+ money: {
49
+ open: MoneyTotal[];
50
+ };
51
+ outOfScope: {
52
+ chat: string;
53
+ reason: "other-account" | "linked-chat-limit" | "purpose";
54
+ purpose?: string;
55
+ }[];
56
+ failed: {
57
+ signal: string;
58
+ why: string;
59
+ }[];
60
+ /** Chats worth reading next, highest first. */
61
+ attention: StatusAttention[];
62
+ signals: {
63
+ id: string;
64
+ version: number;
65
+ }[];
66
+ }
67
+ /**
68
+ * Where a project stands, from the store alone: its linked chats and its children's, each read once, then every
69
+ * signal on its own. A signal that fails is named in `failed` and the others still answer. No model call.
70
+ */
71
+ export declare const projectStatus: (store: MessageStore, input: ProjectStatusInput) => Promise<ProjectStatus>;
@@ -0,0 +1,196 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { formatReference } from "../../domain/references.js";
3
+ import { chatCoverage } from "../chat-coverage.js";
4
+ import { canonicalProjectRef, linkedChatsOf } from "../stored-project-gathering.js";
5
+ import { centsText } from "./signals/amounts.js";
6
+ import { STATUS_SIGNALS } from "./signals/index.js";
7
+ export { STATUS_SIGNALS };
8
+ const DAY = 86_400_000;
9
+ /** Messages read per chat; the newest of the window. */
10
+ export const STATUS_WINDOW = 300;
11
+ /** Linked chats read at most, as `projects evidence` reads. */
12
+ export const STATUS_CHATS = 16;
13
+ const CHILD_DEPTH = 2;
14
+ 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
+ const projectsCatalog = async (store) => new Map((await store.knowledge.vocabulary({ kind: "project" })).entries.map((entry) => [`project:${entry.id}`, entry]));
18
+ /** Confirmed `member-of` links from another project; depth two, eight at most, a project seen twice skipped. */
19
+ const childrenOf = async (store, root, catalog) => {
20
+ const children = [];
21
+ const cut = [];
22
+ const seen = new Set([root.ref]);
23
+ let level = [root.ref];
24
+ for (let depth = 1; level.length > 0; depth++) {
25
+ const below = [];
26
+ for (const parent of level)
27
+ for (const relation of await store.knowledge.relations(null, parent)) {
28
+ if (!relation.confirmed || relation.kind !== "member-of" || relation.to !== parent)
29
+ continue;
30
+ if (!relation.from.startsWith("project:"))
31
+ continue;
32
+ const ref = relation.from;
33
+ const entry = catalog.get(ref);
34
+ if (!entry)
35
+ continue;
36
+ if (seen.has(ref)) {
37
+ if (!cut.some((one) => one.ref === ref) && !children.some((one) => one.ref === ref))
38
+ cut.push({ ref, reason: "cycle" });
39
+ continue;
40
+ }
41
+ seen.add(ref);
42
+ if (depth > CHILD_DEPTH)
43
+ cut.push({ ref, reason: "depth" });
44
+ else if (children.length >= CHILDREN)
45
+ cut.push({ ref, reason: "limit" });
46
+ else {
47
+ children.push({ ref, key: entry.key, name: entry.name, parent, depth });
48
+ below.push(ref);
49
+ }
50
+ }
51
+ level = below;
52
+ }
53
+ return { children, cut };
54
+ };
55
+ const inAccounts = (accounts, provider, account) => accounts === undefined || accounts.some((one) => one.provider === provider && one.account === account);
56
+ const failure = (error) => (error instanceof Error ? error.message : String(error));
57
+ /** An open ask, or an expected payment fact; an ask that a payment fact cites is counted once, as the fact. */
58
+ const counted = (items) => {
59
+ const recorded = new Set(items.filter((item) => item.signal === "payments").flatMap((item) => item.evidence.slice(1).map((one) => one.ref)));
60
+ return (item, state) => item.signal === "payments"
61
+ ? state === "expected"
62
+ : item.signal === "amounts" && state === "open" && !item.evidence.some((one) => recorded.has(one.ref));
63
+ };
64
+ const totalsOf = (items) => {
65
+ const totals = new Map();
66
+ const open = counted(items);
67
+ for (const item of items) {
68
+ const data = item.data;
69
+ if (!open(item, data.state) || data.amount === undefined)
70
+ continue;
71
+ const key = `${data.direction}|${data.currency ?? ""}`;
72
+ const total = totals.get(key) ?? {
73
+ direction: data.direction ?? "unknown",
74
+ currency: data.currency ?? null,
75
+ amount: "0.00",
76
+ rows: 0,
77
+ cents: 0,
78
+ };
79
+ total.cents += Math.round(Number(data.amount) * 100);
80
+ total.rows += 1;
81
+ totals.set(key, total);
82
+ }
83
+ return [...totals.values()].map(({ cents, ...total }) => ({ ...total, amount: centsText(cents) }));
84
+ };
85
+ /**
86
+ * Where a project stands, from the store alone: its linked chats and its children's, each read once, then every
87
+ * signal on its own. A signal that fails is named in `failed` and the others still answer. No model call.
88
+ */
89
+ export const projectStatus = async (store, input) => {
90
+ const now = input.asOf ?? Date.now();
91
+ const since = input.after ?? now - (input.days ?? 60) * DAY;
92
+ if (!(since < now))
93
+ throw new CliError("validation_error", "the window must start before it ends");
94
+ const signals = input.signals ?? STATUS_SIGNALS;
95
+ const purposes = new Set(input.purposes ?? DEFAULT_PURPOSES);
96
+ const catalog = await projectsCatalog(store);
97
+ const ref = await canonicalProjectRef(store, input.project);
98
+ const entry = catalog.get(ref);
99
+ if (!entry)
100
+ throw new CliError("not_found", "the owner project is unavailable");
101
+ const root = { ref, key: entry.key, name: entry.name };
102
+ const { children, cut } = await childrenOf(store, root, catalog);
103
+ const outOfScope = [];
104
+ const linked = new Map();
105
+ for (const project of [root, ...children])
106
+ for (const link of await linkedChatsOf(store, project.ref)) {
107
+ const chat = formatReference({ type: "chat", provider: link.provider, account: link.account, chat: link.chat });
108
+ if (!linked.has(chat))
109
+ linked.set(chat, { ...link, project: project.ref });
110
+ }
111
+ const chosen = [];
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 });
117
+ else if (!inAccounts(input.accounts, link.provider, link.account))
118
+ outOfScope.push({ chat, reason: "other-account" });
119
+ else if (chosen.length >= STATUS_CHATS)
120
+ outOfScope.push({ chat, reason: "linked-chat-limit" });
121
+ else
122
+ chosen.push([chat, link]);
123
+ }
124
+ const sinceIso = new Date(since).toISOString();
125
+ const chats = [];
126
+ for (const [chat, link] of chosen) {
127
+ const account = { provider: link.provider, account: link.account };
128
+ const [coverage] = await chatCoverage(store, account, [link.chat]);
129
+ const page = await store.messages.messages(account, link.chat, { limit: STATUS_WINDOW, since: sinceIso });
130
+ chats.push({
131
+ ref: chat,
132
+ account,
133
+ chatId: link.chat,
134
+ title: coverage?.title ?? null,
135
+ kind: (await store.chats.chatKind(account, link.chat)) ?? "unknown",
136
+ role: link.role,
137
+ purpose: "work",
138
+ project: link.project,
139
+ coverage: coverage,
140
+ messages: page.items.filter((message) => Date.parse(message.timestamp) <= now),
141
+ });
142
+ }
143
+ const context = {
144
+ now,
145
+ since,
146
+ chats,
147
+ project: { ...root, children, cut },
148
+ store,
149
+ contextQueries: input.contextQueries ?? [],
150
+ };
151
+ const items = [];
152
+ const failed = [];
153
+ const weights = new Map();
154
+ for (const signal of signals) {
155
+ try {
156
+ const found = signal.scope === "chat"
157
+ ? (await Promise.all(chats.map((chat) => signal.read(context, chat)))).flat()
158
+ : await signal.read(context);
159
+ for (const one of signal.attention?.(found, context) ?? []) {
160
+ const held = weights.get(one.chat) ?? { weight: 0, why: [] };
161
+ held.weight += Math.max(0, Math.min(3, one.weight));
162
+ if (!held.why.includes(one.why))
163
+ held.why.push(one.why);
164
+ weights.set(one.chat, held);
165
+ }
166
+ for (const one of found)
167
+ items.push(one);
168
+ }
169
+ catch (error) {
170
+ failed.push({ signal: signal.id, why: failure(error) });
171
+ }
172
+ }
173
+ const order = new Map(chats.map((chat, index) => [chat.ref, index]));
174
+ const attention = [...weights]
175
+ .filter(([, { weight }]) => weight > 0)
176
+ .map(([chat, { weight, why }]) => ({ chat, weight, why: why.join(", ") }))
177
+ .sort((a, b) => b.weight - a.weight || (order.get(a.chat) ?? 0) - (order.get(b.chat) ?? 0));
178
+ return {
179
+ project: root,
180
+ children: children.map(({ ref, key, name }) => ({
181
+ ref,
182
+ key,
183
+ name,
184
+ chats: chats.filter((chat) => chat.project === ref).length,
185
+ })),
186
+ window: { since: sinceIso, until: new Date(now).toISOString() },
187
+ accounts: [...new Set(chats.map((chat) => `${chat.account.provider}/${chat.account.account}`))],
188
+ chats: chats.map(({ messages: _messages, account: _account, ...chat }) => chat),
189
+ items,
190
+ money: { open: totalsOf(items) },
191
+ outOfScope,
192
+ failed,
193
+ attention,
194
+ signals: signals.map(({ id, version }) => ({ id, version })),
195
+ };
196
+ };
@@ -0,0 +1,10 @@
1
+ import type { Message } from "../../domain/models.js";
2
+ import type { StatusChat, StatusItem, StatusWho } from "./types.js";
3
+ /** The most of a message's text any status item carries. */
4
+ export declare const EXCERPT_CHARS = 80;
5
+ export declare const excerpt: (text: string) => string;
6
+ export declare const messageRef: (chat: StatusChat, message: Pick<Message, "id">) => string;
7
+ export declare const evidenceOf: (chat: StatusChat, ...messages: Pick<Message, "id">[]) => StatusItem["evidence"];
8
+ export declare const whoOf: (message: Pick<Message, "outgoing" | "senderName">) => StatusWho;
9
+ export declare const fromOwner: (message: Pick<Message, "outgoing">) => boolean;
10
+ export declare const day: (at: number | string) => string;
@@ -0,0 +1,23 @@
1
+ import { formatLocator } from "../../domain/locator.js";
2
+ /** The most of a message's text any status item carries. */
3
+ export const EXCERPT_CHARS = 80;
4
+ export const excerpt = (text) => {
5
+ const flat = text.replace(/\s+/g, " ").trim();
6
+ const letters = Array.from(flat);
7
+ return letters.length <= EXCERPT_CHARS ? flat : `${letters.slice(0, EXCERPT_CHARS - 1).join("")}…`;
8
+ };
9
+ export const messageRef = (chat, message) => formatLocator({
10
+ provider: chat.account.provider,
11
+ account: chat.account.account,
12
+ chat: chat.chatId,
13
+ message: message.id,
14
+ });
15
+ export const evidenceOf = (chat, ...messages) => messages.slice(0, 8).map((message) => ({ ref: messageRef(chat, message), fingerprint: null }));
16
+ // Person refs wait for the shared ref summaries; until then the sender's stored name is the label.
17
+ export const whoOf = (message) => ({
18
+ ref: null,
19
+ name: message.outgoing === true ? null : message.senderName,
20
+ owner: message.outgoing === true,
21
+ });
22
+ export const fromOwner = (message) => message.outgoing === true;
23
+ export const day = (at) => new Date(at).toISOString().slice(0, 10);
@@ -0,0 +1,20 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export type MoneyDirection = "to_us" | "from_us" | "unknown";
3
+ export interface AmountsData {
4
+ /** Decimal as text, two places: money is never a float. */
5
+ amount: string;
6
+ currency: string | null;
7
+ direction: MoneyDirection;
8
+ /** `paid-mentioned` is a message saying so, never a payment the owner recorded. */
9
+ state: "open" | "paid-mentioned" | "mentioned";
10
+ /** The first ask, or the first mention when nobody asked. */
11
+ since: string;
12
+ lastAsked: string | null;
13
+ asks: number;
14
+ }
15
+ export declare const centsText: (cents: number) => string;
16
+ /**
17
+ * Every message with an amount and a money word, folded by amount and currency per chat: the first ask is `since`, a
18
+ * later "paid" message naming the same amount makes it `paid-mentioned`. Instalments are not summed into a total.
19
+ */
20
+ export declare const amounts: StatusSignal<AmountsData>;
@@ -0,0 +1,91 @@
1
+ import { MONEY_DONE, MONEY_REQUEST } from "../../../domain/money-words.js";
2
+ import { MONEY_DONE_FORMS } from "../../../domain/status-words.js";
3
+ import { DEFAULT_TASK_CATEGORIES } from "../../../domain/task-categories.js";
4
+ import { amountsOf } from "../../../search/task-signature.js";
5
+ import { moneyClassesOf } from "../../money-words.js";
6
+ import { listsHit } from "../../task-categories.js";
7
+ import { evidenceOf, fromOwner, whoOf } from "../items.js";
8
+ const MONEY = DEFAULT_TASK_CATEGORIES.filter((category) => category.name === "money");
9
+ // Past forms first: their stem is also a request's (`оплатил` and `оплати` → `оплат`).
10
+ const verbOf = (text) => {
11
+ if (MONEY_DONE_FORMS.test(text))
12
+ return "done";
13
+ const classes = moneyClassesOf(text);
14
+ if (classes.includes(MONEY_DONE))
15
+ return "done";
16
+ return classes.includes(MONEY_REQUEST) ? "request" : "none";
17
+ };
18
+ const roleDirection = (role) => {
19
+ const named = role?.trim().toLowerCase();
20
+ return named === "client" ? "to_us" : named === "contractor" ? "from_us" : "unknown";
21
+ };
22
+ /** The sender's side decides: a stem cannot tell an ask from a report, so who wrote it is the evidence. */
23
+ const directionOf = (verb, owner, role) => {
24
+ if (verb === "request")
25
+ return owner ? "to_us" : "from_us";
26
+ if (verb === "done")
27
+ return owner ? "from_us" : "to_us";
28
+ return roleDirection(role);
29
+ };
30
+ const rowsOf = (chat) => chat.messages.flatMap((message) => {
31
+ const found = amountsOf(message.text);
32
+ if (!found.length)
33
+ return [];
34
+ const verb = verbOf(message.text);
35
+ if (verb === "none" && !listsHit(message.text, MONEY).length)
36
+ return [];
37
+ const direction = directionOf(verb, fromOwner(message), chat.role);
38
+ return found.map(({ value, currency }) => ({
39
+ message,
40
+ cents: Math.round(value * 100),
41
+ currency,
42
+ verb,
43
+ direction,
44
+ }));
45
+ });
46
+ export const centsText = (cents) => `${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, "0")}`;
47
+ /**
48
+ * Every message with an amount and a money word, folded by amount and currency per chat: the first ask is `since`, a
49
+ * later "paid" message naming the same amount makes it `paid-mentioned`. Instalments are not summed into a total.
50
+ */
51
+ export const amounts = {
52
+ id: "amounts",
53
+ version: 1,
54
+ scope: "chat",
55
+ read: async (_context, chat) => {
56
+ if (!chat)
57
+ return [];
58
+ const groups = new Map();
59
+ for (const row of rowsOf(chat)) {
60
+ const key = `${row.cents}|${row.currency ?? ""}`;
61
+ groups.set(key, [...(groups.get(key) ?? []), row]);
62
+ }
63
+ return [...groups.values()].map((rows) => {
64
+ const asks = rows.filter((row) => row.verb === "request");
65
+ const done = rows.filter((row) => row.verb === "done");
66
+ const lastAsk = asks.at(-1);
67
+ const first = (asks[0] ?? done[0] ?? rows[0]);
68
+ const paidAfter = done.some((row) => !lastAsk || Date.parse(row.message.timestamp) >= Date.parse(lastAsk.message.timestamp));
69
+ const state = asks.length ? (paidAfter ? "paid-mentioned" : "open") : done.length ? "paid-mentioned" : "mentioned";
70
+ const cited = asks.length ? [...asks, ...(paidAfter ? done.slice(-1) : [])] : rows;
71
+ return {
72
+ signal: "amounts",
73
+ chat: chat.ref,
74
+ at: first.message.timestamp,
75
+ who: whoOf(first.message),
76
+ evidence: evidenceOf(chat, ...new Set(cited.map((row) => row.message))),
77
+ basis: "pattern",
78
+ data: {
79
+ amount: centsText(first.cents),
80
+ currency: first.currency,
81
+ direction: first.direction,
82
+ state,
83
+ since: first.message.timestamp,
84
+ lastAsked: lastAsk?.message.timestamp ?? null,
85
+ asks: asks.length,
86
+ },
87
+ };
88
+ });
89
+ },
90
+ attention: (items) => items.flatMap((item) => item.chat !== null && item.data.state === "open" ? [{ chat: item.chat, weight: 2, why: "money open" }] : []),
91
+ };
@@ -0,0 +1,13 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export interface ChildrenData {
3
+ child: string;
4
+ key: string | null;
5
+ name: string | null;
6
+ depth: number | null;
7
+ /** Linked chats read for it; a chat linked to two projects counts once, under the first. */
8
+ chats: number | null;
9
+ /** Why it was not read: deeper than two levels, seen twice, or past eight children. */
10
+ cut: "depth" | "cycle" | "limit" | null;
11
+ }
12
+ /** Child projects (a confirmed `member-of` link to this one) that were read, and those cut. */
13
+ export declare const children: StatusSignal<ChildrenData>;
@@ -0,0 +1,28 @@
1
+ /** Child projects (a confirmed `member-of` link to this one) that were read, and those cut. */
2
+ export const children = {
3
+ id: "children",
4
+ version: 1,
5
+ scope: "project",
6
+ read: async (context) => {
7
+ const item = (data) => ({
8
+ signal: "children",
9
+ chat: null,
10
+ at: null,
11
+ who: null,
12
+ evidence: [{ ref: data.child, fingerprint: null }],
13
+ basis: "store",
14
+ data,
15
+ });
16
+ return [
17
+ ...context.project.children.map((child) => item({
18
+ child: child.ref,
19
+ key: child.key,
20
+ name: child.name,
21
+ depth: child.depth,
22
+ chats: context.chats.filter((chat) => chat.project === child.ref).length,
23
+ cut: null,
24
+ })),
25
+ ...context.project.cut.map(({ ref, reason }) => item({ child: ref, key: null, name: null, depth: null, chats: null, cut: reason })),
26
+ ];
27
+ },
28
+ };
@@ -0,0 +1,13 @@
1
+ import type { StatusSignal } from "../types.js";
2
+ export interface CoverageData {
3
+ state: "complete" | "partial" | "unknown";
4
+ held: {
5
+ messages: number;
6
+ oldestAt: string | null;
7
+ newestAt: string | null;
8
+ };
9
+ /** The window starts before the oldest message the store holds, so the window is not read whole. */
10
+ beforeHeld: boolean;
11
+ }
12
+ /** What the store holds of a chat it does not hold whole; the answer adds the line that syncs it. */
13
+ export declare const coverage: StatusSignal<CoverageData>;