@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
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,39 @@ 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.
|
|
9
41
|
|
|
10
42
|
## 0.396.0 — 23.10.2026
|
|
11
43
|
|
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";
|
|
@@ -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) => {
|
|
@@ -38,6 +39,7 @@ const extractCommand = (messenger) => annotate(new Command("extract"), {
|
|
|
38
39
|
effects: FETCH,
|
|
39
40
|
examples: [`${messenger.app.command} personal attachments extract --chat "Book club" --limit 10`],
|
|
40
41
|
})
|
|
42
|
+
.summary("extract text from retained message files")
|
|
41
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")
|
|
42
44
|
.option("--chat <chat>", `only this chat's files; ${messenger.chatArgument}`)
|
|
43
45
|
.option("--from-dir <dir>", "match files in this nonrecursive directory; needs --chat")
|
|
@@ -127,7 +129,9 @@ const row = ({ message, position, name, local_path, extraction }) => `${message}
|
|
|
127
129
|
const listCommand = (messenger) => withQuery(withPaging(annotate(new Command("list"), {
|
|
128
130
|
effects: REMOTE_READ,
|
|
129
131
|
examples: [`${messenger.app.command} personal attachments list --limit 10`],
|
|
130
|
-
})
|
|
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) {
|
|
131
135
|
const context = messengerContext(this, messenger);
|
|
132
136
|
const query = listQuery(typedQuery(words), ATTACHMENT_QUERY, "attachments list");
|
|
133
137
|
const found = await context.withServices((services) => services.attachments.list({ ...pageOf(context.settings), ...(query ? { query } : {}) }));
|
|
@@ -175,6 +179,7 @@ const textCommand = (messenger) => {
|
|
|
175
179
|
name: "text",
|
|
176
180
|
description: "the text of the file",
|
|
177
181
|
})
|
|
182
|
+
.summary("save the text read from one message file")
|
|
178
183
|
.description("keep the text an agent read from a file — a scan, a photo — so search finds it; nothing is sent")
|
|
179
184
|
.argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
|
|
180
185
|
.argument("[message]", "the message id")
|
|
@@ -196,9 +201,14 @@ const textCommand = (messenger) => {
|
|
|
196
201
|
return text;
|
|
197
202
|
};
|
|
198
203
|
/** Files of stored messages: what is in them, for `content:` in a search. Nothing here is sent. */
|
|
199
|
-
export const attachmentsCommand = (messenger) =>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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)
|
|
@@ -7,7 +7,9 @@ import { lines, wholeList } from "./chat-answers.js";
|
|
|
7
7
|
import { messengerContext, refuseLocalWrite } from "./context.js";
|
|
8
8
|
/** The chats whose member lists `serve` fetches daily; kept in the local store, never sent. */
|
|
9
9
|
export const trackingCommand = (messenger) => {
|
|
10
|
-
const tracking = new Command("tracking")
|
|
10
|
+
const tracking = new Command("tracking")
|
|
11
|
+
.summary("chats whose member lists are fetched daily")
|
|
12
|
+
.description("the chats whose member lists serve fetches daily into the local store — chats members fetch --track adds one");
|
|
11
13
|
annotate(tracking.command("list"), { effects: LOCAL_READ })
|
|
12
14
|
.description("every tracked chat: since when, and its last member count")
|
|
13
15
|
.action(async function () {
|
|
@@ -22,6 +24,7 @@ export const trackingCommand = (messenger) => {
|
|
|
22
24
|
].join(" ")));
|
|
23
25
|
});
|
|
24
26
|
annotate(tracking.command("show"), { effects: LOCAL_READ })
|
|
27
|
+
.summary("one chat's member tracking and daily counts")
|
|
25
28
|
.description(`one chat: whether it is tracked, and its member count per day for the last ${TRACKED_DAYS} days`)
|
|
26
29
|
.argument("<chat>", messenger.chatArgument)
|
|
27
30
|
.action(async function (chat) {
|
|
@@ -21,7 +21,7 @@ const SHELLS = ["zsh", "bash", "fish", "powershell"];
|
|
|
21
21
|
*/
|
|
22
22
|
export const completeCommand = (messenger, config, options = {}) => annotate(new Command("complete"), { effects: LOCAL_READ, mcp: false })
|
|
23
23
|
.description(`shell completion: \`${messenger.app.command} complete zsh\` prints the script to source`)
|
|
24
|
-
.argument("[words...]")
|
|
24
|
+
.argument("[words...]", "zsh for the shell script, or command words to complete")
|
|
25
25
|
.allowUnknownOption()
|
|
26
26
|
.helpOption(false)
|
|
27
27
|
.action(async function (words) {
|
|
@@ -68,7 +68,8 @@ export const contactsCommand = (messenger) => {
|
|
|
68
68
|
}));
|
|
69
69
|
renderAnswer(this, context, itemOf(contactAnswer(account, card)));
|
|
70
70
|
}));
|
|
71
|
-
annotate(contacts.command("profile"), { effects: REMOTE_READ })
|
|
71
|
+
annotate(contacts.command("profile"), { effects: REMOTE_READ, output: { entity: "contact_profile" } })
|
|
72
|
+
.summary("a person's profile from the messenger")
|
|
72
73
|
.description("everything the messenger says about one person — handles, flags, last seen, when they registered — and " +
|
|
73
74
|
"how many of their messages the store holds in each chat you share, the first and the last, and the " +
|
|
74
75
|
"earlier names and usernames the store saw them with")
|
|
@@ -86,6 +87,7 @@ export const contactsCommand = (messenger) => {
|
|
|
86
87
|
}));
|
|
87
88
|
});
|
|
88
89
|
const contextCommand = annotate(contacts.command("context"), { effects: REMOTE_READ })
|
|
90
|
+
.summary("what the local archive knows about a person")
|
|
89
91
|
.description("what the store holds about one person, in every messenger linked to them: shared chats, the last " +
|
|
90
92
|
"messages each way, their recent messages, where others mentioned them — never connects")
|
|
91
93
|
.argument("<person>", "their id, @username, or part of their name")
|
|
@@ -167,6 +169,7 @@ export const contactsCommand = (messenger) => {
|
|
|
167
169
|
}));
|
|
168
170
|
});
|
|
169
171
|
annotate(contacts.command("timeline"), { effects: REMOTE_READ })
|
|
172
|
+
.summary("one person's activity across their linked identities")
|
|
170
173
|
.description("everything one person took part in, in every messenger linked to them — messages they wrote or were " +
|
|
171
174
|
"mentioned in, chats, mail, meetings, tasks — newest first, from the store; never connects")
|
|
172
175
|
.argument("<person>", "their id, @username, or part of their name")
|
|
@@ -208,6 +211,7 @@ export const contactsCommand = (messenger) => {
|
|
|
208
211
|
}));
|
|
209
212
|
});
|
|
210
213
|
annotate(contacts.command("check"), { effects: REMOTE_READ })
|
|
214
|
+
.summary("check a person's profile and messages for signs of abuse")
|
|
211
215
|
.description(registriesCover(messenger.provider)
|
|
212
216
|
? "whether one person looks like a bot, a fake or a spammer: their profile, what they wrote in the store, " +
|
|
213
217
|
`and the public ban lists (${Object.values(REGISTRIES)
|
|
@@ -235,6 +239,7 @@ export const contactsCommand = (messenger) => {
|
|
|
235
239
|
renderAnswer(this, context, itemOf(snakeKeys(answer)));
|
|
236
240
|
});
|
|
237
241
|
annotate(contacts.command("link"), { effects: LOCAL_WRITE })
|
|
242
|
+
.summary("link identities that belong to one person")
|
|
238
243
|
.description("record that two people in the store are one person — the same name is never enough")
|
|
239
244
|
.argument("<person>", "their id, @username, or part of their name")
|
|
240
245
|
.argument("<other>", "the same in another messenger of the store, as <messenger>:<person> — max:Ana; me: it is you, as zoom:<id> me")
|
|
@@ -255,7 +260,8 @@ export const contactsCommand = (messenger) => {
|
|
|
255
260
|
* ⚠ **The number is asked for or piped, never an argument**: argv is read by `ps` and kept by shell
|
|
256
261
|
* history, and a phone number is personal data.
|
|
257
262
|
*/
|
|
258
|
-
annotate(contacts.command("lookup"), { effects: REMOTE_READ })
|
|
263
|
+
annotate(contacts.command("lookup"), { effects: REMOTE_READ, output: { entity: "contact_lookup" } })
|
|
264
|
+
.summary("find an account by a phone number")
|
|
259
265
|
.description("who has this phone number — asks for it, or reads it from stdin; never an argument")
|
|
260
266
|
// Commander's own refusal would repeat the number on stderr.
|
|
261
267
|
.allowExcessArguments()
|
|
@@ -267,7 +273,7 @@ export const contactsCommand = (messenger) => {
|
|
|
267
273
|
const phone = phoneOf(await readSecret("phone number: ", { ...secretInputOf(this), echo: true }));
|
|
268
274
|
renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.people.lookup(phone)))));
|
|
269
275
|
});
|
|
270
|
-
annotate(contacts.command("sync"), { effects: FETCH })
|
|
276
|
+
annotate(contacts.command("sync"), { effects: FETCH, output: { entity: "contact_sync" } })
|
|
271
277
|
.description("take the whole contact list from the messenger into the local store")
|
|
272
278
|
.action(async function () {
|
|
273
279
|
const context = messengerContext(this, messenger);
|