@wirecat/cli-core 0.397.0 → 0.398.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +12 -9
- package/dist/cli/bot/admins.js +2 -2
- package/dist/cli/bot/command.js +2 -2
- package/dist/cli/bot/setup.js +3 -3
- package/dist/cli/commands-command.js +1 -1
- package/dist/cli/config-command.js +1 -1
- package/dist/cli/guide.js +0 -1
- package/dist/cli/help/command-help.d.ts +9 -2
- package/dist/cli/help/command-help.js +22 -4
- package/dist/cli/help/first-screen.js +3 -3
- package/dist/cli/messenger/account-command.js +2 -1
- package/dist/cli/messenger/archive-commands.js +2 -2
- package/dist/cli/messenger/backfill-command.js +3 -3
- package/dist/cli/messenger/catch-up-answers.d.ts +2 -0
- package/dist/cli/messenger/catch-up-answers.js +5 -1
- package/dist/cli/messenger/chats-command.js +2 -1
- package/dist/cli/messenger/complete-command.js +5 -1
- package/dist/cli/messenger/contacts-command.js +2 -1
- package/dist/cli/messenger/context.d.ts +5 -0
- package/dist/cli/messenger/doctor-command.js +2 -2
- package/dist/cli/messenger/flood-command.js +1 -1
- package/dist/cli/messenger/inbox.js +1 -0
- package/dist/cli/messenger/lacks.d.ts +7 -0
- package/dist/cli/messenger/lacks.js +18 -1
- package/dist/cli/messenger/media-archive-move-command.js +0 -1
- package/dist/cli/messenger/media-archive-timer-command.js +2 -2
- package/dist/cli/messenger/messages-command.js +2 -1
- package/dist/cli/messenger/models-command.js +5 -2
- package/dist/cli/messenger/replies-command.js +1 -1
- package/dist/cli/messenger/review.js +7 -2
- package/dist/cli/messenger/schedule-command.js +3 -3
- package/dist/cli/messenger/search-command.js +2 -2
- package/dist/cli/messenger/serve-command.js +5 -1
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/server-command.js +12 -3
- package/dist/cli/messenger/store-maintenance-command.js +3 -3
- package/dist/cli/messenger/watch-command.js +5 -1
- package/dist/cli/runs/command.js +2 -3
- package/dist/cli/skill-command.js +1 -1
- package/dist/domain/models.d.ts +2 -0
- package/dist/services/deps.d.ts +1 -1
- package/dist/services/deps.js +1 -1
- package/dist/services/inbox.js +5 -5
- package/dist/services/search-kind.js +1 -1
- package/package.json +2 -2
- package/parity.json +66 -192
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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
|
+
## 0.398.0 — 23.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Top-level summaries, listeners and shell completion include examples in help and command discovery.
|
|
13
|
+
|
|
14
|
+
- Clearing a remembered flood hold includes an example in help and command discovery.
|
|
15
|
+
|
|
16
|
+
- **`Messenger.never`**: the adapter methods a messenger can never have, as opposed to `lacks`, what it does not have
|
|
17
|
+
yet. Their commands refuse as before and are also left out of help, `commands --json` and MCP. A command a
|
|
18
|
+
messenger only `lacks` stays in sight. For WhatsApp: `endOtherSessions` (only the phone ends other devices) and
|
|
19
|
+
`removeContact`.
|
|
20
|
+
|
|
21
|
+
- **`review --offline` reads the local store** instead of refusing, for every messenger. It shows only what was
|
|
22
|
+
already synced and says so (`missing` in JSON, a warning on the terminal); its next step starts from the same
|
|
23
|
+
point, and `--new` keeps its saved point, so an online review still sees what was never synced.
|
|
24
|
+
|
|
25
|
+
- **MCP offers more commands**, each limited by the profile's permissions: `runs list|show|path`, `store status|info|check|export|reindex`,
|
|
26
|
+
`store jobs cancel|retry|clear`, `attachments move`, `attachments jobs status`, `config show`, `commands schema`, `guide`,
|
|
27
|
+
`skill show`, `doctor report`, the `models` lists, `replies status|test|consents show`, `schedule list`,
|
|
28
|
+
`server status|logs`, and on the bot server `list`, `chats leave`, `chats admins add|remove`, `commands set|clear` and
|
|
29
|
+
`webhooks list|set|delete`. What stays off and why is in [the messenger rules](docs/dev/STANDARD.md#mcp).
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
|
|
33
|
+
- `@modelcontextprotocol/server` 2.3.0 clears the three high-severity advisories `npm audit` reported for every
|
|
34
|
+
WireCat CLI.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **A pasted help example runs.** The `Example:` line of an account command's `--help`, generated or written, names
|
|
39
|
+
the profile `personal` (`tg personal messages list Team`), so it no longer fails with "name a profile".
|
|
40
|
+
|
|
41
|
+
- `search mail` with no mail in the store points at `memo sync --only mail`; it named a `--mail` option memo does not have.
|
|
42
|
+
|
|
43
|
+
- `parity.json` matches tg and max: `guide`, `mentions`, `runs search`, `tasks reject` and `--brief`/`--full` are in
|
|
44
|
+
both, and each gap planned for tg names its tg-cli backlog id.
|
|
45
|
+
|
|
46
|
+
- `search all --help` says the messenger's server is asked by default and names `--backend archive` and `--offline`
|
|
47
|
+
to keep it to the local store.
|
|
48
|
+
|
|
8
49
|
## 0.397.0 — 23.10.2026
|
|
9
50
|
|
|
10
51
|
### Changed — may break callers
|
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# @wirecat/cli-core
|
|
6
6
|
|
|
7
|
-
The engine behind the WireCat command-line tools for Telegram, MAX, mail, notes
|
|
7
|
+
The engine behind the WireCat command-line tools for Telegram, MAX, WhatsApp, mail, notes, meetings and Google
|
|
8
|
+
Drive. Everything that
|
|
8
9
|
does not depend on one messenger lives here: the data model, a local SQLite store with full-text and semantic
|
|
9
10
|
search, a Lucene query language, the shared commands, an MCP server for AI agents, and the guard around every
|
|
10
11
|
write. A messenger plugs in as one adapter and gets all of it.
|
|
@@ -36,6 +37,7 @@ write. A messenger plugs in as one adapter and gets all of it.
|
|
|
36
37
|
| [Testing](docs/dev/TESTING.md) · [Parity audit](docs/dev/PARITY-AUDIT.md) | The sandbox, the coverage floor, the checks across tg and max |
|
|
37
38
|
| [Tool boundaries](docs/dev/TOOL-BOUNDARIES.md) | Which tool owns which command |
|
|
38
39
|
| [API reference](docs/REFERENCE.md) | Every entry point and the detailed contract of each feature |
|
|
40
|
+
| [Roadmap](docs/roadmap.md) | What is being built next for developers on this package |
|
|
39
41
|
| [Changelog](CHANGELOG.md) | What each version changed, and what may break callers |
|
|
40
42
|
|
|
41
43
|
## What it is
|
|
@@ -72,7 +74,7 @@ It runs on Node 22.16+ and 24+, and on Bun.
|
|
|
72
74
|
|
|
73
75
|
```mermaid
|
|
74
76
|
flowchart TB
|
|
75
|
-
tools["tg · max · memo · zm · drive"] --> interface
|
|
77
|
+
tools["tg · max · wa · memo · zm · drive"] --> interface
|
|
76
78
|
subgraph core ["@wirecat/cli-core"]
|
|
77
79
|
interface["Interface: CLI commands and MCP tools"] --> services["Services: one use case each"]
|
|
78
80
|
services --> ports["Ports: MessengerAdapter · MeetingSource · TaskStore"]
|
|
@@ -88,7 +90,7 @@ Five layers, and each calls only the layers below it: domain, adapters behind a
|
|
|
88
90
|
|
|
89
91
|
- **The CLI and the MCP server are two adapters over the same services.** A command and its MCP tool answer with
|
|
90
92
|
the same data, because one service produces it.
|
|
91
|
-
- **A port with one required core and optional groups.** `MessengerCore` is what every messenger must do.
|
|
93
|
+
- **A port with one required core and optional groups.** `MessengerCore` is what every messenger must do. The other
|
|
92
94
|
groups — server reads, polls, forums, reactions and the rest — are optional and reached through `capability()`.
|
|
93
95
|
A messenger without a group refuses the command before connecting; it never crashes and never silently drops an
|
|
94
96
|
option.
|
|
@@ -116,8 +118,9 @@ code and its document must agree. A search plan that reads an index once per mes
|
|
|
116
118
|
- **One query language for every list and every source.** A TypeScript port of the Apache Lucene 9.12.3 parsers,
|
|
117
119
|
with no runtime parser dependency. Phrases, `AND` / `OR` / `NOT`, `+required`, grouping, field groups like
|
|
118
120
|
`from:(alice OR bob)`, ranges `date:[2026-01-01 TO 2026-02-01}`, comparisons `size>10MB`, wildcards and regular
|
|
119
|
-
expressions. 27 fields: sender, chat, date, file name, MIME type, transcript, subject, label and more.
|
|
120
|
-
|
|
121
|
+
expressions. 27 fields: sender, chat, date, file name, MIME type, transcript, subject, label and more.
|
|
122
|
+
Proximity `"invoice paid"~3` finds the words close together in one message; the fuzzy operator `word~1` is
|
|
123
|
+
refused with an error that names the alternative.
|
|
121
124
|
[Syntax](docs/search/query-language.md) · [grammar](docs/search/query-language-spec.md)
|
|
122
125
|
- **Any three letters inside a word.** An FTS5 trigram index finds `port` inside `report`. A
|
|
123
126
|
second, word index ranks by BM25.
|
|
@@ -211,8 +214,9 @@ they deliberately differ. A rule changes in its own pull request, before the cod
|
|
|
211
214
|
- **Built for agents.** `--no-input` never prompts. Input and output are bounded (16 MiB in, 4 MiB out) and fail
|
|
212
215
|
visibly instead of truncating. One-shot commands time out after 30 seconds and close what they hold. `--dry-run`
|
|
213
216
|
previews any write.
|
|
214
|
-
- **The tool never picks an account.**
|
|
215
|
-
|
|
217
|
+
- **The tool never picks an account.** Every account command names its profile — the first word in tg, max and
|
|
218
|
+
wa, an option such as `--account` or `--profile` in the others; without one, the command stops and lists the
|
|
219
|
+
choices — even when there is only one.
|
|
216
220
|
- **Run records keep ids and timings, never content.** A failure is kept by default, so `runs show` can explain it
|
|
217
221
|
without anyone sending a log full of messages.
|
|
218
222
|
- **Tests that cannot touch real data.** A sandbox keeps every test away from the owner's store, and no test may
|
|
@@ -253,8 +257,7 @@ describe("the adapter keeps the port's promises", () => {
|
|
|
253
257
|
**Before 1.0, any release may break a caller.** Releases are frequent, sometimes several a day, and any of them
|
|
254
258
|
may change any entry point. Every such change is listed in the [changelog](CHANGELOG.md) under "Changed — may break
|
|
255
259
|
callers", with what to change. tg-cli and max-cli pin an exact version and move to a new one in their own pull request.
|
|
256
|
-
Pin an exact version too
|
|
257
|
-
release has.
|
|
260
|
+
Pin an exact version too.
|
|
258
261
|
The full list of entry points and what each holds is in the [API reference](docs/REFERENCE.md).
|
|
259
262
|
|
|
260
263
|
⚠ **Errors are recognised by shape, not by class** (`isCliFailure`). A package linked during development brings its
|
package/dist/cli/bot/admins.js
CHANGED
|
@@ -35,7 +35,7 @@ export const botAdminsCommand = (bot) => {
|
|
|
35
35
|
renderAnswer(this, context, wholeList("bot chats admins list", admins.map(snakeKeys)));
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
-
annotate(command.command("add"), { effects: REMOTE_WRITE
|
|
38
|
+
annotate(command.command("add"), { effects: REMOTE_WRITE })
|
|
39
39
|
.description("make a member an admin with these rights")
|
|
40
40
|
.argument("<chat>", CHAT)
|
|
41
41
|
.argument("<person>", PERSON)
|
|
@@ -61,7 +61,7 @@ export const botAdminsCommand = (bot) => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
-
annotate(command.command("remove"), { effects: REMOTE_WRITE
|
|
64
|
+
annotate(command.command("remove"), { effects: REMOTE_WRITE })
|
|
65
65
|
.description("take an admin's rights back; they stay a member")
|
|
66
66
|
.argument("<chat>", CHAT)
|
|
67
67
|
.argument("<person>", PERSON)
|
package/dist/cli/bot/command.js
CHANGED
|
@@ -111,7 +111,7 @@ const meCommand = (bot) => annotate(new Command("me"), { effects: REMOTE_READ })
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
-
const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effects: LOCAL_READ
|
|
114
|
+
const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effects: LOCAL_READ })
|
|
115
115
|
.description(`every name on this machine that has a bot token; --check asks ${bot.name ?? "the messenger"} which bot each is`)
|
|
116
116
|
.option("--check", "ask the messenger who each bot is, with its token")
|
|
117
117
|
.action(async function () {
|
|
@@ -216,7 +216,7 @@ const chatsCommand = (bot) => {
|
|
|
216
216
|
renderItem(this, context, found);
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
-
annotate(command.command("leave"), { effects: REMOTE_WRITE
|
|
219
|
+
annotate(command.command("leave"), { effects: REMOTE_WRITE })
|
|
220
220
|
.description("take the bot out of a chat; only an admin of the chat can bring it back")
|
|
221
221
|
.argument("<chat>", "a chat id, or the title of a chat this bot has seen")
|
|
222
222
|
.action(async function (chat) {
|
package/dist/cli/bot/setup.js
CHANGED
|
@@ -48,13 +48,13 @@ export const botMenuCommand = (bot) => {
|
|
|
48
48
|
renderAnswer(command, context, wholeList("bot commands list", entries));
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
|
-
annotate(command.command("set"), { effects: REMOTE_WRITE
|
|
51
|
+
annotate(command.command("set"), { effects: REMOTE_WRITE })
|
|
52
52
|
.description("replace the whole menu: each command as name=description, e.g. start=Begin")
|
|
53
53
|
.argument("<commands...>", "name=description, one per command")
|
|
54
54
|
.action(async function (typed) {
|
|
55
55
|
await replace(this, typed.map(entryOf), "bot.commands.set");
|
|
56
56
|
});
|
|
57
|
-
annotate(command.command("clear"), { effects: REMOTE_WRITE
|
|
57
|
+
annotate(command.command("clear"), { effects: REMOTE_WRITE })
|
|
58
58
|
.description("empty the menu")
|
|
59
59
|
.action(async function () {
|
|
60
60
|
await replace(this, [], "bot.commands.clear");
|
|
@@ -97,7 +97,7 @@ export const botCallbacksCommand = (bot) => new Command("callbacks").description
|
|
|
97
97
|
}));
|
|
98
98
|
/** `bot webhooks`: where the messenger pushes this bot's updates instead of being polled. */
|
|
99
99
|
export const botWebhooksCommand = (bot) => {
|
|
100
|
-
const command =
|
|
100
|
+
const command = new Command("webhooks").description("where the messenger pushes this bot's updates — while one is set, `bot watch` gets nothing");
|
|
101
101
|
annotate(command.command("list"), { effects: REMOTE_READ })
|
|
102
102
|
.description("the webhooks this bot has")
|
|
103
103
|
.action(async function () {
|
|
@@ -50,7 +50,7 @@ export const commandsCommand = (app) => {
|
|
|
50
50
|
exit_codes: { ok: 0, [CRASH_CODE]: GENERIC_FAILURE, ...EXIT_CODES },
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
-
command.addCommand(annotate(new Command("schema"), { effects: NO_EFFECTS
|
|
53
|
+
command.addCommand(annotate(new Command("schema"), { effects: NO_EFFECTS })
|
|
54
54
|
.description("one command's argv and result schemas, effects, permissions and retry guidance")
|
|
55
55
|
.argument("<path...>", "one command path, for example: stats messages show")
|
|
56
56
|
.action(function (path) {
|
|
@@ -15,7 +15,7 @@ import { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } fro
|
|
|
15
15
|
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
16
16
|
*/
|
|
17
17
|
export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
18
|
-
const command =
|
|
18
|
+
const command = new Command("config").description("the settings in force, and where each one came from");
|
|
19
19
|
annotate(command.command("show"), { effects: LOCAL_READ, examples: [`${app.command} personal config show`] })
|
|
20
20
|
.description("the profile, the profiles that exist, and each setting with where it came from")
|
|
21
21
|
.option("--bot", "the settings a bot command on this profile gets, rather than the personal account's")
|
package/dist/cli/guide.js
CHANGED
|
@@ -116,7 +116,6 @@ const scenarioText = (cli, scenario, profile) => [
|
|
|
116
116
|
export const guideCommand = (app, options) => {
|
|
117
117
|
const command = annotate(new Command("guide"), {
|
|
118
118
|
effects: NO_EFFECTS,
|
|
119
|
-
mcp: false,
|
|
120
119
|
examples: [`${app.command} guide`, `${app.command} guide <name> --json`],
|
|
121
120
|
})
|
|
122
121
|
.description("scenarios: the commands for a common task, in order, and why each")
|
|
@@ -7,6 +7,8 @@ export interface NextChoice {
|
|
|
7
7
|
}
|
|
8
8
|
/** Overrides the "Next usually" a command's help derives from types. Kept off `annotate`, like its section. */
|
|
9
9
|
export declare const nextUsually: <T extends Command>(command: T, choice: NextChoice) => T;
|
|
10
|
+
/** The profile an example names, so a pasted one runs; the name the first screen's "Start here" uses. */
|
|
11
|
+
export declare const EXAMPLE_PROFILE = "personal";
|
|
10
12
|
export interface CommandHelp {
|
|
11
13
|
returns: string;
|
|
12
14
|
next: readonly string[];
|
|
@@ -20,8 +22,13 @@ export declare const commandHelp: (command: Command) => CommandHelp;
|
|
|
20
22
|
export declare const commandHelpText: (command: Command) => string;
|
|
21
23
|
/** The same, as Markdown lines for `docs/commands.md`: hand it to cli-base's `commandsPage` per command. */
|
|
22
24
|
export declare const commandHelpMarkdown: (root: Command, path: readonly string[]) => string[];
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Puts Returns, Next usually and Example on every leaf command's `--help`, worked out when the help is shown.
|
|
27
|
+
* With `profile`, an example of an account command gets the profile word it would fail without.
|
|
28
|
+
*/
|
|
29
|
+
export declare const installCommandHelp: (program: Command, { profile }?: {
|
|
30
|
+
profile?: boolean;
|
|
31
|
+
}) => void;
|
|
25
32
|
/** Which leaf commands still show a generated example, so the important ones get a written one. Report only. */
|
|
26
33
|
export declare const exampleReport: (program: Command) => {
|
|
27
34
|
written: string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { metaOf } from "@wirecat/cli-base/commands";
|
|
2
2
|
import { writes } from "@wirecat/cli-base/standards";
|
|
3
3
|
import { commandPathOf, outputOf } from "../command-contract.js";
|
|
4
|
+
import { PROFILE_FREE } from "../profile.js";
|
|
4
5
|
import { commandAt } from "../steps.js";
|
|
5
6
|
import { short } from "./commands-text.js";
|
|
6
7
|
/** Argument names onto the kinds `domain/references.ts` names; a plural takes the same kind. */
|
|
@@ -164,16 +165,28 @@ const generatedExample = (command) => {
|
|
|
164
165
|
return [flag];
|
|
165
166
|
return [flag, option.argChoices?.[0] ?? sample(placeholder ?? option.attributeName())];
|
|
166
167
|
});
|
|
167
|
-
// No profile word, as on the first screen and in written examples: the Usage line already shows `<profile>`.
|
|
168
168
|
return [rootOf(command).name(), ...commandPathOf(command), ...words, ...options].join(" ");
|
|
169
169
|
};
|
|
170
|
+
const profiled = new WeakSet();
|
|
171
|
+
/** The profile an example names, so a pasted one runs; the name the first screen's "Start here" uses. */
|
|
172
|
+
export const EXAMPLE_PROFILE = "personal";
|
|
173
|
+
const withProfile = (command, example) => {
|
|
174
|
+
const root = rootOf(command);
|
|
175
|
+
const top = commandPathOf(command)[0];
|
|
176
|
+
if (!profiled.has(root) || top === undefined || PROFILE_FREE.has(top))
|
|
177
|
+
return example;
|
|
178
|
+
const prefix = `${root.name()} ${top}`;
|
|
179
|
+
return example === prefix || example.startsWith(`${prefix} `)
|
|
180
|
+
? `${root.name()} ${EXAMPLE_PROFILE} ${example.slice(root.name().length + 1)}`
|
|
181
|
+
: example;
|
|
182
|
+
};
|
|
170
183
|
/** Returns, Next usually and Example for one command, from its definition alone. */
|
|
171
184
|
export const commandHelp = (command) => {
|
|
172
185
|
const written = metaOf(command).examples ?? [];
|
|
173
186
|
return {
|
|
174
187
|
returns: returnsOf(command),
|
|
175
188
|
next: nextOf(command).map(call),
|
|
176
|
-
examples: written.length > 0 ? written : [generatedExample(command)],
|
|
189
|
+
examples: (written.length > 0 ? written : [generatedExample(command)]).map((one) => withProfile(command, one)),
|
|
177
190
|
origin: written.length > 0 ? "written" : "generated",
|
|
178
191
|
};
|
|
179
192
|
};
|
|
@@ -197,8 +210,13 @@ export const commandHelpMarkdown = (root, path) => {
|
|
|
197
210
|
`**Example:** ${code(examples)}`,
|
|
198
211
|
];
|
|
199
212
|
};
|
|
200
|
-
/**
|
|
201
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Puts Returns, Next usually and Example on every leaf command's `--help`, worked out when the help is shown.
|
|
215
|
+
* With `profile`, an example of an account command gets the profile word it would fail without.
|
|
216
|
+
*/
|
|
217
|
+
export const installCommandHelp = (program, { profile = true } = {}) => {
|
|
218
|
+
if (profile)
|
|
219
|
+
profiled.add(program);
|
|
202
220
|
for (const command of leaves(program))
|
|
203
221
|
command.addHelpText("after", () => commandHelpText(command));
|
|
204
222
|
};
|
|
@@ -4,7 +4,7 @@ import { Command, Help, Option } from "commander";
|
|
|
4
4
|
import wrapAnsi from "wrap-ansi";
|
|
5
5
|
import { NO_EFFECTS } from "../effects.js";
|
|
6
6
|
import { commandAt } from "../steps.js";
|
|
7
|
-
import { installCommandHelp } from "./command-help.js";
|
|
7
|
+
import { EXAMPLE_PROFILE, installCommandHelp } from "./command-help.js";
|
|
8
8
|
/** Every WireCat tool, as "Works with" names it; a tool's own entry is left out of its screen. */
|
|
9
9
|
export const WIRECAT_TOOLS = [
|
|
10
10
|
{ command: "tg", does: "Telegram in the same local store: search, review and tasks offline" },
|
|
@@ -56,7 +56,7 @@ export const firstScreen = (program, screen = {}) => {
|
|
|
56
56
|
const can = (screen.can ?? []).map(({ command, does }) => [command, does]);
|
|
57
57
|
const works = WIRECAT_TOOLS.filter((tool) => tool.command !== cli).map(({ command, shown, does }) => [shown ?? command, does]);
|
|
58
58
|
const column = Math.max(22, widest([...can, ...works].map(([left]) => left))) + 2;
|
|
59
|
-
const start = screen.start ?? (commandAt(program, "setup") ? `${cli}
|
|
59
|
+
const start = screen.start ?? (commandAt(program, "setup") ? `${cli} ${EXAMPLE_PROFILE} setup` : undefined);
|
|
60
60
|
return [
|
|
61
61
|
wrapAnsi(`${cli} — ${pitch}`, WIDTH),
|
|
62
62
|
"",
|
|
@@ -95,7 +95,7 @@ export const installHelp = (program, screen = {}) => {
|
|
|
95
95
|
? firstScreen(program, screen)
|
|
96
96
|
: (own.formatHelp ?? Help.prototype.formatHelp).call(helper, command, helper),
|
|
97
97
|
});
|
|
98
|
-
installCommandHelp(program);
|
|
98
|
+
installCommandHelp(program, { profile: screen.profile !== false });
|
|
99
99
|
program.addOption(new Option("--help-all", "the full help: every command and option").hideHelp());
|
|
100
100
|
program.on("option:help-all", () => {
|
|
101
101
|
full = true;
|
|
@@ -12,6 +12,7 @@ import { recalledAccount } from "./accounts.js";
|
|
|
12
12
|
import { photoHelp, readProfilePhoto } from "./admin-commands.js";
|
|
13
13
|
import { accountAnswer, renderItem, wholeList } from "./chat-answers.js";
|
|
14
14
|
import { messengerContext } from "./context.js";
|
|
15
|
+
import { hideNever } from "./lacks.js";
|
|
15
16
|
import { privacyCommand } from "./records-command.js";
|
|
16
17
|
export const accountCommand = (messenger) => {
|
|
17
18
|
const tool = `${messenger.app.command} personal`;
|
|
@@ -80,5 +81,5 @@ export const accountCommand = (messenger) => {
|
|
|
80
81
|
.addCommand(accountSessionsCommand(messenger));
|
|
81
82
|
if (messenger.privacy === true)
|
|
82
83
|
account.addCommand(privacyCommand(messenger));
|
|
83
|
-
return account;
|
|
84
|
+
return hideNever(messenger, account);
|
|
84
85
|
};
|
|
@@ -64,7 +64,7 @@ export const storeCommand = (messenger) => {
|
|
|
64
64
|
* What the local store holds, per chat — read from the store alone. Whether a fetch reached a
|
|
65
65
|
* chat's very first message is not recorded, so it is not claimed: the stretches held are shown.
|
|
66
66
|
*/
|
|
67
|
-
const statusCommand = (messenger) => annotate(new Command("status"), { effects: LOCAL_READ
|
|
67
|
+
const statusCommand = (messenger) => annotate(new Command("status"), { effects: LOCAL_READ })
|
|
68
68
|
.summary("what the local archive holds and what is missing")
|
|
69
69
|
.description("per chat: messages stored, the oldest and newest, and the stretches held completely")
|
|
70
70
|
.argument("[chat]", messenger.chatArgument)
|
|
@@ -132,7 +132,7 @@ const clearCommand = (messenger) => annotate(new Command("clear"), { effects: LO
|
|
|
132
132
|
* `--format markdown` for a transcript a person reads. With `--output-dir`, any number of chats into a
|
|
133
133
|
* folder, and each later run into the same folder adds only what changed.
|
|
134
134
|
*/
|
|
135
|
-
const exportCommand = (messenger) => destroysNothing(annotate(new Command("export"), { effects: LOCAL_WRITE
|
|
135
|
+
const exportCommand = (messenger) => destroysNothing(annotate(new Command("export"), { effects: LOCAL_WRITE }))
|
|
136
136
|
.summary("export stored messages to files")
|
|
137
137
|
.description("a chat's stored messages, oldest first: --jsonl one message per line, --json the list envelope of messages; never asks the messenger")
|
|
138
138
|
.argument("[chat...]", `${messenger.chatArgument}; several with --output-dir`)
|
|
@@ -237,7 +237,7 @@ export const jobsCommand = (messenger) => {
|
|
|
237
237
|
: await context.withServices((services) => services.archive.held(chatId)).catch(() => undefined);
|
|
238
238
|
renderAnswer(this, context, itemOf(snakeKeys({ ...jobDetails(job), ...(held ? { held } : {}), log: job.log })));
|
|
239
239
|
});
|
|
240
|
-
destroysNothing(annotate(command.command("cancel"), { effects: LOCAL_WRITE
|
|
240
|
+
destroysNothing(annotate(command.command("cancel"), { effects: LOCAL_WRITE }))
|
|
241
241
|
.summary("cancel a queued or running archive job")
|
|
242
242
|
.description("stop a running background job after its current page; a later sync resumes where it stopped")
|
|
243
243
|
.argument("<job>", "the job id")
|
|
@@ -251,7 +251,7 @@ export const jobsCommand = (messenger) => {
|
|
|
251
251
|
process.kill(job.pid, "SIGTERM");
|
|
252
252
|
renderAnswer(this, context, itemOf({ job: job.id, cancelled: true }));
|
|
253
253
|
});
|
|
254
|
-
destroysNothing(annotate(command.command("retry"), { effects: LOCAL_WRITE
|
|
254
|
+
destroysNothing(annotate(command.command("retry"), { effects: LOCAL_WRITE }))
|
|
255
255
|
.summary("retry an archive job")
|
|
256
256
|
.description("start a partial, failed or died job again, as a new job; the sync resumes where the store stopped")
|
|
257
257
|
.argument("[job]", "the job id")
|
|
@@ -292,7 +292,7 @@ export const jobsCommand = (messenger) => {
|
|
|
292
292
|
if (items.length === 0 && context.format === "pretty")
|
|
293
293
|
context.renderer.note("no partial, failed or died jobs to retry");
|
|
294
294
|
});
|
|
295
|
-
destroysNothing(annotate(command.command("clear"), { effects: LOCAL_WRITE
|
|
295
|
+
destroysNothing(annotate(command.command("clear"), { effects: LOCAL_WRITE }))
|
|
296
296
|
.description("forget finished jobs and remove their logs; a running job is kept")
|
|
297
297
|
.action(function () {
|
|
298
298
|
const context = messengerContext(this, messenger);
|
|
@@ -49,6 +49,8 @@ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, r
|
|
|
49
49
|
}[][] | undefined;
|
|
50
50
|
}[];
|
|
51
51
|
}>;
|
|
52
|
+
/** What `review --offline` cannot see. */
|
|
53
|
+
export declare const OFFLINE_REVIEW = "offline: only what was already synced to the local store; run review without --offline for the rest";
|
|
52
54
|
/**
|
|
53
55
|
* A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
|
|
54
56
|
* Saved points (`--new`) carry this themselves, and an `--unanswered` look is not a review to continue.
|
|
@@ -48,7 +48,7 @@ const nameOf = (chat) => chat.title ?? chat.id;
|
|
|
48
48
|
* What every catch-up answer shares: the messages, oldest first, each naming its chat; what was left out as
|
|
49
49
|
* `missing`; and a chat cut short as a `messages.list` step, after the steps the command puts first.
|
|
50
50
|
*/
|
|
51
|
-
const catchUpList = (command, found, hearing, reader, { transcribe, first = [], meta = {} }) => {
|
|
51
|
+
const catchUpList = (command, found, hearing, reader, { transcribe, first = [], meta = {}, left = [], }) => {
|
|
52
52
|
const read = found.chats
|
|
53
53
|
.flatMap((chat) => heard(chat.messages, hearing).map((message) => ({ ...message, chatTitle: chat.title })))
|
|
54
54
|
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
@@ -70,6 +70,7 @@ const catchUpList = (command, found, hearing, reader, { transcribe, first = [],
|
|
|
70
70
|
? [{ field: "items", why: `${found.quiet} muted or archived chats left out`, step: `${command}.all` }]
|
|
71
71
|
: []),
|
|
72
72
|
...hearingMissing(hearing, transcribe),
|
|
73
|
+
...left,
|
|
73
74
|
];
|
|
74
75
|
return listOf(command, {
|
|
75
76
|
items: read.map((message) => summaryOf(message, reader)),
|
|
@@ -81,6 +82,8 @@ const catchUpList = (command, found, hearing, reader, { transcribe, first = [],
|
|
|
81
82
|
});
|
|
82
83
|
};
|
|
83
84
|
export const inboxAnswer = (inbox, hearing, reader, transcribe, meta = {}) => catchUpList("inbox", inbox, hearing, reader, { transcribe, meta });
|
|
85
|
+
/** What `review --offline` cannot see. */
|
|
86
|
+
export const OFFLINE_REVIEW = "offline: only what was already synced to the local store; run review without --offline for the rest";
|
|
84
87
|
const quoted = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
85
88
|
/**
|
|
86
89
|
* A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
|
|
@@ -95,6 +98,7 @@ export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat,
|
|
|
95
98
|
return catchUpList("review", found, hearing, reader, {
|
|
96
99
|
transcribe,
|
|
97
100
|
first: continues ? [nextStep("review.next", { args: { query: query.join(" ") } })] : [],
|
|
101
|
+
...(found.offline ? { left: [{ field: "items", why: OFFLINE_REVIEW }] } : {}),
|
|
98
102
|
...(meta === undefined ? {} : { meta }),
|
|
99
103
|
});
|
|
100
104
|
};
|
|
@@ -19,6 +19,7 @@ import { membersCommand } from "./chats-members-command.js";
|
|
|
19
19
|
import { markReadCommand } from "./chats-read-command.js";
|
|
20
20
|
import { trackingCommand } from "./chats-tracking-command.js";
|
|
21
21
|
import { messengerContext } from "./context.js";
|
|
22
|
+
import { hideNever } from "./lacks.js";
|
|
22
23
|
import { archiveCommands, chatDeletionCommands, chatPinCommands, markUnreadCommand, mediaCommand, muteCommands, } from "./records-command.js";
|
|
23
24
|
export const chatsCommand = (messenger) => {
|
|
24
25
|
const chats = new Command("chats").description("the account's chats");
|
|
@@ -153,7 +154,7 @@ export const chatsCommand = (messenger) => {
|
|
|
153
154
|
if (command)
|
|
154
155
|
annotate(command, { examples: [`${messenger.app.command} personal chats ${path} ${args}`.trimEnd()] });
|
|
155
156
|
}
|
|
156
|
-
return chats;
|
|
157
|
+
return hideNever(messenger, chats);
|
|
157
158
|
};
|
|
158
159
|
/** Where the history is pushed, the chat list and a chat's card answer from the store and never connect. */
|
|
159
160
|
const storedRead = (messenger) => (messenger.history === "store" ? LOCAL_READ : REMOTE_READ);
|
|
@@ -19,7 +19,11 @@ const SHELLS = ["zsh", "bash", "fish", "powershell"];
|
|
|
19
19
|
* ⚠ **A Tab never connects and never creates anything.** Names come from the message store if it
|
|
20
20
|
* exists — no store, no names — and nothing goes to stderr: a shell shows whatever it is given.
|
|
21
21
|
*/
|
|
22
|
-
export const completeCommand = (messenger, config, options = {}) => annotate(new Command("complete"), {
|
|
22
|
+
export const completeCommand = (messenger, config, options = {}) => annotate(new Command("complete"), {
|
|
23
|
+
effects: LOCAL_READ,
|
|
24
|
+
mcp: false,
|
|
25
|
+
examples: [`${messenger.app.command} complete zsh`],
|
|
26
|
+
})
|
|
23
27
|
.description(`shell completion: \`${messenger.app.command} complete zsh\` prints the script to source`)
|
|
24
28
|
.argument("[words...]", "zsh for the shell script, or command words to complete")
|
|
25
29
|
.allowUnknownOption()
|
|
@@ -19,6 +19,7 @@ import { commandAt, nextStep } from "../steps.js";
|
|
|
19
19
|
import { contactWriteCommands } from "./admin-contacts-command.js";
|
|
20
20
|
import { contactAnswer, lines, pageOf } from "./chat-answers.js";
|
|
21
21
|
import { messengerContext } from "./context.js";
|
|
22
|
+
import { hideNever } from "./lacks.js";
|
|
22
23
|
import { CONTACTS_QUERY, notesAllowed } from "./people-queries.js";
|
|
23
24
|
import { privatePeopleCommands } from "./private-people-command.js";
|
|
24
25
|
import { syncsFirst } from "./search-sync-options.js";
|
|
@@ -315,5 +316,5 @@ export const contactsCommand = (messenger) => {
|
|
|
315
316
|
if (command)
|
|
316
317
|
annotate(command, { examples: [`${messenger.app.command} personal contacts ${path} ${args}`.trimEnd()] });
|
|
317
318
|
}
|
|
318
|
-
return contacts;
|
|
319
|
+
return hideNever(messenger, contacts);
|
|
319
320
|
};
|
|
@@ -95,6 +95,11 @@ export interface Messenger extends ServiceMessenger {
|
|
|
95
95
|
* connecting, as the adapter would refuse after; one connection per device makes that connection a cost.
|
|
96
96
|
*/
|
|
97
97
|
lacks?: readonly (keyof MessengerAdapter)[];
|
|
98
|
+
/**
|
|
99
|
+
* Adapter methods this messenger can never have, where `lacks` is what it does not have yet: their commands refuse
|
|
100
|
+
* the same way and are also hidden from help, `commands` and MCP. WhatsApp: only the phone ends other sessions.
|
|
101
|
+
*/
|
|
102
|
+
never?: readonly (keyof MessengerAdapter)[];
|
|
98
103
|
/** Whether a message can be deleted for the owner alone; yes when unset. No: every delete is for everyone (WhatsApp). */
|
|
99
104
|
deleteForMe?: boolean;
|
|
100
105
|
/** Whether the messenger has forum topics: `--topic`, `topic:` in `messages list` and the `topics_*` tools; yes when unset. */
|
|
@@ -57,7 +57,7 @@ const INCLUDES = [
|
|
|
57
57
|
const EXCLUDES = ["message text", "chat titles", "names", "phone numbers", "the session", "the app credentials"];
|
|
58
58
|
const reportCommand = (messenger) => {
|
|
59
59
|
const { app } = messenger;
|
|
60
|
-
const report = annotate(new Command("report"), { effects: REMOTE_READ
|
|
60
|
+
const report = annotate(new Command("report"), { effects: REMOTE_READ })
|
|
61
61
|
.description("what a problem report holds; writes nothing")
|
|
62
62
|
.action(function () {
|
|
63
63
|
const output = outputFor(this);
|
|
@@ -74,7 +74,7 @@ const reportCommand = (messenger) => {
|
|
|
74
74
|
`About a given run: ${create} --run <id> (ids: ${lineOf(this, "runs list")})`,
|
|
75
75
|
].join("\n"));
|
|
76
76
|
});
|
|
77
|
-
destroysNothing(annotate(report.command("create"), { effects: LOCAL_WRITE }))
|
|
77
|
+
destroysNothing(annotate(report.command("create"), { effects: LOCAL_WRITE, mcp: false }))
|
|
78
78
|
.description("write a problem report to a file, and say where to send it")
|
|
79
79
|
.option("--run <id>", "the run the report is about; the newest failed one if not given")
|
|
80
80
|
.option("--output <file>", "where to write it; a new file in this directory if not given")
|
|
@@ -14,7 +14,7 @@ export const floodCommand = (messenger) => {
|
|
|
14
14
|
const name = messenger.name ?? app.command;
|
|
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
|
-
.addCommand(grantsAuthority(annotate(new Command("clear"), { effects: LOCAL_WRITE }))
|
|
17
|
+
.addCommand(grantsAuthority(annotate(new Command("clear"), { effects: LOCAL_WRITE, examples: [`${app.command} personal flood clear`] }))
|
|
18
18
|
.summary("clear a profile's stored rate-limit hold")
|
|
19
19
|
.description(`forget them, lift the hold and the profile's pace, once ${name} no longer limits the account; changes nothing there`)
|
|
20
20
|
.action(async function () {
|
|
@@ -29,6 +29,7 @@ export const PARTIAL_NOTE = "the messenger did not list every chat, and did not
|
|
|
29
29
|
* `date>2h` is a one-off look from a time of your choosing and leaves the points where they were.
|
|
30
30
|
*/
|
|
31
31
|
export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
32
|
+
examples: [`${messenger.app.command} personal inbox --limit 10`],
|
|
32
33
|
effects: REMOTE_READ,
|
|
33
34
|
output: { entity: "message", view: "summary" },
|
|
34
35
|
query: INBOX_QUERY,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
1
2
|
import type { MessengerAdapter } from "../../ports/messenger.js";
|
|
2
3
|
type Need = readonly [method: keyof MessengerAdapter, what: string];
|
|
3
4
|
/**
|
|
@@ -9,10 +10,16 @@ export declare const COMMAND_NEEDS: Readonly<Record<string, Need>>;
|
|
|
9
10
|
interface Lacking {
|
|
10
11
|
channelComments?: boolean;
|
|
11
12
|
lacks?: readonly (keyof MessengerAdapter)[];
|
|
13
|
+
never?: readonly (keyof MessengerAdapter)[];
|
|
12
14
|
history?: "server" | "store";
|
|
13
15
|
}
|
|
14
16
|
/** The one method this command needs and this messenger never has; none when it has it or the store answers. */
|
|
15
17
|
export declare const lackedNeed: (messenger: Lacking | undefined, path: string) => Need | undefined;
|
|
16
18
|
/** Refuses, before a connection, a command whose one method this messenger never has. */
|
|
17
19
|
export declare const refuseLacking: (messenger: Lacking | undefined, path: readonly string[]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Hides under `group` each command whose one method this messenger can `never` have, from help, `commands` and MCP;
|
|
22
|
+
* typed anyway, it still refuses. A method it only `lacks` keeps its command in sight, since it may come.
|
|
23
|
+
*/
|
|
24
|
+
export declare const hideNever: <T extends Command>(messenger: Lacking, group: T) => T;
|
|
18
25
|
export {};
|
|
@@ -32,7 +32,7 @@ export const lackedNeed = (messenger, path) => {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
if (path === "messages comments" && messenger?.channelComments === false)
|
|
34
34
|
return need;
|
|
35
|
-
if (!messenger?.lacks?.includes(need[0]))
|
|
35
|
+
if (!messenger?.lacks?.includes(need[0]) && !messenger?.never?.includes(need[0]))
|
|
36
36
|
return undefined;
|
|
37
37
|
return messenger.history === "store" && STORE_ANSWERS.has(path) ? undefined : need;
|
|
38
38
|
};
|
|
@@ -42,3 +42,20 @@ export const refuseLacking = (messenger, path) => {
|
|
|
42
42
|
if (need)
|
|
43
43
|
throw new CliError("validation_error", `this messenger cannot ${need[1]}`);
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Hides under `group` each command whose one method this messenger can `never` have, from help, `commands` and MCP;
|
|
47
|
+
* typed anyway, it still refuses. A method it only `lacks` keeps its command in sight, since it may come.
|
|
48
|
+
*/
|
|
49
|
+
export const hideNever = (messenger, group) => {
|
|
50
|
+
const walk = (command, path) => {
|
|
51
|
+
for (const child of command.commands) {
|
|
52
|
+
const at = [...path, child.name()];
|
|
53
|
+
const need = COMMAND_NEEDS[at.join(" ")];
|
|
54
|
+
if (need && messenger.never?.includes(need[0]))
|
|
55
|
+
child._hidden = true;
|
|
56
|
+
walk(child, at);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(group, [group.name()]);
|
|
60
|
+
return group;
|
|
61
|
+
};
|
|
@@ -9,7 +9,6 @@ import { messengerContext, refuseLocalWrite } from "./context.js";
|
|
|
9
9
|
export const addMediaArchiveMove = (attachments, messenger) => {
|
|
10
10
|
annotate(attachments.command("move"), {
|
|
11
11
|
effects: LOCAL_WRITE,
|
|
12
|
-
mcp: false,
|
|
13
12
|
examples: [
|
|
14
13
|
`${messenger.app.command} attachments move ./media-archive ./older-images --before-time 2026-10-01T00:00:00Z --dry-run`,
|
|
15
14
|
],
|
|
@@ -11,11 +11,11 @@ import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
|
11
11
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
12
12
|
import { messengerContext } from "./context.js";
|
|
13
13
|
export const addMediaArchiveTimers = (attachments, messenger) => {
|
|
14
|
-
const archive =
|
|
14
|
+
const archive = attachments.command("jobs").description("daily media archive timers for this profile");
|
|
15
15
|
for (const action of ["install", "start", "stop", "status", "uninstall"]) {
|
|
16
16
|
const command = annotate(archive.command(action), {
|
|
17
17
|
effects: action === "status" ? LOCAL_READ : LOCAL_WRITE,
|
|
18
|
-
mcp: false,
|
|
18
|
+
...(action === "status" ? {} : { mcp: false }),
|
|
19
19
|
examples: [`${messenger.app.command} attachments jobs ${action} ./media-archive`],
|
|
20
20
|
})
|
|
21
21
|
.description({
|
|
@@ -17,6 +17,7 @@ import { linksCommand } from "./conversations-command.js";
|
|
|
17
17
|
import { downloadSubcommand } from "./download-command.js";
|
|
18
18
|
import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
|
|
19
19
|
import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
|
|
20
|
+
import { hideNever } from "./lacks.js";
|
|
20
21
|
import { threadAnswer } from "./message-answers.js";
|
|
21
22
|
import { messageItem, messageItems, viewOf } from "./message-items.js";
|
|
22
23
|
import { commentsCommand } from "./messages-comments-command.js";
|
|
@@ -174,7 +175,7 @@ export const messagesCommand = (messenger) => {
|
|
|
174
175
|
messages.addCommand(commentsCommand(messenger));
|
|
175
176
|
messages.addCommand(linksCommand(messenger));
|
|
176
177
|
messages.addCommand(syncCommand(messenger));
|
|
177
|
-
return messages;
|
|
178
|
+
return hideNever(messenger, messages);
|
|
178
179
|
};
|
|
179
180
|
/** Mail lives in the mail tables of the store, whichever messenger CLI reads it. */
|
|
180
181
|
const isMailLocator = (chat, message) => message === undefined && isLocator(chat) && parseLocator(chat).provider === "email";
|