@wirecat/cli-core 0.545.0 → 0.547.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 (175) hide show
  1. package/CHANGELOG.md +228 -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/history-query.d.ts +10 -4
  34. package/dist/cli/messenger/history-query.js +87 -31
  35. package/dist/cli/messenger/messages-command.js +12 -23
  36. package/dist/cli/messenger/messages-comments-command.js +4 -2
  37. package/dist/cli/messenger/messages-edit-command.js +1 -0
  38. package/dist/cli/messenger/messages-press-command.js +1 -0
  39. package/dist/cli/messenger/messages-send-command.js +1 -0
  40. package/dist/cli/messenger/observed.js +2 -2
  41. package/dist/cli/messenger/polls-command.js +1 -0
  42. package/dist/cli/messenger/private-people-command.js +5 -1
  43. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  44. package/dist/cli/messenger/stored.js +56 -3
  45. package/dist/cli/messenger/topics-command.js +1 -1
  46. package/dist/cli/option-hints.d.ts +6 -0
  47. package/dist/cli/option-hints.js +15 -0
  48. package/dist/cli/program.js +17 -6
  49. package/dist/cli/schedule/schedule-command.js +1 -0
  50. package/dist/domain/message-link.d.ts +3 -0
  51. package/dist/domain/message-link.js +9 -14
  52. package/dist/domain/money-words.d.ts +11 -0
  53. package/dist/domain/money-words.js +38 -0
  54. package/dist/domain/providers.d.ts +2 -0
  55. package/dist/domain/providers.js +15 -3
  56. package/dist/domain/ref-arguments.d.ts +31 -0
  57. package/dist/domain/ref-arguments.js +79 -0
  58. package/dist/domain/references.d.ts +20 -2
  59. package/dist/domain/references.js +132 -47
  60. package/dist/errors/reasons.js +31 -1
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.js +1 -1
  63. package/dist/mail/cli/index.js +4 -4
  64. package/dist/mail/index.d.ts +2 -0
  65. package/dist/mail/index.js +2 -0
  66. package/dist/mail/partial.d.ts +34 -0
  67. package/dist/mail/partial.js +94 -0
  68. package/dist/mail/server-search.js +17 -9
  69. package/dist/mail/source.d.ts +13 -2
  70. package/dist/mail/source.js +7 -1
  71. package/dist/mail/sync-types.d.ts +112 -0
  72. package/dist/mail/sync-types.js +1 -0
  73. package/dist/mail/sync.d.ts +4 -106
  74. package/dist/mail/sync.js +49 -4
  75. package/dist/mail/testing/fake.d.ts +3 -1
  76. package/dist/mail/testing/fake.js +10 -2
  77. package/dist/meetings/mcp/index.d.ts +15 -15
  78. package/dist/oauth/client-file.d.ts +14 -0
  79. package/dist/oauth/client-file.js +28 -0
  80. package/dist/oauth/index.d.ts +1 -0
  81. package/dist/oauth/index.js +1 -0
  82. package/dist/ports/messenger.d.ts +3 -2
  83. package/dist/resolve.d.ts +3 -1
  84. package/dist/resolve.js +8 -0
  85. package/dist/search/task-signature.d.ts +5 -1
  86. package/dist/search/task-signature.js +27 -9
  87. package/dist/services/agenda.d.ts +5 -0
  88. package/dist/services/agenda.js +3 -1
  89. package/dist/services/deps.d.ts +0 -2
  90. package/dist/services/deps.js +0 -2
  91. package/dist/services/documents/ingestion.js +1 -1
  92. package/dist/services/documents/knowledge.d.ts +10 -6
  93. package/dist/services/documents/knowledge.js +25 -11
  94. package/dist/services/index.d.ts +6 -1
  95. package/dist/services/index.js +6 -1
  96. package/dist/services/message-resolution.d.ts +4 -1
  97. package/dist/services/message-resolution.js +19 -3
  98. package/dist/services/messages.d.ts +2 -0
  99. package/dist/services/messages.js +40 -36
  100. package/dist/services/money-words.d.ts +3 -0
  101. package/dist/services/money-words.js +5 -0
  102. package/dist/services/proposal-tasks.d.ts +3 -2
  103. package/dist/services/proposal-tasks.js +5 -2
  104. package/dist/services/proposals/ids.d.ts +5 -0
  105. package/dist/services/proposals/ids.js +43 -0
  106. package/dist/services/proposals/index.d.ts +3 -0
  107. package/dist/services/proposals/index.js +2 -0
  108. package/dist/services/proposals/kinds/action.d.ts +7 -0
  109. package/dist/services/proposals/kinds/action.js +45 -0
  110. package/dist/services/proposals/kinds/fact.d.ts +4 -0
  111. package/dist/services/proposals/kinds/fact.js +43 -0
  112. package/dist/services/proposals/kinds/findings.d.ts +7 -0
  113. package/dist/services/proposals/kinds/findings.js +75 -0
  114. package/dist/services/proposals/kinds/key-document.d.ts +9 -0
  115. package/dist/services/proposals/kinds/key-document.js +75 -0
  116. package/dist/services/proposals/kinds/memory.d.ts +4 -0
  117. package/dist/services/proposals/kinds/memory.js +42 -0
  118. package/dist/services/proposals/kinds/relationship.d.ts +10 -0
  119. package/dist/services/proposals/kinds/relationship.js +82 -0
  120. package/dist/services/proposals/kinds/shared.d.ts +25 -0
  121. package/dist/services/proposals/kinds/shared.js +49 -0
  122. package/dist/services/proposals/registry.d.ts +9 -0
  123. package/dist/services/proposals/registry.js +129 -0
  124. package/dist/services/proposals/types.d.ts +85 -0
  125. package/dist/services/proposals/types.js +1 -0
  126. package/dist/services/questions.d.ts +7 -2
  127. package/dist/services/questions.js +13 -4
  128. package/dist/services/server-search.d.ts +7 -4
  129. package/dist/services/server-search.js +5 -2
  130. package/dist/services/stored-project-gathering.d.ts +9 -0
  131. package/dist/services/stored-project-gathering.js +9 -4
  132. package/dist/services/task-categories.d.ts +2 -0
  133. package/dist/services/task-categories.js +19 -8
  134. package/dist/services/tasks.js +8 -4
  135. package/dist/store/decisions.d.ts +65 -0
  136. package/dist/store/decisions.js +55 -0
  137. package/dist/store/evidence-findings.d.ts +6 -6
  138. package/dist/store/index.d.ts +4 -3
  139. package/dist/store/index.js +2 -1
  140. package/dist/store/meeting-findings.d.ts +2 -0
  141. package/dist/store/messages.d.ts +4 -9
  142. package/dist/store/sqlite/actors.d.ts +13 -2
  143. package/dist/store/sqlite/actors.js +24 -1
  144. package/dist/store/sqlite/email-locations.d.ts +16 -0
  145. package/dist/store/sqlite/email-locations.js +26 -0
  146. package/dist/store/sqlite/emails.d.ts +6 -2
  147. package/dist/store/sqlite/emails.js +2 -1
  148. package/dist/store/sqlite/evidence-findings.js +13 -3
  149. package/dist/store/sqlite/evidence-sources.js +2 -2
  150. package/dist/store/sqlite/facts.d.ts +16 -8
  151. package/dist/store/sqlite/facts.js +48 -15
  152. package/dist/store/sqlite/findings-core.d.ts +2 -1
  153. package/dist/store/sqlite/findings-core.js +4 -4
  154. package/dist/store/sqlite/identities.js +1 -1
  155. package/dist/store/sqlite/knowledge.d.ts +43 -7
  156. package/dist/store/sqlite/knowledge.js +62 -17
  157. package/dist/store/sqlite/meeting-findings.js +6 -6
  158. package/dist/store/sqlite/memories.d.ts +12 -8
  159. package/dist/store/sqlite/memories.js +33 -10
  160. package/dist/store/sqlite/notes.d.ts +5 -1
  161. package/dist/store/sqlite/notes.js +22 -6
  162. package/dist/store/sqlite/open-store.d.ts +4 -1
  163. package/dist/store/sqlite/open-store.js +8 -2
  164. package/dist/store/sqlite/open.d.ts +3 -0
  165. package/dist/store/sqlite/proposed-actions.d.ts +21 -8
  166. package/dist/store/sqlite/proposed-actions.js +29 -9
  167. package/dist/store/sqlite/reads.d.ts +13 -2
  168. package/dist/store/sqlite/reads.js +8 -4
  169. package/dist/store/sqlite/relation-proposals.d.ts +34 -0
  170. package/dist/store/sqlite/relation-proposals.js +114 -0
  171. package/dist/store/sqlite/things.d.ts +0 -1
  172. package/dist/store/sqlite/things.js +25 -24
  173. package/dist/store/store.d.ts +3 -0
  174. package/dist/store/store.js +2 -0
  175. package/package.json +1 -1
@@ -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))
@@ -2,12 +2,15 @@ import type { Id } from "../domain/models.js";
2
2
  import type { ServiceDeps } from "./deps.js";
3
3
  import type { SearchQuery } from "./messages.js";
4
4
  export declare const SERVER_SEARCH_KEY = "messages.server-search";
5
- /** `maxMessageBytes`: a candidate larger than this is not downloaded; a sync reads it, and the answer says so. */
5
+ /**
6
+ * `textBytes`: a mail candidate is read up to this many bytes, its headers and text; one cut short is stored partial
7
+ * and the next sync reads it whole.
8
+ */
6
9
  export declare const SERVER_BOUNDS: {
7
10
  timeMs: number;
8
11
  perCall: number;
9
12
  maxCalls: number;
10
- maxMessageBytes: number;
13
+ textBytes: number;
11
14
  };
12
15
  export declare const DEFAULT_BACKEND = "both";
13
16
  export type Backend = "archive" | "server" | "both";
@@ -26,8 +29,8 @@ export interface ServerSearched {
26
29
  chat: string | null;
27
30
  reason: "rate_limited" | "search_failed" | "time_or_abort_bound";
28
31
  }[];
29
- /** Candidates larger than `SERVER_BOUNDS.maxMessageBytes`, left on the server unread. */
30
- tooLarge?: number;
32
+ /** Candidates stored from their first `SERVER_BOUNDS.textBytes`, without the attachments; a sync reads them whole. */
33
+ partial?: number;
31
34
  complete: boolean;
32
35
  }
33
36
  export type HitSource = "archive" | "server" | "both";
@@ -2,8 +2,11 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { capability } from "../ports/messenger.js";
3
3
  import { prepareLucene } from "./messages-search.js";
4
4
  export const SERVER_SEARCH_KEY = "messages.server-search";
5
- /** `maxMessageBytes`: a candidate larger than this is not downloaded; a sync reads it, and the answer says so. */
6
- export const SERVER_BOUNDS = { timeMs: 5_000, perCall: 100, maxCalls: 3, maxMessageBytes: 5_000_000 };
5
+ /**
6
+ * `textBytes`: a mail candidate is read up to this many bytes, its headers and text; one cut short is stored partial
7
+ * and the next sync reads it whole.
8
+ */
9
+ export const SERVER_BOUNDS = { timeMs: 5_000, perCall: 100, maxCalls: 3, textBytes: 262_144 };
7
10
  export const DEFAULT_BACKEND = "both";
8
11
  export const sourceKey = (chatId, id) => JSON.stringify([chatId, id]);
9
12
  const merge = (one, other) => {
@@ -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
  };
@@ -260,7 +260,9 @@ export const tasksService = (deps, categories) => {
260
260
  const link = {
261
261
  from: (await store.tasks.details(target.id)).ref,
262
262
  kind: "evidence",
263
- origin: origin === "owner" ? "owner" : "suggested",
263
+ ...(origin === "owner"
264
+ ? { origin: "owner", by: "owner" }
265
+ : { origin: "suggested", confirmed: false }),
264
266
  };
265
267
  try {
266
268
  await store.notes.addLink({ ...link, to: note ? note.ref : source });
@@ -448,17 +450,19 @@ export const tasksService = (deps, categories) => {
448
450
  account: account.account,
449
451
  chat: await storedChatId(deps.messenger, chat, store, account, { includeLeft: true }),
450
452
  })));
451
- const origin = by === "owner" ? "owner" : "suggested";
453
+ const origin = by === "owner"
454
+ ? { origin: "owner", by: "owner" }
455
+ : { origin: "suggested", confirmed: false };
452
456
  for (const target of [...written, ...chatRefs]) {
453
457
  try {
454
- await store.notes.addLink({ from, to: target, kind: kind, origin });
458
+ await store.notes.addLink({ from, to: target, kind: kind, ...origin });
455
459
  }
456
460
  catch (error) {
457
461
  // Only text that names nothing typed, or a message not saved yet; a mistyped `person:9` is refused.
458
462
  const unsaved = error instanceof CliError && error.code === "not_found" && target.startsWith("msg:");
459
463
  if (!unsaved && !(error instanceof CliError && error.code === "validation_error"))
460
464
  throw error;
461
- await store.notes.addLink({ from, targetText: target, kind: kind, origin });
465
+ await store.notes.addLink({ from, targetText: target, kind: kind, ...origin });
462
466
  }
463
467
  }
464
468
  if (!written.some((target) => target.startsWith("msg:")))
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Who proposed something waiting for the owner. `unknown` is a suggestion that names no author, such as an
3
+ * unconfirmed link written without provenance: only the owner decides it.
4
+ */
5
+ export type Proposer = {
6
+ type: "person";
7
+ id: string;
8
+ } | {
9
+ type: "bot";
10
+ id: string;
11
+ name: string;
12
+ kind: "agent" | "rule";
13
+ } | {
14
+ type: "unknown";
15
+ };
16
+ /** Who decides: the owner, or an agent the host lets decide for the owner, with its reason. */
17
+ export type Decider = "owner" | {
18
+ bot: string;
19
+ model?: string;
20
+ reason: string;
21
+ };
22
+ export interface DecisionPolicy {
23
+ /** The host's `agent_decisions`: `false` keeps every decision with the owner. */
24
+ agentDecisions: boolean;
25
+ /** A rule scorer's proposal is the owner's alone to decide. */
26
+ ruleProposalsOwnerOnly: boolean;
27
+ }
28
+ export declare const DEFAULT_DECISION_POLICY: DecisionPolicy;
29
+ export interface Refusal {
30
+ id: string;
31
+ code: "permission_error" | "validation_error" | "not_found";
32
+ reason: string;
33
+ message: string;
34
+ }
35
+ /**
36
+ * The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
37
+ * Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
38
+ */
39
+ export declare const mayDecide: (row: {
40
+ id: string;
41
+ proposedBy: Proposer;
42
+ }, by: Decider, policy?: DecisionPolicy) => Refusal | null;
43
+ /** One bot whatever its letter case or spacing: `Claude-Code` is `claude-code`. */
44
+ export declare const botKey: (name: string) => string;
45
+ /**
46
+ * Removing or reversing a row: the owner always may; an agent only withdraws its own row while it is still
47
+ * proposed. Everything else stays the owner's.
48
+ */
49
+ export declare const assertMayWithdraw: (row: {
50
+ id: string;
51
+ proposedBy: Proposer;
52
+ proposed: boolean;
53
+ }, by: Decider | Exclude<Decider, "owner"> | {
54
+ bot: string;
55
+ }) => void;
56
+ /** `mayDecide` for a store method: throws the refusal instead of returning it. */
57
+ export declare const assertMayDecide: (row: {
58
+ id: string;
59
+ proposedBy: Proposer;
60
+ }, by: Decider, policy?: DecisionPolicy) => void;
61
+ /** The author a decision is recorded under. */
62
+ export declare const deciderAuthor: (by: Decider) => "owner" | {
63
+ bot: string;
64
+ model?: string;
65
+ };
@@ -0,0 +1,55 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ export const DEFAULT_DECISION_POLICY = { agentDecisions: true, ruleProposalsOwnerOnly: true };
3
+ /**
4
+ * The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
5
+ * Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
6
+ */
7
+ export const mayDecide = (row, by, policy = DEFAULT_DECISION_POLICY) => {
8
+ if (by === "owner")
9
+ return null;
10
+ const refuse = (code, reason, message) => ({
11
+ id: row.id,
12
+ code,
13
+ reason,
14
+ message,
15
+ });
16
+ if (!policy.agentDecisions)
17
+ return refuse("permission_error", "proposals.agent-decisions-off", "agent decisions are turned off; the owner decides");
18
+ if (typeof by.reason !== "string" || !by.reason.trim() || by.reason.length > 2000)
19
+ return refuse("validation_error", "proposal.reason-required", "an agent deciding for the owner gives a reason");
20
+ const proposer = row.proposedBy;
21
+ if (proposer.type === "unknown")
22
+ return refuse("permission_error", "proposal.owner-only", "this proposal names no author; only the owner decides it");
23
+ if (proposer.type === "bot" && proposer.kind === "rule" && policy.ruleProposalsOwnerOnly)
24
+ return refuse("permission_error", "proposal.owner-only", `proposed by ${proposer.name}: only the owner decides it`);
25
+ if (proposer.type === "bot" && proposer.kind === "agent" && botKey(proposer.name) === botKey(by.bot))
26
+ return refuse("permission_error", "proposal.own", `${by.bot} proposed this; another decider must decide it`);
27
+ return null;
28
+ };
29
+ /** One bot whatever its letter case or spacing: `Claude-Code` is `claude-code`. */
30
+ export const botKey = (name) => name
31
+ .trim()
32
+ .toLowerCase()
33
+ .replace(/[\s_]+/g, "-");
34
+ /**
35
+ * Removing or reversing a row: the owner always may; an agent only withdraws its own row while it is still
36
+ * proposed. Everything else stays the owner's.
37
+ */
38
+ export const assertMayWithdraw = (row, by) => {
39
+ if (by === "owner")
40
+ return;
41
+ const proposer = row.proposedBy;
42
+ if (row.proposed && proposer.type === "bot" && proposer.kind === "agent" && botKey(proposer.name) === botKey(by.bot))
43
+ return;
44
+ throw new CliError("permission_error", `only the owner removes or reverses ${row.id}`, {
45
+ reason: "proposal.owner-only",
46
+ });
47
+ };
48
+ /** `mayDecide` for a store method: throws the refusal instead of returning it. */
49
+ export const assertMayDecide = (row, by, policy = DEFAULT_DECISION_POLICY) => {
50
+ const refusal = mayDecide(row, by, policy);
51
+ if (refusal)
52
+ throw new CliError(refusal.code, refusal.message, { reason: refusal.reason });
53
+ };
54
+ /** The author a decision is recorded under. */
55
+ export const deciderAuthor = (by) => by === "owner" ? "owner" : { bot: by.bot, ...(by.model === undefined ? {} : { model: by.model }) };
@@ -1,16 +1,14 @@
1
1
  import { type FindingOutcome, type FindingsInput, type FindingsReceipt } from "../meetings/index.js";
2
+ import type { Decider } from "./decisions.js";
2
3
  import type { EvidenceSource } from "./sqlite/evidence-sources.js";
3
4
  export declare const EVIDENCE_FINDINGS_KIND = "evidence_findings";
4
5
  export declare const validateEvidenceFindings: (input: FindingsInput) => FindingsInput;
5
6
  /** Who decided: the owner, or an agent the host let decide for the owner, with its reason. */
6
- export type EvidenceFindingsDecider = "owner" | {
7
- bot: string;
8
- model?: string;
9
- reason: string;
10
- };
7
+ export type EvidenceFindingsDecider = Decider;
11
8
  export interface EvidenceFindingsReceipt extends FindingsReceipt {
12
9
  decision: {
13
10
  by: "owner";
11
+ reason?: string;
14
12
  } | {
15
13
  by: "agent";
16
14
  bot: string;
@@ -31,6 +29,7 @@ export interface EvidenceFindingsBatch {
31
29
  type: "person" | "bot";
32
30
  id: string;
33
31
  name: string | null;
32
+ kind: "agent" | "rule" | null;
34
33
  };
35
34
  decidedBy: {
36
35
  type: "person" | "bot";
@@ -62,7 +61,8 @@ export interface EvidenceFindingsStore {
62
61
  review(id: string, input: {
63
62
  fingerprint: string;
64
63
  accept: string[];
65
- by?: EvidenceFindingsDecider;
64
+ by: EvidenceFindingsDecider;
65
+ reason?: string;
66
66
  }): Promise<EvidenceFindingsBatch>;
67
67
  /** Each reference the batch cites, read back once; one gone from the store since is `missing`. */
68
68
  sources(id: string): Promise<EvidenceSource[]>;
@@ -7,6 +7,7 @@ export { note } from "../entities/note.js";
7
7
  export type { FindingEvidence, FindingOutcome, MeetingFactFinding, MeetingFinding, MeetingFindingsInput, MeetingFindingsReceipt, MeetingTaskFinding, } from "../meetings/index.js";
8
8
  export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, type PriorityLabel, priorityLabel, priorityOf, priorityOption, TASK_STATES, type TaskState, } from "../tasks/index.js";
9
9
  export type { AccountMail, AccountMeetings, AccountStore } from "./account-store.js";
10
+ export { assertMayDecide, DEFAULT_DECISION_POLICY, type Decider, type DecisionPolicy, mayDecide, type Proposer, type Refusal, } from "./decisions.js";
10
11
  export type { DocumentTransferOptions, DocumentTransferPreview, DocumentTransferReceipt, } from "./document-transfer/index.js";
11
12
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
12
13
  export type { CacheDatabase, CacheStatement, OpenDatabase, SqlValue } from "./driver.js";
@@ -30,7 +31,7 @@ export type { BotUpdate, BotUpdateStore } from "./sqlite/bot-updates.js";
30
31
  export type { CatalogActor, CatalogEntry, CatalogInput, CatalogQuery, CatalogStore } from "./sqlite/catalog.js";
31
32
  export { CATALOG_QUERY } from "./sqlite/catalog.js";
32
33
  export type { ChatMetadata } from "./sqlite/chats.js";
33
- export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
34
+ export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, PartialEmail, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
34
35
  export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
35
36
  export type { EvidenceSource } from "./sqlite/evidence-sources.js";
36
37
  export type { Fact, FactInput, FactsStore } from "./sqlite/facts.js";
@@ -49,8 +50,8 @@ export type { NoteIndexState } from "./sqlite/note-index.js";
49
50
  export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, type NoteQuery, type NoteSearch, } from "./sqlite/note-search.js";
50
51
  export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
51
52
  export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
52
- export type { ProposalInput, ProposedAction, ProposedActionsStore } from "./sqlite/proposed-actions.js";
53
- export { PROPOSAL_STATUSES } from "./sqlite/proposed-actions.js";
53
+ export { PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
54
+ export type { LinkDecision } from "./sqlite/relation-proposals.js";
54
55
  export { fillSearchIndex, resetSearchIndex, type SearchIndexFill, type SearchIndexState, searchIndexState, } from "./sqlite/search-index.js";
55
56
  export type { SyncStateSave } from "./sqlite/sync.js";
56
57
  export type { TagCreationOptions, TagCreationReview } from "./sqlite/tag-creation.js";
@@ -2,6 +2,7 @@ export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES
2
2
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
3
3
  export { note } from "../entities/note.js";
4
4
  export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, priorityLabel, priorityOf, priorityOption, TASK_STATES, } from "../tasks/index.js";
5
+ export { assertMayDecide, DEFAULT_DECISION_POLICY, mayDecide, } from "./decisions.js";
5
6
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
6
7
  export { PRAGMAS } from "./driver.js";
7
8
  export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
@@ -21,7 +22,7 @@ export { EVENT_FIELDS, } from "./sqlite/event-search.js";
21
22
  export { IMPORTED_DOCUMENT_QUERY } from "./sqlite/imported-documents.js";
22
23
  export { ORGANIZATION_KINDS, PROJECT_TYPES, SCOPES } from "./sqlite/knowledge.js";
23
24
  export { NOTE_FIELDS, } from "./sqlite/note-search.js";
24
- export { PROPOSAL_STATUSES } from "./sqlite/proposed-actions.js";
25
+ export { PROPOSAL_STATUSES, proposerOfAction, } from "./sqlite/proposed-actions.js";
25
26
  export { fillSearchIndex, resetSearchIndex, searchIndexState, } from "./sqlite/search-index.js";
26
27
  export { PROJECT_TASK_FIELDS, TASK_SORTS } from "./sqlite/task-board.js";
27
28
  export { certaintyOf } from "./sqlite/task-close.js";
@@ -1,4 +1,5 @@
1
1
  import type { FindingOutcome, MeetingFindingsInput, MeetingFindingsReceipt } from "../meetings/index.js";
2
+ import type { Decider } from "./decisions.js";
2
3
  import type { AccountKey } from "./types.js";
3
4
  export interface MeetingFindingsBatch {
4
5
  id: string;
@@ -36,5 +37,6 @@ export interface MeetingFindingsStore {
36
37
  review(account: AccountKey, id: string, input: {
37
38
  fingerprint: string;
38
39
  accept: string[];
40
+ by: Decider;
39
41
  }): Promise<MeetingFindingsBatch>;
40
42
  }
@@ -2,6 +2,7 @@ import type { CounterObservations } from "../domain/counters.js";
2
2
  import type { Delivery, Id, Message, Page, PersonReaction, PollVote, Reactions, Reactor, WindowedMessage } from "../domain/models.js";
3
3
  import { type DocumentLinkMessage } from "./sqlite/document-links.js";
4
4
  import type { StoreContext } from "./sqlite/open.js";
5
+ import * as reads from "./sqlite/reads.js";
5
6
  import type { AccountKey, DeletionScope, IngestedVia, MessageFilter, SenderChatStats, StoredHit } from "./types.js";
6
7
  export interface AccountMessages {
7
8
  /**
@@ -40,16 +41,10 @@ export interface AccountMessages {
40
41
  /** Oldest to newest, like a provider's history page. */
41
42
  /**
42
43
  * `threadId` keeps one forum topic's; `outgoing` only the owner's (true) or only the others' (false); `starred`
43
- * only the ones the owner starred (true) or did not (false).
44
+ * only the ones the owner starred (true) or did not (false). `after` reads forward instead of back, and `until`
45
+ * ends the window; `reads.MessagePageWindow` says how.
44
46
  */
45
- messages(key: AccountKey, chatId: Id, window: {
46
- limit: number;
47
- before?: Id;
48
- since?: string;
49
- threadId?: Id;
50
- outgoing?: boolean;
51
- starred?: boolean;
52
- }): Promise<Page<Message>>;
47
+ messages(key: AccountKey, chatId: Id, window: reads.MessagePageWindow): Promise<Page<Message>>;
53
48
  /** How many stored messages the chat has, sent at `since` or later when it is given. */
54
49
  countMessages(key: AccountKey, chatId: Id, options?: {
55
50
  since?: string;
@@ -1,3 +1,4 @@
1
+ import type { Proposer } from "../decisions.js";
1
2
  import type { CacheDatabase } from "../driver.js";
2
3
  /** Who made or owns something: the store's polymorphic actor pair. */
3
4
  export interface Actor {
@@ -6,7 +7,7 @@ export interface Actor {
6
7
  }
7
8
  /** How the task package names who acted; each maps to one stable actor row. */
8
9
  export type Origin = "owner" | "agent" | "rule";
9
- declare const BOT_KINDS: readonly ["agent", "script", "integration"];
10
+ export declare const BOT_KINDS: readonly ["agent", "rule", "script", "integration"];
10
11
  /** The owner's person row, which the initial migration seeds. */
11
12
  export declare const ownerPerson: (database: CacheDatabase) => Actor;
12
13
  /** A bot by its unique handle, registered on first use. */
@@ -17,4 +18,14 @@ export declare const botNamed: (database: CacheDatabase, name: string, now: numb
17
18
  */
18
19
  export declare const actorOfOrigin: (database: CacheDatabase, origin: Origin) => Actor;
19
20
  export declare const originOfActor: (database: CacheDatabase, type: unknown, id: unknown) => Origin | undefined;
20
- export {};
21
+ /** Who proposed a stored row, from its actor pair; the seeded bot `rule` and a bot of kind `rule` are rules. */
22
+ export declare const proposerOf: (database: CacheDatabase, type: unknown, id: unknown) => Proposer;
23
+ /** Who proposed something, as a list filter: any agent, any rule, or one bot by its handle. */
24
+ export type ProposedByFilter = "agent" | "rule" | {
25
+ bot: string;
26
+ };
27
+ /** The SQL that keeps rows whose actor pair matches `by`; the seeded bot `rule` counts as a rule. */
28
+ export declare const proposedBySql: (type: string, id: string, by: ProposedByFilter | undefined) => {
29
+ sql: string;
30
+ params: string[];
31
+ };
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- const BOT_KINDS = ["agent", "script", "integration"];
2
+ export const BOT_KINDS = ["agent", "rule", "script", "integration"];
3
3
  /** The owner's person row, which the initial migration seeds. */
4
4
  export const ownerPerson = (database) => {
5
5
  const found = database.prepare("SELECT id FROM persons WHERE owner = 1 ORDER BY id LIMIT 1").get();
@@ -41,3 +41,26 @@ export const originOfActor = (database, type, id) => {
41
41
  return "owner";
42
42
  return database.prepare("SELECT name FROM bots WHERE id = ?").get(Number(id))?.name === "rule" ? "rule" : "agent";
43
43
  };
44
+ /** Who proposed a stored row, from its actor pair; the seeded bot `rule` and a bot of kind `rule` are rules. */
45
+ export const proposerOf = (database, type, id) => {
46
+ if (type === "person" && id != null)
47
+ return { type: "person", id: String(id) };
48
+ if (type !== "bot" || id == null)
49
+ return { type: "unknown" };
50
+ const bot = database.prepare("SELECT name, kind FROM bots WHERE id = ?").get(Number(id));
51
+ if (!bot)
52
+ return { type: "unknown" };
53
+ const name = String(bot.name);
54
+ return { type: "bot", id: String(id), name, kind: bot.kind === "rule" || name === "rule" ? "rule" : "agent" };
55
+ };
56
+ /** The SQL that keeps rows whose actor pair matches `by`; the seeded bot `rule` counts as a rule. */
57
+ export const proposedBySql = (type, id, by) => {
58
+ if (by === undefined)
59
+ return { sql: "1", params: [] };
60
+ const bot = (condition) => `${type} = 'bot' AND EXISTS (SELECT 1 FROM bots pb WHERE pb.id = ${id} AND ${condition})`;
61
+ if (by === "rule")
62
+ return { sql: bot("(pb.kind = 'rule' OR pb.name = 'rule')"), params: [] };
63
+ if (by === "agent")
64
+ return { sql: bot("pb.kind <> 'rule' AND pb.name <> 'rule'"), params: [] };
65
+ return { sql: bot("pb.name = ?"), params: [by.bot] };
66
+ };
@@ -1,3 +1,4 @@
1
+ import type { Metadata } from "../../meetings/index.js";
1
2
  import type { CacheDatabase } from "../driver.js";
2
3
  /** Where an IMAP server holds an email: `folder` is the mailbox's external id. */
3
4
  export interface EmailLocationInput {
@@ -20,5 +21,20 @@ export declare const locate: (database: CacheDatabase, accountId: number, emailI
20
21
  changes: number;
21
22
  };
22
23
  export declare const folderEmails: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string) => FolderEmail[];
24
+ /**
25
+ * An email stored from its first bytes only (`metadata.partial`): a sync reads it whole and clears the mark.
26
+ * `partialEmails` lists an account's live ones newest first, with how many there are in all.
27
+ */
28
+ export interface PartialEmail {
29
+ externalId: string;
30
+ threadExternalId: string;
31
+ receivedAt: number | null;
32
+ metadata: Metadata;
33
+ locations: EmailLocationInput[];
34
+ }
35
+ export declare const partialEmails: (database: CacheDatabase, accountId: number, limit: number) => {
36
+ emails: PartialEmail[];
37
+ total: number;
38
+ };
23
39
  export declare const setLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>) => number;
24
40
  export declare const forgetLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, uids: number[]) => void;
@@ -1,3 +1,4 @@
1
+ import { fromJson } from "./events.js";
1
2
  export const locate = (database, accountId, emailId, where) => database
2
3
  .prepare(`INSERT OR REPLACE INTO email_locations (mailbox_id, uidvalidity, uid, email_id)
3
4
  SELECT id, ?, ?, ? FROM mailboxes WHERE account_id = ? AND external_id = ?`)
@@ -21,6 +22,31 @@ export const folderEmails = (database, accountId, folder, uidvalidity) => databa
21
22
  locations: Number(row.locations),
22
23
  folders: Number(row.folders),
23
24
  }));
25
+ export const partialEmails = (database, accountId, limit) => {
26
+ const rows = database
27
+ .prepare(`SELECT e.external_id, t.external_id AS thread, coalesce(e.received_at, e.sent_at) AS at, e.metadata,
28
+ count(*) OVER () AS total,
29
+ (SELECT json_group_array(json_object('folder', b.external_id, 'uidvalidity', l.uidvalidity, 'uid', l.uid))
30
+ FROM email_locations l JOIN mailboxes b ON b.id = l.mailbox_id WHERE l.email_id = e.id) AS places
31
+ FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
32
+ WHERE e.account_id = ? AND e.deleted_at IS NULL AND json_extract(e.metadata, '$.partial') IS NOT NULL
33
+ ORDER BY at DESC, e.id DESC LIMIT ?`)
34
+ .all(accountId, limit);
35
+ return {
36
+ total: rows.length ? Number(rows[0]?.total) : 0,
37
+ emails: rows.map((row) => ({
38
+ externalId: String(row.external_id),
39
+ threadExternalId: String(row.thread),
40
+ receivedAt: row.at === null ? null : Number(row.at),
41
+ metadata: fromJson(row.metadata),
42
+ locations: JSON.parse(String(row.places)).map((place) => ({
43
+ folder: String(place.folder),
44
+ uidvalidity: String(place.uidvalidity),
45
+ uid: Number(place.uid),
46
+ })),
47
+ })),
48
+ };
49
+ };
24
50
  export const setLocations = (database, accountId, folder, uidvalidity, found) => {
25
51
  database
26
52
  .prepare(`DELETE FROM email_locations WHERE uidvalidity <> ?