@wirecat/cli-core 0.546.0 → 0.548.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 +173 -0
- package/dist/calendar/cli/answers.d.ts +7 -2
- package/dist/calendar/cli/answers.js +12 -4
- package/dist/calendar/cli/index.js +9 -8
- package/dist/calendar/feeds.d.ts +19 -0
- package/dist/calendar/feeds.js +52 -9
- package/dist/calendar/source.d.ts +10 -2
- package/dist/calendar/sync.d.ts +2 -0
- package/dist/calendar/sync.js +10 -6
- package/dist/calendar/testing/memory.js +39 -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 +10 -4
- package/dist/cli/messenger/messages-comments-command.js +4 -2
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/option-hints.d.ts +6 -0
- package/dist/cli/option-hints.js +15 -0
- package/dist/errors/reasons.js +13 -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/event-store.d.ts +10 -0
- package/dist/meetings/testing/event-store-contract.js +36 -0
- 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 +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/messages.d.ts +2 -0
- package/dist/services/messages.js +25 -23
- 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/server-search.d.ts +7 -4
- package/dist/services/server-search.js +5 -2
- 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/accounts.d.ts +8 -0
- package/dist/store/sqlite/accounts.js +2 -2
- package/dist/store/sqlite/actors.d.ts +13 -2
- package/dist/store/sqlite/actors.js +24 -1
- package/dist/store/sqlite/calendar-forget.d.ts +17 -0
- package/dist/store/sqlite/calendar-forget.js +68 -0
- package/dist/store/sqlite/calendar.js +8 -0
- 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/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/knowledge.d.ts +43 -7
- package/dist/store/sqlite/knowledge.js +55 -15
- 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 +18 -2
- 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/store.d.ts +3 -0
- package/dist/store/store.js +2 -0
- package/package.json +1 -1
|
@@ -9,14 +9,20 @@ export declare const historyQuery: (messenger: {
|
|
|
9
9
|
}) => import("@wirecat/cli-base/query").QueryRegistry;
|
|
10
10
|
declare const COMMENTS_QUERY: import("@wirecat/cli-base/query").QueryRegistry;
|
|
11
11
|
export { COMMENTS_QUERY };
|
|
12
|
-
type Start = Pick<ListWindow, "before" | "beforeTime" | "after">;
|
|
12
|
+
type Start = Pick<ListWindow, "before" | "beforeTime" | "after" | "until">;
|
|
13
13
|
/**
|
|
14
|
-
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward.
|
|
15
|
-
*
|
|
14
|
+
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. A window's end
|
|
15
|
+
* travels as `~<epoch ms>`, so a resumed read stops where the first one would have. Ids are opaque, so only the first
|
|
16
|
+
* colon splits it.
|
|
16
17
|
*/
|
|
17
18
|
export declare const cursorOf: (start: Start, edge: {
|
|
18
19
|
first?: string;
|
|
19
20
|
last?: string;
|
|
20
21
|
}) => string | undefined;
|
|
21
|
-
/** A list reads one way from one place: the query or the cursor names at most one
|
|
22
|
+
/** A list reads one way from one place: the query or the cursor names at most one window. */
|
|
22
23
|
export declare const historyWindow: (words: QueryWords, cursor: string | undefined, command: string, registry?: import("@wirecat/cli-base/query").QueryRegistry) => Omit<ListWindow, "limit">;
|
|
24
|
+
/**
|
|
25
|
+
* The query `page.next` reruns with its cursor: the topic and the stars, never the bounds, which the cursor carries
|
|
26
|
+
* and which would name a second starting point beside it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const resumeQuery: (words: QueryWords, command: string, registry?: import("@wirecat/cli-base/query").QueryRegistry) => string | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import {
|
|
3
|
-
import { listQuery, nativeField,
|
|
2
|
+
import { dateRange, defineQuery, queryError, timezoneOf } from "@wirecat/cli-base/query";
|
|
3
|
+
import { listQuery, nativeField, nativeTermLists, typedQuery } from "../list-query.js";
|
|
4
4
|
/** What a chat's history can be read by: where it starts, and one topic. */
|
|
5
5
|
export const HISTORY_QUERY = defineQuery({
|
|
6
6
|
entity: "message",
|
|
@@ -36,52 +36,108 @@ const COMMENTS_QUERY = defineQuery({
|
|
|
36
36
|
});
|
|
37
37
|
export { COMMENTS_QUERY };
|
|
38
38
|
/**
|
|
39
|
-
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward.
|
|
40
|
-
*
|
|
39
|
+
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. A window's end
|
|
40
|
+
* travels as `~<epoch ms>`, so a resumed read stops where the first one would have. Ids are opaque, so only the first
|
|
41
|
+
* colon splits it.
|
|
41
42
|
*/
|
|
42
43
|
export const cursorOf = (start, edge) => {
|
|
43
44
|
const forward = start.after !== undefined;
|
|
44
45
|
const id = forward ? edge.last : edge.first;
|
|
45
|
-
|
|
46
|
+
if (id === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const end = forward && start.until !== undefined ? `~${start.until}` : "";
|
|
49
|
+
return `${forward ? "after" : "before"}:${id}${end}`;
|
|
46
50
|
};
|
|
47
51
|
const isId = (value) => value !== "" && !/[\s\p{Cc}]/u.test(value);
|
|
48
52
|
const fromCursor = (cursor) => {
|
|
49
53
|
const at = cursor.indexOf(":");
|
|
50
|
-
const [way,
|
|
54
|
+
const [way, rest] = at < 0 ? [cursor, ""] : [cursor.slice(0, at), cursor.slice(at + 1)];
|
|
55
|
+
const ended = way === "after" ? /^(.*)~(\d+)$/u.exec(rest) : null;
|
|
56
|
+
const id = ended ? ended[1] : rest;
|
|
51
57
|
if (!isId(id) || (way !== "before" && way !== "after"))
|
|
52
58
|
throw new CliError("validation_error", `--cursor takes what page.next gave, before:<id> or after:<id> — not "${cursor}"`);
|
|
53
|
-
|
|
59
|
+
if (way === "before")
|
|
60
|
+
return { before: id };
|
|
61
|
+
return { after: { id }, ...(ended ? { until: Number(ended[2]) } : {}) };
|
|
54
62
|
};
|
|
55
|
-
|
|
63
|
+
/** A term's lower and upper bound; a date's as moments in the zone, an exclusive end 1 ms inside. */
|
|
64
|
+
const boundsOf = (term, command) => {
|
|
56
65
|
const context = { command };
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const hasLower = term.value !== "*";
|
|
67
|
+
const hasUpper = term.upper !== undefined && term.upper !== "*";
|
|
68
|
+
const span = term.span;
|
|
69
|
+
if (term.field === "id") {
|
|
70
|
+
if (hasLower === hasUpper)
|
|
71
|
+
queryError("query.unsupported-operator", span, "id takes one side: id< or id>", context);
|
|
72
|
+
return hasLower
|
|
73
|
+
? { lower: { start: { after: { id: term.value } }, span, date: false } }
|
|
74
|
+
: { upper: { start: { before: term.upper }, span, date: false } };
|
|
75
|
+
}
|
|
76
|
+
if (!hasLower && !hasUpper)
|
|
77
|
+
queryError("query.unsupported-operator", span, `${term.field} needs a start, an end or both`, context);
|
|
78
|
+
const range = dateRange(term.value, term.upper ?? "*", term.lower_inclusive ?? true, term.upper_inclusive ?? true, timezoneOf(), span);
|
|
79
|
+
if ((hasLower && range.lower === undefined) || (hasUpper && range.upper === undefined))
|
|
80
|
+
queryError("query.invalid-date", span, "that is not a time", context);
|
|
81
|
+
return {
|
|
82
|
+
// The messenger and the store read strictly after a start, so an inclusive one moves 1 ms back.
|
|
83
|
+
...(range.lower === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { lower: { start: { after: { time: range.lower - (range.lower_inclusive ? 1 : 0) } }, span, date: true } }),
|
|
86
|
+
...(range.upper === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { upper: { start: { until: range.upper - (range.upper_inclusive ? 0 : 1) }, span, date: true } }),
|
|
89
|
+
};
|
|
69
90
|
};
|
|
70
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The window the query names. A start reads forward from it, oldest first, and stops at an end; an end alone reads
|
|
93
|
+
* back from it, newest page first.
|
|
94
|
+
*/
|
|
95
|
+
const windowOf = (terms, command) => {
|
|
96
|
+
const context = { command };
|
|
97
|
+
const lowers = [];
|
|
98
|
+
const uppers = [];
|
|
99
|
+
for (const term of terms) {
|
|
100
|
+
const { lower, upper } = boundsOf(term, command);
|
|
101
|
+
if (lower)
|
|
102
|
+
lowers.push(lower);
|
|
103
|
+
if (upper)
|
|
104
|
+
uppers.push(upper);
|
|
105
|
+
}
|
|
106
|
+
const [lower, extraLower] = lowers;
|
|
107
|
+
const [upper, extraUpper] = uppers;
|
|
108
|
+
if (extraLower)
|
|
109
|
+
queryError("query.unsupported-placement", extraLower.span, "the window has one start", context);
|
|
110
|
+
if (extraUpper)
|
|
111
|
+
queryError("query.unsupported-placement", extraUpper.span, "the window has one end", context);
|
|
112
|
+
if (lower && upper && !upper.date)
|
|
113
|
+
queryError("query.unsupported-placement", upper.span, "a window from a start ends at a date: date<…", context);
|
|
114
|
+
if (lower)
|
|
115
|
+
return { ...lower.start, ...upper?.start };
|
|
116
|
+
if (upper?.start.until !== undefined)
|
|
117
|
+
return { beforeTime: upper.start.until + 1 };
|
|
118
|
+
return upper?.start ?? {};
|
|
119
|
+
};
|
|
120
|
+
/** A list reads one way from one place: the query or the cursor names at most one window. */
|
|
71
121
|
export const historyWindow = (words, cursor, command, registry = HISTORY_QUERY) => {
|
|
72
|
-
const terms =
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (cursor !== undefined)
|
|
77
|
-
starts.push(fromCursor(cursor));
|
|
78
|
-
if (starts.length > 1)
|
|
122
|
+
const terms = nativeTermLists(listQuery(typedQuery(words), registry, command), command, ["date", "id"]);
|
|
123
|
+
const bounds = [...(terms.get("date") ?? []), ...(terms.get("id") ?? [])];
|
|
124
|
+
const start = windowOf(bounds, command);
|
|
125
|
+
if (cursor !== undefined && bounds.length > 0)
|
|
79
126
|
throw new CliError("validation_error", "the query and --cursor name two starting points; give one of them");
|
|
80
|
-
const topic = terms.
|
|
81
|
-
const starred = terms.
|
|
127
|
+
const topic = terms.get("topic")?.[0]?.value;
|
|
128
|
+
const starred = terms.get("starred")?.[0]?.value.toLowerCase();
|
|
82
129
|
return {
|
|
83
|
-
...
|
|
130
|
+
...(cursor === undefined ? start : fromCursor(cursor)),
|
|
84
131
|
...(topic === undefined ? {} : { threadId: topic }),
|
|
85
132
|
...(starred === undefined ? {} : { starred: starred === "true" }),
|
|
86
133
|
};
|
|
87
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* The query `page.next` reruns with its cursor: the topic and the stars, never the bounds, which the cursor carries
|
|
137
|
+
* and which would name a second starting point beside it.
|
|
138
|
+
*/
|
|
139
|
+
export const resumeQuery = (words, command, registry = HISTORY_QUERY) => {
|
|
140
|
+
const terms = nativeTermLists(listQuery(typedQuery(words), registry, command), command, ["date", "id"]);
|
|
141
|
+
const kept = ["topic", "starred"].flatMap((field) => terms.get(field)?.map((term) => `${field}:${term.value}`) ?? []);
|
|
142
|
+
return kept.length > 0 ? kept.join(" ") : undefined;
|
|
143
|
+
};
|
|
@@ -12,6 +12,7 @@ import { modelWith, spoken } from "../../speech/hearing.js";
|
|
|
12
12
|
import { REMOTE_READ } from "../effects.js";
|
|
13
13
|
import { itemOf, listOf, renderAnswer } from "../envelope.js";
|
|
14
14
|
import { queryHelp } from "../list-query.js";
|
|
15
|
+
import { withOptionHints } from "../option-hints.js";
|
|
15
16
|
import { positiveCount } from "../paging.js";
|
|
16
17
|
import { syncCommand } from "./backfill-command.js";
|
|
17
18
|
import { messengerContext } from "./context.js";
|
|
@@ -19,7 +20,7 @@ import { linksCommand } from "./conversations-command.js";
|
|
|
19
20
|
import { downloadSubcommand } from "./download-command.js";
|
|
20
21
|
import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
|
|
21
22
|
import { historyLimitGaps } from "./history-gaps.js";
|
|
22
|
-
import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
|
|
23
|
+
import { cursorOf, historyQuery, historyWindow, resumeQuery } from "./history-query.js";
|
|
23
24
|
import { hideNever } from "./lacks.js";
|
|
24
25
|
import { threadAnswer } from "./message-answers.js";
|
|
25
26
|
import { messageItem, messageItems, viewOf } from "./message-items.js";
|
|
@@ -36,6 +37,8 @@ import { sendCommand } from "./messages-send-command.js";
|
|
|
36
37
|
import { coverageGaps } from "./search-answers.js";
|
|
37
38
|
import { threadOptions, threadRequest } from "./thread-options.js";
|
|
38
39
|
import { transcribeSubcommand } from "./transcribe-command.js";
|
|
40
|
+
const WINDOW_EXAMPLE = "date>=2026-10-01 date<2026-10-20";
|
|
41
|
+
const TIME_FLAGS = ["--since", "--since-time", "--until", "--until-time", "--after-time", "--before-time"];
|
|
39
42
|
/** `messages`: reading, and sending through the guard. A CLI may add its own subcommands. */
|
|
40
43
|
export const messagesCommand = (messenger) => {
|
|
41
44
|
const messages = new Command("messages").description("read and send messages");
|
|
@@ -51,8 +54,8 @@ export const messagesCommand = (messenger) => {
|
|
|
51
54
|
.addHelpText("after", queryHelp(history))
|
|
52
55
|
.argument("<chat>", messenger.chatArgument)
|
|
53
56
|
.argument("[query...]", `${messenger.topics === false
|
|
54
|
-
?
|
|
55
|
-
:
|
|
57
|
+
? `where to start or which window: id<42, ${WINDOW_EXAMPLE}`
|
|
58
|
+
: `where to start or which window, and which topic: id<42, ${WINDOW_EXAMPLE}, topic:12`}${messenger.messageStars === true ? "; starred:true keeps the ones you starred" : ""}`)
|
|
56
59
|
.option("--limit <n>", "how many", positiveCount("--limit"))
|
|
57
60
|
.option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it")
|
|
58
61
|
.option(...TRANSCRIBE_OPTION)
|
|
@@ -73,6 +76,8 @@ export const messagesCommand = (messenger) => {
|
|
|
73
76
|
const next = page.hasMore
|
|
74
77
|
? cursorOf(window, { first: page.items[0]?.id, last: page.items.at(-1)?.id })
|
|
75
78
|
: undefined;
|
|
79
|
+
const typed = next === undefined ? query.join(" ") : resumeQuery(query, "messages list", history);
|
|
80
|
+
const queryArg = typed ? { query: typed } : {};
|
|
76
81
|
// Offline, the store is the whole answer: say how much of the chat it holds. Online the messenger is read.
|
|
77
82
|
const gaps = context.settings.offline
|
|
78
83
|
? await context.withStore(async (store, account) => {
|
|
@@ -88,13 +93,14 @@ export const messagesCommand = (messenger) => {
|
|
|
88
93
|
has_more: page.hasMore,
|
|
89
94
|
paging: "cursor",
|
|
90
95
|
...(next === undefined ? {} : { cursor: next }),
|
|
91
|
-
args: { chat, ...
|
|
96
|
+
args: { chat, ...queryArg },
|
|
92
97
|
next: gaps.next,
|
|
93
98
|
missing: [...hearingMissing(hearing, transcribe === true), ...gaps.missing],
|
|
94
99
|
...(gaps.missing.length > 0 ? { meta: { complete: false } } : {}),
|
|
95
100
|
}), () => renderMessages(spokenItems(page.items, hearing), viewOf(context, messenger)));
|
|
96
101
|
});
|
|
97
102
|
annotate(list, { output: { entity: "message" }, query: history });
|
|
103
|
+
withOptionHints(list, Object.fromEntries(TIME_FLAGS.map((flag) => [flag, `messages list reads a window by its query: '${WINDOW_EXAMPLE}'`])));
|
|
98
104
|
const readWindow = async (command, chat, message, window) => {
|
|
99
105
|
const context = messengerContext(command, messenger);
|
|
100
106
|
const { found, reader } = await context.withServices(async (services) => ({
|
|
@@ -7,7 +7,7 @@ import { listOf, renderAnswer } from "../envelope.js";
|
|
|
7
7
|
import { queryHelp } from "../list-query.js";
|
|
8
8
|
import { positiveCount } from "../paging.js";
|
|
9
9
|
import { messengerContext } from "./context.js";
|
|
10
|
-
import { COMMENTS_QUERY, cursorOf, historyWindow } from "./history-query.js";
|
|
10
|
+
import { COMMENTS_QUERY, cursorOf, historyWindow, resumeQuery } from "./history-query.js";
|
|
11
11
|
import { messageItems, viewOf } from "./message-items.js";
|
|
12
12
|
export const commentsCommand = (messenger) => {
|
|
13
13
|
const otherPosts = messenger.readsComments === true && messenger.channelComments === false;
|
|
@@ -42,13 +42,15 @@ export const commentsCommand = (messenger) => {
|
|
|
42
42
|
reader: await services.messages.reader(),
|
|
43
43
|
}));
|
|
44
44
|
const next = found.hasMore ? cursorOf(window, { first: found.items[0]?.id }) : undefined;
|
|
45
|
+
const typed = next === undefined ? query.join(" ") : resumeQuery(query, "messages comments", COMMENTS_QUERY);
|
|
46
|
+
const queryArg = typed ? { query: typed } : {};
|
|
45
47
|
renderAnswer(this, context, listOf("messages comments", {
|
|
46
48
|
items: messageItems(found.items, reader),
|
|
47
49
|
limit,
|
|
48
50
|
has_more: found.hasMore,
|
|
49
51
|
paging: "cursor",
|
|
50
52
|
...(next === undefined ? {} : { cursor: next }),
|
|
51
|
-
args: { chat, post, ...
|
|
53
|
+
args: { chat, post, ...queryArg },
|
|
52
54
|
}), () => renderMessages(found.items, viewOf(context, messenger)));
|
|
53
55
|
});
|
|
54
56
|
return annotate(command, { output: { entity: "message" }, query: COMMENTS_QUERY });
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
+
error?: undefined;
|
|
6
7
|
path: string;
|
|
7
8
|
exists: boolean;
|
|
8
|
-
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* A flag a command does not take, but an agent is likely to type, fails with what to type instead rather than
|
|
4
|
+
* commander's bare "unknown option".
|
|
5
|
+
*/
|
|
6
|
+
export declare const withOptionHints: (command: Command, hints: Readonly<Record<string, string>>) => Command;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A flag a command does not take, but an agent is likely to type, fails with what to type instead rather than
|
|
3
|
+
* commander's bare "unknown option".
|
|
4
|
+
*/
|
|
5
|
+
export const withOptionHints = (command, hints) => {
|
|
6
|
+
const unknown = command;
|
|
7
|
+
const fallback = unknown.unknownOption.bind(command);
|
|
8
|
+
unknown.unknownOption = (flag) => {
|
|
9
|
+
const hint = hints[flag.split("=")[0]];
|
|
10
|
+
if (hint === undefined)
|
|
11
|
+
return fallback(flag);
|
|
12
|
+
return command.error(`unknown option '${flag}' — ${hint}`, { code: "commander.unknownOption" });
|
|
13
|
+
};
|
|
14
|
+
return command;
|
|
15
|
+
};
|
package/dist/errors/reasons.js
CHANGED
|
@@ -110,7 +110,11 @@ const OTHERS = [
|
|
|
110
110
|
["search.server-failed", "provider_unavailable", "the server did not answer a search"],
|
|
111
111
|
["search.server-timeout", "timeout", "the server did not answer within --server-time"],
|
|
112
112
|
["search.server-capped", "outcome_unknown", "the server found more than one search reads, or a branch was not sent"],
|
|
113
|
-
[
|
|
113
|
+
[
|
|
114
|
+
"search.server-partial",
|
|
115
|
+
"outcome_unknown",
|
|
116
|
+
"a message the server found was stored without its attachments; the next sync reads it whole",
|
|
117
|
+
],
|
|
114
118
|
validation("store.refused", "the local store refused the document text"),
|
|
115
119
|
["store.not-writable", "permission_error", "the message store cannot be written from this process"],
|
|
116
120
|
[
|
|
@@ -145,6 +149,14 @@ const OTHERS = [
|
|
|
145
149
|
validation("name.taken", "the name is already used"),
|
|
146
150
|
validation("period.ambiguous", "the period can be read more than one way"),
|
|
147
151
|
validation("proposal.wrong-state", "the proposal is not in a state that allows this"),
|
|
152
|
+
validation("proposal.reason-required", "an agent deciding for the owner gives a reason"),
|
|
153
|
+
validation("proposal.decider-required", "a decision names who decides: the owner or an agent with a reason"),
|
|
154
|
+
validation("proposal.ids", "the proposal ids cannot be read"),
|
|
155
|
+
["proposal.own", "permission_error", "an agent never decides its own proposal"],
|
|
156
|
+
["proposal.owner-only", "permission_error", "only the owner decides this proposal"],
|
|
157
|
+
["proposals.agent-decisions-off", "permission_error", "agent decisions are turned off; the owner decides"],
|
|
158
|
+
["proposal.unknown-kind", "not_found", "no registered proposal kind holds this id"],
|
|
159
|
+
validation("relationship.exists", "a confirmed relationship already links them"),
|
|
148
160
|
validation("ranking.invalid", "the ranking is not valid"),
|
|
149
161
|
validation("ref.malformed", "the reference has a known kind but not its shape"),
|
|
150
162
|
validation("ref.unknown-kind", "the text names no kind of reference"),
|
package/dist/mail/cli/index.js
CHANGED
|
@@ -74,11 +74,11 @@ const serverIssues = (server) => {
|
|
|
74
74
|
...FAILED[reason],
|
|
75
75
|
source: "server",
|
|
76
76
|
}));
|
|
77
|
-
const
|
|
78
|
-
? [{ code: "outcome_unknown", reason: "search.server-
|
|
77
|
+
const partial = server.partial
|
|
78
|
+
? [{ code: "outcome_unknown", reason: "search.server-partial", source: "server" }]
|
|
79
79
|
: [];
|
|
80
|
-
return failed.length ||
|
|
81
|
-
? [...failed, ...
|
|
80
|
+
return failed.length || partial.length
|
|
81
|
+
? [...failed, ...partial]
|
|
82
82
|
: [{ code: "outcome_unknown", reason: "search.server-capped", source: "server" }];
|
|
83
83
|
};
|
|
84
84
|
const searchCommand = (mail) => {
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { type AuthVerdicts, HEADER_FIELDS, headersOf, type MailHeaders, mailHead
|
|
|
7
7
|
export { type MailIdsInput, mailIds } from "./ids.js";
|
|
8
8
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
9
9
|
export { type Address, htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, type ParsedMail, parseMail, } from "./parse.js";
|
|
10
|
+
export { PARTIAL, type PartialOutcome } from "./partial.js";
|
|
10
11
|
export { PRESET_NAMES, PRESETS, type PresetName, presetMatches } from "./presets.js";
|
|
11
12
|
export { domainsToAge, groupKey, type Probe, type ProbeGroup, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
12
13
|
export { type MailFolderProgress, mailProgress } from "./progress.js";
|
|
@@ -21,5 +22,6 @@ export { DEMOTE_AT, type DomainAges, demotedSenders, type LookupResult, SCORE_VE
|
|
|
21
22
|
export { type MailCriteria, mailCriteriaOf, narrows } from "./server-query.js";
|
|
22
23
|
export { type MailServerAccess, type MailServerRequest, searchMailServer } from "./server-search.js";
|
|
23
24
|
export type { Listed, MailSource, Quick, Searched } from "./source.js";
|
|
25
|
+
export { SEARCH_REFUSED, searchRefused } from "./source.js";
|
|
24
26
|
export { listedOf, type StoredMail } from "./stored.js";
|
|
25
27
|
export { MAX_ATTACHMENT_CHARS, type MailAccount, type MailIndexed, type MailSyncOptions, type MailSyncResult, RETRYABLE_ATTACHMENTS, syncMail, } from "./sync.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { HEADER_FIELDS, headersOf, mailHeadersOf } from "./headers.js";
|
|
|
7
7
|
export { mailIds } from "./ids.js";
|
|
8
8
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
9
9
|
export { htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, parseMail, } from "./parse.js";
|
|
10
|
+
export { PARTIAL } from "./partial.js";
|
|
10
11
|
export { PRESET_NAMES, PRESETS, presetMatches } from "./presets.js";
|
|
11
12
|
export { domainsToAge, groupKey, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
12
13
|
export { mailProgress } from "./progress.js";
|
|
@@ -20,5 +21,6 @@ export { FRAUD_SIGNALS, FREE_MAIL, registrableOf, SCORE_RULE, scoreGroup, WEIGHT
|
|
|
20
21
|
export { DEMOTE_AT, demotedSenders, SCORE_VERSION, scoreSenders, } from "./sender-scores.js";
|
|
21
22
|
export { mailCriteriaOf, narrows } from "./server-query.js";
|
|
22
23
|
export { searchMailServer } from "./server-search.js";
|
|
24
|
+
export { SEARCH_REFUSED, searchRefused } from "./source.js";
|
|
23
25
|
export { listedOf } from "./stored.js";
|
|
24
26
|
export { MAX_ATTACHMENT_CHARS, RETRYABLE_ATTACHMENTS, syncMail, } from "./sync.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BudgetStop } from "../background/budget.js";
|
|
2
|
+
import type { LoadEngine } from "../readers/types.js";
|
|
3
|
+
import type { MessageStore } from "../store/index.js";
|
|
4
|
+
import type { MailSource } from "./source.js";
|
|
5
|
+
/** The `emails.metadata` key of an email stored from its first bytes: `{ bytes }` read of its `size`. */
|
|
6
|
+
export declare const PARTIAL = "partial";
|
|
7
|
+
/** How many partial emails one sync reads whole at most; the rest wait for the next run. */
|
|
8
|
+
export declare const PARTIAL_PER_RUN = 100;
|
|
9
|
+
export interface PartialOutcome {
|
|
10
|
+
/** Read whole this run, the mark cleared. */
|
|
11
|
+
completed: number;
|
|
12
|
+
/** Still partial after this run. */
|
|
13
|
+
left: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads whole the emails a server search stored from their first bytes, so the archive holds their attachments too.
|
|
17
|
+
* Sync never reads stored mail again, so without this step a partial email would stay partial. Gmail's are found by
|
|
18
|
+
* their id; elsewhere at a stored location, trusted only when the bytes read there carry the stored id.
|
|
19
|
+
*/
|
|
20
|
+
export declare const completePartial: ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, loadEngine, now, take, signal, }: {
|
|
21
|
+
store: MessageStore;
|
|
22
|
+
source: MailSource;
|
|
23
|
+
accountId: number;
|
|
24
|
+
address: string;
|
|
25
|
+
/** Where Gmail's are looked for by id; asked only when a partial email is there to read. */
|
|
26
|
+
folders: () => Promise<readonly string[]>;
|
|
27
|
+
maxBodyChars: number;
|
|
28
|
+
maxAttachmentChars: number;
|
|
29
|
+
loadEngine: LoadEngine;
|
|
30
|
+
now: number;
|
|
31
|
+
/** A budgeted run's next request, or why there is none. */
|
|
32
|
+
take?: () => BudgetStop | null;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}) => Promise<PartialOutcome>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { FETCH_CHUNK, readAttachments } from "./attachments.js";
|
|
2
|
+
import { parsedEmail } from "./email-input.js";
|
|
3
|
+
import { mailHeadersOf } from "./headers.js";
|
|
4
|
+
import { parseMail } from "./parse.js";
|
|
5
|
+
/** The `emails.metadata` key of an email stored from its first bytes: `{ bytes }` read of its `size`. */
|
|
6
|
+
export const PARTIAL = "partial";
|
|
7
|
+
/** How many partial emails one sync reads whole at most; the rest wait for the next run. */
|
|
8
|
+
export const PARTIAL_PER_RUN = 100;
|
|
9
|
+
/**
|
|
10
|
+
* Reads whole the emails a server search stored from their first bytes, so the archive holds their attachments too.
|
|
11
|
+
* Sync never reads stored mail again, so without this step a partial email would stay partial. Gmail's are found by
|
|
12
|
+
* their id; elsewhere at a stored location, trusted only when the bytes read there carry the stored id.
|
|
13
|
+
*/
|
|
14
|
+
export const completePartial = async ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, loadEngine, now, take, signal, }) => {
|
|
15
|
+
const { emails, total } = await store.mail.partialEmails(accountId, PARTIAL_PER_RUN);
|
|
16
|
+
if (emails.length === 0)
|
|
17
|
+
return { completed: 0, left: 0 };
|
|
18
|
+
const byId = new Map(emails.map((email) => [email.externalId, email]));
|
|
19
|
+
const items = new Map();
|
|
20
|
+
if (source.find) {
|
|
21
|
+
for (const folder of await folders()) {
|
|
22
|
+
const left = emails.filter((email) => !items.has(email)).map(({ externalId }) => externalId);
|
|
23
|
+
if (left.length === 0)
|
|
24
|
+
break;
|
|
25
|
+
if (take?.())
|
|
26
|
+
return { completed: 0, left: total };
|
|
27
|
+
for (const found of await source.find(left, folder)) {
|
|
28
|
+
const email = byId.get(found.messageId);
|
|
29
|
+
if (email && !items.has(email))
|
|
30
|
+
items.set(email, found);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (source.identify)
|
|
35
|
+
for (const email of emails) {
|
|
36
|
+
const place = email.locations[0];
|
|
37
|
+
if (place)
|
|
38
|
+
items.set(email, {
|
|
39
|
+
uid: String(place.uid),
|
|
40
|
+
folder: place.folder,
|
|
41
|
+
receivedAt: new Date(email.receivedAt ?? 0).toISOString(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
let completed = 0;
|
|
45
|
+
const wanted = [...items];
|
|
46
|
+
for (let start = 0; start < wanted.length; start += FETCH_CHUNK) {
|
|
47
|
+
if (signal?.aborted || take?.())
|
|
48
|
+
break;
|
|
49
|
+
const chunk = wanted.slice(start, start + FETCH_CHUNK);
|
|
50
|
+
let raws;
|
|
51
|
+
try {
|
|
52
|
+
raws = await source.fetchRaw(chunk.map(([, item]) => item));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// A partial email stays readable as it is; the next run tries again.
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const attachments = [];
|
|
59
|
+
for (const [index, [email, item]] of chunk.entries()) {
|
|
60
|
+
const raw = raws[index];
|
|
61
|
+
if (!(raw instanceof Uint8Array))
|
|
62
|
+
continue;
|
|
63
|
+
// A UID means another message after a UIDVALIDITY change; only the stored id proves it is this one.
|
|
64
|
+
if (!source.find && source.identify?.(raw, item).messageId !== email.externalId)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const mail = await parseMail(raw, maxBodyChars);
|
|
68
|
+
const files = await readAttachments(mail, email.externalId, { loadEngine, maxAttachmentChars, at: now, ...(signal ? { signal } : {}) }, attachments);
|
|
69
|
+
const { [PARTIAL]: _partial, ...metadata } = email.metadata ?? {};
|
|
70
|
+
const whole = { ...item, messageId: email.externalId, threadId: email.threadExternalId };
|
|
71
|
+
await store.mail.saveThread({
|
|
72
|
+
accountId,
|
|
73
|
+
externalId: email.threadExternalId,
|
|
74
|
+
now,
|
|
75
|
+
emails: [
|
|
76
|
+
{
|
|
77
|
+
...parsedEmail(mail, whole, mailHeadersOf(raw), address),
|
|
78
|
+
receivedAt: email.receivedAt,
|
|
79
|
+
size: raw.byteLength,
|
|
80
|
+
metadata,
|
|
81
|
+
attachments: files,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
completed++;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
if (signal?.aborted)
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { completed, left: total - completed };
|
|
94
|
+
};
|
|
@@ -9,6 +9,7 @@ import { parsedEmail } from "./email-input.js";
|
|
|
9
9
|
import { factsOf } from "./facts.js";
|
|
10
10
|
import { mailHeadersOf } from "./headers.js";
|
|
11
11
|
import { parseMail } from "./parse.js";
|
|
12
|
+
import { PARTIAL } from "./partial.js";
|
|
12
13
|
import { storedMailRules } from "./rule-stored.js";
|
|
13
14
|
import { ruleTagger, settleRuleTags } from "./rule-tags.js";
|
|
14
15
|
import { decide, tagsOf } from "./rules.js";
|
|
@@ -84,6 +85,7 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
84
85
|
const ruled = new Map();
|
|
85
86
|
const inputs = [];
|
|
86
87
|
const saved = [];
|
|
88
|
+
let cutShort = 0;
|
|
87
89
|
let opening;
|
|
88
90
|
let openFailure;
|
|
89
91
|
let unsupported = false;
|
|
@@ -146,17 +148,12 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
146
148
|
sources.set(sourceKey(item.threadId, item.messageId), held ? "both" : "server");
|
|
147
149
|
if (held)
|
|
148
150
|
continue;
|
|
149
|
-
// A big message would hold the connection and the parser past the bound; sync reads it whole later.
|
|
150
|
-
if ((item.size ?? 0) > SERVER_BOUNDS.maxMessageBytes) {
|
|
151
|
-
report.tooLarge = (report.tooLarge ?? 0) + 1;
|
|
152
|
-
report.complete = false;
|
|
153
|
-
sources.delete(sourceKey(item.threadId, item.messageId));
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
151
|
fresh.push(item);
|
|
157
152
|
ruled.set(item, { ruleId: rule?.id ?? null, tags: tagsOf(rules, facts(item)) });
|
|
158
153
|
}
|
|
159
|
-
|
|
154
|
+
// A big message would hold the connection and the parser past the bound: its first bytes carry the text.
|
|
155
|
+
const bytes = SERVER_BOUNDS.textBytes;
|
|
156
|
+
const raws = fresh.length && open ? await source.fetchRaw(fresh, { bytes }) : [];
|
|
160
157
|
// Parsing and reading attachments stay inside the bound: after it, nothing more is read or kept.
|
|
161
158
|
const now = Date.now();
|
|
162
159
|
for (const [index, item] of fresh.entries()) {
|
|
@@ -166,8 +163,12 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
166
163
|
try {
|
|
167
164
|
if (raw === undefined || raw instanceof Error)
|
|
168
165
|
throw raw ?? new Error("not read");
|
|
166
|
+
const cut = raw.byteLength >= bytes && (item.size === undefined || item.size > raw.byteLength);
|
|
169
167
|
const mail = await parseMail(raw);
|
|
170
|
-
|
|
168
|
+
// An attachment cut short is not the file: none is kept until the sync reads the whole message.
|
|
169
|
+
const files = cut
|
|
170
|
+
? []
|
|
171
|
+
: await readAttachments(mail, item.messageId, { loadEngine: importEngine, maxAttachmentChars: MAX_ATTACHMENT_CHARS, at: now, signal: stop }, []);
|
|
171
172
|
if (!open)
|
|
172
173
|
return;
|
|
173
174
|
const folder = item.folder;
|
|
@@ -189,12 +190,15 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
189
190
|
: { locations: [{ folder, uidvalidity: validity, uid: Number(item.uid) }] }),
|
|
190
191
|
...(item.seen === undefined ? {} : { read: item.seen }),
|
|
191
192
|
...(item.flagged === undefined ? {} : { flagged: item.flagged }),
|
|
193
|
+
...(cut ? { metadata: { [PARTIAL]: { bytes: raw.byteLength } } } : {}),
|
|
192
194
|
mailRuleId: ruled.get(item)?.ruleId ?? null,
|
|
193
195
|
attachments: files,
|
|
194
196
|
},
|
|
195
197
|
],
|
|
196
198
|
});
|
|
197
199
|
saved.push(item);
|
|
200
|
+
if (cut)
|
|
201
|
+
cutShort++;
|
|
198
202
|
}
|
|
199
203
|
catch {
|
|
200
204
|
sources.delete(sourceKey(item.threadId, item.messageId));
|
|
@@ -248,6 +252,10 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
248
252
|
}
|
|
249
253
|
await settleRuleTags(store, accountId, rules, tagger.outcome());
|
|
250
254
|
report.new = inputs.length;
|
|
255
|
+
if (cutShort) {
|
|
256
|
+
report.partial = cutShort;
|
|
257
|
+
report.complete = false;
|
|
258
|
+
}
|
|
251
259
|
report.returned = sources.size;
|
|
252
260
|
return { report, sources };
|
|
253
261
|
};
|
package/dist/mail/source.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { MailHeaders } from "./headers.js";
|
|
2
2
|
import type { FileStamp } from "./resume.js";
|
|
3
3
|
import type { MailCriteria } from "./server-query.js";
|
|
4
|
+
/**
|
|
5
|
+
* The reason of the CliError `search`, `query` and `present` throw when the server answers SEARCH with NO or BAD:
|
|
6
|
+
* read as "nothing found", it would look like an empty folder and prove its stored mail deleted.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SEARCH_REFUSED = "mail.search-refused";
|
|
9
|
+
export declare const searchRefused: (error: unknown) => boolean;
|
|
4
10
|
export interface Listed {
|
|
5
11
|
uid: string;
|
|
6
12
|
/** Gmail's own ids: the same in every folder, and they survive a UID reset. */
|
|
@@ -94,6 +100,11 @@ export interface MailSource {
|
|
|
94
100
|
present?(uids: readonly string[], folder: string): Promise<string[]>;
|
|
95
101
|
/** Puts labels back on messages in `folder`, by Gmail id. */
|
|
96
102
|
label?(messageIds: readonly string[], folder: string, labels: readonly string[]): Promise<void>;
|
|
97
|
-
/**
|
|
98
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Each item's raw RFC 5322 bytes, in order; one that could not be read is an Error, not a failed call. With
|
|
105
|
+
* `bytes`, at most the first `bytes` of each: a message cut short comes back shorter than its `size`.
|
|
106
|
+
*/
|
|
107
|
+
fetchRaw(items: Quick[], options?: {
|
|
108
|
+
bytes?: number;
|
|
109
|
+
}): Promise<(Uint8Array | Error)[]>;
|
|
99
110
|
}
|
package/dist/mail/source.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
/**
|
|
3
|
+
* The reason of the CliError `search`, `query` and `present` throw when the server answers SEARCH with NO or BAD:
|
|
4
|
+
* read as "nothing found", it would look like an empty folder and prove its stored mail deleted.
|
|
5
|
+
*/
|
|
6
|
+
export const SEARCH_REFUSED = "mail.search-refused";
|
|
7
|
+
export const searchRefused = (error) => error instanceof CliError && error.details.reason === SEARCH_REFUSED;
|