@wirecat/cli-core 0.343.0 → 0.344.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 +49 -0
- package/dist/cli/bot/context.js +8 -4
- package/dist/cli/commands-command.js +9 -12
- package/dist/cli/help/commands-text.d.ts +7 -0
- package/dist/cli/help/commands-text.js +43 -0
- package/dist/cli/help/first-screen.d.ts +36 -0
- package/dist/cli/help/first-screen.js +106 -0
- package/dist/cli/help/sections.d.ts +19 -0
- package/dist/cli/help/sections.js +76 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/list-query.js +2 -4
- package/dist/cli/messenger/complete-command.js +2 -0
- package/dist/cli/messenger/context.js +12 -11
- package/dist/cli/messenger/mcp-command.js +1 -1
- package/dist/cli/messenger/messages-search-command.js +2 -2
- package/dist/cli/messenger/search-command.js +3 -3
- package/dist/cli/moved.js +1 -1
- package/dist/cli/profile.d.ts +5 -1
- package/dist/cli/profile.js +13 -3
- package/dist/cli/program.d.ts +4 -1
- package/dist/cli/program.js +21 -6
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/meetings/cli/index.d.ts +1 -0
- package/dist/meetings/cli/index.js +3 -4
- package/dist/meetings/cli/project-evidence.js +3 -3
- package/dist/meetings/cli/query-options.d.ts +6 -0
- package/dist/meetings/cli/query-options.js +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.344.0 — 18.10.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- **`<tool> --help` and a bare `<tool>` show a short first screen instead of commander's full help**: the tool's
|
|
15
|
+
pitch, the usage line, one sentence that account commands start with the profile, what the tool can do, the other
|
|
16
|
+
WireCat tools, and where to go next (`commands`, `help options`, `skill show`, and `setup` or `guide` when the tool
|
|
17
|
+
has them). It no longer lists every command or the options for every command, so a test that looks for a command
|
|
18
|
+
or a global option in the root `--help` must read `--help-all`, which prints the old help, or `help options`.
|
|
19
|
+
Exit codes are unchanged: `--help` exits 0, a bare run exits 2.
|
|
20
|
+
- **`<tool> commands` without `--json` prints a text list, by section, even when piped.** It printed the whole JSON
|
|
21
|
+
document (hundreds of kilobytes) to any reader that was not a terminal, and a table in one. Now every command that
|
|
22
|
+
runs is one line — its usage and the first clause of its description, `(writes)` when it changes something —
|
|
23
|
+
under Read, Write, Organise, Automate, Set up or Other. `commands <path>` narrows it. `--json` and `--jsonl` print
|
|
24
|
+
the same document as before; only the `commands` command's own description changed. A script that parsed the
|
|
25
|
+
output of a bare `commands` must pass `--json`.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`registerCommands(server, prefix, commands)` and the `McpCommand` type** (`@wirecat/cli-core/mcp`): the three
|
|
30
|
+
tools `<prefix>_tools_search`, `<prefix>_read` and `<prefix>_write` that tg and max serve in place of one tool per
|
|
31
|
+
command. A CLI without the messenger `mcpCommand`, such as drive, can now serve its own commands the same way.
|
|
32
|
+
- **`ProgramDefinition.firstScreen` and `installHelp(program, screen)`** (`@wirecat/cli-core/cli`): a tool
|
|
33
|
+
supplies its `pitch` and 6–8 `can` lines (`{ command, does }`, the command without the profile);
|
|
34
|
+
`installHelp` puts the same screen on a program the tool builds itself, with `profile: false` for a tool
|
|
35
|
+
whose commands take no leading profile and `start` for its first command. A tool that supplies nothing gets its
|
|
36
|
+
description and no "What you can do" block.
|
|
37
|
+
- **`<tool> help options`** prints the options for every command; `help <command>` prints that command's help.
|
|
38
|
+
`help` and `--help-all` are hidden, so `commands --json` and the MCP tools are unchanged.
|
|
39
|
+
- **`inSection(command, section)` and `SECTIONS`** (`@wirecat/cli-core/cli`): where `commands` lists a group and
|
|
40
|
+
everything under it — `read`, `write`, `organise`, `automate` or `setup`; a command without one, or under none,
|
|
41
|
+
is listed under Other. `createProgram` puts cli-core's own groups (`chats`, `messages`, `tags`, `schedule`,
|
|
42
|
+
`config`…) in theirs; a tool sets its own groups (`session`, `setup`). The section is kept off the commands
|
|
43
|
+
document.
|
|
44
|
+
|
|
10
45
|
## 0.343.0 — 18.10.2026
|
|
11
46
|
|
|
12
47
|
### Added
|
|
@@ -54,6 +89,20 @@ shape of this file.
|
|
|
54
89
|
permission governs those commands. Commands that a profile's permissions govern still need one.
|
|
55
90
|
- **`serve`'s relay socket fits on macOS with a deep `TMPDIR`.** When the temporary folder is too deep for a socket
|
|
56
91
|
path too, the socket goes under `/tmp` (still in an owner-only folder). It made tg's macOS release check fail.
|
|
92
|
+
- **Help and errors say what a person typed.** The usage line of every command that uses a profile now reads
|
|
93
|
+
`<tool> <profile> …` (`Usage: tg <profile> mcp`); `commands`, `complete`, `help`, `skill` and `upgrade` keep the bare
|
|
94
|
+
form. Consumers' help snapshots change with this version.
|
|
95
|
+
- A command that takes a free query (`chats list`, `meetings list`, `meetings projects evidence`) refuses a word that
|
|
96
|
+
starts with `--` as an unknown option, with commander's suggestion (`unknown option '--limt' (Did you mean
|
|
97
|
+
--limit?)`), instead of reading it as query text; a negated term like `-kind:channel` is still query, and after `--`
|
|
98
|
+
every word is.
|
|
99
|
+
- A commander error is one line: `unknown command 'chts' (Did you mean chats?) …`.
|
|
100
|
+
- The profile may follow global options: `tg --json personal chats list` works.
|
|
101
|
+
- `complete --help` prints the help and exits 0.
|
|
102
|
+
- A bot with no token, given a chat by name, says to store a token (exit 4) instead of "no chat matches".
|
|
103
|
+
- "nothing recorded for profile" says "run the command once without --offline" only when `--offline` was given, and
|
|
104
|
+
names the profile in `serve`/`watch`.
|
|
105
|
+
- `mcp setup` says "1 tool that may write"; the search help calls memo "the separate memo CLI".
|
|
57
106
|
|
|
58
107
|
### Security
|
|
59
108
|
|
package/dist/cli/bot/context.js
CHANGED
|
@@ -29,6 +29,7 @@ export const botContext = (command, bot) => {
|
|
|
29
29
|
base.track(adapter);
|
|
30
30
|
return adapter;
|
|
31
31
|
};
|
|
32
|
+
const noToken = () => new CliError("authentication_error", `no bot token for profile "${profile}" — run \`${words} auth set\``);
|
|
32
33
|
return {
|
|
33
34
|
...base,
|
|
34
35
|
profile,
|
|
@@ -64,7 +65,11 @@ export const botContext = (command, bot) => {
|
|
|
64
65
|
const trimmed = typed.trim();
|
|
65
66
|
if (/^-?\d+$/.test(trimmed) || /^user:\d+$/.test(trimmed))
|
|
66
67
|
return trimmed;
|
|
67
|
-
|
|
68
|
+
const known = registry.list();
|
|
69
|
+
// A bot that never had a token has seen no chat, and its missing token is what needs fixing.
|
|
70
|
+
if (known.length === 0 && !tokens.read())
|
|
71
|
+
throw noToken();
|
|
72
|
+
return pickChat(trimmed, known).id;
|
|
68
73
|
},
|
|
69
74
|
readSecret: (prompt) => {
|
|
70
75
|
return bot.readSecret?.(command, prompt) ?? readSecret(prompt, secretInputOf(command));
|
|
@@ -74,9 +79,8 @@ export const botContext = (command, bot) => {
|
|
|
74
79
|
/** A client for the stored token, or a refusal that says how to store one. */
|
|
75
80
|
authenticated: (options = {}) => {
|
|
76
81
|
const stored = tokens.read();
|
|
77
|
-
if (!stored)
|
|
78
|
-
throw
|
|
79
|
-
}
|
|
82
|
+
if (!stored)
|
|
83
|
+
throw noToken();
|
|
80
84
|
return connect(stored.token, options);
|
|
81
85
|
},
|
|
82
86
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { CliError, EXIT_CODES, GENERIC_FAILURE } from "@wirecat/cli-base";
|
|
2
|
-
import { annotate, describeOptions, describeProgram
|
|
3
|
-
import { CRASH_CODE
|
|
2
|
+
import { annotate, describeOptions, describeProgram } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { CRASH_CODE } from "@wirecat/cli-base/standards";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { commandContract, findCommand } from "./command-contract.js";
|
|
6
|
-
import { outputFor } from "./context.js";
|
|
6
|
+
import { environmentOf, outputFor } from "./context.js";
|
|
7
7
|
import { NO_EFFECTS } from "./effects.js";
|
|
8
8
|
import { renderDocument } from "./envelope.js";
|
|
9
|
+
import { commandsText } from "./help/commands-text.js";
|
|
9
10
|
import { rootOf } from "./profile.js";
|
|
10
11
|
/**
|
|
11
12
|
* The major version of the JSON every command prints. The output types live in this package, so the
|
|
@@ -18,22 +19,18 @@ export const CONTRACT = 3;
|
|
|
18
19
|
*/
|
|
19
20
|
export const commandsCommand = (app) => {
|
|
20
21
|
const command = annotate(new Command("commands"), { effects: NO_EFFECTS })
|
|
21
|
-
.description("
|
|
22
|
+
.description("every command by section, one line each; --json: the contract, one path per call")
|
|
22
23
|
.argument("[path...]", "one command path, for example: search messages; inspect other groups in separate calls")
|
|
23
24
|
.action(function (path) {
|
|
24
25
|
const root = rootOf(this);
|
|
25
26
|
const output = outputFor(this);
|
|
26
|
-
const { renderer, format } = output;
|
|
27
27
|
const all = describeProgram(root);
|
|
28
28
|
const scoped = path.length > 0 ? scopeOf(root, all, path) : undefined;
|
|
29
29
|
const commands = scoped ? [scoped.command] : all;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
writes: effects !== undefined && writes(effects) ? "yes" : "",
|
|
35
|
-
...(state ? { state } : {}),
|
|
36
|
-
})));
|
|
30
|
+
const { json, jsonl } = this.optsWithGlobals();
|
|
31
|
+
// Asked by format, not by terminal: piped, an agent got the whole JSON tree it never asked for.
|
|
32
|
+
if (json === undefined && !jsonl && !environmentOf(this).agent) {
|
|
33
|
+
output.streams.data(commandsText(root, commands, scoped?.command.path).replace(/\n$/, ""));
|
|
37
34
|
return;
|
|
38
35
|
}
|
|
39
36
|
renderDocument(output, {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CommandInfo } from "@wirecat/cli-base/commands";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
/**
|
|
4
|
+
* `<tool> commands` for a person or an agent reading text: every command that runs, one line each, under its
|
|
5
|
+
* section. `scope` is the group asked for (`commands messages`), whose commands are all listed.
|
|
6
|
+
*/
|
|
7
|
+
export declare const commandsText: (root: Command, commands: readonly CommandInfo[], scope?: readonly string[]) => string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { writes } from "@wirecat/cli-base/standards";
|
|
2
|
+
import { findCommand } from "../command-contract.js";
|
|
3
|
+
import { SECTIONS, sectionOf } from "./sections.js";
|
|
4
|
+
const DESCRIPTION = 90;
|
|
5
|
+
const COLUMN = 52;
|
|
6
|
+
/** The first clause that fits: a line per command is the point, the whole text is in `--help`. */
|
|
7
|
+
const short = (text) => {
|
|
8
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
9
|
+
if (line.length <= DESCRIPTION)
|
|
10
|
+
return line;
|
|
11
|
+
const cut = line.slice(0, DESCRIPTION);
|
|
12
|
+
return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), DESCRIPTION / 2))}…`;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* `<tool> commands` for a person or an agent reading text: every command that runs, one line each, under its
|
|
16
|
+
* section. `scope` is the group asked for (`commands messages`), whose commands are all listed.
|
|
17
|
+
*/
|
|
18
|
+
export const commandsText = (root, commands, scope) => {
|
|
19
|
+
const cli = root.name();
|
|
20
|
+
const runs = (info) => [
|
|
21
|
+
...(info.commands.length === 0 || info.runnable ? [info] : []),
|
|
22
|
+
...info.commands.flatMap(runs),
|
|
23
|
+
];
|
|
24
|
+
const bySection = new Map(Object.keys(SECTIONS).map((key) => [key, []]));
|
|
25
|
+
for (const info of commands.flatMap(runs))
|
|
26
|
+
bySection.get(sectionOf(findCommand(root, info.path)))?.push(info);
|
|
27
|
+
const where = scope ? `${cli} ${scope.join(" ")}` : cli;
|
|
28
|
+
const lines = [
|
|
29
|
+
`${where} commands, by section. One command's arguments and options: ${cli} <command> --help.`,
|
|
30
|
+
`As JSON: ${cli} commands${scope ? ` ${scope.join(" ")}` : ""} --json. (writes) marks a command that changes something.`,
|
|
31
|
+
];
|
|
32
|
+
for (const [section, infos] of bySection) {
|
|
33
|
+
if (infos.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
lines.push("", SECTIONS[section]);
|
|
36
|
+
for (const { usage, summary, description, effects } of infos) {
|
|
37
|
+
const what = short(summary ?? description);
|
|
38
|
+
const mark = effects === undefined || writes(effects) ? " (writes)" : "";
|
|
39
|
+
lines.push(` ${usage.padEnd(COLUMN)} ${what}${mark}`.trimEnd());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return `${lines.join("\n")}\n`;
|
|
43
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export interface FirstScreenLine {
|
|
3
|
+
/** The command without the tool's name or the profile: `review --since 1d`. */
|
|
4
|
+
command: string;
|
|
5
|
+
does: string;
|
|
6
|
+
}
|
|
7
|
+
/** What a tool says on its first screen; the rest of the screen is the same for every WireCat tool. */
|
|
8
|
+
export interface FirstScreen {
|
|
9
|
+
/** One sentence after `tg —`. Unset, the program's description. */
|
|
10
|
+
pitch?: string;
|
|
11
|
+
/** 6–8 lines, the ones a plain API client would not give. Unset, the block is left out. */
|
|
12
|
+
can?: readonly FirstScreenLine[];
|
|
13
|
+
/** `false` for a tool whose commands take no leading profile: no profile sentence, no `<profile>` in the usage. */
|
|
14
|
+
profile?: boolean;
|
|
15
|
+
/** The first command to run. Unset, `<tool> personal setup` when the tool has `setup`. */
|
|
16
|
+
start?: string;
|
|
17
|
+
}
|
|
18
|
+
interface Tool {
|
|
19
|
+
command: string;
|
|
20
|
+
shown?: string;
|
|
21
|
+
does: string;
|
|
22
|
+
}
|
|
23
|
+
/** Every WireCat tool, as "Works with" names it; a tool's own entry is left out of its screen. */
|
|
24
|
+
export declare const WIRECAT_TOOLS: readonly Tool[];
|
|
25
|
+
/**
|
|
26
|
+
* The screen `<tool> --help` and a bare `<tool>` show: what the tool is for, what it does that a client would not,
|
|
27
|
+
* the other tools, and where the full list, the scenarios and the options are. It never lists every command.
|
|
28
|
+
*/
|
|
29
|
+
export declare const firstScreen: (program: Command, screen?: FirstScreen) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Puts the first screen on `--help`, a bare run and `help`; `help options` prints the options for every command and
|
|
32
|
+
* `--help-all` the full commander help. `help` and `--help-all` are hidden, so `commands --json` and MCP do not
|
|
33
|
+
* change. For a tool that builds its own program; `createProgram` calls it for tg, max and wa.
|
|
34
|
+
*/
|
|
35
|
+
export declare const installHelp: (program: Command, screen?: FirstScreen) => Command;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { Command, Help, Option } from "commander";
|
|
4
|
+
import wrapAnsi from "wrap-ansi";
|
|
5
|
+
import { NO_EFFECTS } from "../effects.js";
|
|
6
|
+
import { commandAt } from "../steps.js";
|
|
7
|
+
/** Every WireCat tool, as "Works with" names it; a tool's own entry is left out of its screen. */
|
|
8
|
+
export const WIRECAT_TOOLS = [
|
|
9
|
+
{ command: "tg", does: "Telegram in the same local store: search, review and tasks offline" },
|
|
10
|
+
{ command: "max", does: "MAX in the same local store: search, review and tasks offline" },
|
|
11
|
+
{ command: "wa", does: "WhatsApp in the same local store: search, review and tasks offline" },
|
|
12
|
+
{ command: "zm", does: "Zoom meetings: transcripts and summaries, searchable with your messages" },
|
|
13
|
+
{ command: "drive", does: "Google Drive and Docs as local text, searchable with your messages" },
|
|
14
|
+
{ command: "memo", shown: "memo context <person>", does: "one person across messengers, mail, notes and meetings" },
|
|
15
|
+
];
|
|
16
|
+
const WIDTH = 100;
|
|
17
|
+
const widest = (texts) => texts.reduce((most, text) => Math.max(most, text.length), 0);
|
|
18
|
+
const block = (title, rows, column) => rows.length === 0 ? [] : ["", `${title}:`, ...rows.map(([left, right]) => ` ${left.padEnd(column)}${right}`)];
|
|
19
|
+
/** Only commands the tool has: `guide` and `setup` are not in every tool. */
|
|
20
|
+
const pointers = (program, cli, start, profile) => {
|
|
21
|
+
const has = (path) => commandAt(program, path) !== undefined;
|
|
22
|
+
const next = [
|
|
23
|
+
has("commands") ? [`${cli} commands`, "every command, by section"] : undefined,
|
|
24
|
+
has("guide") ? [`${cli} guide`, "scenarios: command sequences for common tasks"] : undefined,
|
|
25
|
+
[`${cli} help options`, "output, offline, timeouts, dry run"],
|
|
26
|
+
].filter((row) => row !== undefined);
|
|
27
|
+
const rows = [
|
|
28
|
+
...(start ? [["Start here:", start, profile ? "(any profile name works)" : ""]] : []),
|
|
29
|
+
...next.map(([command = "", does = ""], at) => [at === 0 ? "Next:" : "", command, does]),
|
|
30
|
+
...(has("skill show") ? [["For agents:", `${cli} skill show`, "rules and the discovery loop"]] : []),
|
|
31
|
+
];
|
|
32
|
+
const width = widest(rows.map(([, command]) => command)) + 2;
|
|
33
|
+
return ["", ...rows.map(([label, command, does]) => `${label.padEnd(14)}${command.padEnd(width)}${does}`.trimEnd())];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The screen `<tool> --help` and a bare `<tool>` show: what the tool is for, what it does that a client would not,
|
|
37
|
+
* the other tools, and where the full list, the scenarios and the options are. It never lists every command.
|
|
38
|
+
*/
|
|
39
|
+
export const firstScreen = (program, screen = {}) => {
|
|
40
|
+
const cli = program.name();
|
|
41
|
+
const profile = screen.profile !== false;
|
|
42
|
+
const pitch = screen.pitch ?? program.description().split("\n")[0] ?? "";
|
|
43
|
+
const can = (screen.can ?? []).map(({ command, does }) => [command, does]);
|
|
44
|
+
const works = WIRECAT_TOOLS.filter((tool) => tool.command !== cli).map(({ command, shown, does }) => [shown ?? command, does]);
|
|
45
|
+
const column = Math.max(22, widest([...can, ...works].map(([left]) => left))) + 2;
|
|
46
|
+
const start = screen.start ?? (commandAt(program, "setup") ? `${cli} personal setup` : undefined);
|
|
47
|
+
return [
|
|
48
|
+
wrapAnsi(`${cli} — ${pitch}`, WIDTH),
|
|
49
|
+
"",
|
|
50
|
+
`Usage: ${cli} ${profile ? "<profile> " : ""}<command> [options]`,
|
|
51
|
+
...(profile ? ["Account commands start with your profile, a name you choose; none is ever assumed."] : []),
|
|
52
|
+
...block("What you can do", can, column),
|
|
53
|
+
...block("Works with", works, column),
|
|
54
|
+
...pointers(program, cli, start, profile),
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
};
|
|
58
|
+
const optionsScreen = (program) => {
|
|
59
|
+
const helper = program.createHelp();
|
|
60
|
+
const width = helper.longestOptionTermLength(program, helper);
|
|
61
|
+
return [
|
|
62
|
+
`Options for every ${program.name()} command:`,
|
|
63
|
+
...helper
|
|
64
|
+
.visibleOptions(program)
|
|
65
|
+
.map((option) => helper.formatItem(helper.optionTerm(option), width, helper.optionDescription(option), helper)),
|
|
66
|
+
"",
|
|
67
|
+
].join("\n");
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Puts the first screen on `--help`, a bare run and `help`; `help options` prints the options for every command and
|
|
71
|
+
* `--help-all` the full commander help. `help` and `--help-all` are hidden, so `commands --json` and MCP do not
|
|
72
|
+
* change. For a tool that builds its own program; `createProgram` calls it for tg, max and wa.
|
|
73
|
+
*/
|
|
74
|
+
export const installHelp = (program, screen = {}) => {
|
|
75
|
+
let full = false;
|
|
76
|
+
const own = program.configureHelp();
|
|
77
|
+
program.configureHelp({
|
|
78
|
+
...own,
|
|
79
|
+
// Commander copies this configuration to children made with `.command()`, so only the root gets the screen.
|
|
80
|
+
formatHelp: (command, helper) => command === program && !full
|
|
81
|
+
? firstScreen(program, screen)
|
|
82
|
+
: (own.formatHelp ?? Help.prototype.formatHelp).call(helper, command, helper),
|
|
83
|
+
});
|
|
84
|
+
program.addOption(new Option("--help-all", "the full help: every command and option").hideHelp());
|
|
85
|
+
program.on("option:help-all", () => {
|
|
86
|
+
full = true;
|
|
87
|
+
program.help();
|
|
88
|
+
});
|
|
89
|
+
if (program.commands.some((command) => command.name() === "help"))
|
|
90
|
+
return program;
|
|
91
|
+
const help = annotate(new Command("help"), { effects: NO_EFFECTS, mcp: false })
|
|
92
|
+
.description("this screen; help options: the options for every command; help <command>: that command's help")
|
|
93
|
+
.argument("[topic...]", "options, or a command path")
|
|
94
|
+
.action((topic) => {
|
|
95
|
+
if (topic.length === 0)
|
|
96
|
+
return program.outputHelp();
|
|
97
|
+
if (topic.length === 1 && topic[0] === "options")
|
|
98
|
+
return program.configureOutput().writeOut?.(optionsScreen(program));
|
|
99
|
+
const command = commandAt(program, topic.join(" "));
|
|
100
|
+
if (!command)
|
|
101
|
+
throw new CliError("validation_error", `no command "${topic.join(" ")}" — run \`${program.name()} help\`, or \`${program.name()} help options\``, { available: program.commands.map((one) => one.name()).filter((name) => name !== "help") });
|
|
102
|
+
command.outputHelp();
|
|
103
|
+
});
|
|
104
|
+
program.addCommand(help, { hidden: true });
|
|
105
|
+
return program;
|
|
106
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
export declare const SECTIONS: {
|
|
3
|
+
readonly read: "Read";
|
|
4
|
+
readonly write: "Write";
|
|
5
|
+
readonly organise: "Organise";
|
|
6
|
+
readonly automate: "Automate";
|
|
7
|
+
readonly setup: "Set up";
|
|
8
|
+
readonly other: "Other";
|
|
9
|
+
};
|
|
10
|
+
export type HelpSection = Exclude<keyof typeof SECTIONS, "other">;
|
|
11
|
+
/**
|
|
12
|
+
* Where `<tool> commands` lists the command and everything under it. Kept off `annotate`, whose metadata becomes
|
|
13
|
+
* fields of `commands --json`, which must not change.
|
|
14
|
+
*/
|
|
15
|
+
export declare const inSection: <T extends Command>(command: T, section: HelpSection) => T;
|
|
16
|
+
/** The nearest section on the command or above it; `other` when none is set. */
|
|
17
|
+
export declare const sectionOf: (command: Command) => keyof typeof SECTIONS;
|
|
18
|
+
/** Puts each of cli-core's groups mounted on `program` in its section, unless the tool already chose one. */
|
|
19
|
+
export declare const sectionCoreGroups: (program: Command) => void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const SECTIONS = {
|
|
2
|
+
read: "Read",
|
|
3
|
+
write: "Write",
|
|
4
|
+
organise: "Organise",
|
|
5
|
+
automate: "Automate",
|
|
6
|
+
setup: "Set up",
|
|
7
|
+
other: "Other",
|
|
8
|
+
};
|
|
9
|
+
const sections = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Where `<tool> commands` lists the command and everything under it. Kept off `annotate`, whose metadata becomes
|
|
12
|
+
* fields of `commands --json`, which must not change.
|
|
13
|
+
*/
|
|
14
|
+
export const inSection = (command, section) => {
|
|
15
|
+
sections.set(command, section);
|
|
16
|
+
return command;
|
|
17
|
+
};
|
|
18
|
+
/** The nearest section on the command or above it; `other` when none is set. */
|
|
19
|
+
export const sectionOf = (command) => {
|
|
20
|
+
for (let at = command; at; at = at.parent) {
|
|
21
|
+
const section = sections.get(at);
|
|
22
|
+
if (section)
|
|
23
|
+
return section;
|
|
24
|
+
}
|
|
25
|
+
return "other";
|
|
26
|
+
};
|
|
27
|
+
/** The groups cli-core builds, by name; a tool sets its own with `inSection`. */
|
|
28
|
+
const CORE_SECTIONS = {
|
|
29
|
+
attachments: "read",
|
|
30
|
+
chats: "read",
|
|
31
|
+
contacts: "read",
|
|
32
|
+
conversations: "read",
|
|
33
|
+
images: "read",
|
|
34
|
+
inbox: "read",
|
|
35
|
+
meetings: "read",
|
|
36
|
+
mentions: "read",
|
|
37
|
+
messages: "read",
|
|
38
|
+
review: "read",
|
|
39
|
+
search: "read",
|
|
40
|
+
stats: "read",
|
|
41
|
+
topics: "read",
|
|
42
|
+
watch: "read",
|
|
43
|
+
flood: "write",
|
|
44
|
+
polls: "write",
|
|
45
|
+
reactions: "write",
|
|
46
|
+
recipients: "write",
|
|
47
|
+
sends: "write",
|
|
48
|
+
metadata: "organise",
|
|
49
|
+
searches: "organise",
|
|
50
|
+
tags: "organise",
|
|
51
|
+
tasks: "organise",
|
|
52
|
+
bot: "automate",
|
|
53
|
+
mcp: "automate",
|
|
54
|
+
replies: "automate",
|
|
55
|
+
runs: "automate",
|
|
56
|
+
schedule: "automate",
|
|
57
|
+
serve: "automate",
|
|
58
|
+
server: "automate",
|
|
59
|
+
account: "setup",
|
|
60
|
+
commands: "setup",
|
|
61
|
+
complete: "setup",
|
|
62
|
+
config: "setup",
|
|
63
|
+
doctor: "setup",
|
|
64
|
+
models: "setup",
|
|
65
|
+
skill: "setup",
|
|
66
|
+
store: "setup",
|
|
67
|
+
upgrade: "setup",
|
|
68
|
+
};
|
|
69
|
+
/** Puts each of cli-core's groups mounted on `program` in its section, unless the tool already chose one. */
|
|
70
|
+
export const sectionCoreGroups = (program) => {
|
|
71
|
+
for (const command of program.commands) {
|
|
72
|
+
const section = CORE_SECTIONS[command.name()];
|
|
73
|
+
if (section && !sections.has(command))
|
|
74
|
+
inSection(command, section);
|
|
75
|
+
}
|
|
76
|
+
};
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
|
56
56
|
export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
|
|
57
57
|
export { type AnswerOutput, answerSchema, type Body, type ItemBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
58
58
|
export { withAbort } from "./execution.js";
|
|
59
|
+
export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
|
|
60
|
+
export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
|
|
59
61
|
export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
60
62
|
export { answerFields, checkJson, type EntityFields, entityFields, type JsonFieldsResolver, type JsonRequest, liftJson, } from "./json-fields.js";
|
|
61
63
|
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -51,6 +51,8 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
|
51
51
|
export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
|
|
52
52
|
export { answerSchema, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
53
53
|
export { withAbort } from "./execution.js";
|
|
54
|
+
export { firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
|
|
55
|
+
export { inSection, SECTIONS } from "./help/sections.js";
|
|
54
56
|
export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
55
57
|
export { answerFields, checkJson, entityFields, liftJson, } from "./json-fields.js";
|
|
56
58
|
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
package/dist/cli/list-query.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
2
2
|
import { dateRange, defineQuery, fieldsHelp, fieldsNamed, OPERATORS_BY_TYPE, parseLucene, QUERY_LIMITS, queryError, queryFieldsOf, validateQuery, } from "@wirecat/cli-base/query";
|
|
3
3
|
import * as v from "valibot";
|
|
4
|
+
import { queryWordsAfterOptions } from "../meetings/cli/index.js";
|
|
4
5
|
import { listOf } from "./envelope.js";
|
|
5
6
|
export const LIST_QUERY_GUIDE = "https://github.com/WireCatLabs/cli-core/blob/main/docs/search/query-language.md";
|
|
6
7
|
/**
|
|
@@ -11,10 +12,7 @@ export const withQuery = (command, registry) => {
|
|
|
11
12
|
const query = defineQuery(registry);
|
|
12
13
|
annotate(command, { query, output: { entity: registry.entity } });
|
|
13
14
|
const shown = askable(query);
|
|
14
|
-
|
|
15
|
-
command
|
|
16
|
-
.allowUnknownOption()
|
|
17
|
-
.argument("[query...]", shown.length > 0
|
|
15
|
+
queryWordsAfterOptions(command).argument("[query...]", shown.length > 0
|
|
18
16
|
? "field:value terms that narrow the list — the fields are below"
|
|
19
17
|
: "field:value terms; this list filters by none yet");
|
|
20
18
|
if (shown.length > 0)
|
|
@@ -32,6 +32,8 @@ export const completeCommand = (messenger, config, options = {}) => annotate(new
|
|
|
32
32
|
// Commander drops the `--` from the operands, so only the raw words tell a request from a shell name.
|
|
33
33
|
const raw = root.rawArgs;
|
|
34
34
|
if (!raw.includes("--")) {
|
|
35
|
+
if (words.includes("--help") || words.includes("-h"))
|
|
36
|
+
this.help();
|
|
35
37
|
const [shell] = words;
|
|
36
38
|
if (!shell || !SHELLS.includes(shell)) {
|
|
37
39
|
throw new CliError("validation_error", `name a shell: ${app.command} complete ${SHELLS.join(" | ")}`);
|
|
@@ -230,16 +230,17 @@ export const connectedFor = async (command, messenger, base, options) => {
|
|
|
230
230
|
...connected(connection, messenger, base, options.events, store, options.listen ? { listen: true } : {}),
|
|
231
231
|
};
|
|
232
232
|
};
|
|
233
|
-
const unrecorded = (command, messenger, profile) => {
|
|
234
|
-
const
|
|
235
|
-
const setup = rootOf(command).commands.some((one) => one.name() === "setup")
|
|
236
|
-
|
|
237
|
-
:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
233
|
+
const unrecorded = (command, messenger, profile, offline) => {
|
|
234
|
+
const words = `${messenger.app.command} ${asFirstWord(profile)}`;
|
|
235
|
+
const setup = rootOf(command).commands.some((one) => one.name() === "setup") ? `\`${words}setup\`` : undefined;
|
|
236
|
+
if (messenger.history === "store")
|
|
237
|
+
return new CliError("not_found", `nothing recorded for profile "${profile}" yet — ${setup ? `link it with ${setup}, then ` : ""}run ` +
|
|
238
|
+
`\`${words}serve\` or \`${words}watch\` once to fill the local store`);
|
|
239
|
+
const ways = [
|
|
240
|
+
...(setup ? [`log in with ${setup}`] : []),
|
|
241
|
+
...(offline ? ["run the command once without --offline"] : []),
|
|
242
|
+
];
|
|
243
|
+
return new CliError("not_found", `nothing recorded for profile "${profile}" yet — ${ways.length > 0 ? ways.join(", or ") : "log in to it first"}`);
|
|
243
244
|
};
|
|
244
245
|
/** NEED-506 A: without it, an agent cannot tell a quiet chat from a store nothing keeps up to date. */
|
|
245
246
|
const warnUnserved = async (messenger, profile, { env, renderer }, store) => {
|
|
@@ -300,7 +301,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
|
|
|
300
301
|
const accountFor = () => {
|
|
301
302
|
const account = messenger.accountOf?.(command) ?? recalledAccount(app, provider, profile, base.env);
|
|
302
303
|
if (!account)
|
|
303
|
-
throw unrecorded(command, messenger, profile);
|
|
304
|
+
throw unrecorded(command, messenger, profile, base.settings.offline);
|
|
304
305
|
return account;
|
|
305
306
|
};
|
|
306
307
|
const guard = messenger.guard?.(command, base.settings, warn) ??
|
|
@@ -113,7 +113,7 @@ export const mcpCommand = (messenger) => {
|
|
|
113
113
|
const server = entry;
|
|
114
114
|
const { potentialWrites } = await probeStdio(server);
|
|
115
115
|
if (potentialWrites.length > 0 && this.opts().allowWrites !== true)
|
|
116
|
-
throw new Error(`this profile offers ${potentialWrites.length} tools that may write; review its permissions, then pass --allow-writes to install it`);
|
|
116
|
+
throw new Error(`this profile offers ${potentialWrites.length} ${potentialWrites.length === 1 ? "tool" : "tools"} that may write; review its permissions, then pass --allow-writes to install it`);
|
|
117
117
|
renderAnswer(this, context, itemOf({ ...installStdioEntry(client, name, server), potential_writes: potentialWrites.length }));
|
|
118
118
|
}
|
|
119
119
|
catch (error) {
|
|
@@ -46,8 +46,8 @@ export const messagesSearchCommand = (messenger) => {
|
|
|
46
46
|
coverage says what was searched and coverage.next what would improve it.`
|
|
47
47
|
: `Search reads the local archive: what \`${messenger.app.command} serve\` has kept since the account was linked.
|
|
48
48
|
Keep serve running so new messages are searchable.`} Notes are
|
|
49
|
-
\`${messenger.app.command} search notes\`, messages and notes at once \`${messenger.app.command} search all\`; mail
|
|
50
|
-
\`memo search mail\`.
|
|
49
|
+
\`${messenger.app.command} search notes\`, messages and notes at once \`${messenger.app.command} search all\`; for mail,
|
|
50
|
+
the separate memo CLI: \`memo search mail\`.
|
|
51
51
|
A bare word looks in message text, voice transcripts, file names and file text. text:, transcript:,
|
|
52
52
|
content: (the text of files) and filename: (their names) narrow it to one —
|
|
53
53
|
\`${messenger.app.command} search messages 'content:invoice'\`. chat: narrows it to one chat; has:voice, has:file and
|
|
@@ -15,7 +15,7 @@ import { topicsSearchCommand } from "./topics-command.js";
|
|
|
15
15
|
const onlyOf = (value) => {
|
|
16
16
|
const chosen = value.split(",").map((one) => one.trim());
|
|
17
17
|
if (chosen.includes("mail"))
|
|
18
|
-
throw new CliError("validation_error", "mail is memo
|
|
18
|
+
throw new CliError("validation_error", "mail is searched by the separate memo CLI: install @wirecat/cli-memo, then `memo search mail …`");
|
|
19
19
|
const unknown = chosen.find((one) => !SOURCE_SEARCHED.includes(one));
|
|
20
20
|
if (unknown !== undefined || chosen.length === 0)
|
|
21
21
|
throw new CliError("validation_error", `--only takes ${SOURCE_SEARCHED.join(", ")}, separated by commas; meetings when --source names a meeting account`);
|
|
@@ -37,7 +37,7 @@ const allCommand = (messenger) => {
|
|
|
37
37
|
? "search messenger messages and notes in the local store, and the messenger's server for messages " +
|
|
38
38
|
"(--backend; notes are local only)"
|
|
39
39
|
: "search messenger messages and notes in the local store") +
|
|
40
|
-
", and a meeting account's transcripts when --source names one — best match first; not mail (memo search mail), " +
|
|
40
|
+
", and a meeting account's transcripts when --source names one — best match first; not mail (the separate memo CLI searches it: memo search mail), " +
|
|
41
41
|
"tasks, people, memories or projects. Use it when you do not know where something was written; search messages " +
|
|
42
42
|
"or notes reads one kind with all its fields")
|
|
43
43
|
.argument("<query...>", 'strict Lucene query: words, "phrases", AND/OR/NOT, field groups and date ranges')
|
|
@@ -149,7 +149,7 @@ const notesCommand = (messenger) => annotate(new Command("notes"), { effects: LO
|
|
|
149
149
|
* hierarchy"). `topics` only where the CLI has forum topics.
|
|
150
150
|
*/
|
|
151
151
|
export const searchCommand = (messenger, { topics = false } = {}) => {
|
|
152
|
-
const search = new Command("search").description("find things by text: one resource when you know it (messages, notes), or search all across messages and notes; mail
|
|
152
|
+
const search = new Command("search").description("find things by text: one resource when you know it (messages, notes), or search all across messages and notes; for mail, the separate memo CLI: memo search mail");
|
|
153
153
|
search
|
|
154
154
|
.addCommand(allCommand(messenger))
|
|
155
155
|
.addCommand(messagesSearchCommand(messenger))
|
package/dist/cli/moved.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* "unknown command"; a path the program still mounts never reaches it.
|
|
5
5
|
*/
|
|
6
6
|
export const MOVED_TO_MEMO = {
|
|
7
|
-
"search mail": { what: "mail is memo
|
|
7
|
+
"search mail": { what: "mail is searched by the separate memo CLI", memo: "memo search mail …" },
|
|
8
8
|
};
|
|
9
9
|
export const movedPointer = (path) => {
|
|
10
10
|
const moved = MOVED_TO_MEMO[path];
|
package/dist/cli/profile.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Command } from "commander";
|
|
|
2
2
|
/** How the command line names this profile: always the first word, since none is assumed. */
|
|
3
3
|
export declare const asFirstWord: (profile: string) => string;
|
|
4
4
|
export declare const usableProfileName: (profile: string) => string;
|
|
5
|
+
/** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
|
|
6
|
+
export declare const PROFILE_FREE: ReadonlySet<string>;
|
|
5
7
|
/**
|
|
6
8
|
* Every word that means a command at the top level. Taken off the built program rather than
|
|
7
9
|
* written out, because a list written out goes stale the day a command is added — and `help` is in
|
|
@@ -13,10 +15,12 @@ export declare const commandWords: (program: Command) => Set<string>;
|
|
|
13
15
|
* `TG_PROFILE=personal tg chats list`, with no flag in either. An option is never a profile, so
|
|
14
16
|
* `tg --json chats list` still works.
|
|
15
17
|
*/
|
|
16
|
-
export declare const liftProfile: (argv: string[], words: Set<string>) => {
|
|
18
|
+
export declare const liftProfile: (argv: string[], words: Set<string>, valued?: ReadonlySet<string>) => {
|
|
17
19
|
profile?: string;
|
|
18
20
|
rest: string[];
|
|
19
21
|
};
|
|
22
|
+
/** The root's options that take the next word as their value: `--timeout 30s`. */
|
|
23
|
+
export declare const valuedOptions: (program: Command) => Set<string>;
|
|
20
24
|
/**
|
|
21
25
|
* Refused at creation, which is the only moment it can still be explained: a profile named after a
|
|
22
26
|
* command could never be selected, because `tg chats` has to mean the command.
|
package/dist/cli/profile.js
CHANGED
|
@@ -14,6 +14,8 @@ export const usableProfileName = (profile) => {
|
|
|
14
14
|
}
|
|
15
15
|
return profile;
|
|
16
16
|
};
|
|
17
|
+
/** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
|
|
18
|
+
export const PROFILE_FREE = new Set(["commands", "complete", "help", "skill", "upgrade"]);
|
|
17
19
|
/**
|
|
18
20
|
* Every word that means a command at the top level. Taken off the built program rather than
|
|
19
21
|
* written out, because a list written out goes stale the day a command is added — and `help` is in
|
|
@@ -25,12 +27,20 @@ export const commandWords = (program) => new Set(["help", ...program.commands.fl
|
|
|
25
27
|
* `TG_PROFILE=personal tg chats list`, with no flag in either. An option is never a profile, so
|
|
26
28
|
* `tg --json chats list` still works.
|
|
27
29
|
*/
|
|
28
|
-
export const liftProfile = (argv, words) => {
|
|
29
|
-
|
|
30
|
+
export const liftProfile = (argv, words, valued = new Set()) => {
|
|
31
|
+
// `tg --json personal chats list`: options before the profile are skipped over, with the value an option takes.
|
|
32
|
+
let at = 0;
|
|
33
|
+
while (argv[at]?.startsWith("-") && argv[at] !== "--")
|
|
34
|
+
at += valued.has(argv[at]) ? 2 : 1;
|
|
35
|
+
const first = argv[at];
|
|
30
36
|
if (first === undefined || first.startsWith("-") || words.has(first))
|
|
31
37
|
return { rest: argv };
|
|
32
|
-
return { profile: first, rest };
|
|
38
|
+
return { profile: first, rest: [...argv.slice(0, at), ...argv.slice(at + 1)] };
|
|
33
39
|
};
|
|
40
|
+
/** The root's options that take the next word as their value: `--timeout 30s`. */
|
|
41
|
+
export const valuedOptions = (program) => new Set(program.options
|
|
42
|
+
.filter((option) => option.required)
|
|
43
|
+
.flatMap((option) => [option.long, option.short].filter((flag) => flag !== undefined)));
|
|
34
44
|
/**
|
|
35
45
|
* Refused at creation, which is the only moment it can still be explained: a profile named after a
|
|
36
46
|
* command could never be selected, because `tg chats` has to mean the command.
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type AppIdentity } from "../ports/app.js";
|
|
3
3
|
import { type BaseEnvironment } from "./context.js";
|
|
4
|
+
import { type FirstScreen } from "./help/first-screen.js";
|
|
4
5
|
import { type GlobalFlags, type ResolveOptions, type Settings } from "./settings.js";
|
|
5
6
|
export interface ProgramDefinition {
|
|
6
7
|
app: AppIdentity;
|
|
@@ -11,6 +12,8 @@ export interface ProgramDefinition {
|
|
|
11
12
|
resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint">;
|
|
12
13
|
};
|
|
13
14
|
configure?: (program: Command) => void;
|
|
15
|
+
/** The tool's pitch and "what you can do" lines on `--help` and a bare run. */
|
|
16
|
+
firstScreen?: FirstScreen;
|
|
14
17
|
prepare?: (program: Command, environment: RunOptions) => void | Promise<void>;
|
|
15
18
|
onFailure?: (error: unknown, program: Command) => void | Promise<void>;
|
|
16
19
|
}
|
|
@@ -23,7 +26,7 @@ export interface ProgramOptions {
|
|
|
23
26
|
* The command tree, built fresh on each call. Commander is global state by default —
|
|
24
27
|
* `exitOverride` and the write hooks turn it into a value a test can drive.
|
|
25
28
|
*/
|
|
26
|
-
export declare const createProgram: ({ app, commands, configure }: ProgramDefinition, { out, err }?: ProgramOptions) => Command;
|
|
29
|
+
export declare const createProgram: ({ app, commands, configure, firstScreen }: ProgramDefinition, { out, err }?: ProgramOptions) => Command;
|
|
27
30
|
export type RunOptions = BaseEnvironment & Record<string, unknown>;
|
|
28
31
|
/**
|
|
29
32
|
* An interactive command run without a terminal — an agent's `session start --qr-file` — waits on a person scanning a
|
package/dist/cli/program.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { CliError, errorObjectOf, errorRecordOf, exitCodeFor, exitCodeOfError, processStreams, renderError, resolvePaths, } from "@wirecat/cli-base";
|
|
3
3
|
import { metaOf, refuseJoinedPath } from "@wirecat/cli-base/commands";
|
|
4
4
|
import { skillHint } from "@wirecat/cli-base/skill";
|
|
5
|
-
import { Command } from "commander";
|
|
5
|
+
import { Command, Help } from "commander";
|
|
6
6
|
import { relayCommand } from "../background/relay.js";
|
|
7
7
|
import { failingNotice, scheduledRuns } from "../background/schedules.js";
|
|
8
8
|
import { parseDuration } from "../domain/duration.js";
|
|
@@ -18,11 +18,13 @@ import { refuseWriteBeforeConnecting } from "./early-refusal.js";
|
|
|
18
18
|
import { changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, STORE_CHANGE_REFUSAL, } from "./effects.js";
|
|
19
19
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
20
20
|
import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
|
|
21
|
+
import { installHelp } from "./help/first-screen.js";
|
|
22
|
+
import { sectionCoreGroups } from "./help/sections.js";
|
|
21
23
|
import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
|
|
22
24
|
import { checkJson, liftJson } from "./json-fields.js";
|
|
23
25
|
import { movedPath, movedPointer } from "./moved.js";
|
|
24
26
|
import { PreviewComplete, preview } from "./preview.js";
|
|
25
|
-
import { commandWords, liftProfile } from "./profile.js";
|
|
27
|
+
import { commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
|
|
26
28
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
27
29
|
import { settingsFor } from "./settings.js";
|
|
28
30
|
import { commandAt } from "./steps.js";
|
|
@@ -30,7 +32,7 @@ import { commandAt } from "./steps.js";
|
|
|
30
32
|
* The command tree, built fresh on each call. Commander is global state by default —
|
|
31
33
|
* `exitOverride` and the write hooks turn it into a value a test can drive.
|
|
32
34
|
*/
|
|
33
|
-
export const createProgram = ({ app, commands, configure }, { out, err } = {}) => {
|
|
35
|
+
export const createProgram = ({ app, commands, configure, firstScreen }, { out, err } = {}) => {
|
|
34
36
|
const program = new Command();
|
|
35
37
|
program
|
|
36
38
|
.name(app.command)
|
|
@@ -61,6 +63,16 @@ export const createProgram = ({ app, commands, configure }, { out, err } = {}) =
|
|
|
61
63
|
for (const command of commands())
|
|
62
64
|
program.addCommand(command);
|
|
63
65
|
configure?.(program);
|
|
66
|
+
sectionCoreGroups(program);
|
|
67
|
+
installHelp(program, { pitch: app.description, ...firstScreen });
|
|
68
|
+
// Commander builds a usage line from the command tree, where the profile is not a word.
|
|
69
|
+
for (const top of program.commands) {
|
|
70
|
+
if (PROFILE_FREE.has(top.name()))
|
|
71
|
+
continue;
|
|
72
|
+
forEachCommand(top, (command) => command.configureHelp({
|
|
73
|
+
commandUsage: (shown) => `${app.command} <profile>${new Help().commandUsage(shown).slice(app.command.length)}`,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
64
76
|
// Depth-first: Commander does not pass `configureOutput` down to a command added with
|
|
65
77
|
// `addCommand`, so `tg messages --help` would write to the real terminal.
|
|
66
78
|
if (out || err) {
|
|
@@ -327,7 +339,7 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
327
339
|
forEachCommand(program, (child) => child.exitOverride());
|
|
328
340
|
// Before commander, not inside it: commander has no hook that runs before it decides which
|
|
329
341
|
// subcommand it is looking at.
|
|
330
|
-
const { profile, rest: lifted } = liftProfile(argv, commandWords(program));
|
|
342
|
+
const { profile, rest: lifted } = liftProfile(argv, commandWords(program), valuedOptions(program));
|
|
331
343
|
// `wa 'chats list' --help` reads "chats list" as a profile and prints the root help with exit 0.
|
|
332
344
|
try {
|
|
333
345
|
// Only before `--`: what follows is a positional, an agent's text among them, never a command word.
|
|
@@ -364,7 +376,9 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
364
376
|
try {
|
|
365
377
|
await control.race(() => program.parseAsync(rest, { from: "user" }));
|
|
366
378
|
const code = exit.code ?? (process.exitCode === undefined ? 0 : Number(process.exitCode));
|
|
367
|
-
const hint = code === 0 && rest[0] !== "commands" && !rest.includes("--quiet")
|
|
379
|
+
const hint = code === 0 && rest[0] !== "commands" && rest[0] !== "help" && !rest.includes("--quiet")
|
|
380
|
+
? hintFor(definition, rest, options)
|
|
381
|
+
: undefined;
|
|
368
382
|
if (hint)
|
|
369
383
|
streams.diagnostic(hint);
|
|
370
384
|
return code;
|
|
@@ -384,7 +398,8 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
384
398
|
logVersion(argvLog);
|
|
385
399
|
if (error.exitCode === 0)
|
|
386
400
|
return 0;
|
|
387
|
-
|
|
401
|
+
// Commander puts its "(Did you mean …?)" on a line of its own; an error is one line.
|
|
402
|
+
let message = error.message.replace(/^error: /, "").replace(/\s*\n\s*/g, " ");
|
|
388
403
|
if (error.code === "commander.help") {
|
|
389
404
|
if (reporting.tty ?? process.stdout.isTTY === true) {
|
|
390
405
|
streams.diagnostic(help.replace(/\n$/, ""));
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { type AgentAnswer, type AgentContext, type AgentProgram, type AgentTool,
|
|
|
2
2
|
export { type BotServerOptions, createBotServer, type RunBotCommand } from "./bot/server.js";
|
|
3
3
|
export { revokeAll } from "./http/oauth.js";
|
|
4
4
|
export { type HttpOptions, MCP_PATH, serveOverHttp } from "./http/serve.js";
|
|
5
|
+
export { type McpCommand, registerCommands } from "./surface.js";
|
package/dist/mcp/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { runCommandForAgent, toolsFromCommands, } from "./agent.js";
|
|
|
3
3
|
export { createBotServer } from "./bot/server.js";
|
|
4
4
|
export { revokeAll } from "./http/oauth.js";
|
|
5
5
|
export { MCP_PATH, serveOverHttp } from "./http/serve.js";
|
|
6
|
+
export { registerCommands } from "./surface.js";
|
|
@@ -14,4 +14,5 @@ export interface MeetingCommandDeps {
|
|
|
14
14
|
export declare const addMeetingCommands: (program: Command, deps: MeetingCommandDeps) => void;
|
|
15
15
|
export { addMeetingPassageCommand, createMeetingPassageCommand, type MeetingPassageFlags, meetingPassageRequest, } from "./passage.js";
|
|
16
16
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
17
|
+
export { queryWordsAfterOptions } from "./query-options.js";
|
|
17
18
|
export { addMeetingSearchCommand, createMeetingSearchCommand, type MeetingHitSearch, type MeetingHitSearchOptions, type MeetingSearchFlags, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -7,6 +7,7 @@ import { MEETING_QUERY } from "../item.js";
|
|
|
7
7
|
import { pull } from "../pull.js";
|
|
8
8
|
import { listed, MEETING_QUERY_GUIDE, MeetingError, meetingDetailsAnswer, meetingPeople, meetingSummaryAnswer, meetingsList, meetingsQuery, meetingTranscriptAnswer, } from "../reads.js";
|
|
9
9
|
import { SAVE_HELP } from "../save-hints.js";
|
|
10
|
+
import { queryWordsAfterOptions } from "./query-options.js";
|
|
10
11
|
const integer = (text) => {
|
|
11
12
|
if (!/^[1-9]\d*$/.test(text) || !Number.isSafeInteger(Number(text)))
|
|
12
13
|
throw new InvalidArgumentError("expected a positive integer");
|
|
@@ -39,10 +40,7 @@ export const addMeetingCommands = (program, deps) => {
|
|
|
39
40
|
const opts = command.optsWithGlobals();
|
|
40
41
|
deps.write(value, opts.json ? "json" : opts.jsonl ? "jsonl" : "text");
|
|
41
42
|
};
|
|
42
|
-
const list = group
|
|
43
|
-
.command("list")
|
|
44
|
-
.description("stored meetings, newest first")
|
|
45
|
-
.allowUnknownOption()
|
|
43
|
+
const list = queryWordsAfterOptions(group.command("list").description("stored meetings, newest first"))
|
|
46
44
|
.argument("[query...]", "field:value terms that narrow the list — the fields are below")
|
|
47
45
|
.addHelpText("after", `\n${fieldsHelp(MEETING_QUERY, MEETING_QUERY_GUIDE)}`);
|
|
48
46
|
annotate(list, { effects: LOCAL_READ, query: defineQuery(MEETING_QUERY), output: { entity: "meeting" } });
|
|
@@ -139,4 +137,5 @@ export const addMeetingCommands = (program, deps) => {
|
|
|
139
137
|
};
|
|
140
138
|
export { addMeetingPassageCommand, createMeetingPassageCommand, meetingPassageRequest, } from "./passage.js";
|
|
141
139
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
140
|
+
export { queryWordsAfterOptions } from "./query-options.js";
|
|
142
141
|
export { addMeetingSearchCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -3,6 +3,7 @@ import { annotate } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { fieldsHelp } from "@wirecat/cli-base/query";
|
|
4
4
|
import { readMeetingProjectEvidence } from "../project-evidence.js";
|
|
5
5
|
import { MEETING_PROJECT_EVIDENCE_QUERY } from "../project-evidence-query.js";
|
|
6
|
+
import { queryWordsAfterOptions } from "./query-options.js";
|
|
6
7
|
const integer = (value) => {
|
|
7
8
|
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
8
9
|
throw new CliError("validation_error", "expected a positive safe integer");
|
|
@@ -14,12 +15,11 @@ export const addMeetingProjectEvidenceCommand = (program, deps) => {
|
|
|
14
15
|
throw new CliError("configuration_error", "meeting command group is unavailable");
|
|
15
16
|
const projects = group.commands.find((command) => command.name() === "projects") ??
|
|
16
17
|
group.command("projects").description("project-name evidence in stored meetings");
|
|
17
|
-
return annotate(projects
|
|
18
|
+
return annotate(queryWordsAfterOptions(projects
|
|
18
19
|
.command("evidence")
|
|
19
|
-
.description("find cited project-name and alias evidence in the local meeting index; does not refresh it or confirm membership")
|
|
20
|
+
.description("find cited project-name and alias evidence in the local meeting index; does not refresh it or confirm membership"))
|
|
20
21
|
.argument("<project>", "existing project name, key or explicit alias")
|
|
21
22
|
.argument("[query...]", "exact ID fields and a UTC started_at range, joined by AND")
|
|
22
|
-
.allowUnknownOption()
|
|
23
23
|
.addHelpText("after", `\n${fieldsHelp(MEETING_PROJECT_EVIDENCE_QUERY)}`)
|
|
24
24
|
.option("--limit <n>", "hits in one name page, 1 through 1000 (default 100)", integer)
|
|
25
25
|
.option("--cursor <cursor>", "continue with the same project and frozen selection from next")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Lets a query's `-kind:channel` through as a negated term while a mistyped `--limt` still fails as an unknown
|
|
4
|
+
* option, with commander's suggestion; after `--` every word is query.
|
|
5
|
+
*/
|
|
6
|
+
export declare const queryWordsAfterOptions: (command: Command) => Command;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lets a query's `-kind:channel` through as a negated term while a mistyped `--limt` still fails as an unknown
|
|
3
|
+
* option, with commander's suggestion; after `--` every word is query.
|
|
4
|
+
*/
|
|
5
|
+
export const queryWordsAfterOptions = (command) => {
|
|
6
|
+
command.allowUnknownOption();
|
|
7
|
+
const parse = command.parseOptions.bind(command);
|
|
8
|
+
command.parseOptions = (args) => {
|
|
9
|
+
const parsed = parse(args);
|
|
10
|
+
const end = parsed.unknown.indexOf("--");
|
|
11
|
+
const typo = parsed.unknown.slice(0, end < 0 ? undefined : end).find((word) => word.startsWith("--"));
|
|
12
|
+
if (typo !== undefined) {
|
|
13
|
+
command.allowUnknownOption(false);
|
|
14
|
+
command.unknownOption(typo);
|
|
15
|
+
}
|
|
16
|
+
if (end >= 0)
|
|
17
|
+
parsed.unknown.splice(end, 1);
|
|
18
|
+
return parsed;
|
|
19
|
+
};
|
|
20
|
+
return command;
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.344.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|