@wirecat/cli-core 0.391.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 +29 -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/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/store-maintenance-command.d.ts +1 -1
- 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 +1 -1
- package/parity.json +339 -107
|
@@ -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");
|
|
@@ -2,9 +2,9 @@ import { Command } from "commander";
|
|
|
2
2
|
import type { Messenger } from "./context.js";
|
|
3
3
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
4
4
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
5
|
+
error?: undefined;
|
|
5
6
|
path: string;
|
|
6
7
|
exists: boolean;
|
|
7
|
-
error?: undefined;
|
|
8
8
|
} | {
|
|
9
9
|
path: string;
|
|
10
10
|
exists: boolean;
|
|
@@ -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.392.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",
|