@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
@@ -202,6 +202,8 @@ export declare const messageSyncChat: v.StrictObjectSchema<{
202
202
  readonly reached_since: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
203
203
  readonly reached_last: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
204
204
  readonly stopped: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
205
+ /** The history limits stopped the read: a group too large to read, or the window's start reached. */
206
+ readonly limited: v.OptionalSchema<v.PicklistSchema<["group_too_large", "older_than_window"], undefined>, undefined>;
205
207
  readonly tasks: v.OptionalSchema<v.StrictObjectSchema<{
206
208
  readonly added: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
207
209
  readonly closed: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -250,6 +252,12 @@ export declare const messageSyncAll: v.StrictObjectSchema<{
250
252
  readonly retryable: v.BooleanSchema<undefined>;
251
253
  readonly retry_after_ms: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
252
254
  }, v.UnknownSchema, undefined>, undefined>;
255
+ readonly limited: v.OptionalSchema<v.PicklistSchema<["group_too_large", "older_than_window"], undefined>, undefined>;
256
+ }, undefined>, undefined>;
257
+ /** How many chats the history limits left unread (big groups) or read only to the window's start. */
258
+ readonly limited: v.OptionalSchema<v.StrictObjectSchema<{
259
+ readonly group_too_large: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
260
+ readonly older_than_window: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
253
261
  }, undefined>, undefined>;
254
262
  readonly tasks: v.OptionalSchema<v.StrictObjectSchema<{
255
263
  readonly added: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -328,6 +336,8 @@ export declare const messageSync: v.UnionSchema<[v.StrictObjectSchema<{
328
336
  readonly reached_since: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
329
337
  readonly reached_last: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
330
338
  readonly stopped: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
339
+ /** The history limits stopped the read: a group too large to read, or the window's start reached. */
340
+ readonly limited: v.OptionalSchema<v.PicklistSchema<["group_too_large", "older_than_window"], undefined>, undefined>;
331
341
  readonly tasks: v.OptionalSchema<v.StrictObjectSchema<{
332
342
  readonly added: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
333
343
  readonly closed: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -375,6 +385,12 @@ export declare const messageSync: v.UnionSchema<[v.StrictObjectSchema<{
375
385
  readonly retryable: v.BooleanSchema<undefined>;
376
386
  readonly retry_after_ms: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
377
387
  }, v.UnknownSchema, undefined>, undefined>;
388
+ readonly limited: v.OptionalSchema<v.PicklistSchema<["group_too_large", "older_than_window"], undefined>, undefined>;
389
+ }, undefined>, undefined>;
390
+ /** How many chats the history limits left unread (big groups) or read only to the window's start. */
391
+ readonly limited: v.OptionalSchema<v.StrictObjectSchema<{
392
+ readonly group_too_large: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
393
+ readonly older_than_window: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
378
394
  }, undefined>, undefined>;
379
395
  readonly tasks: v.OptionalSchema<v.StrictObjectSchema<{
380
396
  readonly added: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -46,6 +46,7 @@ const tasks = v.optional(v.strictObject({
46
46
  held: v.optional(count),
47
47
  held_chats: v.optional(count),
48
48
  }));
49
+ const historyLimited = v.picklist(["group_too_large", "older_than_window"]);
49
50
  export const messageSyncChat = v.strictObject({
50
51
  issue: v.optional(actionableError),
51
52
  resume: v.optional(v.strictObject({ before: v.optional(text) })),
@@ -59,6 +60,8 @@ export const messageSyncChat = v.strictObject({
59
60
  reached_since: v.optional(v.literal(true)),
60
61
  reached_last: v.optional(v.literal(true)),
61
62
  stopped: v.optional(v.literal(true)),
63
+ /** The history limits stopped the read: a group too large to read, or the window's start reached. */
64
+ limited: v.optional(historyLimited),
62
65
  tasks,
63
66
  /** `--full-history`: the chat's pushed history is now kept in full; `false` when the store does not know the chat. */
64
67
  history_included: v.optional(v.boolean()),
@@ -78,7 +81,10 @@ export const messageSyncAll = v.strictObject({
78
81
  stopped: v.optional(v.literal(true)),
79
82
  error: v.optional(text),
80
83
  issue: v.optional(actionableError),
84
+ limited: v.optional(historyLimited),
81
85
  })),
86
+ /** How many chats the history limits left unread (big groups) or read only to the window's start. */
87
+ limited: v.optional(v.strictObject({ group_too_large: count, older_than_window: count })),
82
88
  tasks,
83
89
  });
84
90
  export const messageSyncStarted = v.strictObject({ job: id, pid: count, chat: text, log: text });
@@ -33,6 +33,7 @@ const SIGN_IN = [
33
33
  ["sign-in.client-file-invalid", "configuration_error", "the file is not a Desktop app OAuth client JSON"],
34
34
  ];
35
35
  const OTHERS = [
36
+ ["query.time-limit", "timeout", "the search took longer than its time limit; the query itself is valid"],
36
37
  ["model.offline", "permission_error", "downloading a model needs the network; --offline was requested"],
37
38
  [
38
39
  "contract.unacknowledged-override",
@@ -144,6 +144,7 @@ export const serveAppOverHttp = async (command, context, options, http) => {
144
144
  const listening = await serveOverHttp(build, {
145
145
  ...http,
146
146
  appName: app.command,
147
+ reaches: `your ${options.name}`,
147
148
  onCode: (code, expires) => context.renderer.note(context.env[mcpHttpMarker(app.envPrefix)] === undefined
148
149
  ? `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`
149
150
  : `a new login code for a browser app: \`${app.command} ${context.settings.profile} mcp server status\``),
@@ -14,11 +14,12 @@ export interface OwnerLogin {
14
14
  * the browser apps need dynamic registration — but nobody gets a token without the one-time code
15
15
  * printed in the owner's terminal. Five wrong codes delay further attempts for thirty seconds.
16
16
  */
17
- export declare const ownerLogin: ({ issuer, resource, file, appName, onCode, now, }: {
17
+ export declare const ownerLogin: ({ issuer, resource, file, appName, reaches, onCode, now, }: {
18
18
  issuer: URL;
19
19
  resource: URL;
20
20
  file: string;
21
21
  appName: string;
22
+ reaches?: string;
22
23
  /** The code for the next login, for the owner's eyes only: their terminal, or the server's status file. */
23
24
  onCode: (code: string, expires: Date) => void;
24
25
  now?: () => number;
@@ -51,7 +51,7 @@ const redirectAllowed = (uri) => {
51
51
  * the browser apps need dynamic registration — but nobody gets a token without the one-time code
52
52
  * printed in the owner's terminal. Five wrong codes delay further attempts for thirty seconds.
53
53
  */
54
- export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () => Date.now(), }) => {
54
+ export const ownerLogin = ({ issuer, resource, file, appName, reaches = `your ${appName} data`, onCode, now = () => Date.now(), }) => {
55
55
  const store = tokenFile(file);
56
56
  const codes = new Map();
57
57
  let login = { value: "", expires: 0 };
@@ -153,7 +153,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
153
153
  .join("");
154
154
  const who = escapeHtml(client.name ?? "an application");
155
155
  const target = new URL(params.get("redirect_uri") ?? "");
156
- const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(appName)} login</title><style>body{font:16px system-ui,sans-serif;max-width:28rem;margin:3rem auto;padding:0 1rem;color:#1d2430}input[type=text]{font:20px ui-monospace,monospace;letter-spacing:.1em;width:100%;padding:.5rem;margin:.5rem 0 1rem;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem}.error{color:#b42318}</style></head><body><h1>${escapeHtml(appName)}</h1><p>An app that calls itself <strong>${who}</strong> asks to reach your messages through <code>${escapeHtml(appName)} mcp --http</code>.</p><p>After you allow it, the login goes to <strong>${escapeHtml(target.host)}</strong>. Allow only if that is the app you are connecting, such as chatgpt.com or claude.ai.</p><p>Type the code shown in WireCat Desktop, or in the terminal where the server runs. If you did not start this login, close this page.</p>${message ? `<p class="error">${escapeHtml(message)}</p>` : ""}<form method="post" action="${escapeHtml(`${base}/authorize`)}">${hidden}<label>Login code<input type="text" name="login_code" autocomplete="off" autofocus required></label><button type="submit">Allow</button></form></body></html>`;
156
+ const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(appName)} login</title><style>body{font:16px system-ui,sans-serif;max-width:28rem;margin:3rem auto;padding:0 1rem;color:#1d2430}input[type=text]{font:20px ui-monospace,monospace;letter-spacing:.1em;width:100%;padding:.5rem;margin:.5rem 0 1rem;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem}.error{color:#b42318}</style></head><body><h1>${escapeHtml(appName)}</h1><p>An app that calls itself <strong>${who}</strong> asks to reach ${escapeHtml(reaches)} through <code>${escapeHtml(appName)} mcp serve --http</code>.</p><p>After you allow it, the login goes to <strong>${escapeHtml(target.host)}</strong>. Allow only if that is the app you are connecting, such as chatgpt.com or claude.ai.</p><p>Type the code shown in WireCat Desktop, or in the terminal where the server runs. If you did not start this login, close this page.</p>${message ? `<p class="error">${escapeHtml(message)}</p>` : ""}<form method="post" action="${escapeHtml(`${base}/authorize`)}">${hidden}<label>Login code<input type="text" name="login_code" autocomplete="off" autofocus required></label><button type="submit">Allow</button></form></body></html>`;
157
157
  return new Response(html, {
158
158
  status: message ? 400 : 200,
159
159
  headers: {
@@ -13,6 +13,8 @@ export interface HttpOptions {
13
13
  /** Where the hashes of issued tokens and registered clients are kept. */
14
14
  tokenFile: string;
15
15
  appName: string;
16
+ /** What the browser login page says an app asks to reach: `your messages`, `your Google Calendar`. */
17
+ reaches?: string;
16
18
  onCode: (code: string, expires: Date) => void;
17
19
  onError?: (error: Error) => void;
18
20
  now?: () => number;
@@ -91,7 +91,7 @@ const toolsIn = async (request) => {
91
91
  * the owner's browser cannot reach the port through a rebound DNS name.
92
92
  */
93
93
  export const serveOverHttp = async (build, options) => {
94
- const { publicUrl, port, tokenFile, appName, onCode, onError, now, lockFile, version } = options;
94
+ const { publicUrl, port, tokenFile, appName, reaches, onCode, onError, now, lockFile, version } = options;
95
95
  const lock = lockFile
96
96
  ? httpLock(lockFile, {
97
97
  pid: process.pid,
@@ -114,6 +114,7 @@ export const serveOverHttp = async (build, options) => {
114
114
  resource,
115
115
  file: tokenFile,
116
116
  appName,
117
+ ...(reaches ? { reaches } : {}),
117
118
  onCode: (code, expires) => {
118
119
  lock?.update({ loginCode: code, loginCodeExpiresAt: expires.toISOString() });
119
120
  onCode(code, expires);
@@ -376,6 +376,9 @@ export declare const callMeetingPreparationTool: (read: MeetingPreparationReader
376
376
  id: string;
377
377
  excerpt: string;
378
378
  offset_ms: number | null;
379
+ language?: string | null | undefined;
380
+ line?: number | undefined;
381
+ lines_total?: number | undefined;
379
382
  match?: "corrected" | "words" | undefined;
380
383
  corrections?: {
381
384
  from: string;
@@ -417,6 +417,7 @@ export const serveOverHttpUntilStopped = async (command, context, messenger, opt
417
417
  const listening = await serveOverHttp(build, {
418
418
  ...http,
419
419
  appName,
420
+ reaches: "your messages",
420
421
  // In the background its log is a file; the code goes only to the lock that `mcp server status` reads.
421
422
  onCode: (code, expires) => context.renderer.note(context.env[mcpHttpMarker(appEnvPrefix)] === undefined
422
423
  ? `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`
@@ -46,7 +46,7 @@ const runnableOnly = (result, offered) => {
46
46
  const body = result.structuredContent;
47
47
  if (result.isError || !Array.isArray(body?.next))
48
48
  return result;
49
- const next = body.next.filter((step) => offered(step.command));
49
+ const next = body.next.filter((step) => step.command === undefined || offered(step.command));
50
50
  const { next: _, ...rest } = body;
51
51
  const kept = next.length > 0 ? { ...rest, next } : rest;
52
52
  return {
@@ -15,6 +15,11 @@ export declare const meetingHit: v.StrictObjectSchema<{
15
15
  readonly excerpt: v.StringSchema<undefined>;
16
16
  /** How far into the meeting the passage is. */
17
17
  readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
18
+ /** A transcript passage's transcript language (`ru`, `en`…), `null` when unknown. */
19
+ readonly language: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
20
+ /** A transcript passage's line, from 1, of `lines_total`. */
21
+ 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>;
22
+ readonly lines_total: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
18
23
  /** `corrected`: a query word matched only a garbled form of itself, listed in `corrections`. */
19
24
  readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
20
25
  readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
@@ -16,6 +16,11 @@ export const meetingHit = v.strictObject({
16
16
  excerpt: text,
17
17
  /** How far into the meeting the passage is. */
18
18
  offset_ms: v.nullable(count),
19
+ /** A transcript passage's transcript language (`ru`, `en`…), `null` when unknown. */
20
+ language: v.optional(v.nullable(text)),
21
+ /** A transcript passage's line, from 1, of `lines_total`. */
22
+ line: v.optional(v.pipe(count, v.minValue(1))),
23
+ lines_total: v.optional(count),
19
24
  /** `corrected`: a query word matched only a garbled form of itself, listed in `corrections`. */
20
25
  match: v.optional(v.picklist(["words", "corrected"])),
21
26
  corrections: v.optional(v.array(v.strictObject({ from: text, to: v.array(text) }))),
@@ -190,6 +195,9 @@ export const meetingHitItem = (hit) => ({
190
195
  id: String(hit.id),
191
196
  excerpt: hit.text,
192
197
  offset_ms: hit.startMs,
198
+ ...(hit.language === undefined ? {} : { language: hit.language }),
199
+ ...(hit.line === undefined ? {} : { line: hit.line }),
200
+ ...(hit.linesTotal === undefined ? {} : { lines_total: hit.linesTotal }),
193
201
  untrusted: hit.source === undefined ? ["excerpt"] : ["excerpt", "source.title"],
194
202
  ...(hit.match === undefined ? {} : { match: hit.match }),
195
203
  ...(hit.corrections === undefined ? {} : { corrections: hit.corrections }),
@@ -146,6 +146,20 @@ const addMeetingCommandsDefinition = (program, deps) => {
146
146
  throw new MeetingError("validation_error", "file account differs from selected account");
147
147
  print(await importFiles(files, deps.store, deps.now?.()), command);
148
148
  });
149
+ output(annotate(group.command("repair"), {
150
+ effects: LOCAL_WRITE,
151
+ output: { entity: "meeting_repair" },
152
+ }).description("preview filling in what older builds left out of stored transcripts (their language); --yes stores it"))
153
+ .option("--yes", "store the detected languages; otherwise only preview")
154
+ .action(async (opts, command) => {
155
+ if (!deps.store.fillTranscriptLanguages)
156
+ throw new MeetingError("configuration_error", "this store cannot fill transcript languages");
157
+ print(await deps.store.fillTranscriptLanguages({
158
+ accountId: deps.accountId,
159
+ apply: opts.yes === true,
160
+ now: deps.now?.() ?? Date.now(),
161
+ }), command);
162
+ });
149
163
  const eventList = queryWordsAfterOptions(annotate(program.command("events").description("the owner's stored events"), {
150
164
  effects: { remote: "none", local: "none" },
151
165
  }).command("list"))
@@ -1,4 +1,4 @@
1
- import { MEETING_FACT_TYPES as FACT_TYPES } from "./meeting-context.js";
1
+ import { FACT_TYPES, type Payment } from "./payment.js";
2
2
  declare const TASK_TYPES: readonly ["bug", "feature", "chore"];
3
3
  export interface FindingEvidence {
4
4
  reference: string;
@@ -14,9 +14,18 @@ interface FindingBase {
14
14
  }
15
15
  export interface FactFinding extends FindingBase {
16
16
  kind: "fact";
17
- type: (typeof FACT_TYPES)[number];
17
+ type: Exclude<(typeof FACT_TYPES)[number], "payment">;
18
18
  statement: string;
19
19
  }
20
+ /** A payment an agent read; confirming it records a `payment` fact. A paid one supersedes the expected one. */
21
+ export interface PaymentFinding extends FindingBase {
22
+ kind: "payment";
23
+ /** The fact's full sentence; without one, the payment's title. */
24
+ statement?: string;
25
+ payment: Payment;
26
+ /** The `fact:` of the expected payment this one replaces, once it is paid or cancelled. */
27
+ supersedes?: string;
28
+ }
20
29
  export interface TaskFinding extends FindingBase {
21
30
  kind: "task";
22
31
  type: (typeof TASK_TYPES)[number];
@@ -28,7 +37,7 @@ export interface TaskFinding extends FindingBase {
28
37
  assignee?: string;
29
38
  assigneeEvidence?: string[];
30
39
  }
31
- export type Finding = FactFinding | TaskFinding;
40
+ export type Finding = FactFinding | TaskFinding | PaymentFinding;
32
41
  export interface FindingsInput {
33
42
  schemaVersion: 1;
34
43
  key: string;
@@ -59,6 +68,8 @@ export interface FindingsRules {
59
68
  reference: (raw: string) => string;
60
69
  /** A fact finding may name no project, and any finding may name a subject. */
61
70
  anySubject?: boolean;
71
+ /** Payment findings are staged on a project's evidence, not from a meeting. */
72
+ payments?: boolean;
62
73
  }
63
74
  export declare const validateFindings: (input: FindingsInput, rules: FindingsRules) => FindingsInput;
64
75
  export declare const findingFingerprint: ({ id: _id, ...finding }: Finding) => string;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { CliError } from "@wirecat/cli-base";
3
- import { MEETING_FACT_TYPES as FACT_TYPES } from "./meeting-context.js";
3
+ import { FACT_TYPES, validatePayment } from "./payment.js";
4
4
  import { meetingTimestamp } from "./timestamp.js";
5
5
  const TASK_TYPES = ["bug", "feature", "chore"];
6
6
  const invalid = (message) => {
@@ -69,12 +69,26 @@ export const validateFindings = (input, rules) => {
69
69
  };
70
70
  if (finding.kind === "fact") {
71
71
  fields(finding, ["id", "project", "subject", "evidence", "kind", "type", "statement"], rules.singular);
72
+ if (finding.type === "payment")
73
+ return invalid("a payment is a finding of kind payment");
72
74
  if (!FACT_TYPES.includes(finding.type))
73
75
  return invalid("unknown fact finding type");
74
76
  return { ...base, kind: "fact", type: finding.type, statement: text(finding.statement, 2000) };
75
77
  }
78
+ if (finding.kind === "payment" && rules.payments) {
79
+ fields(finding, ["id", "project", "subject", "evidence", "kind", "statement", "payment", "supersedes"], rules.singular);
80
+ if (finding.supersedes !== undefined && !/^fact:[1-9]\d*$/.test(String(finding.supersedes)))
81
+ return invalid("a payment finding supersedes a fact: reference");
82
+ return {
83
+ ...base,
84
+ kind: "payment",
85
+ ...(finding.statement === undefined ? {} : { statement: text(finding.statement, 2000) }),
86
+ payment: validatePayment(finding.payment),
87
+ ...(finding.supersedes === undefined ? {} : { supersedes: finding.supersedes }),
88
+ };
89
+ }
76
90
  if (finding.kind !== "task")
77
- return invalid("a finding is a fact or task");
91
+ return invalid(`a finding is a fact${rules.payments ? ", payment" : ""} or task`);
78
92
  fields(finding, [
79
93
  "id",
80
94
  "project",
@@ -9,17 +9,19 @@ export { entryInWindow, eventChange } from "./event-store.js";
9
9
  export { linkEvent } from "./events.js";
10
10
  export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
11
11
  export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
12
- export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFingerprint, validateFindings, } from "./findings-rules.js";
12
+ export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFingerprint, type PaymentFinding, validateFindings, } from "./findings-rules.js";
13
13
  export { liftMeetingFields, MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
14
14
  export { type ImportFile, importFiles } from "./import.js";
15
15
  export { MEETING_ISSUES, MEETING_QUERY, type MeetingIssue, type MeetingItem, type MeetingOwner, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
16
16
  export { joinKeyOf, joinUrlWithoutQuery } from "./join-key.js";
17
+ export { transcriptLanguage } from "./language.js";
17
18
  export { CHAT_MESSAGES_READ, type MeetingChatLine, type MeetingChatLinesInput, type MeetingChatLinesStore, type MeetingLink, } from "./links.js";
18
- export { createMeetingContext, MEETING_FACT_TYPES, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
19
+ export { createMeetingContext, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
19
20
  export { createMeetingManagement, dispatchMeetingMutation, type ManagedMeeting, type ManagedMeetingChange, type MeetingManagement, type MeetingManagementPorts, type MeetingMutationOptions, type MeetingMutationProposal, type MeetingMutationReceipt, type MeetingOperationOptions, type MeetingProviderContext, validateMeetingChange, } from "./meeting-management.js";
20
21
  export type { Attachment, ChatLine, Event, EventSeries, JsonValue, Meeting, MeetingSeries, Metadata, Occurrence, Participant, RecordTimes, SourceChatLine, SourceMeeting, Summary, SummaryContent, Transcript, TranscriptLine, TranscriptRow, } from "./model.js";
21
22
  export { type MeetingParticipantCandidate, type MeetingParticipantPage, type MeetingParticipantReader, type MeetingParticipantSearchOptions, type MeetingParticipantSearchStore, normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
22
23
  export { type MeetingContextCue, type MeetingHitContextOptions, type MeetingHitContextPacket, type MeetingPassageOutputFormat, type MeetingPassageReader, type MeetingPassageRequest, type MeetingPassageResult, meetingHitContextLimits, validateMeetingPassage, } from "./passage.js";
24
+ export { FACT_TYPES, PAYMENT_DIRECTIONS, PAYMENT_STATES, type Payment, paymentTitle, validatePayment, } from "./payment.js";
23
25
  export { type MeetingPreparationContext, type MeetingPreparationContextRequest, type MeetingPreparationContextStore, type PreparationPage, validateMeetingPreparationContext, } from "./preparation-context.js";
24
26
  export type { MeetingPreparationFact, MeetingPreparationKnowledgeRequest, MeetingPreparationKnowledgeStore, } from "./preparation-knowledge.js";
25
27
  export type { MeetingPreparationNote, MeetingPreparationNotesSelection, MeetingPreparationNotesStore, } from "./preparation-notes.js";
@@ -50,7 +52,7 @@ export type { MeetingSeriesCandidate, MeetingSeriesInventoryPage, MeetingSeriesI
50
52
  export { validateMeetingSeriesInventoryCursor } from "./series-inventory-validation.js";
51
53
  export type { MeetingSource, SourceParticipant } from "./source.js";
52
54
  export { aggregateMeetingStats, type MeetingSpeakerStats, type MeetingStats, type MeetingStatsInput, type MeetingStatsRow, type MeetingStatsSelection, type MeetingStatsStore, type MeetingWeekStats, } from "./stats.js";
53
- export type { ChatInput, ContractCase, EventCandidateFilter, IdentityInput, MeetingDetails, MeetingFilter, MeetingHitSource, MeetingInput, MeetingSave, MeetingStore, NewRecord, ParticipantInput, SearchHit, TranscriptInput, TranscriptRowInput, } from "./store.js";
55
+ export type { ChatInput, ContractCase, EventCandidateFilter, IdentityInput, MeetingDetails, MeetingFilter, MeetingHitSource, MeetingInput, MeetingSave, MeetingStore, NewRecord, ParticipantInput, SearchHit, TranscriptInput, TranscriptLanguageFill, TranscriptRowInput, } from "./store.js";
54
56
  export type { MeetingTranscriptReceiptStore, MeetingTranscriptStore, TranscriptAppend, TranscriptAppendReceipt, UnlinkedTranscriptInput, } from "./transcript-store.js";
55
57
  export { parseTranscriptText, TranscriptTextError, type TranscriptTextHeader } from "./transcript-text.js";
56
58
  export { type MeetingMediaDescriptor, type MeetingRecognitionSegment, type MeetingTranscriptionContext, type MeetingTranscriptionOptions, type MeetingTranscriptionReceipt, type MeetingTranscriptionSource, transcribeMeeting, } from "./transcription.js";
@@ -11,11 +11,13 @@ export { liftMeetingFields, MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQu
11
11
  export { importFiles } from "./import.js";
12
12
  export { MEETING_ISSUES, MEETING_QUERY, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
13
13
  export { joinKeyOf, joinUrlWithoutQuery } from "./join-key.js";
14
+ export { transcriptLanguage } from "./language.js";
14
15
  export { CHAT_MESSAGES_READ, } from "./links.js";
15
- export { createMeetingContext, MEETING_FACT_TYPES, validateMeetingContextChange, } from "./meeting-context.js";
16
+ export { createMeetingContext, validateMeetingContextChange, } from "./meeting-context.js";
16
17
  export { createMeetingManagement, dispatchMeetingMutation, validateMeetingChange, } from "./meeting-management.js";
17
18
  export { normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
18
19
  export { meetingHitContextLimits, validateMeetingPassage, } from "./passage.js";
20
+ export { FACT_TYPES, PAYMENT_DIRECTIONS, PAYMENT_STATES, paymentTitle, validatePayment, } from "./payment.js";
19
21
  export { validateMeetingPreparationContext, } from "./preparation-context.js";
20
22
  export { readMeetingProjectEvidence } from "./project-evidence.js";
21
23
  export { meetingProjectEvidence, meetingProjectEvidenceAnswer, } from "./project-evidence-answer.js";
@@ -1,3 +1,4 @@
1
+ import { transcriptLanguage } from "./language.js";
1
2
  export const contentHash = async (text) => {
2
3
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
3
4
  return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -11,7 +12,7 @@ export const speakerPosition = (name, participants) => {
11
12
  export const transcriptInput = async (lines, participants, source, hashText = JSON.stringify(lines), format = null) => ({
12
13
  source,
13
14
  format,
14
- language: null,
15
+ language: transcriptLanguage(lines.map((line) => line.text)),
15
16
  contentHash: await contentHash(hashText),
16
17
  externalCreatedAt: null,
17
18
  metadata: null,
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The language most of a transcript is in, from its letters: Cyrillic is Russian (Ukrainian when its own letters
3
+ * show), Latin is decided by frequent words. `null` when the text is too short, mixed, or in neither script.
4
+ */
5
+ export declare const transcriptLanguage: (texts: Iterable<string>) => string | null;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Words frequent in one Latin-script language and rare in the others here. Letters alone cannot tell Spanish from
3
+ * English, and calling a Spanish meeting English is worse than calling it nothing.
4
+ */
5
+ const LATIN_WORDS = {
6
+ en: ["the", "and", "is", "that", "you", "this", "what", "have", "with", "are"],
7
+ es: ["el", "los", "las", "por", "pero", "para", "está", "muy", "también", "hay"],
8
+ de: ["der", "und", "ist", "nicht", "das", "ich", "wir", "mit", "auch", "ein"],
9
+ fr: ["le", "les", "est", "une", "pas", "je", "vous", "nous", "avec", "mais"],
10
+ it: ["gli", "della", "sono", "perché", "anche", "questo", "molto", "che", "di", "è"],
11
+ pt: ["não", "você", "isso", "muito", "então", "com", "uma", "também", "mas", "eu"],
12
+ };
13
+ const LATIN_LANGUAGE = new Map(Object.entries(LATIN_WORDS).flatMap(([language, words]) => words.map((word) => [word, language])));
14
+ /** Letters Ukrainian writes and Russian does not. */
15
+ const UKRAINIAN = /[іїєґ]/giu;
16
+ const CYRILLIC = /\p{Script=Cyrillic}/gu;
17
+ const LATIN = /\p{Script=Latin}/gu;
18
+ const MIN_LETTERS = 20;
19
+ /** A meeting in one language keeps a few lines of another; a share below this is a mixed meeting. */
20
+ const DOMINANT = 0.75;
21
+ const countOf = (text, pattern) => text.match(pattern)?.length ?? 0;
22
+ const latinLanguage = (texts) => {
23
+ const votes = new Map();
24
+ for (const text of texts)
25
+ for (const word of text.toLowerCase().split(/[^\p{L}]+/u)) {
26
+ const language = LATIN_LANGUAGE.get(word);
27
+ if (language)
28
+ votes.set(language, (votes.get(language) ?? 0) + 1);
29
+ }
30
+ const [first, second] = [...votes].sort((a, b) => b[1] - a[1]);
31
+ if (!first || first[1] < 3 || (second && first[1] < 2 * second[1]))
32
+ return null;
33
+ return first[0];
34
+ };
35
+ /**
36
+ * The language most of a transcript is in, from its letters: Cyrillic is Russian (Ukrainian when its own letters
37
+ * show), Latin is decided by frequent words. `null` when the text is too short, mixed, or in neither script.
38
+ */
39
+ export const transcriptLanguage = (texts) => {
40
+ const all = [...texts];
41
+ let cyrillic = 0;
42
+ let latin = 0;
43
+ let ukrainian = 0;
44
+ for (const text of all) {
45
+ cyrillic += countOf(text, CYRILLIC);
46
+ latin += countOf(text, LATIN);
47
+ ukrainian += countOf(text, UKRAINIAN);
48
+ }
49
+ const letters = cyrillic + latin;
50
+ if (letters < MIN_LETTERS)
51
+ return null;
52
+ if (cyrillic / letters >= DOMINANT)
53
+ return ukrainian / cyrillic >= 0.01 ? "uk" : "ru";
54
+ if (latin / letters >= DOMINANT)
55
+ return latinLanguage(all);
56
+ return null;
57
+ };
@@ -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;
@@ -1,7 +1,6 @@
1
1
  import { type MeetingMutationOptions } from "./meeting-management.js";
2
+ import { FACT_TYPES } from "./payment.js";
2
3
  import { type MeetingProviderScope } from "./provider.js";
3
- /** Mirrors the store's fact types; the store checks the type again when it writes. */
4
- export declare const MEETING_FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise"];
5
4
  export type MeetingContextChange = {
6
5
  action: "note.add";
7
6
  text: string;
@@ -19,7 +18,7 @@ export type MeetingContextChange = {
19
18
  } | {
20
19
  action: "fact.add";
21
20
  statement: string;
22
- type: (typeof MEETING_FACT_TYPES)[number];
21
+ type: (typeof FACT_TYPES)[number];
23
22
  scope: "personal" | "work";
24
23
  evidence: readonly string[];
25
24
  subject?: string;
@@ -1,8 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { dispatchMeetingMutation } from "./meeting-management.js";
3
+ import { FACT_TYPES } from "./payment.js";
3
4
  import { snapshotMeetingScope } from "./provider.js";
4
- /** Mirrors the store's fact types; the store checks the type again when it writes. */
5
- export const MEETING_FACT_TYPES = ["fact", "statement", "question", "decision", "promise"];
6
5
  const id = (value) => {
7
6
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
8
7
  throw new CliError("validation_error", "meeting context identifiers must be positive integers");
@@ -63,8 +62,8 @@ export const validateMeetingContextChange = (change) => {
63
62
  });
64
63
  }
65
64
  case "fact.add": {
66
- if (!MEETING_FACT_TYPES.includes(change.type) || !["personal", "work"].includes(change.scope))
67
- throw new CliError("validation_error", `fact type (${MEETING_FACT_TYPES.join(", ")}) and explicit personal/work scope are required`);
65
+ if (!FACT_TYPES.includes(change.type) || !["personal", "work"].includes(change.scope))
66
+ throw new CliError("validation_error", `fact type (${FACT_TYPES.join(", ")}) and explicit personal/work scope are required`);
68
67
  return Object.freeze({
69
68
  action: change.action,
70
69
  statement: text(change.statement, 20_000),
@@ -0,0 +1,27 @@
1
+ /** Every fact type the store takes; findings and meeting changes check against the same list. */
2
+ export declare const FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise", "payment"];
3
+ export declare const PAYMENT_DIRECTIONS: readonly ["to_us", "from_us"];
4
+ export declare const PAYMENT_STATES: readonly ["expected", "paid", "cancelled"];
5
+ /** A payment fact's fields, kept in `facts.metadata.payment`. */
6
+ export interface Payment {
7
+ /** A decimal as text with two places, "50000.00": money is never a float. */
8
+ amount: string;
9
+ /** ISO 4217, "RUB". */
10
+ currency: string;
11
+ direction: (typeof PAYMENT_DIRECTIONS)[number];
12
+ /** `person:`, `organization:` or `project:`; `null` when only a name is known. */
13
+ counterparty: string | null;
14
+ counterparty_name: string | null;
15
+ /** An ISO date. */
16
+ due_at: string | null;
17
+ /** A change of state is a new fact that supersedes this one, so the history and its evidence stay. */
18
+ state: (typeof PAYMENT_STATES)[number];
19
+ /** What it is for, short: "support, September". The fact's statement is the full sentence. */
20
+ item: string | null;
21
+ }
22
+ /** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
23
+ export declare const validatePayment: (input: unknown) => Payment;
24
+ /** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
25
+ export declare const paymentTitle: (fact: {
26
+ payment: Payment | null;
27
+ }) => string;
@@ -0,0 +1,81 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ /** Every fact type the store takes; findings and meeting changes check against the same list. */
3
+ export const FACT_TYPES = ["fact", "statement", "question", "decision", "promise", "payment"];
4
+ export const PAYMENT_DIRECTIONS = ["to_us", "from_us"];
5
+ export const PAYMENT_STATES = ["expected", "paid", "cancelled"];
6
+ const invalid = (message) => {
7
+ throw new CliError("validation_error", message);
8
+ };
9
+ const CURRENCIES = new Set(Intl.supportedValuesOf("currency"));
10
+ const AMOUNT = /^(\d{1,15})(?:\.(\d{1,2}))?$/;
11
+ const COUNTERPARTY = /^(person|organization|project):[1-9]\d*$/;
12
+ const amountOf = (value) => {
13
+ const match = AMOUNT.exec(typeof value === "number" ? String(value) : typeof value === "string" ? value.trim() : "");
14
+ if (!match)
15
+ return invalid("a payment's amount is a positive number with at most 2 decimals, such as 1200.50");
16
+ const amount = `${BigInt(match[1] ?? "0")}.${(match[2] ?? "").padEnd(2, "0")}`;
17
+ if (amount === "0.00")
18
+ return invalid("a payment's amount is more than 0");
19
+ return amount;
20
+ };
21
+ const dateOf = (value) => {
22
+ const date = typeof value === "string" ? value.trim() : "";
23
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date)
24
+ return invalid("a payment's due date is an ISO date, such as 2026-10-31");
25
+ return date;
26
+ };
27
+ const optionalText = (value, what, limit) => {
28
+ if (value === undefined || value === null)
29
+ return null;
30
+ if (typeof value !== "string" || !value.trim() || value.length > limit || value.includes("\0"))
31
+ return invalid(`a payment's ${what} takes 1–${limit} characters`);
32
+ return value.trim();
33
+ };
34
+ const oneOf = (value, allowed, what) => {
35
+ if (typeof value !== "string" || !allowed.includes(value))
36
+ return invalid(`a payment's ${what} is one of ${allowed.join(", ")}`);
37
+ return value;
38
+ };
39
+ const FIELDS = ["amount", "currency", "direction", "counterparty", "counterparty_name", "due_at", "state", "item"];
40
+ /** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
41
+ export const validatePayment = (input) => {
42
+ if (!input || typeof input !== "object" || Array.isArray(input))
43
+ return invalid("a payment is an object");
44
+ const raw = input;
45
+ const unknown = Object.keys(raw).filter((key) => !FIELDS.includes(key));
46
+ if (unknown.length)
47
+ return invalid(`a payment has no field ${unknown.join(", ")}; its fields are ${FIELDS.join(", ")}`);
48
+ const currency = typeof raw.currency === "string" ? raw.currency.trim().toUpperCase() : "";
49
+ if (!/^[A-Z]{3}$/.test(currency) || !CURRENCIES.has(currency))
50
+ return invalid("a payment's currency is an ISO 4217 code, such as EUR or RUB");
51
+ const counterparty = optionalText(raw.counterparty, "counterparty", 80);
52
+ if (counterparty !== null && !COUNTERPARTY.test(counterparty))
53
+ return invalid("a payment's counterparty is a person:, organization: or project: reference");
54
+ return {
55
+ amount: amountOf(raw.amount),
56
+ currency,
57
+ direction: oneOf(raw.direction, PAYMENT_DIRECTIONS, "direction"),
58
+ counterparty,
59
+ counterparty_name: optionalText(raw.counterparty_name, "counterparty name", 200),
60
+ due_at: raw.due_at === undefined || raw.due_at === null ? null : dateOf(raw.due_at),
61
+ state: oneOf(raw.state, PAYMENT_STATES, "state"),
62
+ item: optionalText(raw.item, "item", 200),
63
+ };
64
+ };
65
+ const grouped = (amount) => {
66
+ const [whole = "0", cents = "00"] = amount.split(".");
67
+ const digits = whole.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
68
+ return cents === "00" ? digits : `${digits}.${cents}`;
69
+ };
70
+ /** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
71
+ export const paymentTitle = (fact) => {
72
+ const payment = fact.payment;
73
+ if (!payment)
74
+ return "";
75
+ const who = payment.counterparty_name ?? payment.counterparty;
76
+ const way = payment.direction === "to_us" ? "to us" : "from us";
77
+ const party = who === null ? "" : payment.direction === "to_us" ? ` from ${who}` : ` to ${who}`;
78
+ const due = payment.due_at === null ? "" : `, due ${payment.due_at.slice(8, 10)}.${payment.due_at.slice(5, 7)}`;
79
+ const item = payment.item === null ? "" : ` for ${payment.item}`;
80
+ return `${grouped(payment.amount)} ${payment.currency} ${way}${party}${item}${due} (${payment.state})`;
81
+ };