@wirecat/cli-core 0.545.0 → 0.546.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 (88) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/calendar/busy.js +4 -1
  3. package/dist/calendar/cli/answers.d.ts +31 -0
  4. package/dist/calendar/cli/answers.js +8 -0
  5. package/dist/calendar/cli/index.d.ts +6 -0
  6. package/dist/calendar/cli/index.js +43 -4
  7. package/dist/calendar/cli/remind.js +2 -1
  8. package/dist/calendar/expand.d.ts +17 -0
  9. package/dist/calendar/expand.js +340 -0
  10. package/dist/calendar/feeds.d.ts +96 -0
  11. package/dist/calendar/feeds.js +324 -0
  12. package/dist/calendar/ics.d.ts +15 -4
  13. package/dist/calendar/ics.js +18 -7
  14. package/dist/calendar/index.d.ts +3 -1
  15. package/dist/calendar/index.js +2 -0
  16. package/dist/calendar/server-search.js +3 -1
  17. package/dist/calendar/writes.js +15 -4
  18. package/dist/cli/after-dashes.d.ts +16 -0
  19. package/dist/cli/after-dashes.js +41 -0
  20. package/dist/cli/bot/context.d.ts +1 -1
  21. package/dist/cli/bot/context.js +9 -1
  22. package/dist/cli/bot/messages.js +2 -0
  23. package/dist/cli/bot/setup.js +1 -0
  24. package/dist/cli/contract-templates.js +2 -2
  25. package/dist/cli/entry.d.ts +2 -0
  26. package/dist/cli/entry.js +3 -2
  27. package/dist/cli/index.d.ts +2 -1
  28. package/dist/cli/index.js +2 -1
  29. package/dist/cli/messenger/admin-commands.js +5 -1
  30. package/dist/cli/messenger/admin-contacts-command.js +5 -1
  31. package/dist/cli/messenger/admin-folders-command.js +5 -1
  32. package/dist/cli/messenger/complete-command.js +1 -0
  33. package/dist/cli/messenger/messages-command.js +2 -19
  34. package/dist/cli/messenger/messages-edit-command.js +1 -0
  35. package/dist/cli/messenger/messages-press-command.js +1 -0
  36. package/dist/cli/messenger/messages-send-command.js +1 -0
  37. package/dist/cli/messenger/observed.js +2 -2
  38. package/dist/cli/messenger/polls-command.js +1 -0
  39. package/dist/cli/messenger/private-people-command.js +5 -1
  40. package/dist/cli/messenger/stored.js +56 -3
  41. package/dist/cli/messenger/topics-command.js +1 -1
  42. package/dist/cli/program.js +17 -6
  43. package/dist/cli/schedule/schedule-command.js +1 -0
  44. package/dist/domain/message-link.d.ts +3 -0
  45. package/dist/domain/message-link.js +9 -14
  46. package/dist/domain/money-words.d.ts +11 -0
  47. package/dist/domain/money-words.js +38 -0
  48. package/dist/domain/providers.d.ts +2 -0
  49. package/dist/domain/providers.js +15 -3
  50. package/dist/domain/ref-arguments.d.ts +31 -0
  51. package/dist/domain/ref-arguments.js +79 -0
  52. package/dist/domain/references.d.ts +20 -2
  53. package/dist/domain/references.js +132 -47
  54. package/dist/errors/reasons.js +18 -0
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/meetings/mcp/index.d.ts +15 -15
  58. package/dist/oauth/client-file.d.ts +14 -0
  59. package/dist/oauth/client-file.js +28 -0
  60. package/dist/oauth/index.d.ts +1 -0
  61. package/dist/oauth/index.js +1 -0
  62. package/dist/ports/messenger.d.ts +3 -2
  63. package/dist/resolve.d.ts +3 -1
  64. package/dist/resolve.js +8 -0
  65. package/dist/search/task-signature.d.ts +5 -1
  66. package/dist/search/task-signature.js +27 -9
  67. package/dist/services/agenda.d.ts +5 -0
  68. package/dist/services/agenda.js +3 -1
  69. package/dist/services/index.d.ts +5 -1
  70. package/dist/services/index.js +5 -1
  71. package/dist/services/message-resolution.d.ts +4 -1
  72. package/dist/services/message-resolution.js +19 -3
  73. package/dist/services/messages.js +15 -13
  74. package/dist/services/money-words.d.ts +3 -0
  75. package/dist/services/money-words.js +5 -0
  76. package/dist/services/questions.d.ts +7 -2
  77. package/dist/services/questions.js +13 -4
  78. package/dist/services/stored-project-gathering.d.ts +9 -0
  79. package/dist/services/stored-project-gathering.js +9 -4
  80. package/dist/services/task-categories.d.ts +2 -0
  81. package/dist/services/task-categories.js +19 -8
  82. package/dist/store/sqlite/evidence-sources.js +2 -2
  83. package/dist/store/sqlite/identities.js +1 -1
  84. package/dist/store/sqlite/knowledge.js +7 -2
  85. package/dist/store/sqlite/notes.js +4 -4
  86. package/dist/store/sqlite/things.d.ts +0 -1
  87. package/dist/store/sqlite/things.js +25 -24
  88. package/package.json +1 -1
@@ -401,9 +401,10 @@ export interface HistoryBatch {
401
401
  /**
402
402
  * Takes one pushed batch. The CLI's own handler resolves `true` once the batch is in the local store and `false` when it
403
403
  * could not be written, so a messenger that pushes history once can keep a batch until it is saved; a handler that
404
- * stores nothing returns nothing.
404
+ * stores nothing returns nothing. A command's own handler, called through the store, is handed that answer as `saved`,
405
+ * so what it reports as kept is what reached the store.
405
406
  */
406
- export type HistoryBatchHandler = (batch: HistoryBatch) => void | Promise<boolean>;
407
+ export type HistoryBatchHandler = (batch: HistoryBatch, saved?: Promise<boolean>) => void | Promise<boolean>;
407
408
  /** For a messenger that pushes its history to the client instead of answering for it (`Messenger.history: "store"`). */
408
409
  export interface PushedHistory {
409
410
  /** Chats, people and past messages as the messenger pushes them, until `signal` aborts. */
package/dist/resolve.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import type { Chat, Contact, Id } from "./domain/models.js";
1
+ import type { Chat, Contact, Id, Provider } from "./domain/models.js";
2
2
  /** The part of a store that `pickPerson` reads. */
3
3
  export interface PeopleLookup {
4
+ /** Whose identities these are; a `contact:` reference of another provider is refused when it is set. */
5
+ provider?: Provider;
4
6
  get(id: Id): Contact | undefined;
5
7
  /** Everyone known, in any order. */
6
8
  all(): Contact[];
package/dist/resolve.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CliError, singleLine } from "@wirecat/cli-base";
2
+ import { contactReferenceOf } from "./domain/ref-arguments.js";
2
3
  /** Shaped like a Telegram or MAX id. Another messenger's ids may be any string, so a miss here is not "not an id". */
3
4
  export const isId = (reference) => /^-?\d+$/.test(reference.trim());
4
5
  /**
@@ -31,6 +32,13 @@ export const pickChat = (reference, chats) => {
31
32
  */
32
33
  export const pickPerson = (reference, people) => {
33
34
  const trimmed = reference.trim();
35
+ const contact = contactReferenceOf(trimmed, people.provider);
36
+ if (contact !== undefined) {
37
+ const named = people.get(contact);
38
+ if (named)
39
+ return named;
40
+ throw new CliError("not_found", `no person ${contact} in what this account has seen`);
41
+ }
34
42
  const known = people.get(trimmed);
35
43
  if (known)
36
44
  return known;
@@ -7,7 +7,11 @@ export type TaskAmount = {
7
7
  value: number;
8
8
  currency: string | null;
9
9
  };
10
- /** A bare number is not an amount: `верни 3000 за бензин` has none, so a reminder about it compares as missing. */
10
+ /**
11
+ * A bare number is not an amount: `верни 3000 за бензин` has none, so a reminder about it compares as missing.
12
+ * Takes the text as written; it is lower-cased here.
13
+ */
14
+ export declare const amountsOf: (text: string) => TaskAmount[];
11
15
  export declare const amountOf: (text: string) => TaskAmount | null;
12
16
  /** The creator's calendar date in the machine's time zone, as a day count: "tomorrow" late at night is local. */
13
17
  export declare const dayOf: (ms: number) => number;
@@ -10,18 +10,36 @@ const CURRENCY = [
10
10
  [/^(\$|usd|долл.*|dollars?)$/u, "USD"],
11
11
  ];
12
12
  const currencyOf = (word) => word ? (CURRENCY.find(([pattern]) => pattern.test(word))?.[1] ?? null) : null;
13
- const AMOUNT = /(?<![\p{L}\p{N}])([$€₽])?\s?(\d+(?:[.,]\d+)?)(?:([кk])(?![\p{L}])|\s?(тыс\.?|тысяч\p{L}*))?(?![\p{N}])\s?(₽|руб\p{L}*\.?|р\.|р(?![\p{L}])|rub|eur|евро|€|\$|usd|долл\p{L}*|dollars?|euros?)?(?![\p{L}])/gu;
14
- /** A bare number is not an amount: `верни 3000 за бензин` has none, so a reminder about it compares as missing. */
15
- export const amountOf = (text) => {
16
- for (const [, before, number = "", short, long, after] of text.matchAll(AMOUNT)) {
17
- const multiplier = short ?? long;
18
- if (!before && !after && !multiplier)
13
+ const SEPARATORS = String.raw `[ \u00a0\u2009\u202f.,]`;
14
+ /**
15
+ * Groups of three after a space (also no-break, thin, narrow no-break), a dot or a comma (`50 000`, `1.200`,
16
+ * `$1,200`) are one number, with an optional one or two digit decimal after (`5 000.50`). A lone `1,5` is a decimal.
17
+ */
18
+ const NUMBER = String.raw `\d{1,3}(?:${SEPARATORS}\d{3})+(?:[.,]\d{1,2})?|\d+(?:[.,]\d+)?`;
19
+ const AMOUNT = new RegExp(String.raw `(?<![\p{L}\p{N}])([$€₽])?\s?(${NUMBER})(?:([кk])(?![\p{L}])|\s?(тыс\.?|тысяч\p{L}*))?(?![\p{N}])\s?(₽|руб\p{L}*\.?|р\.|р(?![\p{L}])|rub|eur|евро|€|\$|usd|долл\p{L}*|dollars?|euros?)?(?![\p{L}])`, "gu");
20
+ const GROUPED = new RegExp(String.raw `^(\d{1,3}(?:${SEPARATORS}\d{3})+)([.,]\d{1,2})?$`, "u");
21
+ const amountValue = (number, thousands) => {
22
+ const grouped = GROUPED.exec(number);
23
+ const plain = grouped
24
+ ? `${(grouped[1] ?? "").replace(new RegExp(SEPARATORS, "gu"), "")}${(grouped[2] ?? "").replace(",", ".")}`
25
+ : number.replace(",", ".");
26
+ return Math.round(Number(plain) * (thousands ? 1000 : 1) * 100) / 100;
27
+ };
28
+ /**
29
+ * A bare number is not an amount: `верни 3000 за бензин` has none, so a reminder about it compares as missing.
30
+ * Takes the text as written; it is lower-cased here.
31
+ */
32
+ export const amountsOf = (text) => {
33
+ const found = [];
34
+ for (const [, before, number = "", short, long, after] of text.toLowerCase().matchAll(AMOUNT)) {
35
+ const thousands = (short ?? long) !== undefined;
36
+ if (!before && !after && !thousands)
19
37
  continue;
20
- const value = Math.round(Number(number.replace(",", ".")) * (multiplier ? 1000 : 1));
21
- return { value, currency: currencyOf(before ?? after?.replace(/\.$/u, "")) };
38
+ found.push({ value: amountValue(number, thousands), currency: currencyOf(before ?? after?.replace(/\.$/u, "")) });
22
39
  }
23
- return null;
40
+ return found;
24
41
  };
42
+ export const amountOf = (text) => amountsOf(text)[0] ?? null;
25
43
  const RU_WEEKDAYS = ["воскресен", "понедельник", "вторник", "сред", "четверг", "пятниц", "суббот"];
26
44
  const EN_WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
27
45
  const ES_WEEKDAYS = ["domingo", "lunes", "martes", "miercoles", "jueves", "viernes", "sabado"];
@@ -4,6 +4,11 @@ import { agenda } from "../entities/agenda.js";
4
4
  import { type ChatActivity, type TaskDetails } from "../store/store.js";
5
5
  import { type ChatCoverage } from "./chat-coverage.js";
6
6
  import type { ServiceDeps } from "./deps.js";
7
+ /** How long a chat must be silent before `tasks agenda` lists it, per side. */
8
+ export declare const AGENDA_DEFAULTS: {
9
+ readonly unanswered: 86400000;
10
+ readonly waiting: number;
11
+ };
7
12
  export interface AgendaOptions {
8
13
  /** ms: a task due within this is listed. */
9
14
  dueWithin?: number;
@@ -8,6 +8,8 @@ import { CHAT_KINDS } from "./chats.js";
8
8
  import { taskAccount } from "./task-rules.js";
9
9
  import { taskTagFilters } from "./task-tag-filters.js";
10
10
  const DAY = 86_400_000;
11
+ /** How long a chat must be silent before `tasks agenda` lists it, per side. */
12
+ export const AGENDA_DEFAULTS = { unanswered: DAY, waiting: 7 * DAY };
11
13
  const CLAIMS = ["none", "mine", "others", "any"];
12
14
  const TASK_FIELDS = new Set(["tag", "category", "claimed"]);
13
15
  export const AGENDA_QUERY = {
@@ -35,7 +37,7 @@ export const AGENDA_QUERY = {
35
37
  defaults: ["chat_kind:(dialog OR group)", "claimed:(none OR mine)"],
36
38
  };
37
39
  export const agendaService = (deps, now = Date.now) => ({
38
- show: async ({ dueWithin = DAY, waiting = 7 * DAY, unanswered = DAY, within = 30 * DAY, query, holder } = {}) => {
40
+ show: async ({ dueWithin = DAY, waiting = AGENDA_DEFAULTS.waiting, unanswered = AGENDA_DEFAULTS.unanswered, within = 30 * DAY, query, holder, } = {}) => {
39
41
  const [store, account] = [await deps.store(), await deps.account()];
40
42
  const at = now();
41
43
  const parts = query ? split(query) : { tasks: undefined, chats: undefined };
@@ -1,4 +1,6 @@
1
1
  import { type BotCheckService } from "../botcheck/service.js";
2
+ export { amountOf, amountsOf, type TaskAmount } from "../search/task-signature.js";
3
+ export { AGENDA_DEFAULTS } from "./agenda.js";
2
4
  export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
3
5
  export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
4
6
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
@@ -9,6 +11,7 @@ export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.
9
11
  export type { MoveMediaArchiveOptions } from "./media-archive-move.js";
10
12
  export { moveMediaArchive } from "./media-archive-move.js";
11
13
  export { type DownloadedModel, downloadImageModel, downloadTextModel, type ModelDownloadOptions, } from "./model-downloads.js";
14
+ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, moneyClassesOf, PROMISE } from "./money-words.js";
12
15
  export type { ProjectChatFetcher, ProjectChatFetchProgress, ProjectChatFetchRequest, ProjectEvidenceContext, ProjectEvidenceItem, ProjectEvidencePerson, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, } from "./project-gathering.js";
13
16
  export { gatherProjectEvidence } from "./project-gathering.js";
14
17
  export type { ProjectNeighbour, ProjectNeighbourReader } from "./project-gathering-around.js";
@@ -16,7 +19,8 @@ export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./pr
16
19
  export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
17
20
  export { findEvidenceMentions } from "./project-gathering-mentions.js";
18
21
  export type { ProjectChatFetched, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
19
- export { canonicalProjectRef, gatherStoredProject, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
22
+ export { questions } from "./questions.js";
23
+ export { canonicalProjectRef, gatherStoredProject, type LinkedChat, linkedChatsOf, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
20
24
  import { type AccountService } from "./account.js";
21
25
  import { type AdminService } from "./admin.js";
22
26
  import { type AdminStatisticsService } from "./admin-statistics.js";
@@ -1,14 +1,18 @@
1
1
  import { botCheckService } from "../botcheck/service.js";
2
+ export { amountOf, amountsOf } from "../search/task-signature.js";
3
+ export { AGENDA_DEFAULTS } from "./agenda.js";
2
4
  export { chatCoverage } from "./chat-coverage.js";
3
5
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
4
6
  export { excerpt } from "./excerpt.js";
5
7
  export { archiveMedia, checkMediaArchive } from "./media-archive.js";
6
8
  export { moveMediaArchive } from "./media-archive-move.js";
7
9
  export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
10
+ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, moneyClassesOf, PROMISE } from "./money-words.js";
8
11
  export { gatherProjectEvidence } from "./project-gathering.js";
9
12
  export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
10
13
  export { findEvidenceMentions } from "./project-gathering-mentions.js";
11
- export { canonicalProjectRef, gatherStoredProject, } from "./stored-project-gathering.js";
14
+ export { questions } from "./questions.js";
15
+ export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, } from "./stored-project-gathering.js";
12
16
  import { accountService } from "./account.js";
13
17
  import { adminService } from "./admin.js";
14
18
  import { adminStatisticsService } from "./admin-statistics.js";
@@ -1,4 +1,5 @@
1
- import type { Id } from "../domain/models.js";
1
+ import type { Id, Provider } from "../domain/models.js";
2
+ import { type ProfilesOn } from "../domain/ref-arguments.js";
2
3
  import type { MessengerTraits } from "../ports/traits.js";
3
4
  import type { AccountStore } from "../store/account-store.js";
4
5
  import type { AccountKey, MessageStore } from "../store/store.js";
@@ -21,6 +22,8 @@ export declare const storedChatId: (messenger: Saved, reference: string, store:
21
22
  export declare const chatIdIn: (messenger: Saved, reference: string, store: AccountStore, { includeLeft }?: {
22
23
  includeLeft?: boolean;
23
24
  }) => Promise<string>;
25
+ /** The profiles of `app` the store recorded on an account of this provider, for a refusal to name. */
26
+ export declare const profilesOnAccount: (store: Pick<MessageStore, "accounts">, app: string | undefined, provider: Provider) => ProfilesOn;
24
27
  /** A chat as `--chat` names it, in whichever of the accounts holds it — one of them, or it is an error. */
25
28
  export declare const chatAmong: (messenger: Saved, store: MessageStore, accounts: AccountKey[], reference: string) => Promise<{
26
29
  account: AccountKey;
@@ -1,4 +1,5 @@
1
1
  import { CliError, singleLine } from "@wirecat/cli-base";
2
+ import { chatReferenceOf, ensureAccount } from "../domain/ref-arguments.js";
2
3
  import { isId, pickChat, pickPerson } from "../resolve.js";
3
4
  import { codeOf } from "../sends/guarded.js";
4
5
  import { nothingStored } from "./deps.js";
@@ -28,10 +29,12 @@ const LEFT_EMPTY = "you left this chat, and the store holds none of its messages
28
29
  */
29
30
  export const storedChatId = (messenger, reference, store, account, { includeLeft = false } = {}) => {
30
31
  const renamed = (id) => store.chats.renamedChatId(account, id);
32
+ const profilesOn = profilesOnAccount(store, messenger.app?.command, account.provider);
31
33
  const among = chatIdAmong(messenger, reference, account, {
32
34
  held: () => store.chats.chats(account, {}),
33
35
  renamed,
34
36
  left: async (id) => leftIn(await store.chats.chats(account, byId(id))),
37
+ profilesOn,
35
38
  });
36
39
  if (!includeLeft)
37
40
  return among;
@@ -41,6 +44,7 @@ export const storedChatId = (messenger, reference, store, account, { includeLeft
41
44
  return chatIdAmong(messenger, reference, account, {
42
45
  held: () => store.chats.chats(account, { includeLeft: true }),
43
46
  renamed,
47
+ profilesOn,
44
48
  });
45
49
  });
46
50
  };
@@ -70,14 +74,26 @@ const byId = (id) => ({
70
74
  where: { sql: "chats.external_id = ?", params: [id] },
71
75
  });
72
76
  const leftIn = ({ items }) => items.some((one) => one.membershipState === "left");
77
+ /** The profiles of `app` the store recorded on an account of this provider, for a refusal to name. */
78
+ export const profilesOnAccount = (store, app, provider) => async (account) => {
79
+ if (app === undefined)
80
+ return undefined;
81
+ return Object.entries((await store.accounts.profiles())[app] ?? {})
82
+ .filter(([, key]) => key.provider === provider && key.account === account)
83
+ .map(([name]) => name);
84
+ };
73
85
  /** `left` is given where a left chat is refused; it lets a numeric id the store does not hold through as typed. */
74
- const chatIdAmong = async (messenger, reference, account, { held, renamed, left, }) => {
86
+ const chatIdAmong = async (messenger, reference, account, { held, renamed, left, profilesOn, }) => {
75
87
  const trimmed = reference.trim();
76
88
  if (messenger.savedChatId && ["me", "self", "saved"].includes(trimmed.toLowerCase())) {
77
89
  return messenger.savedChatId(account);
78
90
  }
79
- if (/^-?\d+$/.test(trimmed)) {
80
- const id = (await renamed(trimmed)) ?? trimmed;
91
+ const named = chatReferenceOf(trimmed, account.provider);
92
+ if (named)
93
+ await ensureAccount(named, { account: account.account }, profilesOn);
94
+ if (named || /^-?\d+$/.test(trimmed)) {
95
+ const typed = named?.chat ?? trimmed;
96
+ const id = (await renamed(typed)) ?? typed;
81
97
  if (await left?.(id))
82
98
  throw new CliError("not_found", `you left chat ${id}`);
83
99
  return id;
@@ -3,6 +3,7 @@ import { validateFormattedText, visibleFormattedText } from "../domain/formattin
3
3
  import { formatLocator } from "../domain/locator.js";
4
4
  import { fileLinks, messageLinkTarget, validatePermalink } from "../domain/message-link.js";
5
5
  import { pinDurationsText } from "../domain/pin-duration.js";
6
+ import { ensureAccount } from "../domain/ref-arguments.js";
6
7
  import { threadIdOf } from "../domain/thread.js";
7
8
  import { capability } from "../ports/messenger.js";
8
9
  import { codeOf, guardedWrite, sentIds } from "../sends/guarded.js";
@@ -31,6 +32,14 @@ const filesOf = ({ attachments }) => {
31
32
  export const messagesService = (deps) => {
32
33
  const { guard } = deps;
33
34
  const inStore = async (read) => read(await deps.store(), await deps.account());
35
+ /** A message named by a reference is read only in the account the reference names; the store is not opened to say so. */
36
+ const ownTarget = async (target, account) => {
37
+ if (account.provider !== deps.messenger.provider)
38
+ throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
39
+ if (target.account === undefined)
40
+ return;
41
+ await ensureAccount({ text: target.text, account: target.account }, { account: account.account, ...(deps.profile === undefined ? {} : { profile: deps.profile }) });
42
+ };
34
43
  const serverThenStore = async (store, account, query) => {
35
44
  const server = await searchServer(deps, query);
36
45
  const only = query.backend === "server" && server
@@ -151,11 +160,8 @@ export const messagesService = (deps) => {
151
160
  },
152
161
  around: async (reference, message, window) => {
153
162
  const target = messageLinkTarget(reference, message, deps.messenger.provider);
154
- if (target.account !== undefined) {
155
- const account = await deps.account();
156
- if (account.provider !== deps.messenger.provider || target.account !== account.account)
157
- throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
158
- }
163
+ if (target.account !== undefined)
164
+ await ownTarget(target, await deps.account());
159
165
  if (fromStore(deps)) {
160
166
  return inStore(async (store, account) => store.messages.around(account, await readChatId(deps, target.chat, store, account), target.message, window));
161
167
  }
@@ -163,9 +169,7 @@ export const messagesService = (deps) => {
163
169
  },
164
170
  thread: (reference, message, options) => inStore(async (store, account) => {
165
171
  const target = messageLinkTarget(reference, message, deps.messenger.provider);
166
- if (account.provider !== deps.messenger.provider ||
167
- (target.account !== undefined && target.account !== account.account))
168
- throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
172
+ await ownTarget(target, account);
169
173
  return readThreadContext(store, account, await storedChatId(deps.messenger, target.chat, store, account, { includeLeft: true }), target.message, options);
170
174
  }),
171
175
  comments: async (chat, typedPost, window) => {
@@ -184,9 +188,7 @@ export const messagesService = (deps) => {
184
188
  link: async (reference, message) => {
185
189
  const target = messageLinkTarget(reference, message, deps.messenger.provider);
186
190
  const account = await deps.account();
187
- if (account.provider !== deps.messenger.provider ||
188
- (target.account !== undefined && target.account !== account.account))
189
- throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
191
+ await ownTarget(target, account);
190
192
  if (fromStore(deps)) {
191
193
  const store = await deps.store();
192
194
  const chat = await readChatId(deps, target.chat, store, account);
@@ -227,8 +229,8 @@ export const messagesService = (deps) => {
227
229
  },
228
230
  download: async (reference, message) => {
229
231
  const target = messageLinkTarget(reference, message, deps.messenger.provider);
230
- if (target.account !== undefined && target.account !== (await deps.account()).account)
231
- throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
232
+ if (target.account !== undefined)
233
+ await ownTarget(target, await deps.account());
232
234
  const connection = await deps.connection();
233
235
  const download = capability(connection, "download", "download attachments");
234
236
  return download((await connection.resolve(target.chat)).id, target.message);
@@ -0,0 +1,3 @@
1
+ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, PROMISE } from "../domain/money-words.js";
2
+ /** The money classes the text's words belong to; none for a text with no money verb. */
3
+ export declare const moneyClassesOf: (text: string) => string[];
@@ -0,0 +1,5 @@
1
+ import { MONEY_WORDS } from "../domain/money-words.js";
2
+ import { listsHit } from "./task-categories.js";
3
+ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, PROMISE } from "../domain/money-words.js";
4
+ /** The money classes the text's words belong to; none for a text with no money verb. */
5
+ export const moneyClassesOf = (text) => listsHit(text, MONEY_WORDS);
@@ -9,9 +9,14 @@ export declare const unanswered: (messages: Message[], { answerers, before }: {
9
9
  answerers: ReadonlySet<Id>;
10
10
  before: number;
11
11
  }) => Message[];
12
- /** Every question from others, as `unanswered` reads them, with the message that answered it, if one did. */
13
- export declare const questions: (messages: Message[], { answerers }: {
12
+ /**
13
+ * Every question from others, as `unanswered` reads them, with the message that answered it, if one did.
14
+ * With `asker: "owner"` it is the owner's own questions instead, and anyone but the owner answers: by replying to
15
+ * it, or by speaking next. `answerers` is not read then.
16
+ */
17
+ export declare const questions: (messages: Message[], { answerers, asker }: {
14
18
  answerers: ReadonlySet<Id>;
19
+ asker?: "others" | "owner";
15
20
  }) => {
16
21
  question: Message;
17
22
  answer?: Message;
@@ -9,12 +9,21 @@ const LINKS = /https?:\/\/\S+/g;
9
9
  export const unanswered = (messages, { answerers, before }) => questions(messages, { answerers })
10
10
  .filter(({ question, answer }) => answer === undefined && Date.parse(question.timestamp) < before)
11
11
  .map(({ question }) => question);
12
- /** Every question from others, as `unanswered` reads them, with the message that answered it, if one did. */
13
- export const questions = (messages, { answerers }) => {
14
- const answers = (message) => message.outgoing === true || (message.senderId !== null && answerers.has(message.senderId));
12
+ /**
13
+ * Every question from others, as `unanswered` reads them, with the message that answered it, if one did.
14
+ * With `asker: "owner"` it is the owner's own questions instead, and anyone but the owner answers: by replying to
15
+ * it, or by speaking next. `answerers` is not read then.
16
+ */
17
+ export const questions = (messages, { answerers, asker = "others" }) => {
18
+ const fromOwner = (message) => message.outgoing === true;
19
+ const answers = asker === "owner"
20
+ ? (message) => !fromOwner(message)
21
+ : (message) => fromOwner(message) || (message.senderId !== null && answerers.has(message.senderId));
15
22
  const byId = new Map(messages.map((message) => [message.id, message]));
16
23
  // Telegram names only the id a reply answers; the message itself is found in the window, or not at all.
17
24
  const repliesToAnswerer = (message) => {
25
+ if (asker === "owner")
26
+ return false;
18
27
  const to = message.replyTo?.id ?? message.replyToId;
19
28
  const quoted = to === undefined ? undefined : byId.get(to);
20
29
  if (quoted)
@@ -22,7 +31,7 @@ export const questions = (messages, { answerers }) => {
22
31
  return (message.replyTo !== null && (message.replyTo.outgoing === true || answerers.has(message.replyTo.senderId ?? "")));
23
32
  };
24
33
  return messages.flatMap((message, index) => {
25
- if (answers(message))
34
+ if (asker === "owner" ? !fromOwner(message) : answers(message))
26
35
  return [];
27
36
  const transcript = "transcript" in message && typeof message.transcript === "string" ? message.transcript : "";
28
37
  if (![message.text, transcript].join("\n").replace(LINKS, "").includes("?") && !repliesToAnswerer(message))
@@ -34,6 +34,15 @@ export interface StoredProjectInput {
34
34
  }
35
35
  /** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
36
36
  export declare const canonicalProjectRef: (store: MessageStore, reference: string) => Promise<string>;
37
+ export interface LinkedChat {
38
+ provider: string;
39
+ account: string;
40
+ chat: string;
41
+ /** The link's free role (`source report`, `what worked`), if the owner set one. */
42
+ role: string | null;
43
+ }
44
+ /** Only links the owner confirmed: a suggested one would put a whole chat into the evidence on a guess. */
45
+ export declare const linkedChatsOf: (store: MessageStore, project: string | null) => Promise<LinkedChat[]>;
37
46
  export type StoredProjectGathering = Awaited<ReturnType<typeof gatherProjectEvidence>> & {
38
47
  projectRef: string | null;
39
48
  unreadSources: {
@@ -118,7 +118,7 @@ const memberLanesOf = async (store, project, given, room) => {
118
118
  return { lanes, left };
119
119
  };
120
120
  /** Only links the owner confirmed: a suggested one would put a whole chat into the evidence on a guess. */
121
- const linkedChatsOf = async (store, project) => {
121
+ export const linkedChatsOf = async (store, project) => {
122
122
  if (!project)
123
123
  return [];
124
124
  const chats = [];
@@ -129,9 +129,14 @@ const linkedChatsOf = async (store, project) => {
129
129
  continue;
130
130
  const reference = parseReference(relation.from);
131
131
  if (reference.type === "chat")
132
- chats.push(reference);
132
+ chats.push({
133
+ provider: reference.provider,
134
+ account: reference.account,
135
+ chat: reference.chat,
136
+ role: relation.role,
137
+ });
133
138
  }
134
- return chats.map(({ provider, account, chat }) => ({ provider, account, chat }));
139
+ return chats;
135
140
  };
136
141
  /**
137
142
  * `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
@@ -158,7 +163,7 @@ export const gatherStoredProject = async (store, input) => {
158
163
  const members = await memberLanesOf(store, project.ref, new Set(contacts.flatMap((one) => one.reference ?? [])), room);
159
164
  const people = [...contacts, ...members.lanes];
160
165
  const linked = await linkedChatsOf(store, project.ref);
161
- const linkedChats = linked.slice(0, LINKED_CHATS);
166
+ const linkedChats = linked.slice(0, LINKED_CHATS).map(({ provider, account, chat }) => ({ provider, account, chat }));
162
167
  const budgets = {
163
168
  ...(input.maxReads === undefined ? {} : { maxReads: input.maxReads }),
164
169
  ...(input.limit === undefined ? {} : { maxItems: input.limit }),
@@ -2,6 +2,8 @@ import { type TaskCategory } from "../domain/task-categories.js";
2
2
  export { DEFAULT_TASK_CATEGORIES, FALLBACK_CATEGORY, parseTaskCategory, type TaskCategory, type UnlistedCategory, unlistedNote, withTaskCategory, } from "../domain/task-categories.js";
3
3
  /** A category the list holds, or a refusal that names the list. */
4
4
  export declare const taskCategoryOf: (categories?: readonly TaskCategory[], flag?: string) => (typed: string) => string;
5
+ /** The names of every list the text hits, so a message can be a request and a promise at once. */
6
+ export declare const listsHit: (text: string, categories: readonly TaskCategory[]) => string[];
5
7
  /**
6
8
  * The category with the most distinct trigger stems in the text; none is `other`, a tie goes to the earlier one.
7
9
  * A word hits by its English stem first, so a cognate (`hospital`) counts once, not twice.
@@ -38,15 +38,12 @@ const triggerStems = (categories) => {
38
38
  stemsByList.set(categories, built);
39
39
  return built;
40
40
  };
41
- /**
42
- * The category with the most distinct trigger stems in the text; none is `other`, a tie goes to the earlier one.
43
- * A word hits by its English stem first, so a cognate (`hospital`) counts once, not twice.
44
- */
45
- export const categorize = (text, categories = DEFAULT_TASK_CATEGORIES) => {
41
+ /** For each list the text hits, the number of distinct trigger stems it hit, in list order. */
42
+ const hitsByList = (text, categories) => {
46
43
  const stem = stemmersOf();
47
44
  const english = stem.english.stemTokens(text);
48
45
  const spanish = readsSpanish(text) ? stem.spanish.stemTokens(text) : [];
49
- let best = { name: FALLBACK_CATEGORY, hits: 0 };
46
+ const counted = [];
50
47
  for (const triggers of triggerStems(categories)) {
51
48
  const hit = new Set();
52
49
  english.forEach((word, at) => {
@@ -56,8 +53,22 @@ export const categorize = (text, categories = DEFAULT_TASK_CATEGORIES) => {
56
53
  else if (other !== undefined && triggers.spanish.has(other))
57
54
  hit.add(other);
58
55
  });
59
- if (hit.size > best.hits)
60
- best = { name: triggers.name, hits: hit.size };
56
+ counted.push({ name: triggers.name, hits: hit.size });
61
57
  }
58
+ return counted;
59
+ };
60
+ /** The names of every list the text hits, so a message can be a request and a promise at once. */
61
+ export const listsHit = (text, categories) => hitsByList(text, categories)
62
+ .filter(({ hits }) => hits > 0)
63
+ .map(({ name }) => name);
64
+ /**
65
+ * The category with the most distinct trigger stems in the text; none is `other`, a tie goes to the earlier one.
66
+ * A word hits by its English stem first, so a cognate (`hospital`) counts once, not twice.
67
+ */
68
+ export const categorize = (text, categories = DEFAULT_TASK_CATEGORIES) => {
69
+ let best = { name: FALLBACK_CATEGORY, hits: 0 };
70
+ for (const counted of hitsByList(text, categories))
71
+ if (counted.hits > best.hits)
72
+ best = counted;
62
73
  return best.name;
63
74
  };
@@ -1,5 +1,5 @@
1
1
  import { linkKind } from "./link-kinds.js";
2
- import { canonicalThingReference, referenceOfThing, stateOfThing, storedThing, thingOf } from "./things.js";
2
+ import { referenceOfThing, stateOfThing, storedThing, thingOf } from "./things.js";
3
3
  const EXCERPT_CHARS = 280;
4
4
  const excerptOf = (text) => {
5
5
  if (text == null)
@@ -59,7 +59,7 @@ export const evidenceSourcesOf = (database, from) => database
59
59
  .map((link) => sourceOf(database, storedThing(link.to_type, link.to_id)));
60
60
  const thingNamed = (database, reference) => {
61
61
  try {
62
- return thingOf(database, canonicalThingReference(reference));
62
+ return thingOf(database, reference);
63
63
  }
64
64
  catch {
65
65
  return undefined;
@@ -205,7 +205,7 @@ export const people = ({ orm, database }, provider, { account, accounts: native
205
205
  }
206
206
  }
207
207
  const byId = new Map(everyone.map((person) => [person.id, person]));
208
- return { get: (id) => byId.get(id), all: () => everyone };
208
+ return { provider, get: (id) => byId.get(id), all: () => everyone };
209
209
  };
210
210
  const contactsWhere = (accountKey, key, { query, notes }) => {
211
211
  if (query !== undefined && query.trim().length < 3) {
@@ -127,7 +127,7 @@ export const knowledgeStoreOver = (context) => {
127
127
  if (thing.type === "meeting")
128
128
  return { type: "meeting", id: String(thing.id) };
129
129
  const reference = referenceOfThing(database, thing);
130
- if (reference === undefined || reference.startsWith("meeting:"))
130
+ if (reference === undefined)
131
131
  return undefined;
132
132
  const parsed = parseReference(reference);
133
133
  if (parsed.type === "message")
@@ -136,7 +136,12 @@ export const knowledgeStoreOver = (context) => {
136
136
  return { type: "chat", id: parsed.chat };
137
137
  if (parsed.type === "folder")
138
138
  return { type: "folder", id: parsed.id, path: parsed.path };
139
- if (parsed.type === "entity" || parsed.type === "memory" || parsed.type === "fact" || parsed.type === "bot")
139
+ if (parsed.type === "entity" ||
140
+ parsed.type === "memory" ||
141
+ parsed.type === "fact" ||
142
+ parsed.type === "bot" ||
143
+ parsed.type === "event" ||
144
+ parsed.type === "meeting")
140
145
  return undefined;
141
146
  return { type: parsed.type, id: parsed.id };
142
147
  };
@@ -14,7 +14,7 @@ import { LINK_KINDS, LINK_ORIGINS, linkKind } from "./link-kinds.js";
14
14
  import { reviewLink } from "./link-reviews.js";
15
15
  import { holdingText } from "./note-index.js";
16
16
  import { addTags, ensureTag, labelsOf } from "./tags.js";
17
- import { canonicalThingReference, referenceOfThing, storedThing, thingOf } from "./things.js";
17
+ import { referenceOfThing, storedThing, thingOf } from "./things.js";
18
18
  import { write } from "./transaction.js";
19
19
  export const NOTE_FORMATS = ["obsidian", "markdown"];
20
20
  export { LINK_ORIGINS } from "./link-kinds.js";
@@ -94,7 +94,7 @@ export const linkOf = (database, row) => {
94
94
  };
95
95
  /** The row a reference names, refused when the store holds none. */
96
96
  export const requiredThing = (database, reference, what = "the target") => {
97
- const thing = thingOf(database, canonicalThingReference(reference));
97
+ const thing = thingOf(database, reference);
98
98
  if (!thing)
99
99
  throw new CliError("not_found", `${what} ${reference} is not in the local store`);
100
100
  return thing;
@@ -522,7 +522,7 @@ export const notesStoreOver = (context) => {
522
522
  const { limit, offset } = bounded(options.limit, options.offset);
523
523
  if (options.source !== undefined)
524
524
  oneOf(options.source, ["file", "internal"], "a note's source");
525
- const about = options.about === undefined ? undefined : thingOf(database, canonicalThingReference(options.about));
525
+ const about = options.about === undefined ? undefined : thingOf(database, options.about);
526
526
  if (options.about !== undefined && !about)
527
527
  return { items: [], hasMore: false };
528
528
  const folder = options.folderId === undefined ? null : Number(folderRow(options.folderId).id);
@@ -597,7 +597,7 @@ export const notesStoreOver = (context) => {
597
597
  const pair = (reference) => {
598
598
  if (reference === undefined)
599
599
  return { known: true, type: null, id: null };
600
- const thing = thingOf(database, canonicalThingReference(reference));
600
+ const thing = thingOf(database, reference);
601
601
  return thing ? { known: true, type: thing.type, id: thing.id } : { known: false, type: null, id: null };
602
602
  };
603
603
  const from = pair(options.from);
@@ -24,4 +24,3 @@ export declare const stateOfThing: (database: CacheDatabase, thing: Thing | unde
24
24
  export declare const taskRowOf: (database: CacheDatabase, id: string) => number | undefined;
25
25
  /** The package's id for a task row: the reverse of `taskRowOf`. */
26
26
  export declare const taskIdOf: (database: CacheDatabase, rowId: number) => string | undefined;
27
- export declare const canonicalThingReference: (reference: string) => string;