@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.
- package/CHANGELOG.md +228 -0
- package/dist/calendar/busy.js +4 -1
- package/dist/calendar/cli/answers.d.ts +31 -0
- package/dist/calendar/cli/answers.js +8 -0
- package/dist/calendar/cli/index.d.ts +6 -0
- package/dist/calendar/cli/index.js +43 -4
- package/dist/calendar/cli/remind.js +2 -1
- package/dist/calendar/expand.d.ts +17 -0
- package/dist/calendar/expand.js +340 -0
- package/dist/calendar/feeds.d.ts +96 -0
- package/dist/calendar/feeds.js +324 -0
- package/dist/calendar/ics.d.ts +15 -4
- package/dist/calendar/ics.js +18 -7
- package/dist/calendar/index.d.ts +3 -1
- package/dist/calendar/index.js +2 -0
- package/dist/calendar/server-search.js +3 -1
- package/dist/calendar/writes.js +15 -4
- package/dist/cli/after-dashes.d.ts +16 -0
- package/dist/cli/after-dashes.js +41 -0
- package/dist/cli/bot/context.d.ts +1 -1
- package/dist/cli/bot/context.js +9 -1
- package/dist/cli/bot/messages.js +2 -0
- package/dist/cli/bot/setup.js +1 -0
- package/dist/cli/contract-templates.js +2 -2
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +3 -2
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/admin-commands.js +5 -1
- package/dist/cli/messenger/admin-contacts-command.js +5 -1
- package/dist/cli/messenger/admin-folders-command.js +5 -1
- package/dist/cli/messenger/complete-command.js +1 -0
- package/dist/cli/messenger/history-query.d.ts +10 -4
- package/dist/cli/messenger/history-query.js +87 -31
- package/dist/cli/messenger/messages-command.js +12 -23
- package/dist/cli/messenger/messages-comments-command.js +4 -2
- package/dist/cli/messenger/messages-edit-command.js +1 -0
- package/dist/cli/messenger/messages-press-command.js +1 -0
- package/dist/cli/messenger/messages-send-command.js +1 -0
- package/dist/cli/messenger/observed.js +2 -2
- package/dist/cli/messenger/polls-command.js +1 -0
- package/dist/cli/messenger/private-people-command.js +5 -1
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/stored.js +56 -3
- package/dist/cli/messenger/topics-command.js +1 -1
- package/dist/cli/option-hints.d.ts +6 -0
- package/dist/cli/option-hints.js +15 -0
- package/dist/cli/program.js +17 -6
- package/dist/cli/schedule/schedule-command.js +1 -0
- package/dist/domain/message-link.d.ts +3 -0
- package/dist/domain/message-link.js +9 -14
- package/dist/domain/money-words.d.ts +11 -0
- package/dist/domain/money-words.js +38 -0
- package/dist/domain/providers.d.ts +2 -0
- package/dist/domain/providers.js +15 -3
- package/dist/domain/ref-arguments.d.ts +31 -0
- package/dist/domain/ref-arguments.js +79 -0
- package/dist/domain/references.d.ts +20 -2
- package/dist/domain/references.js +132 -47
- package/dist/errors/reasons.js +31 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mail/cli/index.js +4 -4
- package/dist/mail/index.d.ts +2 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/partial.d.ts +34 -0
- package/dist/mail/partial.js +94 -0
- package/dist/mail/server-search.js +17 -9
- package/dist/mail/source.d.ts +13 -2
- package/dist/mail/source.js +7 -1
- package/dist/mail/sync-types.d.ts +112 -0
- package/dist/mail/sync-types.js +1 -0
- package/dist/mail/sync.d.ts +4 -106
- package/dist/mail/sync.js +49 -4
- package/dist/mail/testing/fake.d.ts +3 -1
- package/dist/mail/testing/fake.js +10 -2
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/oauth/client-file.d.ts +14 -0
- package/dist/oauth/client-file.js +28 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/ports/messenger.d.ts +3 -2
- package/dist/resolve.d.ts +3 -1
- package/dist/resolve.js +8 -0
- package/dist/search/task-signature.d.ts +5 -1
- package/dist/search/task-signature.js +27 -9
- package/dist/services/agenda.d.ts +5 -0
- package/dist/services/agenda.js +3 -1
- package/dist/services/deps.d.ts +0 -2
- package/dist/services/deps.js +0 -2
- package/dist/services/documents/ingestion.js +1 -1
- package/dist/services/documents/knowledge.d.ts +10 -6
- package/dist/services/documents/knowledge.js +25 -11
- package/dist/services/index.d.ts +6 -1
- package/dist/services/index.js +6 -1
- package/dist/services/message-resolution.d.ts +4 -1
- package/dist/services/message-resolution.js +19 -3
- package/dist/services/messages.d.ts +2 -0
- package/dist/services/messages.js +40 -36
- package/dist/services/money-words.d.ts +3 -0
- package/dist/services/money-words.js +5 -0
- package/dist/services/proposal-tasks.d.ts +3 -2
- package/dist/services/proposal-tasks.js +5 -2
- package/dist/services/proposals/ids.d.ts +5 -0
- package/dist/services/proposals/ids.js +43 -0
- package/dist/services/proposals/index.d.ts +3 -0
- package/dist/services/proposals/index.js +2 -0
- package/dist/services/proposals/kinds/action.d.ts +7 -0
- package/dist/services/proposals/kinds/action.js +45 -0
- package/dist/services/proposals/kinds/fact.d.ts +4 -0
- package/dist/services/proposals/kinds/fact.js +43 -0
- package/dist/services/proposals/kinds/findings.d.ts +7 -0
- package/dist/services/proposals/kinds/findings.js +75 -0
- package/dist/services/proposals/kinds/key-document.d.ts +9 -0
- package/dist/services/proposals/kinds/key-document.js +75 -0
- package/dist/services/proposals/kinds/memory.d.ts +4 -0
- package/dist/services/proposals/kinds/memory.js +42 -0
- package/dist/services/proposals/kinds/relationship.d.ts +10 -0
- package/dist/services/proposals/kinds/relationship.js +82 -0
- package/dist/services/proposals/kinds/shared.d.ts +25 -0
- package/dist/services/proposals/kinds/shared.js +49 -0
- package/dist/services/proposals/registry.d.ts +9 -0
- package/dist/services/proposals/registry.js +129 -0
- package/dist/services/proposals/types.d.ts +85 -0
- package/dist/services/proposals/types.js +1 -0
- package/dist/services/questions.d.ts +7 -2
- package/dist/services/questions.js +13 -4
- package/dist/services/server-search.d.ts +7 -4
- package/dist/services/server-search.js +5 -2
- package/dist/services/stored-project-gathering.d.ts +9 -0
- package/dist/services/stored-project-gathering.js +9 -4
- package/dist/services/task-categories.d.ts +2 -0
- package/dist/services/task-categories.js +19 -8
- package/dist/services/tasks.js +8 -4
- package/dist/store/decisions.d.ts +65 -0
- package/dist/store/decisions.js +55 -0
- package/dist/store/evidence-findings.d.ts +6 -6
- package/dist/store/index.d.ts +4 -3
- package/dist/store/index.js +2 -1
- package/dist/store/meeting-findings.d.ts +2 -0
- package/dist/store/messages.d.ts +4 -9
- package/dist/store/sqlite/actors.d.ts +13 -2
- package/dist/store/sqlite/actors.js +24 -1
- package/dist/store/sqlite/email-locations.d.ts +16 -0
- package/dist/store/sqlite/email-locations.js +26 -0
- package/dist/store/sqlite/emails.d.ts +6 -2
- package/dist/store/sqlite/emails.js +2 -1
- package/dist/store/sqlite/evidence-findings.js +13 -3
- package/dist/store/sqlite/evidence-sources.js +2 -2
- package/dist/store/sqlite/facts.d.ts +16 -8
- package/dist/store/sqlite/facts.js +48 -15
- package/dist/store/sqlite/findings-core.d.ts +2 -1
- package/dist/store/sqlite/findings-core.js +4 -4
- package/dist/store/sqlite/identities.js +1 -1
- package/dist/store/sqlite/knowledge.d.ts +43 -7
- package/dist/store/sqlite/knowledge.js +62 -17
- package/dist/store/sqlite/meeting-findings.js +6 -6
- package/dist/store/sqlite/memories.d.ts +12 -8
- package/dist/store/sqlite/memories.js +33 -10
- package/dist/store/sqlite/notes.d.ts +5 -1
- package/dist/store/sqlite/notes.js +22 -6
- package/dist/store/sqlite/open-store.d.ts +4 -1
- package/dist/store/sqlite/open-store.js +8 -2
- package/dist/store/sqlite/open.d.ts +3 -0
- package/dist/store/sqlite/proposed-actions.d.ts +21 -8
- package/dist/store/sqlite/proposed-actions.js +29 -9
- package/dist/store/sqlite/reads.d.ts +13 -2
- package/dist/store/sqlite/reads.js +8 -4
- package/dist/store/sqlite/relation-proposals.d.ts +34 -0
- package/dist/store/sqlite/relation-proposals.js +114 -0
- package/dist/store/sqlite/things.d.ts +0 -1
- package/dist/store/sqlite/things.js +25 -24
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +2 -0
- 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
|
-
/**
|
|
13
|
-
|
|
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
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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
|
-
/**
|
|
6
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/services/tasks.js
CHANGED
|
@@ -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
|
|
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"
|
|
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 =
|
|
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
|
|
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[]>;
|
package/dist/store/index.d.ts
CHANGED
|
@@ -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
|
|
53
|
-
export {
|
|
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";
|
package/dist/store/index.js
CHANGED
|
@@ -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
|
}
|
package/dist/store/messages.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 <> ?
|