@wirecat/cli-core 0.390.0 → 0.392.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 (76) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli/bot/command.js +33 -4
  3. package/dist/cli/bot/port.d.ts +35 -1
  4. package/dist/cli/bot/token.d.ts +8 -1
  5. package/dist/cli/bot/token.js +16 -3
  6. package/dist/cli/bot/watch.d.ts +2 -0
  7. package/dist/cli/bot/watch.js +54 -12
  8. package/dist/cli/bot/webhook-receiver.d.ts +22 -0
  9. package/dist/cli/bot/webhook-receiver.js +68 -0
  10. package/dist/cli/effects.d.ts +2 -0
  11. package/dist/cli/effects.js +8 -2
  12. package/dist/cli/index.d.ts +1 -1
  13. package/dist/cli/messenger/backfill-command.js +5 -1
  14. package/dist/cli/messenger/catch-up.d.ts +0 -19
  15. package/dist/cli/messenger/catch-up.js +0 -43
  16. package/dist/cli/messenger/context.js +6 -3
  17. package/dist/cli/messenger/conversations-command.js +4 -1
  18. package/dist/cli/messenger/download-command.js +4 -1
  19. package/dist/cli/messenger/inbox.js +6 -11
  20. package/dist/cli/messenger/lacks.d.ts +1 -0
  21. package/dist/cli/messenger/lacks.js +6 -1
  22. package/dist/cli/messenger/mcp-command.js +6 -28
  23. package/dist/cli/messenger/messages-command.js +17 -18
  24. package/dist/cli/messenger/messages-comments-command.js +4 -1
  25. package/dist/cli/messenger/messages-delete-command.js +4 -1
  26. package/dist/cli/messenger/messages-edit-command.js +4 -1
  27. package/dist/cli/messenger/messages-evidence-command.js +4 -1
  28. package/dist/cli/messenger/messages-forward-command.js +4 -1
  29. package/dist/cli/messenger/messages-link-command.js +4 -1
  30. package/dist/cli/messenger/messages-scheduled-command.js +4 -1
  31. package/dist/cli/messenger/messages-send-command.js +4 -1
  32. package/dist/cli/messenger/review.d.ts +1 -1
  33. package/dist/cli/messenger/review.js +5 -10
  34. package/dist/cli/messenger/tags-command.js +2 -2
  35. package/dist/cli/messenger/transcribe-command.js +4 -1
  36. package/dist/cli/settings.d.ts +0 -4
  37. package/dist/cli/settings.js +7 -5
  38. package/dist/domain/providers.js +2 -0
  39. package/dist/{mcp/http/node.d.ts → http/listen.d.ts} +3 -2
  40. package/dist/{mcp/http/node.js → http/listen.js} +5 -5
  41. package/dist/http/public-url.d.ts +10 -0
  42. package/dist/http/public-url.js +32 -0
  43. package/dist/mcp/http/serve.d.ts +1 -1
  44. package/dist/mcp/http/serve.js +1 -1
  45. package/dist/meetings/index.d.ts +2 -0
  46. package/dist/meetings/index.js +2 -0
  47. package/dist/meetings/links.d.ts +18 -0
  48. package/dist/meetings/links.js +1 -0
  49. package/dist/meetings/review.d.ts +45 -0
  50. package/dist/meetings/review.js +1 -0
  51. package/dist/search/lucene/registry.d.ts +5 -1
  52. package/dist/search/lucene/registry.js +10 -4
  53. package/dist/search/lucene/types.d.ts +8 -0
  54. package/dist/search/lucene/types.js +4 -0
  55. package/dist/search/question-plan.js +3 -3
  56. package/dist/services/excerpt.js +4 -1
  57. package/dist/services/index.d.ts +2 -0
  58. package/dist/services/index.js +2 -0
  59. package/dist/services/meeting-links.d.ts +33 -0
  60. package/dist/services/meeting-links.js +41 -0
  61. package/dist/services/meeting-review.d.ts +12 -0
  62. package/dist/services/meeting-review.js +8 -0
  63. package/dist/services/messages-search.js +2 -2
  64. package/dist/services/searches.js +3 -3
  65. package/dist/store/sqlite/lucene-fields.d.ts +10 -3
  66. package/dist/store/sqlite/lucene-fields.js +85 -13
  67. package/dist/store/sqlite/lucene.js +16 -11
  68. package/dist/store/sqlite/meeting-chat-lines.d.ts +3 -0
  69. package/dist/store/sqlite/meeting-chat-lines.js +34 -0
  70. package/dist/store/sqlite/meeting-review.d.ts +4 -0
  71. package/dist/store/sqlite/meeting-review.js +97 -0
  72. package/dist/store/sqlite/meetings.d.ts +2 -2
  73. package/dist/store/sqlite/meetings.js +4 -0
  74. package/dist/store/sqlite/mention-drain.js +3 -2
  75. package/package.json +1 -1
  76. package/parity.json +339 -118
@@ -17,7 +17,10 @@ import { nextStep } from "../steps.js";
17
17
  import { messengerContext, refuseLocalWrite } from "./context.js";
18
18
  import { stopOnSignal } from "./patience.js";
19
19
  /** `messages download`: every file of one message, or with `--all` of a whole chat, never over a file already there. */
20
- export const downloadSubcommand = (messages, messenger) => annotate(messages.command("download"), { effects: FETCH })
20
+ export const downloadSubcommand = (messages, messenger) => annotate(messages.command("download"), {
21
+ examples: [`${messenger.app.command} personal messages download "Book club" 101 --output-dir ./downloads`],
22
+ effects: FETCH,
23
+ })
21
24
  .description("save a message's photos, files, videos and voice notes to a folder — or a whole chat's with --all")
22
25
  .argument("<chat>", messenger.chatArgument)
23
26
  .argument("[message]", "the message id; left out with --all")
@@ -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,6 +7,7 @@ type Need = readonly [method: keyof MessengerAdapter, what: string];
7
7
  */
8
8
  export declare const COMMAND_NEEDS: Readonly<Record<string, Need>>;
9
9
  interface Lacking {
10
+ channelComments?: boolean;
10
11
  lacks?: readonly (keyof MessengerAdapter)[];
11
12
  history?: "server" | "store";
12
13
  }
@@ -19,6 +19,7 @@ export const COMMAND_NEEDS = {
19
19
  "contacts remove": ["removeContact", "remove a contact"],
20
20
  "contacts rename": ["renameContact", "rename a contact"],
21
21
  "contacts import": ["importContacts", "import contacts"],
22
+ "messages comments": ["comments", "read comments on a channel post"],
22
23
  "messages pin": ["pin", "pin a message"],
23
24
  "messages unpin": ["unpin", "unpin a message"],
24
25
  };
@@ -27,7 +28,11 @@ const STORE_ANSWERS = new Set(["chats events"]);
27
28
  /** The one method this command needs and this messenger never has; none when it has it or the store answers. */
28
29
  export const lackedNeed = (messenger, path) => {
29
30
  const need = COMMAND_NEEDS[path];
30
- if (!need || !messenger?.lacks?.includes(need[0]))
31
+ if (!need)
32
+ return undefined;
33
+ if (path === "messages comments" && messenger?.channelComments === false)
34
+ return need;
35
+ if (!messenger?.lacks?.includes(need[0]))
31
36
  return undefined;
32
37
  return messenger.history === "store" && STORE_ANSWERS.has(path) ? undefined : need;
33
38
  };
@@ -6,6 +6,7 @@ import { annotate } from "@wirecat/cli-base/commands";
6
6
  import { installerOf } from "@wirecat/cli-base/update";
7
7
  import { Command } from "commander";
8
8
  import { ATTACHMENT_LIMIT_ENV } from "../../attachments/limits.js";
9
+ import { portOf, publicUrlOf } from "../../http/public-url.js";
9
10
  import { envName } from "../../ports/app.js";
10
11
  import { environmentOf } from "../context.js";
11
12
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effects.js";
@@ -17,32 +18,6 @@ import { messengerContext } from "./context.js";
17
18
  const DEFAULT_PORT = 8765;
18
19
  /** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
19
20
  export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "mcp-http", `${profile}.json`);
20
- const publicUrlOf = (app, given) => {
21
- const example = `--public-url https://<name>.ts.net`;
22
- if (!given)
23
- throw new CliError("configuration_error", `--http needs the tunnel's address: ${example}`);
24
- let url;
25
- try {
26
- url = new URL(given);
27
- }
28
- catch {
29
- throw new CliError("validation_error", `--public-url is not an address: ${example}`);
30
- }
31
- const local = url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
32
- if (url.protocol !== "https:" && !local)
33
- throw new CliError("validation_error", `--public-url must be https — the browser apps reach ${app.command} through it`);
34
- if (url.pathname !== "/" || url.search || url.hash)
35
- throw new CliError("validation_error", `--public-url is the tunnel's address only, without a path: ${example}`);
36
- return url;
37
- };
38
- const portOf = (given) => {
39
- if (given === undefined)
40
- return DEFAULT_PORT;
41
- const port = Number(given);
42
- if (!Number.isInteger(port) || port < 1 || port > 65535)
43
- throw new CliError("validation_error", "--port takes 1–65535");
44
- return port;
45
- };
46
21
  const withFlags = (command) => command.option("--permission <key=level>", "make a permission stricter for this server only, like messages.send=deny; it never loosens the profile; repeat for more keys", (value, previous = []) => [...previous, value]);
47
22
  export const mcpCommand = (messenger) => {
48
23
  const { app } = messenger;
@@ -61,11 +36,14 @@ export const mcpCommand = (messenger) => {
61
36
  return;
62
37
  }
63
38
  if (flags.http) {
64
- const publicUrl = publicUrlOf(app, flags.publicUrl);
39
+ const publicUrl = publicUrlOf(flags.publicUrl, {
40
+ missing: "--http",
41
+ reaches: `the browser apps reach ${app.command}`,
42
+ });
65
43
  const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
66
44
  await serveOverHttpUntilStopped(this, context, messenger, {}, {
67
45
  publicUrl,
68
- port: portOf(flags.port),
46
+ port: portOf(flags.port, DEFAULT_PORT),
69
47
  tokenFile: httpTokenFile(app, context.settings.profile, context.env),
70
48
  });
71
49
  return;
@@ -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";
@@ -36,7 +36,10 @@ export const messagesCommand = (messenger) => {
36
36
  const messages = new Command("messages").description("read and send messages");
37
37
  messages.addCommand(evidenceCommand(messenger));
38
38
  const history = historyQuery(messenger);
39
- const list = annotate(messages.command("list"), { effects: REMOTE_READ })
39
+ const list = annotate(messages.command("list"), {
40
+ examples: [`${messenger.app.command} personal messages list "Book club" --limit 20`],
41
+ effects: REMOTE_READ,
42
+ })
40
43
  .description("a chat's messages, oldest to newest")
41
44
  .addHelpText("after", SAVE_HELP)
42
45
  .addHelpText("after", queryHelp(history))
@@ -48,29 +51,19 @@ export const messagesCommand = (messenger) => {
48
51
  .option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it")
49
52
  .option(...TRANSCRIBE_OPTION)
50
53
  .option(...MODEL_OPTION)
51
- .addOption(ownerOnlyOption("--mark-read", "also mark the chat read up to the newest message shown; the other person sees it"))
54
+ .addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
52
55
  .action(async function (chat, query) {
53
56
  const context = messengerContext(this, messenger);
54
- const { cursor, transcribe, model, markRead } = this.opts();
57
+ const { cursor, transcribe, model } = this.opts();
55
58
  const window = historyWindow(query, cursor, "messages list", history);
56
59
  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
60
  const { limit } = context.settings;
64
- const { page, hearing, marked, reader } = await context.withServices(async (services, connect) => {
61
+ const { page, hearing, reader } = await context.withServices(async (services, connect) => {
65
62
  const page = await services.messages.list(chat, { limit, ...window });
66
63
  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
64
  const hearing = await hearForCommand(context, messenger, page.items, transcribe === true, hearWith, connect);
70
- return { page, hearing, marked, reader };
65
+ return { page, hearing, reader };
71
66
  });
72
- if (marked)
73
- context.renderer.note(`marked read up to ${marked.until} (operation ${marked.operationId})`);
74
67
  const next = page.hasMore
75
68
  ? cursorOf(window, { first: page.items[0]?.id, last: page.items.at(-1)?.id })
76
69
  : undefined;
@@ -115,7 +108,10 @@ export const messagesCommand = (messenger) => {
115
108
  }), view);
116
109
  };
117
110
  messages.addCommand(sendCommand(messenger));
118
- annotate(messages.command("show"), { effects: REMOTE_READ })
111
+ annotate(messages.command("show"), {
112
+ examples: [`${messenger.app.command} personal messages show "Book club" 101`],
113
+ effects: REMOTE_READ,
114
+ })
119
115
  .description("one message, by its chat and id or by its msg: locator")
120
116
  .argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
121
117
  .argument("[message]", "the message id")
@@ -124,7 +120,10 @@ export const messagesCommand = (messenger) => {
124
120
  return await readMailWindow(this, chat, { before: 0, after: 0 });
125
121
  await readWindow(this, chat, message, { before: 0, after: 0 });
126
122
  });
127
- threadOptions(annotate(messages.command("context"), { effects: REMOTE_READ }))
123
+ threadOptions(annotate(messages.command("context"), {
124
+ examples: [`${messenger.app.command} personal messages context "Book club" 101 --before-n 3 --after-n 3`],
125
+ effects: REMOTE_READ,
126
+ }))
128
127
  .description("a message and what came either side of it, oldest first")
129
128
  .addHelpText("after", SAVE_HELP)
130
129
  .argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
@@ -10,7 +10,10 @@ import { messengerContext } from "./context.js";
10
10
  import { COMMENTS_QUERY, cursorOf, historyWindow } from "./history-query.js";
11
11
  import { messageItems, viewOf } from "./message-items.js";
12
12
  export const commentsCommand = (messenger) => {
13
- const command = annotate(new Command("comments"), { effects: REMOTE_READ })
13
+ const command = annotate(new Command("comments"), {
14
+ examples: [`${messenger.app.command} personal messages comments "News channel" 101 --limit 20`],
15
+ effects: REMOTE_READ,
16
+ })
14
17
  .description("the comments under a channel post, oldest to newest; they live in its discussion group")
15
18
  .addHelpText("after", queryHelp(COMMENTS_QUERY))
16
19
  .argument("<chat>", `the channel: ${messenger.chatArgument}`)
@@ -11,7 +11,10 @@ import { messengerContext } from "./context.js";
11
11
  * The guard asks first at the default level, `ask`; `--allow-dangerous` is the yes. For everyone only
12
12
  * with `--for-everyone` (max-cli `NEED-239`), and never for an agent (NEED-1423).
13
13
  */
14
- export const deleteCommand = (messenger) => annotate(new Command("delete"), { effects: REMOTE_WRITE })
14
+ export const deleteCommand = (messenger) => annotate(new Command("delete"), {
15
+ examples: [`${messenger.app.command} personal messages delete "Book club" 101`],
16
+ effects: REMOTE_WRITE,
17
+ })
15
18
  .description("delete messages for you only; with --for-everyone, for everyone in the chat")
16
19
  .argument("<chat>", messenger.chatArgument)
17
20
  .argument("<messages...>", `the message ids, by spaces or commas, at most ${DELETE_AT_ONCE}`)
@@ -8,7 +8,10 @@ import { afterWrite, writtenAnswer } from "./message-writes.js";
8
8
  import { HTML_HELP } from "./messages-send-command.js";
9
9
  import { readAll } from "./stdin.js";
10
10
  export const editCommand = (messenger) => {
11
- const edit = annotate(new Command("edit"), { effects: REMOTE_WRITE })
11
+ const edit = annotate(new Command("edit"), {
12
+ examples: [`${messenger.app.command} personal messages edit "Book club" 101 "The meeting starts at 11."`],
13
+ effects: REMOTE_WRITE,
14
+ })
12
15
  .description("change the text of your own message; the other side may have read it already")
13
16
  .argument("<chat>", messenger.chatArgument)
14
17
  .argument("<message>", "the id of your own message")
@@ -6,7 +6,10 @@ import { renderAnswer } from "../envelope.js";
6
6
  import { positiveCount } from "../paging.js";
7
7
  import { messengerContext } from "./context.js";
8
8
  import { evidenceAnswer } from "./message-answers.js";
9
- export const evidenceCommand = (messenger) => annotate(new Command("evidence"), { effects: LOCAL_READ })
9
+ export const evidenceCommand = (messenger) => annotate(new Command("evidence"), {
10
+ examples: [`${messenger.app.command} personal messages evidence "Book club" --limit 10`],
11
+ effects: LOCAL_READ,
12
+ })
10
13
  .description("a bounded evidence packet from stored messages, newest first")
11
14
  .argument("<chat>", messenger.chatArgument)
12
15
  .option("--limit <n>", "how many, 1–100", positiveCount("--limit"))
@@ -6,7 +6,10 @@ import { renderAnswer } from "../envelope.js";
6
6
  import { messengerContext } from "./context.js";
7
7
  import { afterWrite, writtenAnswer } from "./message-writes.js";
8
8
  export const forwardCommand = (messenger) => {
9
- const forward = annotate(new Command("forward"), { effects: REMOTE_WRITE })
9
+ const forward = annotate(new Command("forward"), {
10
+ examples: [`${messenger.app.command} personal messages forward "Book club" 101 --to "Project room"`],
11
+ effects: REMOTE_WRITE,
12
+ })
10
13
  .description("forward one message to another chat")
11
14
  .argument("<chat>", `the chat the message is in: ${messenger.chatArgument}`)
12
15
  .argument("<message>", "the message id")
@@ -4,7 +4,10 @@ import { REMOTE_READ } from "../effects.js";
4
4
  import { itemOf, renderAnswer } from "../envelope.js";
5
5
  import { messengerContext } from "./context.js";
6
6
  export const messageLinkCommand = (messenger) => {
7
- const command = annotate(new Command("link"), { effects: REMOTE_READ })
7
+ const command = annotate(new Command("link"), {
8
+ examples: [`${messenger.app.command} personal messages link "Book club" 101`],
9
+ effects: REMOTE_READ,
10
+ })
8
11
  .description("a message permalink when supported, and its account-scoped locator")
9
12
  .argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
10
13
  .argument("[message]", "the message id")
@@ -8,7 +8,10 @@ import { messengerContext } from "./context.js";
8
8
  import { messageItems, selfOf, viewOf } from "./message-items.js";
9
9
  /** Where to look after a scheduled send ended without an answer: repeating it would schedule a second one. */
10
10
  export const scheduledCommand = (messenger) => {
11
- const command = annotate(new Command("scheduled"), { effects: REMOTE_READ })
11
+ const command = annotate(new Command("scheduled"), {
12
+ examples: [`${messenger.app.command} personal messages scheduled "Book club"`],
13
+ effects: REMOTE_READ,
14
+ })
12
15
  .description("messages waiting to be sent later in a chat, soonest first; cancel one in the app")
13
16
  .argument("<chat>", messenger.chatArgument)
14
17
  .action(async function (chat) {
@@ -16,7 +16,10 @@ import { readAll } from "./stdin.js";
16
16
  */
17
17
  export const HTML_HELP = "the text is HTML: <b>, <i>, <a href>, <code>";
18
18
  export const sendCommand = (messenger) => {
19
- const send = annotate(new Command("send"), { effects: REMOTE_WRITE })
19
+ const send = annotate(new Command("send"), {
20
+ examples: [`${messenger.app.command} personal messages send "Book club" "The meeting starts at 10."`],
21
+ effects: REMOTE_WRITE,
22
+ })
20
23
  .description("send a text message; without [text], the text is read from stdin")
21
24
  .argument("<chat>", messenger.chatArgument)
22
25
  .argument("[text]", "the message");
@@ -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,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)
@@ -8,7 +8,10 @@ import { messengerContext } from "./context.js";
8
8
  import { heardAnswer } from "./message-answers.js";
9
9
  import { keepRefreshedRefs } from "./refreshed-refs.js";
10
10
  /** `messages transcribe`: a voice message as text, by the messenger or by a model on this machine. */
11
- export const transcribeSubcommand = (messages, messenger) => annotate(messages.command("transcribe"), { effects: REMOTE_READ })
11
+ export const transcribeSubcommand = (messages, messenger) => annotate(messages.command("transcribe"), {
12
+ examples: [`${messenger.app.command} personal messages transcribe "Book club" 101`],
13
+ effects: REMOTE_READ,
14
+ })
12
15
  .description(`a voice message as text — by ${messenger.name ?? messenger.app.command} where it can, else by a model on this machine`)
13
16
  .argument("<chat>", messenger.chatArgument)
14
17
  .argument("<message>", "the id of a voice message")
@@ -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,
@@ -6,6 +6,8 @@ export const PROVIDERS = [
6
6
  { name: "telegram-bot", kind: "messages", aliases: ["tg-bot"] },
7
7
  { name: "max", kind: "messages", aliases: [] },
8
8
  { name: "max-bot", kind: "messages", aliases: [] },
9
+ { name: "whatsapp", kind: "messages", aliases: ["wa"] },
10
+ { name: "whatsapp-bot", kind: "messages", aliases: ["wa-bot"] },
9
11
  { name: "email", kind: "mail", aliases: [] },
10
12
  { name: "notes", kind: "notes", aliases: [] },
11
13
  { name: "zoom", kind: "meetings", aliases: [] },
@@ -4,13 +4,14 @@ export interface Listening {
4
4
  url: URL;
5
5
  close(): Promise<void>;
6
6
  }
7
- /** Larger bodies are refused unread: the port faces the internet, and MCP requests are small JSON. */
7
+ /** Larger bodies are refused unread: the port faces the internet. MCP requests are small JSON. */
8
8
  export declare const MAX_BODY: number;
9
9
  /**
10
10
  * Binds `host` only — never every interface: the public address is the owner's tunnel.
11
11
  * `close` ends open streams too, so the process can exit.
12
12
  */
13
- export declare const listen: (handle: Handle, { host, port }: {
13
+ export declare const listen: (handle: Handle, { host, port, maxBody }: {
14
14
  host: string;
15
15
  port: number;
16
+ maxBody?: number;
16
17
  }) => Promise<Listening>;
@@ -1,6 +1,6 @@
1
1
  import { createServer } from "node:http";
2
2
  import { Readable } from "node:stream";
3
- /** Larger bodies are refused unread: the port faces the internet, and MCP requests are small JSON. */
3
+ /** Larger bodies are refused unread: the port faces the internet. MCP requests are small JSON. */
4
4
  export const MAX_BODY = 4 * 1024 * 1024;
5
5
  class TooLarge extends Error {
6
6
  }
@@ -21,7 +21,7 @@ const readBody = (incoming, limit) => new Promise((resolve, reject) => {
21
21
  incoming.once("end", () => resolve(Buffer.concat(chunks)));
22
22
  incoming.once("error", reject);
23
23
  });
24
- const toRequest = async (incoming, origin) => {
24
+ const toRequest = async (incoming, origin, maxBody) => {
25
25
  const headers = new Headers();
26
26
  for (const [name, value] of Object.entries(incoming.headers)) {
27
27
  if (Array.isArray(value))
@@ -31,7 +31,7 @@ const toRequest = async (incoming, origin) => {
31
31
  headers.set(name, value);
32
32
  }
33
33
  const method = incoming.method ?? "GET";
34
- const body = method === "GET" || method === "HEAD" ? undefined : await readBody(incoming, MAX_BODY);
34
+ const body = method === "GET" || method === "HEAD" ? undefined : await readBody(incoming, maxBody);
35
35
  if (body)
36
36
  headers.set("content-length", String(body.length));
37
37
  return new Request(new URL(incoming.url ?? "/", origin), { method, headers, ...(body ? { body } : {}) });
@@ -48,9 +48,9 @@ const send = async (response, outgoing) => {
48
48
  * Binds `host` only — never every interface: the public address is the owner's tunnel.
49
49
  * `close` ends open streams too, so the process can exit.
50
50
  */
51
- export const listen = (handle, { host, port }) => new Promise((resolve, reject) => {
51
+ export const listen = (handle, { host, port, maxBody = MAX_BODY }) => new Promise((resolve, reject) => {
52
52
  const server = createServer((incoming, outgoing) => {
53
- toRequest(incoming, `http://${host}`)
53
+ toRequest(incoming, `http://${host}`, maxBody)
54
54
  .then(handle)
55
55
  .then((response) => send(response, outgoing))
56
56
  .catch((error) => {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The tunnel's address a public listener is reached at, checked one way for every command that opens
3
+ * one. `missing` says what needs it; `reaches` who calls through it, for the https refusal.
4
+ */
5
+ export declare const publicUrlOf: (given: string | undefined, { missing, reaches }: {
6
+ missing: string;
7
+ reaches: string;
8
+ }) => URL;
9
+ /** A local port from `--port`, or the command's own default. */
10
+ export declare const portOf: (given: string | undefined, fallback: number) => number;