@wirecat/cli-core 0.391.0 → 0.393.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 +41 -1
- 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/index.d.ts +1 -1
- package/dist/cli/messenger/backfill-command.js +5 -1
- package/dist/cli/messenger/chats-command.js +50 -0
- package/dist/cli/messenger/contacts-command.js +31 -1
- package/dist/cli/messenger/conversations-command.js +4 -1
- package/dist/cli/messenger/download-command.js +4 -1
- 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 +12 -3
- 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/tags-command.js +14 -4
- package/dist/cli/messenger/transcribe-command.js +4 -1
- 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/store/sqlite/lucene-fields.d.ts +7 -2
- package/dist/store/sqlite/lucene-fields.js +7 -2
- package/dist/store/sqlite/mention-drain.js +3 -2
- package/package.json +3 -3
- package/parity.json +339 -107
|
@@ -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;
|
|
@@ -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))
|
|
@@ -105,7 +108,10 @@ export const messagesCommand = (messenger) => {
|
|
|
105
108
|
}), view);
|
|
106
109
|
};
|
|
107
110
|
messages.addCommand(sendCommand(messenger));
|
|
108
|
-
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
|
+
})
|
|
109
115
|
.description("one message, by its chat and id or by its msg: locator")
|
|
110
116
|
.argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
|
|
111
117
|
.argument("[message]", "the message id")
|
|
@@ -114,7 +120,10 @@ export const messagesCommand = (messenger) => {
|
|
|
114
120
|
return await readMailWindow(this, chat, { before: 0, after: 0 });
|
|
115
121
|
await readWindow(this, chat, message, { before: 0, after: 0 });
|
|
116
122
|
});
|
|
117
|
-
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
|
+
}))
|
|
118
127
|
.description("a message and what came either side of it, oldest first")
|
|
119
128
|
.addHelpText("after", SAVE_HELP)
|
|
120
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");
|
|
@@ -28,8 +28,9 @@ const sourceOf = (value) => {
|
|
|
28
28
|
};
|
|
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
|
+
const tool = `${messenger.app.command} personal`;
|
|
31
32
|
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
|
-
changesStore(annotate(tags.command("auto"), { effects: FETCH }))
|
|
33
|
+
changesStore(annotate(tags.command("auto"), { effects: FETCH, examples: [`${tool} tags auto --chat "Book club" --dry-run`] }))
|
|
33
34
|
.description("derive local group/channel tags from cached metadata using keyword rules")
|
|
34
35
|
.option("--chat <chat>", "a stored group/channel; repeat to select several", (value, previous) => [...previous, value], [])
|
|
35
36
|
.option("--limit <number>", "process at most 1–500 chats", positiveCount("--limit"), 50)
|
|
@@ -43,7 +44,10 @@ export const tagsCommand = (messenger) => {
|
|
|
43
44
|
const done = await context.withServices((services) => services.metadata.auto({ chats: chat, limit: Number(limit), refresh: syncFirst, dryRun }));
|
|
44
45
|
renderAnswer(this, context, itemOf(snakeKeys(done)));
|
|
45
46
|
});
|
|
46
|
-
targetOptions(annotate(tags.command("add"), {
|
|
47
|
+
targetOptions(annotate(tags.command("add"), {
|
|
48
|
+
effects: LOCAL_WRITE,
|
|
49
|
+
examples: [`${tool} tags add reading --chat "Book club"`],
|
|
50
|
+
}).description("put tags on one chat, person, message or task"), messenger, "tag").action(async function (given) {
|
|
47
51
|
const context = messengerContext(this, messenger);
|
|
48
52
|
refuseLocalWrite(context, messenger.app.command, "tags.add");
|
|
49
53
|
const target = this.opts();
|
|
@@ -52,7 +56,10 @@ export const tagsCommand = (messenger) => {
|
|
|
52
56
|
if (done.unchanged.length && context.format === "pretty")
|
|
53
57
|
context.renderer.note(`already there: ${done.unchanged.join(", ")}`);
|
|
54
58
|
});
|
|
55
|
-
targetOptions(annotate(tags.command("remove"), {
|
|
59
|
+
targetOptions(annotate(tags.command("remove"), {
|
|
60
|
+
effects: LOCAL_WRITE,
|
|
61
|
+
examples: [`${tool} tags remove reading --chat "Book club"`],
|
|
62
|
+
}).description("take tags off one chat, person, message or task"), messenger, "untag")
|
|
56
63
|
.option("--source <manual|auto>", "remove only this ownership claim", sourceOf)
|
|
57
64
|
.action(async function (given) {
|
|
58
65
|
const context = messengerContext(this, messenger);
|
|
@@ -63,7 +70,10 @@ export const tagsCommand = (messenger) => {
|
|
|
63
70
|
if (done.unchanged.length && context.format === "pretty")
|
|
64
71
|
context.renderer.note(`not there: ${done.unchanged.join(", ")}`);
|
|
65
72
|
});
|
|
66
|
-
withQuery(annotate(tags.command("list"), {
|
|
73
|
+
withQuery(annotate(tags.command("list"), {
|
|
74
|
+
effects: LOCAL_READ,
|
|
75
|
+
examples: [`${tool} tags list`, `${tool} tags list tag:reading`],
|
|
76
|
+
}).description("what is tagged: this account's chats, messages and tasks, and the people of its messenger"), TAGGING_QUERY).action(async function (words) {
|
|
67
77
|
const context = messengerContext(this, messenger);
|
|
68
78
|
const query = listQuery(typedQuery(words), TAGGING_QUERY, "tags list");
|
|
69
79
|
const found = await context.withServices((services) => services.tags.list({ ...(query ? { query } : {}), ...pageOf(context.settings) }));
|
|
@@ -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/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;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
const EXAMPLE = "--public-url https://<name>.ts.net";
|
|
3
|
+
/**
|
|
4
|
+
* The tunnel's address a public listener is reached at, checked one way for every command that opens
|
|
5
|
+
* one. `missing` says what needs it; `reaches` who calls through it, for the https refusal.
|
|
6
|
+
*/
|
|
7
|
+
export const publicUrlOf = (given, { missing, reaches }) => {
|
|
8
|
+
if (!given)
|
|
9
|
+
throw new CliError("configuration_error", `${missing} needs the tunnel's address: ${EXAMPLE}`);
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(given);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new CliError("validation_error", `--public-url is not an address: ${EXAMPLE}`);
|
|
16
|
+
}
|
|
17
|
+
const local = url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
|
|
18
|
+
if (url.protocol !== "https:" && !local)
|
|
19
|
+
throw new CliError("validation_error", `--public-url must be https — ${reaches} through it`);
|
|
20
|
+
if (url.pathname !== "/" || url.search || url.hash)
|
|
21
|
+
throw new CliError("validation_error", `--public-url is the tunnel's address only, without a path: ${EXAMPLE}`);
|
|
22
|
+
return url;
|
|
23
|
+
};
|
|
24
|
+
/** A local port from `--port`, or the command's own default. */
|
|
25
|
+
export const portOf = (given, fallback) => {
|
|
26
|
+
if (given === undefined)
|
|
27
|
+
return fallback;
|
|
28
|
+
const port = Number(given);
|
|
29
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
30
|
+
throw new CliError("validation_error", "--port takes 1–65535");
|
|
31
|
+
return port;
|
|
32
|
+
};
|
package/dist/mcp/http/serve.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type McpServer } from "@modelcontextprotocol/server";
|
|
2
|
-
import { type Listening } from "
|
|
2
|
+
import { type Listening } from "../../http/listen.js";
|
|
3
3
|
export declare const MCP_PATH = "/mcp";
|
|
4
4
|
export interface HttpOptions {
|
|
5
5
|
/** The tunnel's address, as the browser apps reach it — `https://name.ts.net`. */
|
package/dist/mcp/http/serve.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createMcpHandler, getOAuthProtectedResourceMetadataUrl, hostHeaderValidationResponse, isLegacyRequest, oauthMetadataResponse, requireBearerAuth, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
|
|
3
|
-
import { listen } from "
|
|
3
|
+
import { listen } from "../../http/listen.js";
|
|
4
4
|
import { ownerLogin } from "./oauth.js";
|
|
5
5
|
export const MCP_PATH = "/mcp";
|
|
6
6
|
const LOCAL = ["localhost", "127.0.0.1"];
|
|
@@ -29,8 +29,13 @@ export interface Corpus {
|
|
|
29
29
|
item: string;
|
|
30
30
|
thread: string;
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A voice note's transcript is its attachment's text, whoever wrote it: speech recognition (`speech:<model>`) or an
|
|
34
|
+
* agent, which writes the text of a voice attachment as `agent`. The voice attachment is the one
|
|
35
|
+
* `intoVoiceAttachment` picks.
|
|
36
|
+
*/
|
|
37
|
+
export declare const speech: (alias: string) => string;
|
|
38
|
+
export declare const SPEECH: string;
|
|
34
39
|
export declare const quoted: (value: string) => string;
|
|
35
40
|
/** A leaf's words as an FTS5 match: a phrase, or with `~N` its words at most N apart, in any order. */
|
|
36
41
|
export declare const ftsMatch: (node: ResolvedPredicate) => string;
|
|
@@ -12,8 +12,13 @@ import { STEM_SEPARATOR } from "../../search/stem.js";
|
|
|
12
12
|
import { normalize } from "../normalize.js";
|
|
13
13
|
import { linkMatch } from "./mention-links.js";
|
|
14
14
|
import { valueMatch } from "./phone-countries.js";
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* A voice note's transcript is its attachment's text, whoever wrote it: speech recognition (`speech:<model>`) or an
|
|
17
|
+
* agent, which writes the text of a voice attachment as `agent`. The voice attachment is the one
|
|
18
|
+
* `intoVoiceAttachment` picks.
|
|
19
|
+
*/
|
|
20
|
+
export const speech = (alias) => `(coalesce(${alias}.extractor,'') LIKE 'speech:%' OR ${alias}.kind IN ('voice','video_note') OR coalesce(${alias}.mime,'')='audio/ogg')`;
|
|
21
|
+
export const SPEECH = speech("att");
|
|
17
22
|
export const quoted = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
18
23
|
/** A leaf's words as an FTS5 match: a phrase, or with `~N` its words at most N apart, in any order. */
|
|
19
24
|
export const ftsMatch = (node) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findMentions, MENTION_KINDS, MENTION_VERSIONS, oneTrackedLinkPerSite, telegramTarget, } from "../../domain/mentions.js";
|
|
2
2
|
import { findNames, findUnknownNames, nameIndex } from "../../domain/person-names.js";
|
|
3
3
|
import { createStemmer } from "../../search/stem.js";
|
|
4
|
+
import { speech } from "./lucene-fields.js";
|
|
4
5
|
import { storePhoneCountries } from "./phone-countries.js";
|
|
5
6
|
import { placeOf } from "./place-key.js";
|
|
6
7
|
/** Mentions a store write finds before it commits: an older binary may have queued thousands. */
|
|
@@ -98,10 +99,10 @@ export const SOURCES = {
|
|
|
98
99
|
attachment: {
|
|
99
100
|
count: "SELECT count(*) AS n FROM attachments a JOIN messages m ON m.id = a.attachable_id " +
|
|
100
101
|
"WHERE a.attachable_type = 'message' AND a.text IS NOT NULL AND a.text <> '' AND m.deleted_at IS NULL",
|
|
101
|
-
page:
|
|
102
|
+
page: `SELECT a.id, a.text, CASE WHEN ${speech("a")} THEN 'transcript' ELSE 'file_text' END AS field, ` +
|
|
102
103
|
"m.chat_id FROM attachments a JOIN messages m ON m.id = a.attachable_id WHERE a.id > ? AND a.attachable_type = 'message' " +
|
|
103
104
|
"AND a.text IS NOT NULL AND a.text <> '' AND m.deleted_at IS NULL ORDER BY a.id LIMIT ?",
|
|
104
|
-
one:
|
|
105
|
+
one: `SELECT a.text, CASE WHEN ${speech("a")} THEN 'transcript' ELSE 'file_text' END AS field, ` +
|
|
105
106
|
"m.chat_id FROM attachments a JOIN messages m ON m.id = a.attachable_id WHERE a.id = ? AND a.attachable_type = 'message' " +
|
|
106
107
|
"AND a.text IS NOT NULL AND m.deleted_at IS NULL",
|
|
107
108
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.393.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"@types/node": "^22.20.4",
|
|
208
208
|
"@types/pngjs": "6.0.5",
|
|
209
209
|
"@vitest/coverage-v8": "^5.0.2",
|
|
210
|
-
"@wirecat/cli-base": "0.
|
|
210
|
+
"@wirecat/cli-base": "0.50.0",
|
|
211
211
|
"commander": "15.0.0",
|
|
212
212
|
"cspell": "10.3.6",
|
|
213
213
|
"drizzle-kit": "1.0.0-rc.4",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
},
|
|
228
228
|
"peerDependencies": {
|
|
229
229
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
230
|
-
"@wirecat/cli-base": ">=0.49.0 <0.
|
|
230
|
+
"@wirecat/cli-base": ">=0.49.0 <0.51.0",
|
|
231
231
|
"commander": ">=15"
|
|
232
232
|
},
|
|
233
233
|
"bin": {
|