@wirecat/cli-core 0.550.0 → 0.551.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 (128) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/THIRD_PARTY_NOTICES +41 -0
  3. package/dist/calendar/ics.d.ts +1 -1
  4. package/dist/calendar/ics.js +5 -2
  5. package/dist/calendar/windows-zones.generated.d.ts +1 -0
  6. package/dist/calendar/windows-zones.generated.js +146 -0
  7. package/dist/cli/compact.js +2 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.js +3 -0
  10. package/dist/cli/meeting-findings-input.js +4 -3
  11. package/dist/cli/messenger/backfill-command.js +28 -1
  12. package/dist/cli/messenger/context.js +1 -0
  13. package/dist/cli/messenger/history-gaps.js +4 -4
  14. package/dist/cli/messenger/mcp-command.js +1 -0
  15. package/dist/cli/messenger/messages-delete-command.js +7 -8
  16. package/dist/cli/messenger/projects-command.js +47 -2
  17. package/dist/cli/payment-query.d.ts +8 -0
  18. package/dist/cli/payment-query.js +27 -0
  19. package/dist/cli/project-status-rows.d.ts +8 -0
  20. package/dist/cli/project-status-rows.js +102 -0
  21. package/dist/cli/project-status.d.ts +132 -0
  22. package/dist/cli/project-status.js +248 -0
  23. package/dist/cli/steps.js +4 -1
  24. package/dist/domain/knowledge.d.ts +1 -1
  25. package/dist/domain/knowledge.js +1 -1
  26. package/dist/domain/status-words.d.ts +3 -0
  27. package/dist/domain/status-words.js +44 -0
  28. package/dist/entities/fact.d.ts +12 -1
  29. package/dist/entities/fact.js +13 -0
  30. package/dist/entities/index.d.ts +18 -2
  31. package/dist/entities/meeting-findings.d.ts +1 -1
  32. package/dist/entities/message-transfer.d.ts +16 -0
  33. package/dist/entities/message-transfer.js +6 -0
  34. package/dist/mcp/app-server.js +1 -0
  35. package/dist/mcp/http/oauth.d.ts +2 -1
  36. package/dist/mcp/http/oauth.js +2 -2
  37. package/dist/mcp/http/serve.d.ts +2 -0
  38. package/dist/mcp/http/serve.js +2 -1
  39. package/dist/mcp/server.js +1 -0
  40. package/dist/mcp/surface.js +1 -1
  41. package/dist/meetings/findings-rules.d.ts +14 -3
  42. package/dist/meetings/findings-rules.js +16 -2
  43. package/dist/meetings/index.d.ts +3 -2
  44. package/dist/meetings/index.js +2 -1
  45. package/dist/meetings/meeting-context.d.ts +2 -3
  46. package/dist/meetings/meeting-context.js +3 -4
  47. package/dist/meetings/payment.d.ts +27 -0
  48. package/dist/meetings/payment.js +81 -0
  49. package/dist/services/archive.d.ts +13 -0
  50. package/dist/services/archive.js +56 -5
  51. package/dist/services/deps.d.ts +3 -0
  52. package/dist/services/history-limits.d.ts +2 -0
  53. package/dist/services/index.d.ts +4 -1
  54. package/dist/services/index.js +4 -1
  55. package/dist/services/job-query.d.ts +5 -2
  56. package/dist/services/job-query.js +5 -2
  57. package/dist/services/link-scorers/alias-hits.d.ts +7 -0
  58. package/dist/services/link-scorers/alias-hits.js +54 -0
  59. package/dist/services/link-scorers/chat-senders.d.ts +6 -0
  60. package/dist/services/link-scorers/chat-senders.js +56 -0
  61. package/dist/services/link-scorers/chat-title.d.ts +6 -0
  62. package/dist/services/link-scorers/chat-title.js +44 -0
  63. package/dist/services/link-scorers/email-domain.d.ts +6 -0
  64. package/dist/services/link-scorers/email-domain.js +64 -0
  65. package/dist/services/link-scorers/index.d.ts +9 -0
  66. package/dist/services/link-scorers/index.js +8 -0
  67. package/dist/services/link-scorers/mail-thread.d.ts +3 -0
  68. package/dist/services/link-scorers/mail-thread.js +74 -0
  69. package/dist/services/link-scorers/member-chats.d.ts +3 -0
  70. package/dist/services/link-scorers/member-chats.js +40 -0
  71. package/dist/services/link-scorers/member-dialog.d.ts +3 -0
  72. package/dist/services/link-scorers/member-dialog.js +34 -0
  73. package/dist/services/link-scorers/scan.d.ts +41 -0
  74. package/dist/services/link-scorers/scan.js +244 -0
  75. package/dist/services/link-scorers/shared.d.ts +26 -0
  76. package/dist/services/link-scorers/shared.js +48 -0
  77. package/dist/services/link-scorers/types.d.ts +77 -0
  78. package/dist/services/link-scorers/types.js +1 -0
  79. package/dist/services/project-status/index.d.ts +71 -0
  80. package/dist/services/project-status/index.js +196 -0
  81. package/dist/services/project-status/items.d.ts +10 -0
  82. package/dist/services/project-status/items.js +23 -0
  83. package/dist/services/project-status/signals/amounts.d.ts +20 -0
  84. package/dist/services/project-status/signals/amounts.js +91 -0
  85. package/dist/services/project-status/signals/children.d.ts +13 -0
  86. package/dist/services/project-status/signals/children.js +28 -0
  87. package/dist/services/project-status/signals/coverage.d.ts +13 -0
  88. package/dist/services/project-status/signals/coverage.js +30 -0
  89. package/dist/services/project-status/signals/index.d.ts +3 -0
  90. package/dist/services/project-status/signals/index.js +21 -0
  91. package/dist/services/project-status/signals/last-message.d.ts +4 -0
  92. package/dist/services/project-status/signals/last-message.js +22 -0
  93. package/dist/services/project-status/signals/payments.d.ts +9 -0
  94. package/dist/services/project-status/signals/payments.js +24 -0
  95. package/dist/services/project-status/signals/promises.d.ts +17 -0
  96. package/dist/services/project-status/signals/promises.js +109 -0
  97. package/dist/services/project-status/signals/questions.d.ts +8 -0
  98. package/dist/services/project-status/signals/questions.js +38 -0
  99. package/dist/services/project-status/signals/reply-next.d.ts +19 -0
  100. package/dist/services/project-status/signals/reply-next.js +75 -0
  101. package/dist/services/project-status/signals/topics.d.ts +10 -0
  102. package/dist/services/project-status/signals/topics.js +50 -0
  103. package/dist/services/project-status/types.d.ts +84 -0
  104. package/dist/services/project-status/types.js +1 -0
  105. package/dist/services/proposals/index.d.ts +1 -0
  106. package/dist/services/proposals/index.js +1 -0
  107. package/dist/services/proposals/kinds/findings.d.ts +3 -0
  108. package/dist/services/proposals/kinds/findings.js +8 -1
  109. package/dist/store/decisions.d.ts +5 -1
  110. package/dist/store/decisions.js +5 -1
  111. package/dist/store/evidence-findings.js +1 -0
  112. package/dist/store/index.d.ts +2 -1
  113. package/dist/store/index.js +1 -0
  114. package/dist/store/project-transfer/request.d.ts +1 -1
  115. package/dist/store/project-transfer/schema.d.ts +2 -2
  116. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  117. package/dist/store/sqlite/evidence-findings.js +5 -1
  118. package/dist/store/sqlite/facts.d.ts +13 -0
  119. package/dist/store/sqlite/facts.js +64 -6
  120. package/dist/store/sqlite/findings-core.js +14 -1
  121. package/dist/store/sqlite/knowledge.d.ts +16 -2
  122. package/dist/store/sqlite/knowledge.js +1 -0
  123. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  124. package/dist/store/sqlite/relation-proposals.d.ts +29 -2
  125. package/dist/store/sqlite/relation-proposals.js +75 -4
  126. package/dist/store/store.d.ts +1 -0
  127. package/package.json +3 -3
  128. package/parity.json +11 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Word patterns for project status, ru/en/es, matched on the text as written (case folded here). A stem cannot tell
3
+ * `оплатил` from `оплати`, so the past forms a money stem shares with a request are listed whole: a message with one
4
+ * of them reads as "done", whatever stems it also hits.
5
+ */
6
+ const words = (forms) => new RegExp(`(?<![\\p{L}\\p{N}])(?:${forms.join("|")})(?![\\p{L}\\p{N}])`, "iu");
7
+ export const MONEY_DONE_FORMS = words([
8
+ "оплатил[аи]?",
9
+ "заплатил[аи]?",
10
+ "перевел[аи]?",
11
+ "перевёл",
12
+ "отправил[аи]?",
13
+ "оплачено",
14
+ "переведено",
15
+ "paid",
16
+ "transferred",
17
+ "pagué",
18
+ "pagado",
19
+ "transferí",
20
+ ]);
21
+ /** First person future: "I'll send", "сделаю", "lo envío". Labelled a guess wherever it is shown. */
22
+ export const PROMISE_FORMS = words([
23
+ "сделаю",
24
+ "пришлю",
25
+ "отправлю",
26
+ "подготовлю",
27
+ "проверю",
28
+ "напишу",
29
+ "позвоню",
30
+ "скину",
31
+ "вышлю",
32
+ "оплачу",
33
+ "будет готов[аоы]?",
34
+ "i'?ll (?:send|do|check|call|write|pay|prepare|share|fix)",
35
+ "i will (?:send|do|check|call|write|pay|prepare|share|fix)",
36
+ "will be ready",
37
+ "lo envío",
38
+ "te envío",
39
+ "enviaré",
40
+ "haré",
41
+ "mandaré",
42
+ "te llamo",
43
+ "estará listo",
44
+ ]);
@@ -2,7 +2,7 @@ import * as v from "valibot";
2
2
  export declare const fact: v.StrictObjectSchema<{
3
3
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
4
4
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
5
- readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
5
+ readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
6
6
  readonly text: v.StringSchema<undefined>;
7
7
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
8
8
  readonly project: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -29,6 +29,17 @@ export declare const fact: v.StrictObjectSchema<{
29
29
  /** Who recorded it: the owner, or an agent whose fact waits as proposed. */
30
30
  readonly origin: v.PicklistSchema<["owner", "agent"], undefined>;
31
31
  readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
32
+ /** A `payment` fact's fields. */
33
+ readonly payment: v.OptionalSchema<v.NullableSchema<v.StrictObjectSchema<{
34
+ readonly amount: v.StringSchema<undefined>;
35
+ readonly currency: v.StringSchema<undefined>;
36
+ readonly direction: v.PicklistSchema<readonly ["to_us", "from_us"], undefined>;
37
+ readonly counterparty: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
38
+ readonly counterparty_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
39
+ readonly due_at: v.NullableSchema<v.StringSchema<undefined>, undefined>;
40
+ readonly state: v.PicklistSchema<readonly ["expected", "paid", "cancelled"], undefined>;
41
+ readonly item: v.NullableSchema<v.StringSchema<undefined>, undefined>;
42
+ }, undefined>, undefined>, undefined>;
32
43
  /** With `--sources`: each piece of evidence read back. */
33
44
  readonly sources: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
34
45
  readonly ref: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
@@ -1,6 +1,17 @@
1
1
  import * as v from "valibot";
2
2
  import { FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../domain/knowledge.js";
3
+ import { PAYMENT_DIRECTIONS, PAYMENT_STATES } from "../meetings/index.js";
3
4
  import { author, evidenceSource, id, reference, text, time } from "./fields.js";
5
+ const payment = v.strictObject({
6
+ amount: text,
7
+ currency: text,
8
+ direction: v.picklist(PAYMENT_DIRECTIONS),
9
+ counterparty: v.nullable(reference),
10
+ counterparty_name: v.nullable(text),
11
+ due_at: v.nullable(text),
12
+ state: v.picklist(PAYMENT_STATES),
13
+ item: v.nullable(text),
14
+ });
4
15
  export const fact = v.strictObject({
5
16
  ref: reference,
6
17
  id,
@@ -24,6 +35,8 @@ export const fact = v.strictObject({
24
35
  /** Who recorded it: the owner, or an agent whose fact waits as proposed. */
25
36
  origin: v.picklist(["owner", "agent"]),
26
37
  evidence: v.array(reference),
38
+ /** A `payment` fact's fields. */
39
+ payment: v.optional(v.nullable(payment)),
27
40
  /** With `--sources`: each piece of evidence read back. */
28
41
  sources: v.optional(v.array(evidenceSource)),
29
42
  created_at: time,
@@ -1099,6 +1099,7 @@ export declare const ENTITIES: {
1099
1099
  readonly reached_since: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1100
1100
  readonly reached_last: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1101
1101
  readonly stopped: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1102
+ readonly limited: v.OptionalSchema<v.PicklistSchema<["group_too_large", "older_than_window"], undefined>, undefined>;
1102
1103
  readonly tasks: v.OptionalSchema<v.StrictObjectSchema<{
1103
1104
  readonly added: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
1104
1105
  readonly closed: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -1145,6 +1146,11 @@ export declare const ENTITIES: {
1145
1146
  readonly retryable: v.BooleanSchema<undefined>;
1146
1147
  readonly retry_after_ms: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
1147
1148
  }, v.UnknownSchema, undefined>, undefined>;
1149
+ readonly limited: v.OptionalSchema<v.PicklistSchema<["group_too_large", "older_than_window"], undefined>, undefined>;
1150
+ }, undefined>, undefined>;
1151
+ readonly limited: v.OptionalSchema<v.StrictObjectSchema<{
1152
+ readonly group_too_large: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
1153
+ readonly older_than_window: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
1148
1154
  }, undefined>, undefined>;
1149
1155
  readonly tasks: v.OptionalSchema<v.StrictObjectSchema<{
1150
1156
  readonly added: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -5448,7 +5454,7 @@ export declare const ENTITIES: {
5448
5454
  readonly fact: v.StrictObjectSchema<{
5449
5455
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
5450
5456
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
5451
- readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
5457
+ readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
5452
5458
  readonly text: v.StringSchema<undefined>;
5453
5459
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
5454
5460
  readonly project: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -5471,6 +5477,16 @@ export declare const ENTITIES: {
5471
5477
  readonly scope: v.NullableSchema<v.PicklistSchema<readonly ["personal", "work"], undefined>, undefined>;
5472
5478
  readonly origin: v.PicklistSchema<["owner", "agent"], undefined>;
5473
5479
  readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5480
+ readonly payment: v.OptionalSchema<v.NullableSchema<v.StrictObjectSchema<{
5481
+ readonly amount: v.StringSchema<undefined>;
5482
+ readonly currency: v.StringSchema<undefined>;
5483
+ readonly direction: v.PicklistSchema<readonly ["to_us", "from_us"], undefined>;
5484
+ readonly counterparty: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5485
+ readonly counterparty_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
5486
+ readonly due_at: v.NullableSchema<v.StringSchema<undefined>, undefined>;
5487
+ readonly state: v.PicklistSchema<readonly ["expected", "paid", "cancelled"], undefined>;
5488
+ readonly item: v.NullableSchema<v.StringSchema<undefined>, undefined>;
5489
+ }, undefined>, undefined>, undefined>;
5474
5490
  readonly sources: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
5475
5491
  readonly ref: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5476
5492
  readonly kind: v.StringSchema<undefined>;
@@ -6257,7 +6273,7 @@ export declare const ENTITIES: {
6257
6273
  readonly fingerprint: v.StringSchema<undefined>;
6258
6274
  }, undefined>, undefined>;
6259
6275
  readonly kind: v.LiteralSchema<"fact", undefined>;
6260
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
6276
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
6261
6277
  readonly statement: v.StringSchema<undefined>;
6262
6278
  }, undefined>, v.StrictObjectSchema<{
6263
6279
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
@@ -17,7 +17,7 @@ export declare const meetingFindings: v.StrictObjectSchema<{
17
17
  readonly fingerprint: v.StringSchema<undefined>;
18
18
  }, undefined>, undefined>;
19
19
  readonly kind: v.LiteralSchema<"fact", undefined>;
20
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
20
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
21
21
  readonly statement: v.StringSchema<undefined>;
22
22
  }, undefined>, v.StrictObjectSchema<{
23
23
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
@@ -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 });
@@ -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);
@@ -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 {
@@ -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,18 @@ 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
17
  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";
18
+ export { createMeetingContext, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
19
19
  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
20
  export type { Attachment, ChatLine, Event, EventSeries, JsonValue, Meeting, MeetingSeries, Metadata, Occurrence, Participant, RecordTimes, SourceChatLine, SourceMeeting, Summary, SummaryContent, Transcript, TranscriptLine, TranscriptRow, } from "./model.js";
21
21
  export { type MeetingParticipantCandidate, type MeetingParticipantPage, type MeetingParticipantReader, type MeetingParticipantSearchOptions, type MeetingParticipantSearchStore, normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
22
22
  export { type MeetingContextCue, type MeetingHitContextOptions, type MeetingHitContextPacket, type MeetingPassageOutputFormat, type MeetingPassageReader, type MeetingPassageRequest, type MeetingPassageResult, meetingHitContextLimits, validateMeetingPassage, } from "./passage.js";
23
+ export { FACT_TYPES, PAYMENT_DIRECTIONS, PAYMENT_STATES, type Payment, paymentTitle, validatePayment, } from "./payment.js";
23
24
  export { type MeetingPreparationContext, type MeetingPreparationContextRequest, type MeetingPreparationContextStore, type PreparationPage, validateMeetingPreparationContext, } from "./preparation-context.js";
24
25
  export type { MeetingPreparationFact, MeetingPreparationKnowledgeRequest, MeetingPreparationKnowledgeStore, } from "./preparation-knowledge.js";
25
26
  export type { MeetingPreparationNote, MeetingPreparationNotesSelection, MeetingPreparationNotesStore, } from "./preparation-notes.js";
@@ -12,10 +12,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
14
  export { CHAT_MESSAGES_READ, } from "./links.js";
15
- export { createMeetingContext, MEETING_FACT_TYPES, validateMeetingContextChange, } from "./meeting-context.js";
15
+ export { createMeetingContext, validateMeetingContextChange, } from "./meeting-context.js";
16
16
  export { createMeetingManagement, dispatchMeetingMutation, validateMeetingChange, } from "./meeting-management.js";
17
17
  export { normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
18
18
  export { meetingHitContextLimits, validateMeetingPassage, } from "./passage.js";
19
+ export { FACT_TYPES, PAYMENT_DIRECTIONS, PAYMENT_STATES, paymentTitle, validatePayment, } from "./payment.js";
19
20
  export { validateMeetingPreparationContext, } from "./preparation-context.js";
20
21
  export { readMeetingProjectEvidence } from "./project-evidence.js";
21
22
  export { meetingProjectEvidence, meetingProjectEvidenceAnswer, } from "./project-evidence-answer.js";
@@ -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
+ };