@wirecat/cli-core 0.395.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 +63 -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/commands-command.js +6 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/input-fields.d.ts +3 -0
- package/dist/cli/input-fields.js +27 -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 +32 -10
- 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/conversation-answers.d.ts +5 -0
- 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/message-answers.d.ts +5 -0
- package/dist/cli/messenger/message-writes.d.ts +5 -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-answers.js +2 -0
- package/dist/cli/messenger/search-command.js +5 -1
- package/dist/cli/messenger/searches-command.d.ts +2 -0
- package/dist/cli/messenger/searches-command.js +26 -8
- package/dist/cli/messenger/server-command.js +7 -2
- 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/cli/program.js +5 -2
- package/dist/entities/contact-read.d.ts +119 -0
- package/dist/entities/contact-read.js +50 -0
- package/dist/entities/evidence.d.ts +40 -0
- package/dist/entities/index.d.ts +191 -2
- package/dist/entities/index.js +7 -1
- package/dist/entities/message.d.ts +9 -0
- package/dist/entities/message.js +6 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +15 -0
- 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/search/lucene/registry.js +2 -2
- 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/dist/services/messages-search.js +13 -2
- package/dist/services/messages.d.ts +5 -0
- package/dist/store/search-index.d.ts +4 -1
- package/dist/store/search-index.js +10 -1
- package/dist/store/sqlite/hit-places.d.ts +12 -0
- package/dist/store/sqlite/hit-places.js +76 -0
- package/dist/store/sqlite/lucene-fields.d.ts +8 -0
- package/dist/store/sqlite/lucene-fields.js +6 -4
- package/dist/store/sqlite/lucene.js +25 -70
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +19 -0
- package/dist/store/sqlite/note-index.d.ts +11 -1
- package/dist/store/sqlite/note-index.js +18 -6
- package/dist/store/sqlite/schema.d.ts +39 -0
- package/dist/store/sqlite/schema.js +4 -0
- package/dist/store/sqlite/transcript-query.d.ts +40 -0
- package/dist/store/sqlite/transcript-query.js +108 -0
- package/dist/store/sqlite/words.d.ts +6 -0
- package/package.json +3 -3
- package/parity.json +50 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,69 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 0.397.0 — 23.10.2026
|
|
9
|
+
|
|
10
|
+
### Changed — may break callers
|
|
11
|
+
|
|
12
|
+
- MCP argument names follow singular CLI placeholders: `messages delete` takes `message`, chat export takes `chat`, and duplicate-task operations take `task` (with `other_task` for a pair). Update saved MCP requests using `messages`, `chats` or `dup`. CLI positional values and their order are unchanged.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- The option catalogue records WhatsApp's JPEG-only profile photos. Wording differences can be justified for one command without exempting that option everywhere or hiding a missing option.
|
|
17
|
+
|
|
18
|
+
- **Short command summaries and argument help.** Messenger commands keep their detailed descriptions while help and discovery show concise summaries. Contact notes, aliases, contact surnames and shell completion explain their positional arguments.
|
|
19
|
+
|
|
20
|
+
- Message deletion, chat export and duplicate-task commands use singular argument names in help and discovery. Variadic arguments still accept several values, and task argument help identifies each side of a pair.
|
|
21
|
+
|
|
22
|
+
- Contact profiles, phone lookups and address-book sync summaries declare their existing answer fields. Profile schemas retain masked phones, incomplete activity counts, partial provider flags and the source and precision of account-age estimates.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Reaction commands include concrete examples in help and command discovery.
|
|
27
|
+
|
|
28
|
+
- Recipient-list commands include concrete examples in help and command discovery.
|
|
29
|
+
|
|
30
|
+
- Saved-search commands include concrete examples in help and command discovery.
|
|
31
|
+
|
|
32
|
+
- **Independent media archives for messenger CLIs.** `attachments download <dir>` saves files by account,
|
|
33
|
+
chat, message and position, verifies size and SHA-256 on reruns, and preserves completed downloads after
|
|
34
|
+
interruption. `attachments check <dir>` verifies copies offline. `attachments jobs` installs and manages
|
|
35
|
+
opt-in Linux daily timers. Archive files stay independent of message-store deletion; interrupted streams
|
|
36
|
+
restart from byte zero because messenger downloads expose no range validators.
|
|
37
|
+
Download help clarifies that receiving messages saves no media copies and names the command to stop a daily timer.
|
|
38
|
+
`attachments move <dir> <archive-dir> --before-time <time>` moves older images to verified cold storage;
|
|
39
|
+
`--dry-run` previews counts and bytes. Relocation records preserve offline verification and prevent duplicate
|
|
40
|
+
downloads. New manifests retain message dates and image MIME types; undated draft images are left in place.
|
|
41
|
+
|
|
42
|
+
## 0.396.0 — 23.10.2026
|
|
43
|
+
|
|
44
|
+
### Changed — may break callers
|
|
45
|
+
|
|
46
|
+
- **Command discovery uses `--json`.** `commands`, scoped discovery and `commands schema` refuse `--jsonl` with `output.one-object`, instead of printing a line with no stream end record. Use `--json` for the discovery document.
|
|
47
|
+
|
|
48
|
+
- cli-core requires cli-base 0.50.1 or newer within the 0.50 line for profile-preserving command corrections and concise skill summaries.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **Unknown-command explanations are separated.** The explanation of a word read as a profile follows the command error with a dash, keeping both parts readable on one line.
|
|
53
|
+
|
|
54
|
+
- **Input-field errors name the command's flags.** Adapters can report unsupported or conflicting domain fields through `input.field-unsupported` or `input.field-conflict`. The CLI maps declared fields to flags in the human message and structured parameter, while SDK errors stay independent of command syntax. Unknown fields are kept without guessing.
|
|
55
|
+
|
|
56
|
+
- **Joined command hints keep the chosen profile.** A command path passed as one argument is refused with a correction that preserves a separate explicit profile, including when global options come first.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- `SEARCHES_QUERY` (`@wirecat/cli-core/cli`): the query `searches list` takes, for another CLI's own saved searches.
|
|
61
|
+
|
|
62
|
+
- **A voice note's transcript is found in any form of a word, and widened to the spellings speech recognition
|
|
63
|
+
garbles (store version 29).** «риелтор» finds a transcript saying «риелтору», and «реелтора»: always, not only when
|
|
64
|
+
nothing matched, by meeting search's rules — one stem edit from a word the Russian dictionary does not know, never
|
|
65
|
+
a number word, never a phrase or under `exact:`. A hit found only through a garbled spelling has `match:
|
|
66
|
+
"corrected"` and `corrections`, and the answer's `corrections` lists them. New tables `transcript_words`, its
|
|
67
|
+
vocabulary, `transcript_stems` and `transcript_index_pending`, filled before each search; existing transcripts are
|
|
68
|
+
queued on upgrade.
|
|
69
|
+
|
|
70
|
+
- Attachment commands include concrete examples in help and command discovery.
|
|
9
71
|
|
|
10
72
|
## 0.395.0 — 23.10.2026
|
|
11
73
|
|
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ write. A messenger plugs in as one adapter and gets all of it.
|
|
|
25
25
|
|---|---|
|
|
26
26
|
| [Architecture](docs/dev/ARCHITECTURE.md) | The layers, the module map, the store internals and migrations, who consumes what |
|
|
27
27
|
| [Document workflows](docs/dev/DOCUMENTS.md) | Shared file ingestion, sync, scoped knowledge and extraction; the ports a file provider supplies |
|
|
28
|
+
| [Media archives](docs/dev/MEDIA-ARCHIVE.md) | Independent verified file copies, interruption recovery and opt-in daily timers |
|
|
28
29
|
| [Write an adapter](docs/dev/ADAPTERS.md) | How to add a messenger: the port, the `Messenger` description, the contract cases |
|
|
29
30
|
| [Messenger rules](docs/dev/STANDARD.md) | What binds tg, max and wa beyond the [CLI standards handbook](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/README.md): parity, the option catalogue, `bot` |
|
|
30
31
|
| [Glossary](docs/dev/GLOSSARY.md) | One word, one meaning across the tools |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AppIdentity } from "../ports/app.js";
|
|
2
|
+
import type { ServerSystem } from "./system.js";
|
|
3
|
+
export declare const runArchiveTimerCommand: (args: string[], env: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<{
|
|
4
|
+
code: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
}>;
|
|
8
|
+
export interface DailyArchiveTimer {
|
|
9
|
+
name: string;
|
|
10
|
+
unitDirectory: string;
|
|
11
|
+
service: string;
|
|
12
|
+
timer: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DailyArchiveTimerInput {
|
|
15
|
+
app: AppIdentity;
|
|
16
|
+
profile: string;
|
|
17
|
+
directory: string;
|
|
18
|
+
entry: readonly string[];
|
|
19
|
+
env: NodeJS.ProcessEnv;
|
|
20
|
+
chats?: readonly string[];
|
|
21
|
+
sinceTime?: string;
|
|
22
|
+
at?: string;
|
|
23
|
+
timeZone?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const dailyArchiveTimer: (input: DailyArchiveTimerInput) => DailyArchiveTimer;
|
|
26
|
+
export declare const manageDailyArchiveTimer: (plan: DailyArchiveTimer, action: "install" | "start" | "stop" | "status" | "uninstall", system: Pick<ServerSystem, "platform" | "run">, env: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<{
|
|
27
|
+
name: string;
|
|
28
|
+
installed: boolean;
|
|
29
|
+
active: boolean;
|
|
30
|
+
servicePath: string;
|
|
31
|
+
timerPath: string;
|
|
32
|
+
action?: undefined;
|
|
33
|
+
} | {
|
|
34
|
+
active?: undefined;
|
|
35
|
+
name: string;
|
|
36
|
+
installed: boolean;
|
|
37
|
+
action: "install" | "start" | "stop" | "uninstall";
|
|
38
|
+
servicePath: string;
|
|
39
|
+
timerPath: string;
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, realpath, unlink } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { CliError } from "@wirecat/cli-base";
|
|
5
|
+
import { acquireLock, readBoundedUtf8File, syncDirectory, writeFileAtomic } from "../files/index.js";
|
|
6
|
+
import { locationVariables } from "./units.js";
|
|
7
|
+
export const runArchiveTimerCommand = (args, env, signal) => new Promise((resolve, reject) => {
|
|
8
|
+
execFile(args[0] ?? "systemctl", args.slice(1), {
|
|
9
|
+
env,
|
|
10
|
+
signal,
|
|
11
|
+
timeout: 30000,
|
|
12
|
+
maxBuffer: 65536,
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
shell: false,
|
|
15
|
+
}, (error, stdout, stderr) => {
|
|
16
|
+
if (!error)
|
|
17
|
+
return resolve({ code: 0, stdout, stderr });
|
|
18
|
+
if (typeof error.code === "number")
|
|
19
|
+
return resolve({ code: error.code, stdout, stderr });
|
|
20
|
+
reject(new CliError("configuration_error", "archive timer command did not complete; inspect its systemctl status"));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
const missingMediaFile = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
24
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
25
|
+
const quote = (value, expandDollar) => `"${value
|
|
26
|
+
.replaceAll("\\", "\\\\")
|
|
27
|
+
.replaceAll('"', '\\"')
|
|
28
|
+
.replaceAll("%", "%%")
|
|
29
|
+
.replaceAll("$", () => (expandDollar ? "$$" : "$"))}"`;
|
|
30
|
+
const unitFolder = (env) => {
|
|
31
|
+
const folder = env.XDG_CONFIG_HOME ?? (env.HOME ? join(env.HOME, ".config") : "");
|
|
32
|
+
if (!isAbsolute(folder) || /[\p{Cc}]/u.test(folder))
|
|
33
|
+
throw new CliError("configuration_error", "a timer needs an absolute HOME or XDG_CONFIG_HOME");
|
|
34
|
+
return join(folder, "systemd", "user");
|
|
35
|
+
};
|
|
36
|
+
export const dailyArchiveTimer = (input) => {
|
|
37
|
+
const at = input.at ?? "03:00";
|
|
38
|
+
const timeZone = input.timeZone ?? "UTC";
|
|
39
|
+
const environment = locationVariables(input.app, input.env);
|
|
40
|
+
let zone;
|
|
41
|
+
try {
|
|
42
|
+
zone = new Intl.DateTimeFormat("en", { timeZone }).resolvedOptions().timeZone;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new CliError("validation_error", "--timezone needs an IANA time zone");
|
|
46
|
+
}
|
|
47
|
+
if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(at) ||
|
|
48
|
+
!/^[A-Za-z0-9_+/-]+$/.test(zone) ||
|
|
49
|
+
!/^[a-z][a-z0-9-]*$/.test(input.app.command) ||
|
|
50
|
+
!input.profile ||
|
|
51
|
+
!isAbsolute(input.directory) ||
|
|
52
|
+
input.entry.length < 1 ||
|
|
53
|
+
input.entry.some((one) => !isAbsolute(one)) ||
|
|
54
|
+
[
|
|
55
|
+
input.profile,
|
|
56
|
+
input.directory,
|
|
57
|
+
...input.entry,
|
|
58
|
+
...Object.values(environment),
|
|
59
|
+
...(input.chats ?? []),
|
|
60
|
+
input.sinceTime ?? "",
|
|
61
|
+
].some((one) => /[\p{Cc}]/u.test(one)))
|
|
62
|
+
throw new CliError("validation_error", "invalid daily archive timer arguments");
|
|
63
|
+
const scope = digest(JSON.stringify([
|
|
64
|
+
input.app.command,
|
|
65
|
+
input.profile,
|
|
66
|
+
input.directory,
|
|
67
|
+
Object.entries(environment).sort(([a], [b]) => a.localeCompare(b)),
|
|
68
|
+
]));
|
|
69
|
+
const name = `${input.app.command}-media-archive-${scope.slice(0, 32)}`;
|
|
70
|
+
const owner = `# WireCat daily media archive ${scope}`;
|
|
71
|
+
const command = [
|
|
72
|
+
...input.entry,
|
|
73
|
+
input.profile,
|
|
74
|
+
"attachments",
|
|
75
|
+
"download",
|
|
76
|
+
input.directory,
|
|
77
|
+
"--json",
|
|
78
|
+
"--no-input",
|
|
79
|
+
"--timeout",
|
|
80
|
+
"6h",
|
|
81
|
+
...(input.sinceTime ? ["--since-time", input.sinceTime] : []),
|
|
82
|
+
...(input.chats ?? []).flatMap((chat) => ["--chat", chat]),
|
|
83
|
+
];
|
|
84
|
+
const seal = (text) => `${text}# SHA-256 ${digest(text)}\n`;
|
|
85
|
+
return {
|
|
86
|
+
name,
|
|
87
|
+
unitDirectory: unitFolder(input.env),
|
|
88
|
+
service: seal(`${owner}\n[Unit]\nDescription=WireCat media archive\n\n[Service]\nType=oneshot\nUMask=0077\nTimeoutStartSec=6h\n${Object.entries(environment)
|
|
89
|
+
.map(([key, value]) => `Environment=${quote(`${key}=${value}`, false)}\n`)
|
|
90
|
+
.join("")}ExecStart=${command.map((one) => quote(one, true)).join(" ")}\n`),
|
|
91
|
+
timer: seal(`${owner}\n[Unit]\nDescription=WireCat daily media archive\n\n[Timer]\nOnCalendar=*-*-* ${at}:00 ${zone}\nPersistent=true\nUnit=${name}.service\n\n[Install]\nWantedBy=timers.target\n`),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const ownedUnit = async (path, expectedOwner) => {
|
|
95
|
+
let text;
|
|
96
|
+
try {
|
|
97
|
+
text = await readBoundedUtf8File(path, { maxBytes: 65536 });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (missingMediaFile(error))
|
|
101
|
+
return null;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const sealAt = text.lastIndexOf("# SHA-256 ");
|
|
105
|
+
if (!text.startsWith(`${expectedOwner}\n`) ||
|
|
106
|
+
sealAt < 0 ||
|
|
107
|
+
text.slice(sealAt) !== `# SHA-256 ${digest(text.slice(0, sealAt))}\n`)
|
|
108
|
+
throw new CliError("validation_error", "archive timer unit is foreign or edited; it was preserved");
|
|
109
|
+
return text;
|
|
110
|
+
};
|
|
111
|
+
export const manageDailyArchiveTimer = async (plan, action, system, env, signal) => {
|
|
112
|
+
if (system.platform !== "linux")
|
|
113
|
+
throw new CliError("configuration_error", "daily media archive timers require Linux systemd user services");
|
|
114
|
+
if (!isAbsolute(plan.unitDirectory) || !/^[a-z][a-z0-9-]*-media-archive-[a-f0-9]{32}$/.test(plan.name))
|
|
115
|
+
throw new CliError("validation_error", "invalid archive timer paths");
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
await mkdir(plan.unitDirectory, { recursive: true, mode: 0o700 });
|
|
118
|
+
const folder = await realpath(plan.unitDirectory);
|
|
119
|
+
const lease = await acquireLock(join(folder, `.${plan.name}.lock`), { signal });
|
|
120
|
+
const beforeWrite = async () => {
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
if (!(await lease.held()))
|
|
123
|
+
throw new CliError("validation_error", "archive timer lost its writer lock");
|
|
124
|
+
};
|
|
125
|
+
const servicePath = resolve(folder, `${plan.name}.service`);
|
|
126
|
+
const timerPath = resolve(folder, `${plan.name}.timer`);
|
|
127
|
+
const owner = plan.service.split("\n")[0] ?? "";
|
|
128
|
+
const run = async (...args) => {
|
|
129
|
+
await beforeWrite();
|
|
130
|
+
const result = await system.run(["systemctl", "--user", ...args], env);
|
|
131
|
+
if (result.code !== 0)
|
|
132
|
+
throw new CliError("configuration_error", "systemctl could not change the archive timer; inspect its status");
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const service = await ownedUnit(servicePath, owner);
|
|
136
|
+
const timer = await ownedUnit(timerPath, owner);
|
|
137
|
+
const installed = service !== null && timer !== null;
|
|
138
|
+
if (action === "status") {
|
|
139
|
+
const active = installed
|
|
140
|
+
? await system.run(["systemctl", "--user", "is-active", `${plan.name}.timer`], env)
|
|
141
|
+
: undefined;
|
|
142
|
+
return { name: plan.name, installed, active: active?.code === 0, servicePath, timerPath };
|
|
143
|
+
}
|
|
144
|
+
if (action === "install") {
|
|
145
|
+
if ((service !== null && service !== plan.service) || (timer !== null && timer !== plan.timer))
|
|
146
|
+
throw new CliError("validation_error", "this destination already has a different schedule; stop and uninstall it first");
|
|
147
|
+
await beforeWrite();
|
|
148
|
+
if (service === null)
|
|
149
|
+
await writeFileAtomic(servicePath, plan.service, { overwrite: false, beforePublish: beforeWrite });
|
|
150
|
+
if (timer === null)
|
|
151
|
+
await writeFileAtomic(timerPath, plan.timer, { overwrite: false, beforePublish: beforeWrite });
|
|
152
|
+
await run("daemon-reload");
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (!installed)
|
|
156
|
+
throw new CliError("not_found", "archive timer is not installed");
|
|
157
|
+
if (action === "start")
|
|
158
|
+
await run("enable", "--now", `${plan.name}.timer`);
|
|
159
|
+
if (action === "stop")
|
|
160
|
+
await run("disable", "--now", `${plan.name}.timer`);
|
|
161
|
+
if (action === "uninstall") {
|
|
162
|
+
await beforeWrite();
|
|
163
|
+
const active = await system.run(["systemctl", "--user", "is-active", `${plan.name}.timer`], env);
|
|
164
|
+
const enabled = await system.run(["systemctl", "--user", "is-enabled", `${plan.name}.timer`], env);
|
|
165
|
+
const running = await system.run(["systemctl", "--user", "is-active", `${plan.name}.service`], env);
|
|
166
|
+
if (active.code !== 3 || enabled.code !== 1 || running.code !== 3)
|
|
167
|
+
throw new CliError("validation_error", "stop the archive timer and wait for its service to finish before uninstalling");
|
|
168
|
+
await beforeWrite();
|
|
169
|
+
await unlink(servicePath);
|
|
170
|
+
await unlink(timerPath);
|
|
171
|
+
await syncDirectory(folder);
|
|
172
|
+
await run("daemon-reload");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { name: plan.name, installed: action !== "uninstall", action, servicePath, timerPath };
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await lease.release();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
import { execFile } from "node:child_process";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { type Checkpoint, checkpointPath, clearCheckpoint, type Progress, progressLine, progressOf, readCheckpoint, saveCheckpoint, } from "./checkpoints.js";
|
|
2
|
+
export type { DailyArchiveTimer, DailyArchiveTimerInput } from "./daily-archive-timer.js";
|
|
3
|
+
export { dailyArchiveTimer, manageDailyArchiveTimer } from "./daily-archive-timer.js";
|
|
2
4
|
export { holdLock, type Lock, lockPath, readLock, releaseLock, servingProfiles, takeLock } from "./lock.js";
|
|
3
5
|
export { alive, carries, holdersOf } from "./processes.js";
|
|
4
6
|
export { listenRelay, RELAY_PROTOCOL, type RelayAddress, type RelayIo, type RelayRequest, relayCommand, relayPath, } from "./relay.js";
|
package/dist/background/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { checkpointPath, clearCheckpoint, progressLine, progressOf, readCheckpoint, saveCheckpoint, } from "./checkpoints.js";
|
|
2
|
+
export { dailyArchiveTimer, manageDailyArchiveTimer } from "./daily-archive-timer.js";
|
|
2
3
|
export { holdLock, lockPath, readLock, releaseLock, servingProfiles, takeLock } from "./lock.js";
|
|
3
4
|
export { alive, carries, holdersOf } from "./processes.js";
|
|
4
5
|
export { listenRelay, RELAY_PROTOCOL, relayCommand, relayPath, } from "./relay.js";
|
|
@@ -20,6 +20,12 @@ export const CONTRACT = 3;
|
|
|
20
20
|
export const commandsCommand = (app) => {
|
|
21
21
|
const command = annotate(new Command("commands"), { effects: NO_EFFECTS })
|
|
22
22
|
.description("every command by section, one line each; --json: the contract, one path per call")
|
|
23
|
+
.hook("preAction", (_command, action) => {
|
|
24
|
+
if (action.optsWithGlobals().jsonl)
|
|
25
|
+
throw new CliError("validation_error", "command discovery is one JSON object — use --json", {
|
|
26
|
+
reason: "output.one-object",
|
|
27
|
+
});
|
|
28
|
+
})
|
|
23
29
|
.argument("[path...]", "one command path, for example: search messages; inspect other groups in separate calls")
|
|
24
30
|
.action(function (path) {
|
|
25
31
|
const root = rootOf(this);
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export { reviewCommand } from "./messenger/review.js";
|
|
|
108
108
|
export { scheduleCommand } from "./messenger/schedule-command.js";
|
|
109
109
|
export { allHitItem, type HitItem, type MessageHitItem, messageHitItem, type NoteHitItem, noteHitItem, } from "./messenger/search-answers.js";
|
|
110
110
|
export { searchCommand } from "./messenger/search-command.js";
|
|
111
|
-
export { searchesCommand } from "./messenger/searches-command.js";
|
|
111
|
+
export { SEARCHES_QUERY, searchesCommand } from "./messenger/searches-command.js";
|
|
112
112
|
export { serveCommand, servingProfiles } from "./messenger/serve-command.js";
|
|
113
113
|
export { DAY_MS, FIRST_FETCH_MS, type MemberFetches, memberFetches } from "./messenger/serve-members.js";
|
|
114
114
|
export { STEMS_EVERY_MS, STEMS_SLICE_MS, type StemFills, stemFills } from "./messenger/serve-stems.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -103,7 +103,7 @@ export { reviewCommand } from "./messenger/review.js";
|
|
|
103
103
|
export { scheduleCommand } from "./messenger/schedule-command.js";
|
|
104
104
|
export { allHitItem, messageHitItem, noteHitItem, } from "./messenger/search-answers.js";
|
|
105
105
|
export { searchCommand } from "./messenger/search-command.js";
|
|
106
|
-
export { searchesCommand } from "./messenger/searches-command.js";
|
|
106
|
+
export { SEARCHES_QUERY, searchesCommand } from "./messenger/searches-command.js";
|
|
107
107
|
export { serveCommand, servingProfiles } from "./messenger/serve-command.js";
|
|
108
108
|
export { DAY_MS, FIRST_FETCH_MS, memberFetches } from "./messenger/serve-members.js";
|
|
109
109
|
export { STEMS_EVERY_MS, STEMS_SLICE_MS, stemFills } from "./messenger/serve-stems.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { INPUT_FIELD_REASONS } from "../errors/reasons.js";
|
|
2
|
+
export const showInputFields = (object, command) => {
|
|
3
|
+
if (!INPUT_FIELD_REASONS.some((reason) => reason.reason === object.error.reason))
|
|
4
|
+
return;
|
|
5
|
+
const fields = object.error.details?.fields;
|
|
6
|
+
if (!Array.isArray(fields) || fields.length === 0 || !fields.every((field) => typeof field === "string"))
|
|
7
|
+
return;
|
|
8
|
+
const options = new Map();
|
|
9
|
+
for (let at = command; at; at = at.parent)
|
|
10
|
+
for (const option of at.options)
|
|
11
|
+
if (!option.hidden && option.long && !options.has(option.attributeName()))
|
|
12
|
+
options.set(option.attributeName(), option.long);
|
|
13
|
+
const flags = [];
|
|
14
|
+
for (const field of fields) {
|
|
15
|
+
const flag = options.get(field);
|
|
16
|
+
if (!flag)
|
|
17
|
+
return;
|
|
18
|
+
if (!flags.includes(flag))
|
|
19
|
+
flags.push(flag);
|
|
20
|
+
}
|
|
21
|
+
const first = flags[0];
|
|
22
|
+
if (first === undefined)
|
|
23
|
+
return;
|
|
24
|
+
object.error.message = `${flags.join(", ")}: ${object.error.message}`;
|
|
25
|
+
object.error.param = first;
|
|
26
|
+
object.error.details = { ...object.error.details, fields: flags };
|
|
27
|
+
};
|
|
@@ -18,6 +18,7 @@ export const accountCommand = (messenger) => {
|
|
|
18
18
|
const account = new Command("account")
|
|
19
19
|
.description("the logged-in account")
|
|
20
20
|
.addCommand(annotate(new Command("list"), { effects: REMOTE_READ, examples: [`${tool} account list`] })
|
|
21
|
+
.summary("profiles on this computer and their signed-in accounts")
|
|
21
22
|
.description("every profile on this computer, and the account each is logged in as; asks the messenger nothing")
|
|
22
23
|
.action(async function () {
|
|
23
24
|
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
@@ -12,7 +12,9 @@ const SESSIONS_QUERY = unfilteredQuery("account_session", accountSession);
|
|
|
12
12
|
/** `account sessions …` — where else the account is logged in, not this tool's own `session`. */
|
|
13
13
|
export const accountSessionsCommand = (messenger) => {
|
|
14
14
|
const tool = `${messenger.app.command} personal`;
|
|
15
|
-
const sessions = new Command("sessions")
|
|
15
|
+
const sessions = new Command("sessions")
|
|
16
|
+
.summary("devices and apps signed in to this account")
|
|
17
|
+
.description(`where else this account is logged in — not \`${messenger.app.command} session\`, which is this tool's own login`);
|
|
16
18
|
withQuery(annotate(sessions.command("list"), { effects: REMOTE_READ, examples: [`${tool} account sessions list`] }), SESSIONS_QUERY)
|
|
17
19
|
.description("every device and app logged in to this account; nothing is ended")
|
|
18
20
|
.action(async function (words) {
|
|
@@ -58,6 +58,7 @@ export const readProfilePhoto = (messenger, path, env) => readUpload("photo", pa
|
|
|
58
58
|
const updateCommand = (messenger) => {
|
|
59
59
|
const keys = messenger.groupSettings ?? GROUP_SETTINGS;
|
|
60
60
|
const update = annotate(new Command("update"), { effects: REMOTE_WRITE })
|
|
61
|
+
.summary("change a group or channel's profile and settings")
|
|
61
62
|
.description("rename a group or channel, change its description, or turn one of its settings on or off")
|
|
62
63
|
.argument("<chat>", messenger.chatArgument)
|
|
63
64
|
.option("--title <title>", "the new name")
|
|
@@ -10,11 +10,16 @@ const PERSON = "person id — `contacts lookup` finds one — or part of a known
|
|
|
10
10
|
/** The address book's writes, in max-cli's words. */
|
|
11
11
|
export const contactWriteCommands = (messenger) => {
|
|
12
12
|
const simple = [
|
|
13
|
-
[
|
|
13
|
+
[
|
|
14
|
+
"add",
|
|
15
|
+
"add a person to your contacts — `contacts list` still shows only people you have a dialog with",
|
|
16
|
+
"add a person to your contacts",
|
|
17
|
+
],
|
|
14
18
|
["remove", "remove a person from your contacts; the chat stays, a name you gave them may not"],
|
|
15
19
|
["block", "stop a person from writing to you — they need not be a contact"],
|
|
16
20
|
["unblock", "let a blocked person write to you again"],
|
|
17
|
-
].map(([name, description]) => annotate(new Command(name), { effects: REMOTE_WRITE })
|
|
21
|
+
].map(([name, description, summary]) => annotate(new Command(name), { effects: REMOTE_WRITE })
|
|
22
|
+
.summary(summary ?? description)
|
|
18
23
|
.description(description)
|
|
19
24
|
.argument("<person>", PERSON)
|
|
20
25
|
.action(async function (person) {
|
|
@@ -23,10 +28,11 @@ export const contactWriteCommands = (messenger) => {
|
|
|
23
28
|
renderAnswer(this, context, itemOf(snakeKeys(done)));
|
|
24
29
|
}));
|
|
25
30
|
const rename = annotate(new Command("rename"), { effects: REMOTE_WRITE })
|
|
31
|
+
.summary("change a contact's name in the messenger address book")
|
|
26
32
|
.description("rename the contact in the messenger address book; use contacts alias for a private local name")
|
|
27
33
|
.argument("<person>", PERSON)
|
|
28
34
|
.argument("<first-name>", "the name you want to see for them")
|
|
29
|
-
.argument("[last-name]")
|
|
35
|
+
.argument("[last-name]", "the last name you want to see for them")
|
|
30
36
|
.action(async function (person, firstName, lastName) {
|
|
31
37
|
const context = messengerContext(this, messenger);
|
|
32
38
|
const renamed = await context.withServices((services) => services.people.rename(person, firstName, lastName));
|
|
@@ -65,6 +65,7 @@ export const storeCommand = (messenger) => {
|
|
|
65
65
|
* chat's very first message is not recorded, so it is not claimed: the stretches held are shown.
|
|
66
66
|
*/
|
|
67
67
|
const statusCommand = (messenger) => annotate(new Command("status"), { effects: LOCAL_READ, mcp: false })
|
|
68
|
+
.summary("what the local archive holds and what is missing")
|
|
68
69
|
.description("per chat: messages stored, the oldest and newest, and the stretches held completely")
|
|
69
70
|
.argument("[chat]", messenger.chatArgument)
|
|
70
71
|
.action(async function (chat) {
|
|
@@ -132,8 +133,9 @@ const clearCommand = (messenger) => annotate(new Command("clear"), { effects: LO
|
|
|
132
133
|
* folder, and each later run into the same folder adds only what changed.
|
|
133
134
|
*/
|
|
134
135
|
const exportCommand = (messenger) => destroysNothing(annotate(new Command("export"), { effects: LOCAL_WRITE, mcp: false }))
|
|
136
|
+
.summary("export stored messages to files")
|
|
135
137
|
.description("a chat's stored messages, oldest first: --jsonl one message per line, --json the list envelope of messages; never asks the messenger")
|
|
136
|
-
.argument("[
|
|
138
|
+
.argument("[chat...]", `${messenger.chatArgument}; several with --output-dir`)
|
|
137
139
|
.option("--format <format>", "jsonl: one message per line, the default with --output; markdown: a transcript with a heading per day, replies and forwards quoted")
|
|
138
140
|
.option("--since-time <time>", "only from this ISO 8601 time, or 30m / 2h / 1d ago, on")
|
|
139
141
|
.option("--output <file>", "write JSON lines, or the transcript, to this new file, readable only by you")
|
|
@@ -15,6 +15,7 @@ import { withPaging } from "../paging.js";
|
|
|
15
15
|
import { readsStdin } from "../stdin-field.js";
|
|
16
16
|
import { nextStep } from "../steps.js";
|
|
17
17
|
import { messengerContext, refuseLocalWrite } from "./context.js";
|
|
18
|
+
import { addMediaArchiveCommands } from "./media-archive-command.js";
|
|
18
19
|
import { stopOnSignal } from "./patience.js";
|
|
19
20
|
import { readAll } from "./stdin.js";
|
|
20
21
|
const count = (value) => {
|
|
@@ -34,7 +35,11 @@ const summary = (run) => [
|
|
|
34
35
|
]
|
|
35
36
|
.filter(Boolean)
|
|
36
37
|
.join(", ");
|
|
37
|
-
const extractCommand = (messenger) => annotate(new Command("extract"), {
|
|
38
|
+
const extractCommand = (messenger) => annotate(new Command("extract"), {
|
|
39
|
+
effects: FETCH,
|
|
40
|
+
examples: [`${messenger.app.command} personal attachments extract --chat "Book club" --limit 10`],
|
|
41
|
+
})
|
|
42
|
+
.summary("extract text from retained message files")
|
|
38
43
|
.description("read the text of downloaded files — text, PDF/DOCX text layers, ODT/ODS/XLSX/PPTX/EPUB — into the local store, where search finds them")
|
|
39
44
|
.option("--chat <chat>", `only this chat's files; ${messenger.chatArgument}`)
|
|
40
45
|
.option("--from-dir <dir>", "match files in this nonrecursive directory; needs --chat")
|
|
@@ -121,13 +126,21 @@ const extractCommand = (messenger) => annotate(new Command("extract"), { effects
|
|
|
121
126
|
});
|
|
122
127
|
const row = ({ message, position, name, local_path, extraction }) => `${message} #${position} ${name ?? ""} ${extraction ? `${extraction.origin} ${extraction.chars}` : "no text"} ` +
|
|
123
128
|
(local_path ?? "not downloaded");
|
|
124
|
-
const listCommand = (messenger) => withQuery(withPaging(annotate(new Command("list"), {
|
|
129
|
+
const listCommand = (messenger) => withQuery(withPaging(annotate(new Command("list"), {
|
|
130
|
+
effects: REMOTE_READ,
|
|
131
|
+
examples: [`${messenger.app.command} personal attachments list --limit 10`],
|
|
132
|
+
})
|
|
133
|
+
.summary("stored message files, saved paths and text status")
|
|
134
|
+
.description("files of stored messages, newest first: where each was saved and whether its text is held — never the text")), ATTACHMENT_QUERY).action(async function (words) {
|
|
125
135
|
const context = messengerContext(this, messenger);
|
|
126
136
|
const query = listQuery(typedQuery(words), ATTACHMENT_QUERY, "attachments list");
|
|
127
137
|
const found = await context.withServices((services) => services.attachments.list({ ...pageOf(context.settings), ...(query ? { query } : {}) }));
|
|
128
138
|
renderAnswer(this, context, pagedList("attachments list", context.settings, found), () => found.items.length === 0 ? "" : `${found.items.map(row).join("\n")}\n`);
|
|
129
139
|
});
|
|
130
|
-
const showCommand = (messenger) => annotate(new Command("show"), {
|
|
140
|
+
const showCommand = (messenger) => annotate(new Command("show"), {
|
|
141
|
+
effects: REMOTE_READ,
|
|
142
|
+
examples: [`${messenger.app.command} personal attachments show "Book club" 101 --attachment 1`],
|
|
143
|
+
})
|
|
131
144
|
.description("read a bounded chunk of one retained attachment; JSON includes base64 bytes")
|
|
132
145
|
.argument("<chat>", `${messenger.chatArgument}; or a msg: locator alone`)
|
|
133
146
|
.argument("[message]", "the message id")
|
|
@@ -159,10 +172,14 @@ const showCommand = (messenger) => annotate(new Command("show"), { effects: REMO
|
|
|
159
172
|
});
|
|
160
173
|
const textCommand = (messenger) => {
|
|
161
174
|
const text = new Command("text").description("the text of one file, as an agent read it");
|
|
162
|
-
readsStdin(annotate(text.command("set"), {
|
|
175
|
+
readsStdin(annotate(text.command("set"), {
|
|
176
|
+
effects: LOCAL_WRITE,
|
|
177
|
+
examples: [`${messenger.app.command} personal attachments text set "Book club" 101 --text-file ./transcript.txt`],
|
|
178
|
+
}), {
|
|
163
179
|
name: "text",
|
|
164
180
|
description: "the text of the file",
|
|
165
181
|
})
|
|
182
|
+
.summary("save the text read from one message file")
|
|
166
183
|
.description("keep the text an agent read from a file — a scan, a photo — so search finds it; nothing is sent")
|
|
167
184
|
.argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
|
|
168
185
|
.argument("[message]", "the message id")
|
|
@@ -184,9 +201,14 @@ const textCommand = (messenger) => {
|
|
|
184
201
|
return text;
|
|
185
202
|
};
|
|
186
203
|
/** Files of stored messages: what is in them, for `content:` in a search. Nothing here is sent. */
|
|
187
|
-
export const attachmentsCommand = (messenger) =>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
export const attachmentsCommand = (messenger) => {
|
|
205
|
+
const attachments = new Command("attachments")
|
|
206
|
+
.summary("stored message files and their retained text")
|
|
207
|
+
.description("the files of stored messages: their text in the local store, where search finds it")
|
|
208
|
+
.addCommand(extractCommand(messenger))
|
|
209
|
+
.addCommand(listCommand(messenger))
|
|
210
|
+
.addCommand(showCommand(messenger))
|
|
211
|
+
.addCommand(textCommand(messenger));
|
|
212
|
+
addMediaArchiveCommands(attachments, messenger);
|
|
213
|
+
return attachments;
|
|
214
|
+
};
|
|
@@ -39,6 +39,7 @@ export const syncCommand = (messenger) => {
|
|
|
39
39
|
effects: FETCH,
|
|
40
40
|
cooperative: true,
|
|
41
41
|
})
|
|
42
|
+
.summary("fetch and save a chat's message history")
|
|
42
43
|
.description("bring a chat's history in the local store up to date, newest first; run it again to continue; --all syncs every chat")
|
|
43
44
|
.argument("[chat]", messenger.chatArgument)
|
|
44
45
|
.option("--all", `every chat, most recently active first — what search needs; the last ${ALL_SINCE} unless --since-time or --last`)
|
|
@@ -224,6 +225,7 @@ export const jobsCommand = (messenger) => {
|
|
|
224
225
|
context.renderer.note(`no ${query ? "matching " : ""}background sync jobs for this profile`);
|
|
225
226
|
});
|
|
226
227
|
annotate(command.command("show"), { effects: LOCAL_READ })
|
|
228
|
+
.summary("one archive job's status and log")
|
|
227
229
|
.description("one background job — the newest when none is named — and what the store now holds of its chat")
|
|
228
230
|
.argument("[job]", "the job id `messages sync --background` printed")
|
|
229
231
|
.action(async function (id) {
|
|
@@ -236,6 +238,7 @@ export const jobsCommand = (messenger) => {
|
|
|
236
238
|
renderAnswer(this, context, itemOf(snakeKeys({ ...jobDetails(job), ...(held ? { held } : {}), log: job.log })));
|
|
237
239
|
});
|
|
238
240
|
destroysNothing(annotate(command.command("cancel"), { effects: LOCAL_WRITE, mcp: false }))
|
|
241
|
+
.summary("cancel a queued or running archive job")
|
|
239
242
|
.description("stop a running background job after its current page; a later sync resumes where it stopped")
|
|
240
243
|
.argument("<job>", "the job id")
|
|
241
244
|
.action(function (id) {
|
|
@@ -249,6 +252,7 @@ export const jobsCommand = (messenger) => {
|
|
|
249
252
|
renderAnswer(this, context, itemOf({ job: job.id, cancelled: true }));
|
|
250
253
|
});
|
|
251
254
|
destroysNothing(annotate(command.command("retry"), { effects: LOCAL_WRITE, mcp: false }))
|
|
255
|
+
.summary("retry an archive job")
|
|
252
256
|
.description("start a partial, failed or died job again, as a new job; the sync resumes where the store stopped")
|
|
253
257
|
.argument("[job]", "the job id")
|
|
254
258
|
.option("--failed", "every chat whose newest job partial, failed or died")
|
|
@@ -37,6 +37,7 @@ export const chatsCommand = (messenger) => {
|
|
|
37
37
|
// Where the history is pushed, the store answers, --offline too.
|
|
38
38
|
...(messenger.history === "store" ? {} : { offline: false }),
|
|
39
39
|
})
|
|
40
|
+
.summary("stored membership and title changes")
|
|
40
41
|
.description("who joined, left, was added or removed, and by whom — from the chat's service messages; " +
|
|
41
42
|
`the last ${EVENTS_DAYS} days unless the query names time`)
|
|
42
43
|
.argument("<chat>", messenger.chatArgument), { output: { entity: "chat_event" } }), EVENTS_QUERY).action(async function (chat, words) {
|
|
@@ -15,6 +15,7 @@ import { messengerContext } from "./context.js";
|
|
|
15
15
|
export const membersCommand = (messenger) => {
|
|
16
16
|
const members = new Command("members").description("who is in a group");
|
|
17
17
|
members.addCommand(annotate(withQuery(withPaging(annotate(new Command("list"), { effects: REMOTE_READ })
|
|
18
|
+
.summary("group members and their roles")
|
|
18
19
|
.description("everyone in a group, a page at a time, with their role and when they were last seen")
|
|
19
20
|
.argument("<chat>", messenger.chatArgument)), MEMBERS_QUERY), { output: { entity: "member" } }).action(async function (chat, words) {
|
|
20
21
|
const context = messengerContext(this, messenger);
|
|
@@ -23,6 +24,7 @@ export const membersCommand = (messenger) => {
|
|
|
23
24
|
renderAnswer(this, context, body, lines(({ id, name, username, role }) => [id, name ?? "", username ? `@${username}` : "", role ?? ""].join(" ")));
|
|
24
25
|
}));
|
|
25
26
|
members.addCommand(withQuery(annotate(annotate(new Command("audit"), { effects: REMOTE_READ, offline: false })
|
|
27
|
+
.summary("members that look like bots, with reasons")
|
|
26
28
|
.description("members that look like bots, each with its reasons — read from the member list and the local store; " +
|
|
27
29
|
"never one request per person, and it removes nobody")
|
|
28
30
|
.argument("<chat>", messenger.chatArgument)
|
|
@@ -54,6 +56,7 @@ export const membersCommand = (messenger) => {
|
|
|
54
56
|
].join(" ")));
|
|
55
57
|
}));
|
|
56
58
|
members.addCommand(withQuery(annotate(annotate(new Command("history"), { effects: LOCAL_READ })
|
|
59
|
+
.summary("stored membership changes")
|
|
57
60
|
.description("who joined, who left and whose profile changed, oldest first — what chats members fetch recorded in the " +
|
|
58
61
|
"local store; never asks the messenger")
|
|
59
62
|
.argument("<chat>", messenger.chatArgument), { output: { entity: "chat_event" } }), HISTORY_QUERY).action(async function (chat, words) {
|
|
@@ -69,6 +72,7 @@ export const membersCommand = (messenger) => {
|
|
|
69
72
|
].join(" ")));
|
|
70
73
|
}));
|
|
71
74
|
const fetch = annotate(new Command("fetch"), { effects: FETCH, offline: false })
|
|
75
|
+
.summary("save the group's current membership")
|
|
72
76
|
.description("read a group's whole member list into the local store's member history: who joined, who left, daily " +
|
|
73
77
|
"counts and profile changes; someone is recorded as gone only when every member was read")
|
|
74
78
|
.argument("<chat>", messenger.chatArgument);
|
|
@@ -9,6 +9,7 @@ import { messengerContext } from "./context.js";
|
|
|
9
9
|
import { chatStatsAnswer, MEMBERS_UNRECORDED, refuseJsonl } from "./stats-answers.js";
|
|
10
10
|
const PERIODS = ["day", "week"];
|
|
11
11
|
export const statsCommand = (messenger) => annotate(new Command("show"), { effects: REMOTE_READ, output: { entity: "chat_stats" } })
|
|
12
|
+
.summary("activity and participation in one chat")
|
|
12
13
|
.description("a group's or channel's numbers for a period: messages, active members, replies, reactions, questions answered, " +
|
|
13
14
|
`joins and leaves — counted from the local store; ${messenger.history === "store" ? "joins and leaves too" : "joins and leaves are asked of the messenger"}`)
|
|
14
15
|
.argument("<chat>", messenger.chatArgument)
|