@wirecat/cli-core 0.390.0 → 0.391.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 +32 -0
- package/dist/cli/effects.d.ts +2 -0
- package/dist/cli/effects.js +8 -2
- package/dist/cli/messenger/catch-up.d.ts +0 -19
- package/dist/cli/messenger/catch-up.js +0 -43
- package/dist/cli/messenger/context.js +6 -3
- package/dist/cli/messenger/inbox.js +6 -11
- package/dist/cli/messenger/messages-command.js +5 -15
- package/dist/cli/messenger/review.d.ts +1 -1
- package/dist/cli/messenger/review.js +5 -10
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/tags-command.js +2 -2
- package/dist/cli/settings.d.ts +0 -4
- package/dist/cli/settings.js +7 -5
- package/dist/meetings/index.d.ts +2 -0
- package/dist/meetings/index.js +2 -0
- package/dist/meetings/links.d.ts +18 -0
- package/dist/meetings/links.js +1 -0
- package/dist/meetings/review.d.ts +45 -0
- package/dist/meetings/review.js +1 -0
- package/dist/search/lucene/registry.d.ts +5 -1
- package/dist/search/lucene/registry.js +10 -4
- package/dist/search/lucene/types.d.ts +8 -0
- package/dist/search/lucene/types.js +4 -0
- package/dist/search/question-plan.js +3 -3
- package/dist/services/excerpt.js +4 -1
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +2 -0
- package/dist/services/meeting-links.d.ts +33 -0
- package/dist/services/meeting-links.js +41 -0
- package/dist/services/meeting-review.d.ts +12 -0
- package/dist/services/meeting-review.js +8 -0
- package/dist/services/messages-search.js +2 -2
- package/dist/services/searches.js +3 -3
- package/dist/store/sqlite/lucene-fields.d.ts +3 -1
- package/dist/store/sqlite/lucene-fields.js +78 -11
- package/dist/store/sqlite/lucene.js +16 -11
- package/dist/store/sqlite/meeting-chat-lines.d.ts +3 -0
- package/dist/store/sqlite/meeting-chat-lines.js +34 -0
- package/dist/store/sqlite/meeting-review.d.ts +4 -0
- package/dist/store/sqlite/meeting-review.js +97 -0
- package/dist/store/sqlite/meetings.d.ts +2 -2
- package/dist/store/sqlite/meetings.js +4 -0
- package/package.json +1 -1
- package/parity.json +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.391.0 — 23.10.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- Reads never mark chats read: `messages list`, `inbox` and `review` no longer take `--mark-read` or
|
|
15
|
+
`--no-mark-read`. Remove `catch_up_marks_read` from configuration; it is now refused. Use
|
|
16
|
+
`<cli> <profile> chats mark-read <chat>` when you intend to send a read receipt.
|
|
17
|
+
- `tags auto` declares its messenger read, including the preparatory read requested with `--sync-first`.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `readMeetingReview` (`@wirecat/cli-core/services`) and `MeetingStore.meetingReview`: the newest meetings of a
|
|
22
|
+
selection, each with its newest summary (overview, next steps), transcript lines and words, the three who talked
|
|
23
|
+
longest and its confirmed projects. No transcript text is read out.
|
|
24
|
+
- `readMeetingLinks` (`@wirecat/cli-core/services`) and `MeetingStore.meetingChatLines`: links shared in the chat of
|
|
25
|
+
the selected meetings, most shared first, with who shared them, in which meetings, and the Google file each names.
|
|
26
|
+
Found when read, not stored; at most 100000 chat messages, more is an error.
|
|
27
|
+
- A local read with an unknown profile now lists the profiles on this computer in its error and
|
|
28
|
+
`details.profiles`, so a typo does not suggest linking a new account. A configured profile with no
|
|
29
|
+
saved account still explains how to fill its local history.
|
|
30
|
+
|
|
31
|
+
- Unknown permission keys now refuse messenger commands with `configuration_error`, including typos after
|
|
32
|
+
the resource name such as `messages.sedn`. Correct the key or remove it with `config unset`; a warning
|
|
33
|
+
no longer leaves the intended restriction unenforced.
|
|
34
|
+
- **Message search reads `"a b"~N`: the words at most N words apart in one message, in any order.** It was a
|
|
35
|
+
`query.unsupported-operator` error; now it is syntax for `text:`, `exact:`, `transcript:`, `content:` and a bare
|
|
36
|
+
word, N up to 50 (`query.invalid-number` above). Words match in any form, as in a phrase; `exact:` keeps the typed
|
|
37
|
+
forms. Unlike Lucene, order costs nothing: `"a b"~0` finds "b a" too. `word~1` stays an error. A saved search may
|
|
38
|
+
now hold `~N`. `QUERY_OPERATORS` splits the `fuzzy / proximity` row into `proximity` and `fuzzy`.
|
|
39
|
+
|
|
8
40
|
## 0.389.0 — 22.10.2026
|
|
9
41
|
|
|
10
42
|
### Added
|
package/dist/cli/effects.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export declare const isLogin: (command: Command) => boolean;
|
|
|
36
36
|
* (merge/unmerge, claim/release) or keeps the old value in the task's history; under the lock a close only suggests.
|
|
37
37
|
*/
|
|
38
38
|
export declare const destroysNothing: <T extends Command>(command: T) => T;
|
|
39
|
+
/** A local edit remains an edit even when it first fetches metadata from the messenger. */
|
|
40
|
+
export declare const changesStore: <T extends Command>(command: T) => T;
|
|
39
41
|
/**
|
|
40
42
|
* Whether `<PREFIX>_READ_ONLY` refuses the command as a change to the store. Every write that stays on this machine
|
|
41
43
|
* is refused unless `destroysNothing` marks it, so a new one fails closed (WC-PERM-12). A fetch (`remote: "read"`)
|
package/dist/cli/effects.js
CHANGED
|
@@ -52,6 +52,12 @@ export const destroysNothing = (command) => {
|
|
|
52
52
|
KEPT_UNDER_LOCK.add(command);
|
|
53
53
|
return command;
|
|
54
54
|
};
|
|
55
|
+
const STORE_WRITES = new WeakSet();
|
|
56
|
+
/** A local edit remains an edit even when it first fetches metadata from the messenger. */
|
|
57
|
+
export const changesStore = (command) => {
|
|
58
|
+
STORE_WRITES.add(command);
|
|
59
|
+
return command;
|
|
60
|
+
};
|
|
55
61
|
/**
|
|
56
62
|
* Whether `<PREFIX>_READ_ONLY` refuses the command as a change to the store. Every write that stays on this machine
|
|
57
63
|
* is refused unless `destroysNothing` marks it, so a new one fails closed (WC-PERM-12). A fetch (`remote: "read"`)
|
|
@@ -59,8 +65,8 @@ export const destroysNothing = (command) => {
|
|
|
59
65
|
*/
|
|
60
66
|
export const isStoreChange = (command) => {
|
|
61
67
|
const effects = metaOf(command).effects;
|
|
62
|
-
return (effects?.remote === "none" &&
|
|
63
|
-
effects
|
|
68
|
+
return ((effects?.remote === "none" || STORE_WRITES.has(command)) &&
|
|
69
|
+
effects?.local === "write" &&
|
|
64
70
|
!KEPT_UNDER_LOCK.has(command) &&
|
|
65
71
|
!changesAuthority(command));
|
|
66
72
|
};
|
|
@@ -1,22 +1,3 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import type { Id, Message } from "../../domain/models.js";
|
|
3
|
-
import type { MarkedRead } from "../../services/chats.js";
|
|
4
|
-
import type { Services } from "../../services/index.js";
|
|
5
|
-
import type { Settings } from "../settings.js";
|
|
6
|
-
export declare const MARK_READ_OPTION: readonly ["--mark-read", "also mark each chat shown read, up to the newest message shown; the other side sees it"];
|
|
7
|
-
export declare const NO_MARK_READ_OPTION: readonly ["--no-mark-read", "do not, whatever the catch_up_marks_read setting says"];
|
|
8
|
-
/**
|
|
9
|
-
* The typed flag decides; without one, the owner's `catch_up_marks_read` setting, off unless turned on (NEED-566).
|
|
10
|
-
* An agent's run never marks read on that setting: the other side sees it, so only an agent that asks does.
|
|
11
|
-
*/
|
|
12
|
-
export declare const marksRead: (typed: boolean | undefined, settings: Settings, command: Command) => boolean;
|
|
13
2
|
/** Whose `--new` points a run moves: an agent's are its own, so the owner's `inbox --new` still shows everything. */
|
|
14
3
|
export declare const pointsOf: <N extends "inbox" | "review">(command: Command, name: N) => N | `mcp-${N}`;
|
|
15
|
-
/**
|
|
16
|
-
* Marks each chat read up to the newest message shown in it — never further, so what arrived after
|
|
17
|
-
* the read stays unread. Through the send guard, so `permissions.chats.mark-read` applies.
|
|
18
|
-
*/
|
|
19
|
-
export declare const markShown: (services: Services, chats: {
|
|
20
|
-
id: Id;
|
|
21
|
-
messages: Pick<Message, "id" | "timestamp">[];
|
|
22
|
-
}[], warn: (message: string) => void) => Promise<MarkedRead[]>;
|
|
@@ -1,46 +1,3 @@
|
|
|
1
|
-
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
2
1
|
import { environmentOf } from "../context.js";
|
|
3
|
-
export const MARK_READ_OPTION = [
|
|
4
|
-
"--mark-read",
|
|
5
|
-
"also mark each chat shown read, up to the newest message shown; the other side sees it",
|
|
6
|
-
];
|
|
7
|
-
export const NO_MARK_READ_OPTION = ["--no-mark-read", "do not, whatever the catch_up_marks_read setting says"];
|
|
8
|
-
// Neither is offered to an agent (`ownerOnlyOption`): a read tool may run unasked, and the other side sees a mark.
|
|
9
|
-
// An agent marks read with `chats mark-read`, a write.
|
|
10
|
-
/**
|
|
11
|
-
* The typed flag decides; without one, the owner's `catch_up_marks_read` setting, off unless turned on (NEED-566).
|
|
12
|
-
* An agent's run never marks read on that setting: the other side sees it, so only an agent that asks does.
|
|
13
|
-
*/
|
|
14
|
-
export const marksRead = (typed, settings, command) => {
|
|
15
|
-
const marking = typed ?? (environmentOf(command).agent ? false : settings.catchUpMarksRead);
|
|
16
|
-
if (marking && settings.offline) {
|
|
17
|
-
throw new CliError("validation_error", "--mark-read tells the messenger; not with --offline");
|
|
18
|
-
}
|
|
19
|
-
return marking;
|
|
20
|
-
};
|
|
21
2
|
/** Whose `--new` points a run moves: an agent's are its own, so the owner's `inbox --new` still shows everything. */
|
|
22
3
|
export const pointsOf = (command, name) => environmentOf(command).agent ? `mcp-${name}` : name;
|
|
23
|
-
/**
|
|
24
|
-
* Marks each chat read up to the newest message shown in it — never further, so what arrived after
|
|
25
|
-
* the read stays unread. Through the send guard, so `permissions.chats.mark-read` applies.
|
|
26
|
-
*/
|
|
27
|
-
export const markShown = async (services, chats, warn) => {
|
|
28
|
-
const marked = [];
|
|
29
|
-
for (const { id, messages } of chats) {
|
|
30
|
-
const newest = messages.reduce((last, one) => (last === undefined || Date.parse(one.timestamp) >= Date.parse(last.timestamp) ? one : last), undefined);
|
|
31
|
-
if (newest === undefined)
|
|
32
|
-
continue;
|
|
33
|
-
try {
|
|
34
|
-
const { chatId, until } = await services.chats.markRead({ chat: id, until: newest.id });
|
|
35
|
-
marked.push({ chatId, until });
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
// A hold refuses every chat alike; what was read is still the answer, as reads go on during one.
|
|
39
|
-
if (!isCliError(error) || error.details.sendBlock === undefined)
|
|
40
|
-
throw error;
|
|
41
|
-
warn(`${error.message}; nothing more was marked read`);
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return marked;
|
|
46
|
-
};
|
|
@@ -282,7 +282,10 @@ export const connectedFor = async (command, messenger, base, options) => {
|
|
|
282
282
|
}),
|
|
283
283
|
};
|
|
284
284
|
};
|
|
285
|
-
const unrecorded = (command, messenger, profile, offline) => {
|
|
285
|
+
const unrecorded = (command, messenger, profile, { offline, profilesHere }) => {
|
|
286
|
+
if (!profilesHere.includes(profile)) {
|
|
287
|
+
return new CliError("not_found", `no profile named "${profile}" — profiles here: ${profilesHere.length > 0 ? profilesHere.join(", ") : "none"}`, { profiles: profilesHere });
|
|
288
|
+
}
|
|
286
289
|
const words = `${messenger.app.command} ${asFirstWord(profile)}`;
|
|
287
290
|
const setup = rootOf(command).commands.some((one) => one.name() === "setup") ? `\`${words}setup\`` : undefined;
|
|
288
291
|
if (messenger.history === "store")
|
|
@@ -346,14 +349,14 @@ export const messengerContext = (command, messenger, resolving = {}) => {
|
|
|
346
349
|
refuseDenied(command, base.settings);
|
|
347
350
|
refuseLacking(messenger, pathOf(command));
|
|
348
351
|
for (const key of unknownPermissionKeys(base.settings.permissions, knownPermissionKeys(command, messenger.permissionKey))) {
|
|
349
|
-
|
|
352
|
+
throw new CliError("configuration_error", `permissions.${key} names no command — remove it with \`${app.command} ${asFirstWord(profile)}config unset permissions.${key}\``, { permission: key });
|
|
350
353
|
}
|
|
351
354
|
// Read when warning, not now: serve swaps the renderer's methods for log records after this.
|
|
352
355
|
const warn = (text, warning) => base.renderer.warn(text, warning);
|
|
353
356
|
const accountFor = () => {
|
|
354
357
|
const account = messenger.accountOf?.(command) ?? recalledAccount(app, provider, profile, base.env);
|
|
355
358
|
if (!account)
|
|
356
|
-
throw unrecorded(command, messenger, profile, base.settings
|
|
359
|
+
throw unrecorded(command, messenger, profile, base.settings);
|
|
357
360
|
return account;
|
|
358
361
|
};
|
|
359
362
|
const guard = messenger.guard?.(command, base.settings, warn) ??
|
|
@@ -5,11 +5,11 @@ import { renderMessages } from "../../render/messages.js";
|
|
|
5
5
|
import { CHAT_KINDS } from "../../services/chats.js";
|
|
6
6
|
import { kindsOf } from "../../services/inbox.js";
|
|
7
7
|
import { modelWith } from "../../speech/hearing.js";
|
|
8
|
-
import {
|
|
8
|
+
import { REMOTE_READ } from "../effects.js";
|
|
9
9
|
import { renderAnswer } from "../envelope.js";
|
|
10
10
|
import { queryHelp } from "../list-query.js";
|
|
11
11
|
import { positiveCount } from "../paging.js";
|
|
12
|
-
import {
|
|
12
|
+
import { pointsOf } from "./catch-up.js";
|
|
13
13
|
import { catchUpQuery, INBOX_QUERY, inboxAnswer } from "./catch-up-answers.js";
|
|
14
14
|
import { messengerContext } from "./context.js";
|
|
15
15
|
import { hearForCommand, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION } from "./hearing-command.js";
|
|
@@ -42,10 +42,9 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
|
42
42
|
.option("--kind <kinds>", `only chats of these kinds, comma-separated: ${CHAT_KINDS.join(", ")}`)
|
|
43
43
|
.option(...TRANSCRIBE_OPTION)
|
|
44
44
|
.option(...MODEL_OPTION)
|
|
45
|
-
.
|
|
46
|
-
.addOption(ownerOnlyOption(...NO_MARK_READ_OPTION))
|
|
45
|
+
.addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
|
|
47
46
|
.action(async function (query) {
|
|
48
|
-
const { new: fresh, all, kind, transcribe, model,
|
|
47
|
+
const { new: fresh, all, kind, transcribe, model, } = this.opts();
|
|
49
48
|
const hearWith = modelWith(transcribe, model);
|
|
50
49
|
const context = messengerContext(this, messenger);
|
|
51
50
|
const { app } = messenger;
|
|
@@ -64,8 +63,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
|
64
63
|
: undefined;
|
|
65
64
|
const from = since ?? saved?.first;
|
|
66
65
|
const points = saved?.chats;
|
|
67
|
-
const
|
|
68
|
-
const { inbox, hearing, marked, reader } = await context.withServices(async (services, connect) => {
|
|
66
|
+
const { inbox, hearing, reader } = await context.withServices(async (services, connect) => {
|
|
69
67
|
const inbox = await services.inbox.read({
|
|
70
68
|
...(from === undefined ? {} : { since: from }),
|
|
71
69
|
...(points === undefined ? {} : { points }),
|
|
@@ -76,11 +74,8 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
|
76
74
|
const reader = await services.messages.reader();
|
|
77
75
|
const read = inbox.chats.flatMap((chat) => chat.messages.map((message) => ({ ...message, chatTitle: chat.title })));
|
|
78
76
|
const hearing = await hearForCommand(context, messenger, read, transcribe === true, hearWith, connect);
|
|
79
|
-
|
|
80
|
-
return { inbox, hearing, marked, reader };
|
|
77
|
+
return { inbox, hearing, reader };
|
|
81
78
|
});
|
|
82
|
-
if (marked !== undefined)
|
|
83
|
-
renderer.note(`marked ${marked.length} chat(s) read up to the newest message shown`);
|
|
84
79
|
if (inbox.partial && format === "pretty")
|
|
85
80
|
renderer.warn(PARTIAL_NOTE);
|
|
86
81
|
const answer = inboxAnswer(inbox, hearing, reader, transcribe === true, meta);
|
|
@@ -7,7 +7,7 @@ import { renderMessages } from "../../render/messages.js";
|
|
|
7
7
|
import { renderThreadLinks } from "../../render/thread-context.js";
|
|
8
8
|
import { readMail } from "../../services/mail-search.js";
|
|
9
9
|
import { modelWith, spoken } from "../../speech/hearing.js";
|
|
10
|
-
import {
|
|
10
|
+
import { REMOTE_READ } from "../effects.js";
|
|
11
11
|
import { itemOf, listOf, renderAnswer } from "../envelope.js";
|
|
12
12
|
import { queryHelp } from "../list-query.js";
|
|
13
13
|
import { positiveCount } from "../paging.js";
|
|
@@ -48,29 +48,19 @@ export const messagesCommand = (messenger) => {
|
|
|
48
48
|
.option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it")
|
|
49
49
|
.option(...TRANSCRIBE_OPTION)
|
|
50
50
|
.option(...MODEL_OPTION)
|
|
51
|
-
.
|
|
51
|
+
.addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
|
|
52
52
|
.action(async function (chat, query) {
|
|
53
53
|
const context = messengerContext(this, messenger);
|
|
54
|
-
const { cursor, transcribe, model
|
|
54
|
+
const { cursor, transcribe, model } = this.opts();
|
|
55
55
|
const window = historyWindow(query, cursor, "messages list", history);
|
|
56
56
|
const hearWith = modelWith(transcribe, model);
|
|
57
|
-
if (markRead && window.threadId !== undefined) {
|
|
58
|
-
throw new CliError("validation_error", "--mark-read marks the whole chat; use `chats mark-read --topic` for one");
|
|
59
|
-
}
|
|
60
|
-
if (markRead && context.settings.offline) {
|
|
61
|
-
throw new CliError("validation_error", "--mark-read tells the messenger; not with --offline");
|
|
62
|
-
}
|
|
63
57
|
const { limit } = context.settings;
|
|
64
|
-
const { page, hearing,
|
|
58
|
+
const { page, hearing, reader } = await context.withServices(async (services, connect) => {
|
|
65
59
|
const page = await services.messages.list(chat, { limit, ...window });
|
|
66
60
|
const reader = await services.messages.reader();
|
|
67
|
-
const newest = page.items.at(-1);
|
|
68
|
-
const marked = markRead && newest ? await services.chats.markRead({ chat, until: newest.id }) : undefined;
|
|
69
61
|
const hearing = await hearForCommand(context, messenger, page.items, transcribe === true, hearWith, connect);
|
|
70
|
-
return { page, hearing,
|
|
62
|
+
return { page, hearing, reader };
|
|
71
63
|
});
|
|
72
|
-
if (marked)
|
|
73
|
-
context.renderer.note(`marked read up to ${marked.until} (operation ${marked.operationId})`);
|
|
74
64
|
const next = page.hasMore
|
|
75
65
|
? cursorOf(window, { first: page.items[0]?.id, last: page.items.at(-1)?.id })
|
|
76
66
|
: undefined;
|
|
@@ -5,7 +5,7 @@ export declare const unansweredHours: (value: unknown, flag?: string) => number;
|
|
|
5
5
|
/**
|
|
6
6
|
* **Everything said since a point, both sides, in every chat that changed** — the reading half of a
|
|
7
7
|
* review of who owes what (max-cli's `review`). Sorting it is the reader's job, person or agent.
|
|
8
|
-
*
|
|
8
|
+
* Reading never marks a chat read, and `inbox --new` keeps its point. The rules open and close
|
|
9
9
|
* tasks in the local store from what it read (`../../services/task-rules.ts`); nothing reaches the messenger.
|
|
10
10
|
*/
|
|
11
11
|
export declare const reviewCommand: (messenger: Messenger) => Command;
|
|
@@ -6,10 +6,10 @@ import { renderMessages } from "../../render/messages.js";
|
|
|
6
6
|
import { CHAT_KINDS } from "../../services/chats.js";
|
|
7
7
|
import { kindsOf, REVIEW_DAYS, reviewStart, UNANSWERED_HOURS } from "../../services/inbox.js";
|
|
8
8
|
import { modelWith } from "../../speech/hearing.js";
|
|
9
|
-
import {
|
|
9
|
+
import { REMOTE_READ } from "../effects.js";
|
|
10
10
|
import { renderAnswer } from "../envelope.js";
|
|
11
11
|
import { queryHelp } from "../list-query.js";
|
|
12
|
-
import {
|
|
12
|
+
import { pointsOf } from "./catch-up.js";
|
|
13
13
|
import { catchUpQuery, REVIEW_QUERY, reviewAnswer } from "./catch-up-answers.js";
|
|
14
14
|
import { messengerContext } from "./context.js";
|
|
15
15
|
import { heardItems, hearForCommand, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION } from "./hearing-command.js";
|
|
@@ -21,7 +21,7 @@ export const unansweredHours = (value, flag = "--unanswered") => value === true
|
|
|
21
21
|
/**
|
|
22
22
|
* **Everything said since a point, both sides, in every chat that changed** — the reading half of a
|
|
23
23
|
* review of who owes what (max-cli's `review`). Sorting it is the reader's job, person or agent.
|
|
24
|
-
*
|
|
24
|
+
* Reading never marks a chat read, and `inbox --new` keeps its point. The rules open and close
|
|
25
25
|
* tasks in the local store from what it read (`../../services/task-rules.ts`); nothing reaches the messenger.
|
|
26
26
|
*/
|
|
27
27
|
export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
@@ -38,8 +38,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
38
38
|
.option(...TRANSCRIBE_OPTION)
|
|
39
39
|
.option(...MODEL_OPTION)
|
|
40
40
|
.option("--new", "what changed since the last `review --new`, a point per chat — for scheduled runs")
|
|
41
|
-
.
|
|
42
|
-
.addOption(ownerOnlyOption(...NO_MARK_READ_OPTION))
|
|
41
|
+
.addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
|
|
43
42
|
.action(async function (query) {
|
|
44
43
|
const options = this.opts();
|
|
45
44
|
const hearWith = modelWith(options.transcribe, options.model);
|
|
@@ -59,12 +58,11 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
59
58
|
})
|
|
60
59
|
: undefined;
|
|
61
60
|
const since = from ?? saved?.first ?? reviewStart();
|
|
62
|
-
const marking = marksRead(options.markRead, settings, this);
|
|
63
61
|
const hours = options.unanswered === undefined ? undefined : unansweredHours(options.unanswered);
|
|
64
62
|
const kinds = options.kind === undefined ? undefined : kindsOf(options.kind);
|
|
65
63
|
let hearing;
|
|
66
64
|
const transcribe = options.transcribe === true;
|
|
67
|
-
const { found,
|
|
65
|
+
const { found, reader } = await context.withServices(async (services, connect) => {
|
|
68
66
|
const found = await services.inbox.review({
|
|
69
67
|
since,
|
|
70
68
|
...(saved === undefined ? {} : { points: saved.chats }),
|
|
@@ -84,13 +82,10 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
84
82
|
});
|
|
85
83
|
return {
|
|
86
84
|
found,
|
|
87
|
-
marked: marking ? await markShown(services, found.chats, context.renderer.warn) : undefined,
|
|
88
85
|
reader: await services.messages.reader(),
|
|
89
86
|
};
|
|
90
87
|
});
|
|
91
88
|
found.complete &&= (hearing?.unheard.length ?? 0) === 0;
|
|
92
|
-
if (marked !== undefined)
|
|
93
|
-
renderer.note(`marked ${marked.length} chat(s) read up to the newest message shown`);
|
|
94
89
|
notes(found, format === "pretty", renderer);
|
|
95
90
|
renderAnswer(this, context, reviewAnswer(found, hearing, reader, {
|
|
96
91
|
transcribe,
|
|
@@ -2,9 +2,9 @@ import { Command } from "commander";
|
|
|
2
2
|
import type { Messenger } from "./context.js";
|
|
3
3
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
4
4
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
5
|
-
error?: undefined;
|
|
6
5
|
path: string;
|
|
7
6
|
exists: boolean;
|
|
7
|
+
error?: undefined;
|
|
8
8
|
} | {
|
|
9
9
|
path: string;
|
|
10
10
|
exists: boolean;
|
|
@@ -2,7 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { TAGGING_QUERY } from "../../services/tags.js";
|
|
5
|
-
import { LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
5
|
+
import { changesStore, FETCH, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
6
6
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
7
7
|
import { listQuery, pagedList, pageOf, typedQuery, withQuery } from "../list-query.js";
|
|
8
8
|
import { positiveCount } from "../paging.js";
|
|
@@ -29,7 +29,7 @@ const sourceOf = (value) => {
|
|
|
29
29
|
/** The owner's labels, in the local store only: `tag:` in a search finds what they label. */
|
|
30
30
|
export const tagsCommand = (messenger) => {
|
|
31
31
|
const tags = new Command("tags").description("your own labels on chats, people, messages and tasks, kept in the local store and never sent; tag: in a search finds them");
|
|
32
|
-
annotate(tags.command("auto"), { effects:
|
|
32
|
+
changesStore(annotate(tags.command("auto"), { effects: FETCH }))
|
|
33
33
|
.description("derive local group/channel tags from cached metadata using keyword rules")
|
|
34
34
|
.option("--chat <chat>", "a stored group/channel; repeat to select several", (value, previous) => [...previous, value], [])
|
|
35
35
|
.option("--limit <number>", "process at most 1–500 chats", positiveCount("--limit"), 50)
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -78,8 +78,6 @@ export interface Settings extends AISettings {
|
|
|
78
78
|
/** Unset means "decide from the terminal", which is `resolveOutput`'s job, not this one's. */
|
|
79
79
|
color: boolean | undefined;
|
|
80
80
|
senderColors: boolean;
|
|
81
|
-
/** `inbox` and `review` mark each chat they show read, as `--mark-read` does; the other side sees it. Off unless set. */
|
|
82
|
-
catchUpMarksRead: boolean;
|
|
83
81
|
searchCatchUp?: boolean;
|
|
84
82
|
/** Search and `images embed` read the pictures themselves, with a model on this machine. Off until set. */
|
|
85
83
|
pictureSearch?: boolean;
|
|
@@ -297,7 +295,6 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
297
295
|
requests_per_minute: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 0, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
298
296
|
transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
299
297
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
300
|
-
catch_up_marks_read: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
301
298
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
302
299
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
303
300
|
update_check: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
@@ -427,7 +424,6 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
427
424
|
requests_per_minute: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 0, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
428
425
|
transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
429
426
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
430
|
-
catch_up_marks_read: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
431
427
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
432
428
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
433
429
|
}, (issue: v.StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
package/dist/cli/settings.js
CHANGED
|
@@ -48,7 +48,6 @@ const SHARED_PROFILE_ENTRIES = {
|
|
|
48
48
|
requests_per_minute: v.optional(countOrZero),
|
|
49
49
|
transcribe_with: v.optional(v.picklist(["auto", "messenger", "local"], plain("has to be auto, messenger or local"))),
|
|
50
50
|
speech_model: v.optional(v.string(plain("has to be a model id from `models audio list`, in quotes"))),
|
|
51
|
-
catch_up_marks_read: v.optional(flag),
|
|
52
51
|
search_catch_up: v.optional(flag),
|
|
53
52
|
picture_search: v.optional(flag),
|
|
54
53
|
...AI_ENTRIES,
|
|
@@ -116,7 +115,10 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
116
115
|
return loadSnakeCaseConfig(path, extension.schema ?? schema, () => ({ profiles: {} }));
|
|
117
116
|
}
|
|
118
117
|
catch (error) {
|
|
119
|
-
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
throw new CliError("configuration_error", message.includes("catch_up_marks_read")
|
|
120
|
+
? `${message}; remove catch_up_marks_read from the config file and use ${app.command} <profile> chats mark-read <chat>`
|
|
121
|
+
: message);
|
|
120
122
|
}
|
|
121
123
|
};
|
|
122
124
|
/**
|
|
@@ -187,13 +189,15 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
187
189
|
[extension.sourcePaths ? "config file: defaults" : "config defaults", shared],
|
|
188
190
|
];
|
|
189
191
|
const fromLayers = (key, fallback, only = layers) => first(only.map(([from, scope]) => [from, scope?.[key]]), fallback);
|
|
192
|
+
if (fromLayers("catch_up_marks_read", undefined).value !== undefined) {
|
|
193
|
+
throw new CliError("configuration_error", `remove catch_up_marks_read from the config file and use ${app.command} <profile> chats mark-read <chat>`);
|
|
194
|
+
}
|
|
190
195
|
const kindLayers = layers.filter(([from]) => from.startsWith(`config file: ${kind}.`));
|
|
191
196
|
const limit = flags.limit === undefined ? fromLayers("limit", DEFAULT_LIMIT) : { value: flags.limit, from: "flag" };
|
|
192
197
|
const record = flags.record === undefined ? fromLayers("record", false) : { value: flags.record, from: "flag" };
|
|
193
198
|
const timeoutMs = fromLayers("timeout_ms", undefined);
|
|
194
199
|
const color = fromLayers("color", undefined);
|
|
195
200
|
const senderColors = fromLayers("sender_colors", false);
|
|
196
|
-
const catchUpMarksRead = fromLayers("catch_up_marks_read", false);
|
|
197
201
|
const searchCatchUp = fromLayers("search_catch_up", false);
|
|
198
202
|
const pictureSearch = fromLayers("picture_search", false);
|
|
199
203
|
const keepRunsForDays = fromLayers("keep_runs_for_days", DEFAULT_KEEP_RUNS_FOR_DAYS);
|
|
@@ -241,7 +245,6 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
241
245
|
offline: flags.offline === true,
|
|
242
246
|
color: color.value,
|
|
243
247
|
senderColors: senderColors.value,
|
|
244
|
-
catchUpMarksRead: catchUpMarksRead.value,
|
|
245
248
|
searchCatchUp: searchCatchUp.value,
|
|
246
249
|
pictureSearch: pictureSearch.value,
|
|
247
250
|
limit: limit.value,
|
|
@@ -275,7 +278,6 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
275
278
|
commandTimeoutMs: timeout.from,
|
|
276
279
|
color: color.from,
|
|
277
280
|
senderColors: senderColors.from,
|
|
278
|
-
catchUpMarksRead: catchUpMarksRead.from,
|
|
279
281
|
searchCatchUp: searchCatchUp.from,
|
|
280
282
|
pictureSearch: pictureSearch.from,
|
|
281
283
|
record: record.from,
|
package/dist/meetings/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRu
|
|
|
9
9
|
export { MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
|
|
10
10
|
export { type ImportFile, importFiles } from "./import.js";
|
|
11
11
|
export { MEETING_ISSUES, MEETING_QUERY, type MeetingIssue, type MeetingItem, type MeetingOwner, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
|
|
12
|
+
export { CHAT_MESSAGES_READ, type MeetingChatLine, type MeetingChatLinesInput, type MeetingChatLinesStore, } from "./links.js";
|
|
12
13
|
export { createMeetingContext, MEETING_FACT_TYPES, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
|
|
13
14
|
export { createMeetingManagement, dispatchMeetingMutation, type ManagedMeeting, type ManagedMeetingChange, type MeetingManagement, type MeetingManagementPorts, type MeetingMutationOptions, type MeetingMutationProposal, type MeetingMutationReceipt, type MeetingOperationOptions, type MeetingProviderContext, validateMeetingChange, } from "./meeting-management.js";
|
|
14
15
|
export type { Attachment, ChatLine, Event, EventSeries, JsonValue, Meeting, MeetingSeries, Metadata, Occurrence, Participant, RecordTimes, SourceChatLine, SourceMeeting, Summary, SummaryContent, Transcript, TranscriptLine, TranscriptRow, } from "./model.js";
|
|
@@ -32,6 +33,7 @@ export { parseRecordingSyncCheckpoint, type RecordingSyncCheckpoint, type Record
|
|
|
32
33
|
export { type RecordingPartial, RecordingTransferError, type RecordingTransferResponse, type RecordingTransferSource, type RecordingTransferStorage, type RecordingTransferWriter, strongRecordingValidator, transferRecording, } from "./recording-transfer.js";
|
|
33
34
|
export { canonicalMeetingReference, formatMeetingReference, type MeetingReference, parseMeetingReference, } from "./reference.js";
|
|
34
35
|
export { type MeetingRelatedHistoryEntry, type MeetingRelatedHistoryRequest, type MeetingRelatedHistoryStore, validateMeetingRelatedHistory, } from "./related-history.js";
|
|
36
|
+
export { type MeetingReview, type MeetingReviewInput, type MeetingReviewItem, type MeetingReviewSpeaker, type MeetingReviewStore, REVIEW_LIMIT, } from "./review.js";
|
|
35
37
|
export { SAVE_HELP, SAVE_INSTRUCTION } from "./save-hints.js";
|
|
36
38
|
export type { MeetingHitSearchRequest, MeetingSearchIndexInput, MeetingSearchIndexReceipt, MeetingSearchIndexStore, MeetingSearchOptions, MeetingSearchPage, MeetingSearchStore, } from "./search-store.js";
|
|
37
39
|
export { validateMeetingHitSearch } from "./search-validation.js";
|
package/dist/meetings/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { findingFingerprint, validateFindings, } from "./findings-rules.js";
|
|
|
7
7
|
export { MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
|
|
8
8
|
export { importFiles } from "./import.js";
|
|
9
9
|
export { MEETING_ISSUES, MEETING_QUERY, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
|
|
10
|
+
export { CHAT_MESSAGES_READ, } from "./links.js";
|
|
10
11
|
export { createMeetingContext, MEETING_FACT_TYPES, validateMeetingContextChange, } from "./meeting-context.js";
|
|
11
12
|
export { createMeetingManagement, dispatchMeetingMutation, validateMeetingChange, } from "./meeting-management.js";
|
|
12
13
|
export { normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
|
|
@@ -23,6 +24,7 @@ export { parseRecordingSyncCheckpoint, RecordingSyncError, recordingAssetFingerp
|
|
|
23
24
|
export { RecordingTransferError, strongRecordingValidator, transferRecording, } from "./recording-transfer.js";
|
|
24
25
|
export { canonicalMeetingReference, formatMeetingReference, parseMeetingReference, } from "./reference.js";
|
|
25
26
|
export { validateMeetingRelatedHistory, } from "./related-history.js";
|
|
27
|
+
export { REVIEW_LIMIT, } from "./review.js";
|
|
26
28
|
export { SAVE_HELP, SAVE_INSTRUCTION } from "./save-hints.js";
|
|
27
29
|
export { validateMeetingHitSearch } from "./search-validation.js";
|
|
28
30
|
export { validateMeetingSeriesHistory, } from "./series-history.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MeetingStatsSelection } from "./stats.js";
|
|
2
|
+
export interface MeetingChatLinesInput extends MeetingStatsSelection {
|
|
3
|
+
/** Most chat messages to read (default and at most 100000); more is an error, never a silent cut. */
|
|
4
|
+
maxMessages?: number;
|
|
5
|
+
}
|
|
6
|
+
/** One stored chat message with the meeting it was sent in. */
|
|
7
|
+
export interface MeetingChatLine {
|
|
8
|
+
meetingId: number;
|
|
9
|
+
accountId: number;
|
|
10
|
+
title: string | null;
|
|
11
|
+
sentAt: number;
|
|
12
|
+
sender: string | null;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MeetingChatLinesStore {
|
|
16
|
+
meetingChatLines(input: MeetingChatLinesInput): Promise<MeetingChatLine[]>;
|
|
17
|
+
}
|
|
18
|
+
export declare const CHAT_MESSAGES_READ = 100000;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CHAT_MESSAGES_READ = 100_000;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MeetingStatsSelection } from "./stats.js";
|
|
2
|
+
export interface MeetingReviewInput extends MeetingStatsSelection {
|
|
3
|
+
/** Most meetings, newest first (default 50, at most 200). */
|
|
4
|
+
limit?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface MeetingReviewSpeaker {
|
|
7
|
+
name: string | null;
|
|
8
|
+
talkMs: number;
|
|
9
|
+
}
|
|
10
|
+
/** One meeting as a review shows it: what the store holds about it, never transcript text. */
|
|
11
|
+
export interface MeetingReviewItem {
|
|
12
|
+
meetingId: number;
|
|
13
|
+
accountId: number;
|
|
14
|
+
title: string | null;
|
|
15
|
+
startedAt: number | null;
|
|
16
|
+
durationMs: number | null;
|
|
17
|
+
participantsCount: number | null;
|
|
18
|
+
projects: string[];
|
|
19
|
+
/** `null`: no current transcript. */
|
|
20
|
+
transcript: {
|
|
21
|
+
lines: number;
|
|
22
|
+
words: number;
|
|
23
|
+
} | null;
|
|
24
|
+
/** The three who talked longest, from the transcript. */
|
|
25
|
+
speakers: MeetingReviewSpeaker[];
|
|
26
|
+
/** The newest stored summary; `null` when the meeting has none. */
|
|
27
|
+
summary: {
|
|
28
|
+
source: string;
|
|
29
|
+
title: string | null;
|
|
30
|
+
overview: string | null;
|
|
31
|
+
nextSteps: string[];
|
|
32
|
+
url: string | null;
|
|
33
|
+
} | null;
|
|
34
|
+
}
|
|
35
|
+
export interface MeetingReview {
|
|
36
|
+
items: MeetingReviewItem[];
|
|
37
|
+
hasMore: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface MeetingReviewStore {
|
|
40
|
+
meetingReview(input: MeetingReviewInput): Promise<MeetingReview>;
|
|
41
|
+
}
|
|
42
|
+
export declare const REVIEW_LIMIT: {
|
|
43
|
+
readonly default: 50;
|
|
44
|
+
readonly most: 200;
|
|
45
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const REVIEW_LIMIT = { default: 50, most: 200 };
|
|
@@ -70,7 +70,11 @@ export declare const QUERY_OPERATORS: readonly [{
|
|
|
70
70
|
readonly example: "text:/pass(port)?/";
|
|
71
71
|
readonly semantics: "Полный term; bounded subset Lucene RegExp; буквы text приводятся как в индексе";
|
|
72
72
|
}, {
|
|
73
|
-
readonly name: "
|
|
73
|
+
readonly name: "proximity";
|
|
74
|
+
readonly example: '"invoice paid"~3';
|
|
75
|
+
readonly semantics: "Слова одного сообщения, между ними не больше N других слов, в любом порядке; N до 50";
|
|
76
|
+
}, {
|
|
77
|
+
readonly name: "fuzzy";
|
|
74
78
|
readonly example: "invoice~1";
|
|
75
79
|
readonly semantics: "unsupported_operator; формы слова ловит prefix `word*`";
|
|
76
80
|
}, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { queryAstSchema, validateQuery, walkQuery, } from "@wirecat/cli-base/query";
|
|
2
2
|
import * as v from "valibot";
|
|
3
3
|
import { tagOf } from "../../domain/tags.js";
|
|
4
|
-
import { QUERY_LIMITS, SEARCH_QUERY, searchError } from "./types.js";
|
|
4
|
+
import { MOST_WORDS_APART, QUERY_LIMITS, SEARCH_QUERY, searchError } from "./types.js";
|
|
5
5
|
export const FIELD_VERSION = 2;
|
|
6
6
|
const UNITS = {
|
|
7
7
|
"": 1,
|
|
@@ -398,6 +398,7 @@ export const QUERY_FIELDS = [
|
|
|
398
398
|
},
|
|
399
399
|
];
|
|
400
400
|
const MESSAGES = { entity: "message", backend: "sql", fields: QUERY_FIELDS };
|
|
401
|
+
const NEAR_FIELDS = new Set(["text", "exact", "transcript", "content"]);
|
|
401
402
|
/** Field, operator and value of every term, against the message fields. */
|
|
402
403
|
export const validateFields = (ast) => {
|
|
403
404
|
for (const node of walkQuery(ast.root)) {
|
|
@@ -406,6 +407,10 @@ export const validateFields = (ast) => {
|
|
|
406
407
|
const plain = ["term", "phrase"].includes(node.operator) && /^[\w:.+-]+$/u.test(node.value);
|
|
407
408
|
searchError("legacy_date_field", node.span, plain ? `use date${node.field === "after" ? ">=" : "<"}${node.value}` : "use date>=YYYY-MM-DD or date:[a TO b]");
|
|
408
409
|
}
|
|
410
|
+
if (node.slop !== undefined && !NEAR_FIELDS.has(node.field))
|
|
411
|
+
searchError("query.unsupported-operator", node.span, `"a b"~N takes words: ${[...NEAR_FIELDS].join(", ")}`);
|
|
412
|
+
if (node.slop !== undefined && node.slop > MOST_WORDS_APART)
|
|
413
|
+
searchError("query.invalid-number", node.span, `words at most ${MOST_WORDS_APART} apart: "a b"~${MOST_WORDS_APART}`);
|
|
409
414
|
// The message compilers read `field:*` as a pattern, never as "has a value".
|
|
410
415
|
if (field && node.operator === "wildcard" && !field.operators.includes("wildcard"))
|
|
411
416
|
searchError("query.unsupported-operator", node.span, `${field.name} takes ${field.operators.join(", ")}`);
|
|
@@ -499,9 +504,10 @@ export const QUERY_OPERATORS = [
|
|
|
499
504
|
semantics: "Полный term; bounded subset Lucene RegExp; буквы text приводятся как в индексе",
|
|
500
505
|
},
|
|
501
506
|
{
|
|
502
|
-
name: "
|
|
503
|
-
example: "invoice~
|
|
504
|
-
semantics: "
|
|
507
|
+
name: "proximity",
|
|
508
|
+
example: '"invoice paid"~3',
|
|
509
|
+
semantics: "Слова одного сообщения, между ними не больше N других слов, в любом порядке; N до 50",
|
|
505
510
|
},
|
|
511
|
+
{ name: "fuzzy", example: "invoice~1", semantics: "unsupported_operator; формы слова ловит prefix `word*`" },
|
|
506
512
|
{ name: "boost / minimum / intervals", example: "invoice^2", semantics: "query.unsupported-operator" },
|
|
507
513
|
];
|
|
@@ -13,5 +13,13 @@ export declare const QUERY_LIMITS: {
|
|
|
13
13
|
readonly work: 10000000;
|
|
14
14
|
};
|
|
15
15
|
export declare const SEARCH_QUERY: QueryContext;
|
|
16
|
+
/** How message search parses typed text: `"a b"~N` is words at most N apart; notes still refuse `~`. */
|
|
17
|
+
export declare const MESSAGE_PARSING: {
|
|
18
|
+
command?: string;
|
|
19
|
+
guide?: string;
|
|
20
|
+
proximity: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** Further apart than this, two words are rarely about one thing, even in a long message. */
|
|
23
|
+
export declare const MOST_WORDS_APART = 50;
|
|
16
24
|
export declare function searchError(reason: string, span: Span, alternative?: string): never;
|
|
17
25
|
export declare function exhausted(budget: string): never;
|