@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.
- package/CHANGELOG.md +60 -0
- package/dist/cli/bot/command.js +33 -4
- package/dist/cli/bot/port.d.ts +35 -1
- package/dist/cli/bot/token.d.ts +8 -1
- package/dist/cli/bot/token.js +16 -3
- package/dist/cli/bot/watch.d.ts +2 -0
- package/dist/cli/bot/watch.js +54 -12
- package/dist/cli/bot/webhook-receiver.d.ts +22 -0
- package/dist/cli/bot/webhook-receiver.js +68 -0
- package/dist/cli/effects.d.ts +2 -0
- package/dist/cli/effects.js +8 -2
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/messenger/backfill-command.js +5 -1
- 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/conversations-command.js +4 -1
- package/dist/cli/messenger/download-command.js +4 -1
- package/dist/cli/messenger/inbox.js +6 -11
- package/dist/cli/messenger/lacks.d.ts +1 -0
- package/dist/cli/messenger/lacks.js +6 -1
- package/dist/cli/messenger/mcp-command.js +6 -28
- package/dist/cli/messenger/messages-command.js +17 -18
- package/dist/cli/messenger/messages-comments-command.js +4 -1
- package/dist/cli/messenger/messages-delete-command.js +4 -1
- package/dist/cli/messenger/messages-edit-command.js +4 -1
- package/dist/cli/messenger/messages-evidence-command.js +4 -1
- package/dist/cli/messenger/messages-forward-command.js +4 -1
- package/dist/cli/messenger/messages-link-command.js +4 -1
- package/dist/cli/messenger/messages-scheduled-command.js +4 -1
- package/dist/cli/messenger/messages-send-command.js +4 -1
- package/dist/cli/messenger/review.d.ts +1 -1
- package/dist/cli/messenger/review.js +5 -10
- package/dist/cli/messenger/tags-command.js +2 -2
- package/dist/cli/messenger/transcribe-command.js +4 -1
- package/dist/cli/settings.d.ts +0 -4
- package/dist/cli/settings.js +7 -5
- package/dist/domain/providers.js +2 -0
- package/dist/{mcp/http/node.d.ts → http/listen.d.ts} +3 -2
- package/dist/{mcp/http/node.js → http/listen.js} +5 -5
- package/dist/http/public-url.d.ts +10 -0
- package/dist/http/public-url.js +32 -0
- package/dist/mcp/http/serve.d.ts +1 -1
- package/dist/mcp/http/serve.js +1 -1
- 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 +10 -3
- package/dist/store/sqlite/lucene-fields.js +85 -13
- 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/dist/store/sqlite/mention-drain.js +3 -2
- package/package.json +1 -1
- 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"), {
|
|
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 {
|
|
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,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
|
|
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(
|
|
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 {
|
|
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"), {
|
|
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
|
-
.
|
|
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
|
|
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,
|
|
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,
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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"), {
|
|
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
|
-
*
|
|
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,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)
|
|
@@ -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"), {
|
|
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")
|
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/domain/providers.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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;
|