@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/cli/effects.d.ts +2 -0
  3. package/dist/cli/effects.js +8 -2
  4. package/dist/cli/messenger/catch-up.d.ts +0 -19
  5. package/dist/cli/messenger/catch-up.js +0 -43
  6. package/dist/cli/messenger/context.js +6 -3
  7. package/dist/cli/messenger/inbox.js +6 -11
  8. package/dist/cli/messenger/messages-command.js +5 -15
  9. package/dist/cli/messenger/review.d.ts +1 -1
  10. package/dist/cli/messenger/review.js +5 -10
  11. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  12. package/dist/cli/messenger/tags-command.js +2 -2
  13. package/dist/cli/settings.d.ts +0 -4
  14. package/dist/cli/settings.js +7 -5
  15. package/dist/meetings/index.d.ts +2 -0
  16. package/dist/meetings/index.js +2 -0
  17. package/dist/meetings/links.d.ts +18 -0
  18. package/dist/meetings/links.js +1 -0
  19. package/dist/meetings/review.d.ts +45 -0
  20. package/dist/meetings/review.js +1 -0
  21. package/dist/search/lucene/registry.d.ts +5 -1
  22. package/dist/search/lucene/registry.js +10 -4
  23. package/dist/search/lucene/types.d.ts +8 -0
  24. package/dist/search/lucene/types.js +4 -0
  25. package/dist/search/question-plan.js +3 -3
  26. package/dist/services/excerpt.js +4 -1
  27. package/dist/services/index.d.ts +2 -0
  28. package/dist/services/index.js +2 -0
  29. package/dist/services/meeting-links.d.ts +33 -0
  30. package/dist/services/meeting-links.js +41 -0
  31. package/dist/services/meeting-review.d.ts +12 -0
  32. package/dist/services/meeting-review.js +8 -0
  33. package/dist/services/messages-search.js +2 -2
  34. package/dist/services/searches.js +3 -3
  35. package/dist/store/sqlite/lucene-fields.d.ts +3 -1
  36. package/dist/store/sqlite/lucene-fields.js +78 -11
  37. package/dist/store/sqlite/lucene.js +16 -11
  38. package/dist/store/sqlite/meeting-chat-lines.d.ts +3 -0
  39. package/dist/store/sqlite/meeting-chat-lines.js +34 -0
  40. package/dist/store/sqlite/meeting-review.d.ts +4 -0
  41. package/dist/store/sqlite/meeting-review.js +97 -0
  42. package/dist/store/sqlite/meetings.d.ts +2 -2
  43. package/dist/store/sqlite/meetings.js +4 -0
  44. package/package.json +1 -1
  45. 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
@@ -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"`)
@@ -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.local === "write" &&
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
- base.renderer.warn(`permissions.${key} names no command and does nothing — \`config unset permissions.${key}\``);
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.offline);
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 { ownerOnlyOption, REMOTE_READ } from "../effects.js";
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 { MARK_READ_OPTION, markShown, marksRead, NO_MARK_READ_OPTION, pointsOf } from "./catch-up.js";
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
- .addOption(ownerOnlyOption(...MARK_READ_OPTION))
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, markRead, } = this.opts();
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 marking = marksRead(markRead, settings, this);
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
- const marked = marking ? await markShown(services, inbox.chats, renderer.warn) : undefined;
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 { ownerOnlyOption, REMOTE_READ } from "../effects.js";
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
- .addOption(ownerOnlyOption("--mark-read", "also mark the chat read up to the newest message shown; the other person sees it"))
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, markRead } = this.opts();
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, marked, reader } = await context.withServices(async (services, connect) => {
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, marked, reader };
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
- * Nothing is marked read unless asked, and `inbox --new` keeps its point. The rules open and close
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 { ownerOnlyOption, REMOTE_READ } from "../effects.js";
9
+ import { REMOTE_READ } from "../effects.js";
10
10
  import { renderAnswer } from "../envelope.js";
11
11
  import { queryHelp } from "../list-query.js";
12
- import { MARK_READ_OPTION, markShown, marksRead, NO_MARK_READ_OPTION, pointsOf } from "./catch-up.js";
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
- * Nothing is marked read unless asked, and `inbox --new` keeps its point. The rules open and close
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
- .addOption(ownerOnlyOption(...MARK_READ_OPTION))
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, marked, reader } = await context.withServices(async (services, connect) => {
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: LOCAL_WRITE })
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)
@@ -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">, {}>;
@@ -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
- throw new CliError("configuration_error", error instanceof Error ? error.message : String(error));
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,
@@ -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";
@@ -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: "fuzzy / proximity";
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: "fuzzy / proximity",
503
- example: "invoice~1",
504
- semantics: "unsupported_operator; формы слова ловит prefix `word*`",
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;