@wirecat/cli-core 0.396.0 โ 0.397.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 +33 -1
- package/README.md +1 -0
- package/dist/background/daily-archive-timer.d.ts +40 -0
- package/dist/background/daily-archive-timer.js +181 -0
- package/dist/background/index.d.ts +2 -0
- package/dist/background/index.js +1 -0
- package/dist/cli/messenger/account-command.js +1 -0
- package/dist/cli/messenger/account-sessions-command.js +3 -1
- package/dist/cli/messenger/admin-commands.js +1 -0
- package/dist/cli/messenger/admin-contacts-command.js +9 -3
- package/dist/cli/messenger/archive-commands.js +3 -1
- package/dist/cli/messenger/attachments-command.js +17 -7
- package/dist/cli/messenger/backfill-command.js +4 -0
- package/dist/cli/messenger/chats-command.js +1 -0
- package/dist/cli/messenger/chats-members-command.js +4 -0
- package/dist/cli/messenger/chats-stats-command.js +1 -0
- package/dist/cli/messenger/chats-tracking-command.js +4 -1
- package/dist/cli/messenger/complete-command.js +1 -1
- package/dist/cli/messenger/contacts-command.js +9 -3
- package/dist/cli/messenger/conversations-command.js +18 -1
- package/dist/cli/messenger/counters-command.js +1 -0
- package/dist/cli/messenger/download-command.js +1 -0
- package/dist/cli/messenger/flood-command.js +1 -0
- package/dist/cli/messenger/guard-commands.js +5 -4
- package/dist/cli/messenger/inbox.js +1 -0
- package/dist/cli/messenger/mcp-command.js +6 -2
- package/dist/cli/messenger/media-archive-command.d.ts +3 -0
- package/dist/cli/messenger/media-archive-command.js +72 -0
- package/dist/cli/messenger/media-archive-move-command.d.ts +3 -0
- package/dist/cli/messenger/media-archive-move-command.js +40 -0
- package/dist/cli/messenger/media-archive-timer-command.d.ts +3 -0
- package/dist/cli/messenger/media-archive-timer-command.js +70 -0
- package/dist/cli/messenger/messages-comments-command.js +1 -0
- package/dist/cli/messenger/messages-delete-command.js +1 -1
- package/dist/cli/messenger/messages-search-command.js +1 -0
- package/dist/cli/messenger/messages-stats-command.js +1 -0
- package/dist/cli/messenger/models-command.js +3 -0
- package/dist/cli/messenger/private-people-command.js +12 -10
- package/dist/cli/messenger/rankings-command.js +2 -0
- package/dist/cli/messenger/reactions-command.js +8 -2
- package/dist/cli/messenger/retention-command.js +1 -0
- package/dist/cli/messenger/review.js +1 -0
- package/dist/cli/messenger/search-command.js +5 -1
- package/dist/cli/messenger/searches-command.js +24 -7
- package/dist/cli/messenger/server-command.js +7 -2
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/store-maintenance-command.js +7 -0
- package/dist/cli/messenger/tags-command.js +6 -2
- package/dist/cli/messenger/tasks-command.js +10 -7
- package/dist/cli/messenger/transcribe-command.js +1 -0
- package/dist/cli/messenger/watch-command.js +1 -0
- package/dist/entities/contact-read.d.ts +119 -0
- package/dist/entities/contact-read.js +50 -0
- package/dist/entities/index.d.ts +120 -1
- package/dist/entities/index.js +5 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/parity/catalogue.js +1 -1
- package/dist/parity/manifest.d.ts +3 -1
- package/dist/parity/manifest.js +2 -0
- package/dist/parity/wording.js +2 -0
- package/dist/sends/permissions.js +7 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +2 -0
- package/dist/services/media-archive-files.d.ts +10 -0
- package/dist/services/media-archive-files.js +110 -0
- package/dist/services/media-archive-locks.d.ts +6 -0
- package/dist/services/media-archive-locks.js +71 -0
- package/dist/services/media-archive-manifest.d.ts +29 -0
- package/dist/services/media-archive-manifest.js +116 -0
- package/dist/services/media-archive-move.d.ts +21 -0
- package/dist/services/media-archive-move.js +163 -0
- package/dist/services/media-archive.d.ts +34 -0
- package/dist/services/media-archive.js +165 -0
- package/package.json +1 -1
- package/parity.json +50 -1
|
@@ -30,8 +30,11 @@ import { readAll } from "./stdin.js";
|
|
|
30
30
|
* mentions, one sender's messages in a row. From the store alone; nothing is built until asked (phase 3).
|
|
31
31
|
*/
|
|
32
32
|
export const conversationsCommand = (messenger) => {
|
|
33
|
-
const conversations = new Command("conversations")
|
|
33
|
+
const conversations = new Command("conversations")
|
|
34
|
+
.summary("group stored messages into conversations")
|
|
35
|
+
.description("the conversations inside a chat, found in the stored messages by replies, mentions and who wrote next");
|
|
34
36
|
boundOptions(reachesOtherService(annotate(conversations.command("build"), { effects: REMOTE_WRITE }))
|
|
37
|
+
.summary("build conversation links from stored messages")
|
|
35
38
|
.description("find a chat's conversations in what the store holds, replacing the last build; without --chat, every " +
|
|
36
39
|
"chat that changed since its build and every group never built; never asks the messenger")
|
|
37
40
|
.option("--chat <chat>", messenger.chatArgument)
|
|
@@ -111,6 +114,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
111
114
|
renderAnswer(this, context, itemOf(snakeKeys(built)), () => `${built.messages} messages โ ${built.conversations} conversations, ${built.links} links (rules v${built.rulesVersion})\n`);
|
|
112
115
|
});
|
|
113
116
|
withQuery(annotate(conversations.command("list"), { effects: LOCAL_READ })
|
|
117
|
+
.summary("stored conversations in one chat")
|
|
114
118
|
.description("a chat's conversations, the newest first: when, how many messages, how many people")
|
|
115
119
|
.argument("<chat>", messenger.chatArgument), CONVERSATION_QUERY)
|
|
116
120
|
.option("--limit <n>", "how many", positiveCount("--limit"))
|
|
@@ -123,6 +127,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
123
127
|
context.renderer.note("no conversations match");
|
|
124
128
|
});
|
|
125
129
|
annotate(conversations.command("show"), { effects: LOCAL_READ })
|
|
130
|
+
.summary("one conversation and its messages")
|
|
126
131
|
.description("one conversation's messages, oldest first โ by its id, or the one a message is in")
|
|
127
132
|
.argument("<conversation>", `a conversation id from \`conversations list\`; or ${messenger.chatArgument}, with a message`)
|
|
128
133
|
.argument("[message]", "a message id in that chat: show the conversation it is in")
|
|
@@ -151,6 +156,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
151
156
|
renderAnswer(this, context, conversationShown(account, { summary, messages }));
|
|
152
157
|
});
|
|
153
158
|
withModelOptions(annotate(conversations.command("related"), { effects: LOCAL_READ })
|
|
159
|
+
.summary("conversations nearest in meaning to a message's conversation")
|
|
154
160
|
.description("the conversations nearest in meaning to the one a message is in, in every built chat, best first โ " +
|
|
155
161
|
"from the vectors `conversations embed` stored; runs no model")
|
|
156
162
|
.argument("<chat>", messenger.chatArgument)
|
|
@@ -166,6 +172,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
166
172
|
context.renderer.note(`no other conversation has vectors of ${found.model}`);
|
|
167
173
|
});
|
|
168
174
|
withModelOptions(annotate(conversations.command("status"), { effects: LOCAL_READ })
|
|
175
|
+
.summary("which chats are ready for conversation search")
|
|
169
176
|
.description("how fresh each built chat's conversations and vectors are: messages the build has not seen, chunks " +
|
|
170
177
|
"with a current, stale or missing vector")
|
|
171
178
|
.option("--chat <chat>", `only this chat: ${messenger.chatArgument}`)).action(async function () {
|
|
@@ -179,9 +186,11 @@ export const conversationsCommand = (messenger) => {
|
|
|
179
186
|
});
|
|
180
187
|
const batches = conversations
|
|
181
188
|
.command("batches")
|
|
189
|
+
.summary("batches waiting for an agent's conversation links")
|
|
182
190
|
.description("windows of a chat for your own AI agent to link: which earlier message each one answers");
|
|
183
191
|
const sizeOption = (command) => command.option("--size <n>", `messages to answer per batch, ${BATCH_SIZE.min}โ${BATCH_SIZE.max}; ${BATCH_SIZE.default} by default`, positiveCount("--size"));
|
|
184
192
|
sizeOption(annotate(batches.command("status"), { effects: LOCAL_READ })
|
|
193
|
+
.summary("progress of conversation-link batches")
|
|
185
194
|
.description("how many messages still wait for an answer, in how many batches, and how much text")
|
|
186
195
|
.requiredOption("--chat <chat>", messenger.chatArgument)).action(async function () {
|
|
187
196
|
const { chat, size = BATCH_SIZE.default } = this.opts();
|
|
@@ -191,6 +200,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
191
200
|
`${status.characters} characters (about ${status.tokensEstimate} tokens)\n`);
|
|
192
201
|
});
|
|
193
202
|
sizeOption(annotate(batches.command("next"), { effects: LOCAL_READ })
|
|
203
|
+
.summary("get the next batch of messages to link")
|
|
194
204
|
.description("the next window to answer, with the messages before it; message text goes to stdout only")
|
|
195
205
|
.requiredOption("--chat <chat>", messenger.chatArgument)).action(async function () {
|
|
196
206
|
const { chat, size = BATCH_SIZE.default } = this.opts();
|
|
@@ -221,6 +231,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
221
231
|
description: '{ "model", "skill"?, "answers": [{ "message", "parent", "confidence" }] }',
|
|
222
232
|
json: true,
|
|
223
233
|
})
|
|
234
|
+
.summary("save an agent's conversation-link answers")
|
|
224
235
|
.description('store your agent\'s answer to a batch, read as JSON from stdin: { "model", "answers": [{ "message", ' +
|
|
225
236
|
'"parent", "confidence" }] }; all or nothing')
|
|
226
237
|
.requiredOption("--batch <id>", "the batch id `conversations batches next` printed")
|
|
@@ -233,6 +244,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
233
244
|
renderAnswer(this, context, itemOf(stored, { next: [nextStep("conversations.build", { args: { chat: stored.chat } })] }), () => `${stored.stored} answers stored\n`);
|
|
234
245
|
});
|
|
235
246
|
annotate(links.command("clear"), { effects: LOCAL_WRITE })
|
|
247
|
+
.summary("remove an agent's saved conversation-link answers")
|
|
236
248
|
.description("drop your agent's answers for a chat, or only one model's; messages are never touched")
|
|
237
249
|
.requiredOption("--chat <chat>", messenger.chatArgument)
|
|
238
250
|
.option("--model <model>", "only the answers this model gave")
|
|
@@ -244,6 +256,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
244
256
|
renderAnswer(this, context, itemOf(cleared), () => `${cleared.cleared} answers dropped\n`);
|
|
245
257
|
});
|
|
246
258
|
const embed = withModelOptions(reachesOtherService(annotate(new Command("embed"), { effects: REMOTE_WRITE }))
|
|
259
|
+
.summary("prepare conversation vectors for meaning search")
|
|
247
260
|
.description("compute a vector for each chunk of a chat's conversations for search by meaning โ on this machine, or " +
|
|
248
261
|
"with --provider through a service and your key; resumes where it stopped; without --chat, every " +
|
|
249
262
|
"built chat with chunks left, on this machine only")
|
|
@@ -290,6 +303,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
290
303
|
}), () => `${done.embedded} chunks embedded with ${done.model}${done.skipped ? `, ${done.skipped} changed since the build` : ""}\n`);
|
|
291
304
|
});
|
|
292
305
|
withModelOptions(annotate(embed.command("status"), { effects: LOCAL_READ })
|
|
306
|
+
.summary("which conversation vectors are ready or stale")
|
|
293
307
|
.description("how many chunks of a chat have a vector of the model, how many are left, and what is left costs")
|
|
294
308
|
.option("--chat <chat>", messenger.chatArgument)).action(async function () {
|
|
295
309
|
const options = this.optsWithGlobals();
|
|
@@ -303,6 +317,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
303
317
|
"\n");
|
|
304
318
|
});
|
|
305
319
|
withModelOptions(destroysNothing(annotate(embed.command("clear"), { effects: LOCAL_WRITE }))
|
|
320
|
+
.summary("remove stored conversation vectors")
|
|
306
321
|
.description("drop a chat's vectors, or only one model's; messages and conversations are never touched")
|
|
307
322
|
.option("--chat <chat>", messenger.chatArgument)).action(async function () {
|
|
308
323
|
const options = this.optsWithGlobals();
|
|
@@ -479,6 +494,7 @@ export const linksCommand = (messenger) => annotate(new Command("links"), {
|
|
|
479
494
|
examples: [`${messenger.app.command} personal messages links "Book club" 101`],
|
|
480
495
|
effects: LOCAL_READ,
|
|
481
496
|
})
|
|
497
|
+
.summary("why a message belongs to its conversation")
|
|
482
498
|
.description("why a message is in its conversation: each link it has, and the chain of answers back to the start")
|
|
483
499
|
.argument("<chat>", messenger.chatArgument)
|
|
484
500
|
.argument("<message>", "the message id")
|
|
@@ -511,6 +527,7 @@ const readinessLine = ({ chat, state, graph, pending, vectors }, model) => graph
|
|
|
511
527
|
`${vectors.missing} missing of ${vectors.chunks} chunks`;
|
|
512
528
|
/** `search conversations`: the conversations nearest to a query, in meaning and in words. */
|
|
513
529
|
export const conversationsSearchCommand = (messenger) => syncOptions(boundOptions(withModelOptions(reachesOtherService(annotate(new Command("conversations"), { effects: REMOTE_READ })))), messenger)
|
|
530
|
+
.summary("find conversations by words and meaning")
|
|
514
531
|
.description("the conversations nearest to a query in meaning and in words, best first, in one chat or every one โ " +
|
|
515
532
|
"meaning after `conversations embed`; runs on this machine")
|
|
516
533
|
.argument("<query>", "what to look for, in your own words, in any language the model reads")
|
|
@@ -10,6 +10,7 @@ export const countersCommand = (messenger) => {
|
|
|
10
10
|
const parent = new Command("counters").description("per-counter observations and bounded remote refresh");
|
|
11
11
|
for (const operation of ["show", "refresh"]) {
|
|
12
12
|
const command = new Command(operation)
|
|
13
|
+
.summary("refresh message counters from the messenger")
|
|
13
14
|
.description(operation === "show"
|
|
14
15
|
? "show saved counter values and their observation freshness"
|
|
15
16
|
: "read authoritative counters for bounded messages and update their local observations")
|
|
@@ -21,6 +21,7 @@ export const downloadSubcommand = (messages, messenger) => annotate(messages.com
|
|
|
21
21
|
examples: [`${messenger.app.command} personal messages download "Book club" 101 --output-dir ./downloads`],
|
|
22
22
|
effects: FETCH,
|
|
23
23
|
})
|
|
24
|
+
.summary("download files attached to stored messages")
|
|
24
25
|
.description("save a message's photos, files, videos and voice notes to a folder โ or a whole chat's with --all")
|
|
25
26
|
.argument("<chat>", messenger.chatArgument)
|
|
26
27
|
.argument("[message]", "the message id; left out with --all")
|
|
@@ -15,6 +15,7 @@ export const floodCommand = (messenger) => {
|
|
|
15
15
|
return annotate(new Command("flood"), { mcp: false })
|
|
16
16
|
.description(`the waits ${name} asked this profile to keep, and a hold on its writes`)
|
|
17
17
|
.addCommand(grantsAuthority(annotate(new Command("clear"), { effects: LOCAL_WRITE }))
|
|
18
|
+
.summary("clear a profile's stored rate-limit hold")
|
|
18
19
|
.description(`forget them, lift the hold and the profile's pace, once ${name} no longer limits the account; changes nothing there`)
|
|
19
20
|
.action(async function () {
|
|
20
21
|
const context = messengerContext(this, messenger);
|
|
@@ -14,9 +14,10 @@ import { messengerContext } from "./context.js";
|
|
|
14
14
|
* a model that a message talked into writing somewhere, not an agent set on getting around it.
|
|
15
15
|
*/
|
|
16
16
|
export const recipientsCommand = (messenger) => {
|
|
17
|
+
const tool = `${messenger.app.command} personal`;
|
|
17
18
|
const listFor = (profile, env) => new RecipientList(recipientsPathFor(messenger.app, profile, env), messenger.app.command);
|
|
18
19
|
const command = new Command("recipients").description("the chats this profile may send to, when the list is on");
|
|
19
|
-
annotate(command.command("list"), { effects: LOCAL_READ })
|
|
20
|
+
annotate(command.command("list"), { effects: LOCAL_READ, examples: [`${tool} recipients list`] })
|
|
20
21
|
.description("the chats on the list; empty and off until the first add")
|
|
21
22
|
.action(async function () {
|
|
22
23
|
const context = messengerContext(this, messenger);
|
|
@@ -28,7 +29,7 @@ export const recipientsCommand = (messenger) => {
|
|
|
28
29
|
else if (chats.length === 0)
|
|
29
30
|
renderer.note("the recipient list is on and empty โ this profile may send nowhere");
|
|
30
31
|
});
|
|
31
|
-
grantsAuthority(annotate(command.command("add"), { effects: FETCH }))
|
|
32
|
+
grantsAuthority(annotate(command.command("add"), { effects: FETCH, examples: [`${tool} recipients add "Book club"`] }))
|
|
32
33
|
.argument("<chat>", messenger.chatArgument)
|
|
33
34
|
.description("allow sending to this chat; the first add turns the list on")
|
|
34
35
|
.action(async function (chat) {
|
|
@@ -43,7 +44,7 @@ export const recipientsCommand = (messenger) => {
|
|
|
43
44
|
});
|
|
44
45
|
renderAnswer(this, context, itemOf({ id: found.id, title: found.title, added }));
|
|
45
46
|
});
|
|
46
|
-
grantsAuthority(annotate(command.command("remove"), { effects: LOCAL_WRITE }))
|
|
47
|
+
grantsAuthority(annotate(command.command("remove"), { effects: LOCAL_WRITE, examples: [`${tool} recipients remove "Book club"`] }))
|
|
47
48
|
.argument("<chat>", "chat id, or the title as the list shows it")
|
|
48
49
|
.description("stop allowing this chat; the list stays on")
|
|
49
50
|
.action(async function (chat) {
|
|
@@ -54,7 +55,7 @@ export const recipientsCommand = (messenger) => {
|
|
|
54
55
|
throw new CliError("not_found", `${chat.trim()} is not on the recipient list of ${settings.profile}`);
|
|
55
56
|
renderAnswer(this, context, itemOf({ id: gone.id, title: gone.title, removed: true }));
|
|
56
57
|
});
|
|
57
|
-
grantsAuthority(annotate(command.command("clear"), { effects: LOCAL_WRITE }))
|
|
58
|
+
grantsAuthority(annotate(command.command("clear"), { effects: LOCAL_WRITE, examples: [`${tool} recipients clear`] }))
|
|
58
59
|
.description("delete the list, which turns it off: this profile may send to any chat again")
|
|
59
60
|
.action(async function () {
|
|
60
61
|
const context = messengerContext(this, messenger);
|
|
@@ -33,6 +33,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
|
33
33
|
output: { entity: "message", view: "summary" },
|
|
34
34
|
query: INBOX_QUERY,
|
|
35
35
|
})
|
|
36
|
+
.summary("unread chats and their newest messages")
|
|
36
37
|
.description("other people's unread messages in every chat; --new for what arrived since the last check")
|
|
37
38
|
.addHelpText("after", queryHelp(INBOX_QUERY))
|
|
38
39
|
.argument("[query...]", "from when: date>2h, date>2026-10-01; the saved point stays put")
|
|
@@ -21,7 +21,9 @@ export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName:
|
|
|
21
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]);
|
|
22
22
|
export const mcpCommand = (messenger) => {
|
|
23
23
|
const { app } = messenger;
|
|
24
|
-
const command = withFlags(annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false })
|
|
24
|
+
const command = withFlags(annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false })
|
|
25
|
+
.summary("serve this profile's commands to an AI agent")
|
|
26
|
+
.description(`serve this profile to an agent over MCP, on stdin and stdout โ \`claude mcp add ${app.command} -- ${app.command} <profile> mcp\``))
|
|
25
27
|
.option("--http", "serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide")
|
|
26
28
|
.option("--port <port>", `the local port for --http (default ${DEFAULT_PORT})`)
|
|
27
29
|
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net")
|
|
@@ -55,7 +57,9 @@ export const mcpCommand = (messenger) => {
|
|
|
55
57
|
command.addCommand(withFlags(annotate(new Command("config"), {
|
|
56
58
|
effects: LOCAL_READ,
|
|
57
59
|
examples: [`${app.command} personal mcp config`],
|
|
58
|
-
})
|
|
60
|
+
})
|
|
61
|
+
.summary("print this profile's local MCP configuration")
|
|
62
|
+
.description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")).action(async function () {
|
|
59
63
|
const flags = this.optsWithGlobals();
|
|
60
64
|
const context = messengerContext(this, messenger);
|
|
61
65
|
const { settings, env } = context;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { parseDuration } from "../../domain/duration.js";
|
|
4
|
+
import { refusedPlace } from "../../sends/upload.js";
|
|
5
|
+
import { FETCHING } from "../../services/archive.js";
|
|
6
|
+
import { archiveMedia, checkMediaArchive, validateMediaSince } from "../../services/media-archive.js";
|
|
7
|
+
import { environmentOf, setExitCode } from "../context.js";
|
|
8
|
+
import { FETCH, LOCAL_READ } from "../effects.js";
|
|
9
|
+
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
10
|
+
import { messengerContext } from "./context.js";
|
|
11
|
+
import { addMediaArchiveMove } from "./media-archive-move-command.js";
|
|
12
|
+
import { addMediaArchiveTimers } from "./media-archive-timer-command.js";
|
|
13
|
+
import { stopOnSignal } from "./patience.js";
|
|
14
|
+
export const addMediaArchiveCommands = (archive, messenger) => {
|
|
15
|
+
annotate(archive.command("download"), {
|
|
16
|
+
effects: FETCH,
|
|
17
|
+
examples: [`${messenger.app.command} attachments download ./media-archive`],
|
|
18
|
+
})
|
|
19
|
+
.description("archive media from selected chats; verify and keep existing copies")
|
|
20
|
+
.addHelpText("after", `\nMedia is saved only when you request a download; serve does not save these copies.\nTo disable an installed daily backup: ${messenger.app.command} <profile> attachments jobs stop <dir>.`)
|
|
21
|
+
.argument("<dir>", "archive directory; created if missing")
|
|
22
|
+
.option("--chat <chat>", `only this chat; repeat for more; ${messenger.chatArgument}`, (value, previous) => [...previous, value], [])
|
|
23
|
+
.option("--since-time <time>", "include messages from this ISO timestamp with a time zone")
|
|
24
|
+
.option("--pause <duration>", "pause between history pages", messenger.fetching?.pause ?? FETCHING.pause)
|
|
25
|
+
.action(async function (directory) {
|
|
26
|
+
const context = messengerContext(this, messenger);
|
|
27
|
+
const options = this.opts();
|
|
28
|
+
validateMediaSince(options.sinceTime);
|
|
29
|
+
const pauseMs = parseDuration(options.pause, "--pause");
|
|
30
|
+
if (context.settings.offline)
|
|
31
|
+
throw new CliError("validation_error", "archiving media needs online mode");
|
|
32
|
+
if (refusedPlace(directory, messenger.app, context.env))
|
|
33
|
+
throw new CliError("validation_error", "the archive cannot write credential, application or message-store folders");
|
|
34
|
+
const stop = stopOnSignal(this);
|
|
35
|
+
const environment = environmentOf(this);
|
|
36
|
+
const signal = AbortSignal.any([
|
|
37
|
+
stop.signal,
|
|
38
|
+
...[environment.signal, environment.commandSignal].filter((one) => one !== undefined),
|
|
39
|
+
]);
|
|
40
|
+
try {
|
|
41
|
+
const result = await context.withServices((services) => archiveMedia(services, {
|
|
42
|
+
directory,
|
|
43
|
+
chats: options.chat,
|
|
44
|
+
sinceTime: options.sinceTime,
|
|
45
|
+
pauseMs,
|
|
46
|
+
signal,
|
|
47
|
+
pageSize: messenger.history === "store" ? 100 : (messenger.fetching ?? FETCHING).page,
|
|
48
|
+
orderByTime: messenger.history !== "store" && messenger.fetching?.orderBy === "time",
|
|
49
|
+
}));
|
|
50
|
+
renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.saved} saved, ${result.existing} verified copies already present; ${result.chats} chats\n`);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
stop.release();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
annotate(archive.command("check"), {
|
|
57
|
+
effects: LOCAL_READ,
|
|
58
|
+
examples: [`${messenger.app.command} attachments check ./media-archive`],
|
|
59
|
+
})
|
|
60
|
+
.description("rehash all archived files without connecting to the messenger")
|
|
61
|
+
.argument("<dir>", "archive directory")
|
|
62
|
+
.action(async function (directory) {
|
|
63
|
+
const context = messengerContext(this, messenger);
|
|
64
|
+
const environment = environmentOf(this);
|
|
65
|
+
const result = await checkMediaArchive(directory, environment.commandSignal ?? environment.signal);
|
|
66
|
+
if (!result.valid)
|
|
67
|
+
setExitCode(this, 1);
|
|
68
|
+
renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.checked} files checked; ${result.files.filter((one) => !one.valid).length} missing or changed\n`);
|
|
69
|
+
});
|
|
70
|
+
addMediaArchiveTimers(archive, messenger);
|
|
71
|
+
addMediaArchiveMove(archive, messenger);
|
|
72
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { refusedPlace } from "../../sends/upload.js";
|
|
4
|
+
import { moveMediaArchive } from "../../services/media-archive-move.js";
|
|
5
|
+
import { environmentOf } from "../context.js";
|
|
6
|
+
import { LOCAL_WRITE } from "../effects.js";
|
|
7
|
+
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
8
|
+
import { messengerContext, refuseLocalWrite } from "./context.js";
|
|
9
|
+
export const addMediaArchiveMove = (attachments, messenger) => {
|
|
10
|
+
annotate(attachments.command("move"), {
|
|
11
|
+
effects: LOCAL_WRITE,
|
|
12
|
+
mcp: false,
|
|
13
|
+
examples: [
|
|
14
|
+
`${messenger.app.command} attachments move ./media-archive ./older-images --before-time 2026-10-01T00:00:00Z --dry-run`,
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
.description("move older images into another verified archive; remember their new locations")
|
|
18
|
+
.argument("<dir>", "source media archive directory")
|
|
19
|
+
.argument("<archive-dir>", "destination archive directory; created if needed")
|
|
20
|
+
.requiredOption("--before-time <time>", "move images whose message date is before this ISO timestamp with zone")
|
|
21
|
+
.option("--dry-run", "show counts and bytes without moving files or creating the destination")
|
|
22
|
+
.addHelpText("after", "\nCopies are verified at the destination before local image bytes are removed.\nSource manifests retain relocation records; checks follow them and downloads do not fetch them again.\nImages without a recorded message date are left in place and counted as undated.")
|
|
23
|
+
.action(async function (directory, archiveDirectory) {
|
|
24
|
+
const context = messengerContext(this, messenger);
|
|
25
|
+
const options = this.opts();
|
|
26
|
+
if (!options.dryRun)
|
|
27
|
+
refuseLocalWrite(context, messenger.app.command, "attachments.move");
|
|
28
|
+
if ([directory, archiveDirectory].some((folder) => refusedPlace(folder, messenger.app, context.env)))
|
|
29
|
+
throw new CliError("validation_error", "media archives cannot use credential, application or message-store folders");
|
|
30
|
+
const environment = environmentOf(this);
|
|
31
|
+
const result = await moveMediaArchive({
|
|
32
|
+
directory,
|
|
33
|
+
archiveDirectory,
|
|
34
|
+
beforeTime: options.beforeTime,
|
|
35
|
+
dryRun: options.dryRun,
|
|
36
|
+
signal: environment.commandSignal ?? environment.signal,
|
|
37
|
+
});
|
|
38
|
+
renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.dryRun ? `${result.selected} images selected (${result.selectedBytes} bytes)` : `${result.moved} images moved (${result.movedBytes} bytes)`}; ${result.undated} undated images left in place\n`);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { CliError } from "@wirecat/cli-base";
|
|
4
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
5
|
+
import { dailyArchiveTimer, manageDailyArchiveTimer, runArchiveTimerCommand, } from "../../background/daily-archive-timer.js";
|
|
6
|
+
import { thisMachine } from "../../background/system.js";
|
|
7
|
+
import { refusedPlace } from "../../sends/upload.js";
|
|
8
|
+
import { validateMediaSince } from "../../services/media-archive.js";
|
|
9
|
+
import { environmentOf } from "../context.js";
|
|
10
|
+
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
11
|
+
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
12
|
+
import { messengerContext } from "./context.js";
|
|
13
|
+
export const addMediaArchiveTimers = (attachments, messenger) => {
|
|
14
|
+
const archive = annotate(attachments.command("jobs"), { mcp: false }).description("daily media archive timers for this profile");
|
|
15
|
+
for (const action of ["install", "start", "stop", "status", "uninstall"]) {
|
|
16
|
+
const command = annotate(archive.command(action), {
|
|
17
|
+
effects: action === "status" ? LOCAL_READ : LOCAL_WRITE,
|
|
18
|
+
mcp: false,
|
|
19
|
+
examples: [`${messenger.app.command} attachments jobs ${action} ./media-archive`],
|
|
20
|
+
})
|
|
21
|
+
.description({
|
|
22
|
+
install: "install a Linux daily archive timer for this directory; starts nothing",
|
|
23
|
+
start: "enable the installed archive timer at login and start its daily schedule",
|
|
24
|
+
stop: "disable the archive timer; a download already running finishes",
|
|
25
|
+
status: "show whether this directory's archive timer is installed and active",
|
|
26
|
+
uninstall: "remove this directory's stopped archive timer; keep archived files",
|
|
27
|
+
}[action])
|
|
28
|
+
.argument("<dir>", "archive directory, used with the same profile as install");
|
|
29
|
+
if (action !== "status")
|
|
30
|
+
grantsAuthority(command);
|
|
31
|
+
if (action === "install")
|
|
32
|
+
command
|
|
33
|
+
.option("--at <HH:mm>", "daily start time", "03:00")
|
|
34
|
+
.option("--timezone <zone>", "IANA time zone of the daily start time", "UTC")
|
|
35
|
+
.option("--chat <chat>", `only this chat; repeat for more; ${messenger.chatArgument}`, (value, previous) => [...previous, value], [])
|
|
36
|
+
.option("--since-time <time>", "include messages from this ISO timestamp with a time zone");
|
|
37
|
+
command.action(async function (directory) {
|
|
38
|
+
const context = messengerContext(this, messenger);
|
|
39
|
+
const environment = environmentOf(this);
|
|
40
|
+
const signal = environment.commandSignal ?? environment.signal;
|
|
41
|
+
const system = environment.system ?? {
|
|
42
|
+
...thisMachine(),
|
|
43
|
+
run: (args, env) => runArchiveTimerCommand(args, env, signal),
|
|
44
|
+
};
|
|
45
|
+
const options = this.opts();
|
|
46
|
+
validateMediaSince(options.sinceTime);
|
|
47
|
+
if (action === "install" && refusedPlace(directory, messenger.app, context.env))
|
|
48
|
+
throw new CliError("validation_error", "the archive cannot write credential, application or message-store folders");
|
|
49
|
+
// Canonicalize existing destinations so a symlink alias addresses the same timer.
|
|
50
|
+
const root = await realpath(directory).catch((error) => {
|
|
51
|
+
if (error.code !== "ENOENT")
|
|
52
|
+
throw error;
|
|
53
|
+
return resolve(directory);
|
|
54
|
+
});
|
|
55
|
+
const plan = dailyArchiveTimer({
|
|
56
|
+
app: messenger.app,
|
|
57
|
+
profile: context.profile,
|
|
58
|
+
directory: root,
|
|
59
|
+
entry: system.entry,
|
|
60
|
+
env: context.env,
|
|
61
|
+
chats: options.chat,
|
|
62
|
+
sinceTime: options.sinceTime,
|
|
63
|
+
at: options.at,
|
|
64
|
+
timeZone: options.timezone,
|
|
65
|
+
});
|
|
66
|
+
const result = await manageDailyArchiveTimer(plan, action, system, context.env, environment.commandSignal ?? environment.signal);
|
|
67
|
+
renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.name}: ${action}\n`);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -14,6 +14,7 @@ export const commentsCommand = (messenger) => {
|
|
|
14
14
|
examples: [`${messenger.app.command} personal messages comments "News channel" 101 --limit 20`],
|
|
15
15
|
effects: REMOTE_READ,
|
|
16
16
|
})
|
|
17
|
+
.summary("comments on a channel post")
|
|
17
18
|
.description("the comments under a channel post, oldest to newest; they live in its discussion group")
|
|
18
19
|
.addHelpText("after", queryHelp(COMMENTS_QUERY))
|
|
19
20
|
.argument("<chat>", `the channel: ${messenger.chatArgument}`)
|
|
@@ -17,7 +17,7 @@ export const deleteCommand = (messenger) => annotate(new Command("delete"), {
|
|
|
17
17
|
})
|
|
18
18
|
.description("delete messages for you only; with --for-everyone, for everyone in the chat")
|
|
19
19
|
.argument("<chat>", messenger.chatArgument)
|
|
20
|
-
.argument("<
|
|
20
|
+
.argument("<message...>", `the message ids, by spaces or commas, at most ${DELETE_AT_ONCE}`)
|
|
21
21
|
.addOption(ownerOnlyOption("--for-everyone", "delete for everyone in the chat, not only for you โ they cannot get it back"))
|
|
22
22
|
.addOption(ownerOnlyOption("--allow-dangerous", "go ahead without the question an ask level puts before a deletion"))
|
|
23
23
|
.action(async function (chat, messages) {
|
|
@@ -26,6 +26,7 @@ export const messagesSearchCommand = (messenger) => {
|
|
|
26
26
|
});
|
|
27
27
|
const syncing = syncsFirst(messenger) ? "; optionally fetches new messages with --sync-first" : "";
|
|
28
28
|
return command
|
|
29
|
+
.summary("find messages by words and filters")
|
|
29
30
|
.description(messenger.serverSearch
|
|
30
31
|
? `search messenger messages in the local store and on the messenger's server (--backend)${syncing}; --discover searches the archive only`
|
|
31
32
|
: `search messenger messages in the local store โ what was read, fetched or kept by serve${syncing}; --discover searches the archive only`)
|
|
@@ -9,6 +9,7 @@ import { syncOptions, syncRequest, syncsFirst } from "./search-sync-options.js";
|
|
|
9
9
|
import { countsAnswer, refuseJsonl } from "./stats-answers.js";
|
|
10
10
|
const GROUPINGS = ["chat", "sender", "day", "hour"];
|
|
11
11
|
export const messagesStatsCommand = (messenger) => syncOptions(annotate(new Command("show"), { effects: REMOTE_READ, output: { entity: "message_counts" } }), messenger)
|
|
12
|
+
.summary("count stored messages by chat, sender, day or hour")
|
|
12
13
|
.description(`how many stored messages match, by chat, sender, day or hour โ the local store only${syncsFirst(messenger) ? "; optionally fetches new messages with --sync-first" : ""}`)
|
|
13
14
|
.argument("[query...]", "a strict Lucene query, as for search messages; none counts every stored message; with --saved, more words AND-ed to it")
|
|
14
15
|
.option("--by <chat|sender|day|hour>", "what to count by (default: chat)", groupingOf)
|
|
@@ -61,6 +61,7 @@ export const modelsCommand = (messenger) => {
|
|
|
61
61
|
const models = annotate(new Command("models"), { mcp: false }).description("models that run on this machine");
|
|
62
62
|
const audio = models.command("audio").description("speech models for transcribing voice messages");
|
|
63
63
|
withQuery(annotate(audio.command("list"), { effects: LOCAL_READ, examples: [`${tool} models audio list`] }), MODELS_QUERY)
|
|
64
|
+
.summary("speech models and their download status")
|
|
64
65
|
.description("the speech models, most suitable first, which are downloaded, and which one is the default")
|
|
65
66
|
.action(async function (words) {
|
|
66
67
|
const context = baseContext(this, messenger.resolveSettings);
|
|
@@ -107,6 +108,7 @@ export const modelsCommand = (messenger) => {
|
|
|
107
108
|
});
|
|
108
109
|
const text = models.command("text").description("embedding models for searching conversations by meaning");
|
|
109
110
|
withQuery(annotate(text.command("list"), { effects: LOCAL_READ, examples: [`${tool} models text list`] }), MODELS_QUERY)
|
|
111
|
+
.summary("embedding models and their download status")
|
|
110
112
|
.description("the embedding models, most suitable first, which are downloaded, and which one is the default")
|
|
111
113
|
.action(async function (words) {
|
|
112
114
|
const context = baseContext(this, messenger.resolveSettings);
|
|
@@ -127,6 +129,7 @@ export const modelsCommand = (messenger) => {
|
|
|
127
129
|
}))
|
|
128
130
|
.argument("<model>", "a model id from `models text list`")
|
|
129
131
|
.addOption(new Option("--accept-terms", "accept the model's licence terms, for a model that has its own"))
|
|
132
|
+
.summary("download and verify an embedding model")
|
|
130
133
|
.description("download an embedding model once, checked against the sha256 this version expects")
|
|
131
134
|
.action(async function (id, options) {
|
|
132
135
|
const context = baseContext(this, messenger.resolveSettings);
|
|
@@ -16,8 +16,8 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
16
16
|
const alias = new Command("alias").description("a private local display name in the selected account");
|
|
17
17
|
annotate(alias.command("set"), { effects: LOCAL_WRITE })
|
|
18
18
|
.description("your own name for a person in this account; kept on this machine, never sent")
|
|
19
|
-
.argument("<person>")
|
|
20
|
-
.argument("<alias>")
|
|
19
|
+
.argument("<person>", "the person's id, reference or known name")
|
|
20
|
+
.argument("<alias>", "the private name to use for this person")
|
|
21
21
|
.action(async function (person, value) {
|
|
22
22
|
const context = messengerContext(this, messenger);
|
|
23
23
|
refuseLocalWrite(context, messenger.app.command, "contacts.alias.set");
|
|
@@ -25,7 +25,7 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
25
25
|
});
|
|
26
26
|
annotate(alias.command("rm"), { effects: LOCAL_WRITE })
|
|
27
27
|
.description("drop your local name for a person; their own name shows again")
|
|
28
|
-
.argument("<person>")
|
|
28
|
+
.argument("<person>", "the person's id, reference or known name")
|
|
29
29
|
.action(async function (person) {
|
|
30
30
|
const context = messengerContext(this, messenger);
|
|
31
31
|
refuseLocalWrite(context, messenger.app.command, "contacts.alias.rm");
|
|
@@ -34,7 +34,7 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
34
34
|
const notes = new Command("notes").description("your private notes on a stored contact, the same in every account that sees them");
|
|
35
35
|
withQuery(annotate(notes.command("list"), { effects: LOCAL_READ })
|
|
36
36
|
.description("every private note you keep on a person, oldest first")
|
|
37
|
-
.argument("<person>"), NOTES_QUERY).action(async function (person, words) {
|
|
37
|
+
.argument("<person>", "the person's id, reference or known name"), NOTES_QUERY).action(async function (person, words) {
|
|
38
38
|
const context = messengerContext(this, messenger);
|
|
39
39
|
listQuery(typedQuery(words), NOTES_QUERY, "contacts notes list");
|
|
40
40
|
const items = await context.withServices((services) => services.privatePeople.notes(person));
|
|
@@ -42,8 +42,8 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
42
42
|
});
|
|
43
43
|
annotate(notes.command("show"), { effects: LOCAL_READ, output: { entity: "contact_note" } })
|
|
44
44
|
.description("one private note on a person, with its revision for edit")
|
|
45
|
-
.argument("<person>")
|
|
46
|
-
.argument("<id>")
|
|
45
|
+
.argument("<person>", "the person's id, reference or known name")
|
|
46
|
+
.argument("<id>", "the note id from contacts notes list")
|
|
47
47
|
.action(async function (person, id) {
|
|
48
48
|
const context = messengerContext(this, messenger);
|
|
49
49
|
renderAnswer(this, context, itemOf(await context.withServices((services) => services.privatePeople.note(person, id))));
|
|
@@ -56,10 +56,12 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
56
56
|
.description(name === "add"
|
|
57
57
|
? "add a private note on a person from stdin or --file; never sent"
|
|
58
58
|
: "replace a note's text; --revision must match the one you read")
|
|
59
|
-
.argument("<person>")
|
|
59
|
+
.argument("<person>", "the person's id, reference or known name")
|
|
60
60
|
.addOption(ownerOnlyOption("--file <path>", "read note text from a file; omitted or - reads stdin"));
|
|
61
61
|
if (name === "edit")
|
|
62
|
-
command
|
|
62
|
+
command
|
|
63
|
+
.argument("<id>", "the note id from contacts notes list")
|
|
64
|
+
.requiredOption("--revision <n>", "the revision you read before editing");
|
|
63
65
|
command.action(async function (person, id) {
|
|
64
66
|
const context = messengerContext(this, messenger);
|
|
65
67
|
refuseLocalWrite(context, messenger.app.command, `contacts.notes.${name}`);
|
|
@@ -76,8 +78,8 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
76
78
|
}
|
|
77
79
|
annotate(notes.command("remove"), { effects: LOCAL_WRITE })
|
|
78
80
|
.description("delete one private note on a person")
|
|
79
|
-
.argument("<person>")
|
|
80
|
-
.argument("<id>")
|
|
81
|
+
.argument("<person>", "the person's id, reference or known name")
|
|
82
|
+
.argument("<id>", "the note id from contacts notes list")
|
|
81
83
|
.action(async function (person, id) {
|
|
82
84
|
const context = messengerContext(this, messenger);
|
|
83
85
|
refuseLocalWrite(context, messenger.app.command, "contacts.notes.remove");
|
|
@@ -25,6 +25,7 @@ const selectionOf = (value) => {
|
|
|
25
25
|
};
|
|
26
26
|
export const rankingsTopCommand = (messenger, target) => {
|
|
27
27
|
const command = syncOptions(annotate(new Command("top"), { effects: REMOTE_READ, output: { entity: "ranking" } }), messenger)
|
|
28
|
+
.summary("rank messages or people by a measure or score")
|
|
28
29
|
.description(`rank ${target === "messages" ? "stored messages" : "the human authors of stored messages"} by a measure or explainable score; counters are snapshots with per-field observation freshness`)
|
|
29
30
|
.argument("[query...]", "a strict Lucene query; none selects every stored message")
|
|
30
31
|
.addOption(new Option("--measure <name>", "the ranking metric; not with score or weights").choices(target === "messages" ? [...MESSAGE_MEASURES] : [...AUTHOR_MEASURES]))
|
|
@@ -86,6 +87,7 @@ const emojiSplit = (reactions) => [
|
|
|
86
87
|
...(reactions.length > SHOWN_REACTIONS ? [`+${reactions.length - SHOWN_REACTIONS}`] : []),
|
|
87
88
|
].join(" ยท ");
|
|
88
89
|
export const rankingEvidenceCommand = (messenger, target) => annotate(new Command("evidence"), { effects: LOCAL_READ, output: { entity: "evidence" } })
|
|
90
|
+
.summary("evidence for an exact statistics selection")
|
|
89
91
|
.description("bounded messages, answer pairs or retention members from an exact drilldown selection")
|
|
90
92
|
.argument(target === "messages" ? "<message>" : "<person>", target === "messages"
|
|
91
93
|
? "the canonical message locator, or retention cohort reference, from drilldown"
|
|
@@ -6,7 +6,10 @@ import { messengerContext } from "./context.js";
|
|
|
6
6
|
/** `reactions add|remove`: the owner's own reaction on a message. */
|
|
7
7
|
export const reactionsCommand = (messenger) => {
|
|
8
8
|
const reactions = new Command("reactions").description("react to messages");
|
|
9
|
-
annotate(reactions.command("add"), {
|
|
9
|
+
annotate(reactions.command("add"), {
|
|
10
|
+
effects: REMOTE_WRITE,
|
|
11
|
+
examples: [`${messenger.app.command} personal reactions add "Book club" 101 ๐`],
|
|
12
|
+
})
|
|
10
13
|
.description("put your reaction on a message; it replaces the one you had")
|
|
11
14
|
.argument("<chat>", messenger.chatArgument)
|
|
12
15
|
.argument("<message>", "the message id")
|
|
@@ -16,7 +19,10 @@ export const reactionsCommand = (messenger) => {
|
|
|
16
19
|
const reacted = await context.withServices((services) => services.messages.react({ chat, message: message.trim(), emoji: emoji.trim() }));
|
|
17
20
|
renderAnswer(this, context, itemOf(snakeKeys(reacted)));
|
|
18
21
|
});
|
|
19
|
-
annotate(reactions.command("remove"), {
|
|
22
|
+
annotate(reactions.command("remove"), {
|
|
23
|
+
effects: REMOTE_WRITE,
|
|
24
|
+
examples: [`${messenger.app.command} personal reactions remove "Book club" 101`],
|
|
25
|
+
})
|
|
20
26
|
.description("take your reaction off a message")
|
|
21
27
|
.argument("<chat>", messenger.chatArgument)
|
|
22
28
|
.argument("<message>", "the message id")
|
|
@@ -6,6 +6,7 @@ import { positiveCount } from "../paging.js";
|
|
|
6
6
|
import { messengerContext } from "./context.js";
|
|
7
7
|
import { refuseJsonl, retentionAnswer } from "./stats-answers.js";
|
|
8
8
|
export const retentionCommand = (messenger) => annotate(new Command("retention"), { effects: REMOTE_READ, output: { entity: "retention" } })
|
|
9
|
+
.summary("joining cohorts and observed membership checkpoints")
|
|
9
10
|
.description("joining cohorts and observed checkpoint membership from saved roster observations")
|
|
10
11
|
.argument("<chat>", messenger.chatArgument)
|
|
11
12
|
.option("--since-time <time>", "joining period starts at ISO 8601 or a relative time; last 90 days by default")
|
|
@@ -29,6 +29,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
29
29
|
output: { entity: "message", view: "summary" },
|
|
30
30
|
query: REVIEW_QUERY,
|
|
31
31
|
})
|
|
32
|
+
.summary("chats with new activity or unanswered messages")
|
|
32
33
|
.description("every message, yours too, in chats that changed since a point โ for reviewing who owes what")
|
|
33
34
|
.addHelpText("after", queryHelp(REVIEW_QUERY))
|
|
34
35
|
.argument("[query...]", `where the last review ended and which chat: date>2h, chat:"Book club"; ${REVIEW_DAYS} days ago if no date`)
|