@wirecat/cli-core 0.343.0 → 0.345.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 +93 -0
- package/dist/cli/bot/command.js +3 -1
- package/dist/cli/bot/context.js +8 -4
- package/dist/cli/command-contract.d.ts +13 -0
- package/dist/cli/command-contract.js +1 -1
- package/dist/cli/commands-command.js +9 -12
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/guide.d.ts +52 -0
- package/dist/cli/guide.js +307 -0
- package/dist/cli/help/command-help.d.ts +30 -0
- package/dist/cli/help/command-help.js +218 -0
- package/dist/cli/help/commands-text.d.ts +9 -0
- package/dist/cli/help/commands-text.js +43 -0
- package/dist/cli/help/first-screen.d.ts +37 -0
- package/dist/cli/help/first-screen.js +109 -0
- package/dist/cli/help/sections.d.ts +19 -0
- package/dist/cli/help/sections.js +77 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +4 -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/conversations-command.js +4 -1
- package/dist/cli/messenger/inbox.js +1 -1
- package/dist/cli/messenger/mcp-command.js +1 -1
- package/dist/cli/messenger/messages-search-command.js +2 -2
- package/dist/cli/messenger/observed.js +15 -9
- package/dist/cli/messenger/private-people-command.js +10 -1
- package/dist/cli/messenger/review.js +1 -1
- package/dist/cli/messenger/search-command.js +3 -3
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/store-maintenance-command.js +8 -5
- 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 +23 -8
- 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/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1 -0
- package/dist/services/project-gathering-request.d.ts +120 -0
- package/dist/services/project-gathering-request.js +217 -0
- package/dist/services/project-gathering.d.ts +89 -0
- package/dist/services/project-gathering.js +202 -0
- package/package.json +5 -4
- package/parity.json +2 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { metaOf } from "@wirecat/cli-base/commands";
|
|
2
|
+
import { writes } from "@wirecat/cli-base/standards";
|
|
3
|
+
import { commandPathOf, outputOf } from "../command-contract.js";
|
|
4
|
+
import { commandAt } from "../steps.js";
|
|
5
|
+
import { short } from "./commands-text.js";
|
|
6
|
+
/** Argument names onto the kinds `domain/references.ts` names; a plural takes the same kind. */
|
|
7
|
+
const ARGUMENT_KINDS = {
|
|
8
|
+
chat: "chat",
|
|
9
|
+
chats: "chat",
|
|
10
|
+
message: "message",
|
|
11
|
+
messages: "message",
|
|
12
|
+
post: "message",
|
|
13
|
+
person: "person",
|
|
14
|
+
people: "person",
|
|
15
|
+
task: "task",
|
|
16
|
+
meeting: "meeting",
|
|
17
|
+
folder: "folder",
|
|
18
|
+
folders: "folder",
|
|
19
|
+
topic: "topic",
|
|
20
|
+
note: "note",
|
|
21
|
+
document: "document",
|
|
22
|
+
model: "model",
|
|
23
|
+
};
|
|
24
|
+
/** What a command answers with, by its declared entity, onto the same kinds. */
|
|
25
|
+
const ENTITY_KINDS = {
|
|
26
|
+
message: "message",
|
|
27
|
+
message_link: "message",
|
|
28
|
+
chat: "chat",
|
|
29
|
+
contact: "person",
|
|
30
|
+
member: "person",
|
|
31
|
+
person: "person",
|
|
32
|
+
task: "task",
|
|
33
|
+
meeting: "meeting",
|
|
34
|
+
meeting_details: "meeting",
|
|
35
|
+
meeting_hit: "meeting",
|
|
36
|
+
meeting_summary: "meeting",
|
|
37
|
+
folder: "folder",
|
|
38
|
+
topic: "topic",
|
|
39
|
+
note: "note",
|
|
40
|
+
document: "document",
|
|
41
|
+
model: "model",
|
|
42
|
+
};
|
|
43
|
+
/** The group a kind lives in, whose commands come first among the ones that take it. */
|
|
44
|
+
const HOME = {
|
|
45
|
+
chat: "chats",
|
|
46
|
+
message: "messages",
|
|
47
|
+
person: "contacts",
|
|
48
|
+
task: "tasks",
|
|
49
|
+
meeting: "meetings",
|
|
50
|
+
folder: "folders",
|
|
51
|
+
topic: "topics",
|
|
52
|
+
note: "notes",
|
|
53
|
+
document: "documents",
|
|
54
|
+
model: "models",
|
|
55
|
+
};
|
|
56
|
+
const SAMPLES = {
|
|
57
|
+
chat: "Team",
|
|
58
|
+
message: "1234",
|
|
59
|
+
post: "1234",
|
|
60
|
+
person: "Anna",
|
|
61
|
+
people: "Anna",
|
|
62
|
+
task: "12",
|
|
63
|
+
meeting: "123",
|
|
64
|
+
folder: "Work",
|
|
65
|
+
topic: "General",
|
|
66
|
+
model: "default",
|
|
67
|
+
query: '"invoice"',
|
|
68
|
+
text: '"on my way"',
|
|
69
|
+
title: '"Weekly sync"',
|
|
70
|
+
name: "daily",
|
|
71
|
+
id: "1",
|
|
72
|
+
file: "./report.pdf",
|
|
73
|
+
link: "https://example.com/join/abc",
|
|
74
|
+
url: "https://example.com",
|
|
75
|
+
since: "1d",
|
|
76
|
+
duration: "1d",
|
|
77
|
+
limit: "20",
|
|
78
|
+
};
|
|
79
|
+
const MAX_NEXT = 3;
|
|
80
|
+
const choices = new WeakMap();
|
|
81
|
+
/** Overrides the "Next usually" a command's help derives from types. Kept off `annotate`, like its section. */
|
|
82
|
+
export const nextUsually = (command, choice) => {
|
|
83
|
+
choices.set(command, choice);
|
|
84
|
+
return command;
|
|
85
|
+
};
|
|
86
|
+
const kindOfArgument = (argument) => ARGUMENT_KINDS[argument.name()];
|
|
87
|
+
const leaves = (command) => command.commands
|
|
88
|
+
.filter((child) => !child._hidden)
|
|
89
|
+
.flatMap((child) => (child.commands.length === 0 ? [child] : leaves(child)));
|
|
90
|
+
const rootOf = (command) => {
|
|
91
|
+
let at = command;
|
|
92
|
+
while (at.parent)
|
|
93
|
+
at = at.parent;
|
|
94
|
+
return at;
|
|
95
|
+
};
|
|
96
|
+
const call = (command) => [
|
|
97
|
+
rootOf(command).name(),
|
|
98
|
+
...commandPathOf(command),
|
|
99
|
+
...command.registeredArguments.map((one) => (one.required ? `<${one.name()}>` : `[${one.name()}]`)),
|
|
100
|
+
].join(" ");
|
|
101
|
+
const returnsOf = (command) => {
|
|
102
|
+
const output = metaOf(command).output;
|
|
103
|
+
const { item, coverage } = outputOf(command);
|
|
104
|
+
if (!output || coverage !== "declared-entity")
|
|
105
|
+
return short(command.summary() || command.description()) || "not declared";
|
|
106
|
+
const required = item.required ?? [];
|
|
107
|
+
const fields = [
|
|
108
|
+
...required,
|
|
109
|
+
...Object.keys(item.properties ?? {}).filter((name) => !required.includes(name)),
|
|
110
|
+
];
|
|
111
|
+
const noun = (output.view ? `${output.entity} ${output.view}` : output.entity).replaceAll("_", " ");
|
|
112
|
+
const more = fields.length > 8 ? `, and ${fields.length - 8} more` : "";
|
|
113
|
+
const root = rootOf(command);
|
|
114
|
+
const schema = commandAt(root, "commands schema")
|
|
115
|
+
? `; every field: ${root.name()} commands schema ${commandPathOf(command).join(" ")}`
|
|
116
|
+
: "";
|
|
117
|
+
return `${noun} — ${fields.slice(0, 8).join(", ")}${more}${schema}`;
|
|
118
|
+
};
|
|
119
|
+
const nextOf = (command) => {
|
|
120
|
+
const root = rootOf(command);
|
|
121
|
+
const choice = choices.get(command) ?? {};
|
|
122
|
+
const drop = new Set(choice.drop ?? []);
|
|
123
|
+
const pinned = (choice.pin ?? []).flatMap((path) => commandAt(root, path) ?? []);
|
|
124
|
+
const output = metaOf(command).output;
|
|
125
|
+
const kind = output ? ENTITY_KINDS[output.entity] : undefined;
|
|
126
|
+
const all = leaves(root);
|
|
127
|
+
const derived = kind
|
|
128
|
+
? all
|
|
129
|
+
.filter((other) => other !== command && !pinned.includes(other))
|
|
130
|
+
.filter((other) => other.registeredArguments.some((argument) => kindOfArgument(argument) === kind))
|
|
131
|
+
.map((other, order) => ({ other, order }))
|
|
132
|
+
.sort((a, b) => {
|
|
133
|
+
const rank = (other) => [
|
|
134
|
+
Number(writes(metaOf(other).effects)),
|
|
135
|
+
Number(!other.registeredArguments.some((one) => one.required && kindOfArgument(one) === kind)),
|
|
136
|
+
Number(commandPathOf(other)[0] !== HOME[kind]),
|
|
137
|
+
Number(other.parent !== command.parent),
|
|
138
|
+
];
|
|
139
|
+
const [left, right] = [rank(a.other), rank(b.other)];
|
|
140
|
+
return left.reduce((found, value, at) => found || value - (right[at] ?? 0), 0) || a.order - b.order;
|
|
141
|
+
})
|
|
142
|
+
.map(({ other }) => other)
|
|
143
|
+
: [];
|
|
144
|
+
return [...pinned, ...derived].filter((other) => !drop.has(commandPathOf(other).join(" "))).slice(0, MAX_NEXT);
|
|
145
|
+
};
|
|
146
|
+
const sample = (name) => SAMPLES[name] ?? `<${name}>`;
|
|
147
|
+
const generatedExample = (command) => {
|
|
148
|
+
const { query } = metaOf(command);
|
|
149
|
+
const words = command.registeredArguments.flatMap((argument) => {
|
|
150
|
+
const name = argument.name();
|
|
151
|
+
// A list's `[query...]` is its field filters, not text; a search's is the text.
|
|
152
|
+
if (name === "query" && query)
|
|
153
|
+
return [];
|
|
154
|
+
if (!argument.required && SAMPLES[name] === undefined)
|
|
155
|
+
return [];
|
|
156
|
+
return [argument.argChoices?.[0] ?? sample(name)];
|
|
157
|
+
});
|
|
158
|
+
const options = command.options
|
|
159
|
+
.filter((option) => option.mandatory)
|
|
160
|
+
.flatMap((option) => {
|
|
161
|
+
const flag = option.long ?? option.short ?? option.flags;
|
|
162
|
+
const placeholder = /[<[]([^>\]]+)[>\]]/.exec(option.flags)?.[1];
|
|
163
|
+
if (!(option.required || option.optional))
|
|
164
|
+
return [flag];
|
|
165
|
+
return [flag, option.argChoices?.[0] ?? sample(placeholder ?? option.attributeName())];
|
|
166
|
+
});
|
|
167
|
+
// No profile word, as on the first screen and in written examples: the Usage line already shows `<profile>`.
|
|
168
|
+
return [rootOf(command).name(), ...commandPathOf(command), ...words, ...options].join(" ");
|
|
169
|
+
};
|
|
170
|
+
/** Returns, Next usually and Example for one command, from its definition alone. */
|
|
171
|
+
export const commandHelp = (command) => {
|
|
172
|
+
const written = metaOf(command).examples ?? [];
|
|
173
|
+
return {
|
|
174
|
+
returns: returnsOf(command),
|
|
175
|
+
next: nextOf(command).map(call),
|
|
176
|
+
examples: written.length > 0 ? written : [generatedExample(command)],
|
|
177
|
+
origin: written.length > 0 ? "written" : "generated",
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const LABEL = 15;
|
|
181
|
+
/** The lines `--help` prints after a command's options. */
|
|
182
|
+
export const commandHelpText = (command) => {
|
|
183
|
+
const { returns, next, examples } = commandHelp(command);
|
|
184
|
+
const rows = (label, values) => values.map((value, at) => `${(at === 0 ? label : "").padEnd(LABEL)}${value}`);
|
|
185
|
+
return ["", ...rows("Returns:", [returns]), ...rows("Next usually:", next), ...rows("Example:", examples), ""].join("\n");
|
|
186
|
+
};
|
|
187
|
+
/** The same, as Markdown lines for `docs/commands.md`: hand it to cli-base's `commandsPage` per command. */
|
|
188
|
+
export const commandHelpMarkdown = (root, path) => {
|
|
189
|
+
const command = commandAt(root, path.join(" "));
|
|
190
|
+
if (!command || command.commands.length > 0)
|
|
191
|
+
return [];
|
|
192
|
+
const { returns, next, examples } = commandHelp(command);
|
|
193
|
+
const code = (values) => values.map((value) => `\`${value}\``).join(", ");
|
|
194
|
+
return [
|
|
195
|
+
`**Returns:** ${returns}`,
|
|
196
|
+
...(next.length > 0 ? [`**Next usually:** ${code(next)}`] : []),
|
|
197
|
+
`**Example:** ${code(examples)}`,
|
|
198
|
+
];
|
|
199
|
+
};
|
|
200
|
+
/** Puts Returns, Next usually and Example on every leaf command's `--help`, worked out when the help is shown. */
|
|
201
|
+
export const installCommandHelp = (program) => {
|
|
202
|
+
for (const command of leaves(program))
|
|
203
|
+
command.addHelpText("after", () => commandHelpText(command));
|
|
204
|
+
};
|
|
205
|
+
/** Which leaf commands still show a generated example, so the important ones get a written one. Report only. */
|
|
206
|
+
export const exampleReport = (program) => {
|
|
207
|
+
const written = [];
|
|
208
|
+
const generated = [];
|
|
209
|
+
for (const command of leaves(program))
|
|
210
|
+
(commandHelp(command).origin === "written" ? written : generated).push(commandPathOf(command).join(" "));
|
|
211
|
+
const total = written.length + generated.length;
|
|
212
|
+
const text = [
|
|
213
|
+
`${program.name()}: ${written.length} of ${total} commands have a written example; ${generated.length} show a generated one:`,
|
|
214
|
+
...generated.map((path) => ` ${program.name()} ${path}`),
|
|
215
|
+
"",
|
|
216
|
+
].join("\n");
|
|
217
|
+
return { written, generated, text };
|
|
218
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommandInfo } from "@wirecat/cli-base/commands";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
/** The first clause that fits: a line per command is the point, the whole text is in `--help`. */
|
|
4
|
+
export declare const short: (text: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* `<tool> commands` for a person or an agent reading text: every command that runs, one line each, under its
|
|
7
|
+
* section. `scope` is the group asked for (`commands messages`), whose commands are all listed.
|
|
8
|
+
*/
|
|
9
|
+
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
|
+
export 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,37 @@
|
|
|
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`, and Returns, Next usually and Example on every leaf
|
|
32
|
+
* command's `--help`. `help options` prints the options for every command, `--help-all` the full commander help;
|
|
33
|
+
* both are hidden, so `commands --json` and MCP do not change. For a tool that builds its own program;
|
|
34
|
+
* `createProgram` calls it for tg, max and wa.
|
|
35
|
+
*/
|
|
36
|
+
export declare const installHelp: (program: Command, screen?: FirstScreen) => Command;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
import { installCommandHelp } from "./command-help.js";
|
|
8
|
+
/** Every WireCat tool, as "Works with" names it; a tool's own entry is left out of its screen. */
|
|
9
|
+
export const WIRECAT_TOOLS = [
|
|
10
|
+
{ command: "tg", does: "Telegram in the same local store: search, review and tasks offline" },
|
|
11
|
+
{ command: "max", does: "MAX in the same local store: search, review and tasks offline" },
|
|
12
|
+
{ command: "wa", does: "WhatsApp in the same local store: search, review and tasks offline" },
|
|
13
|
+
{ command: "zm", does: "Zoom meetings: transcripts and summaries, searchable with your messages" },
|
|
14
|
+
{ command: "drive", does: "Google Drive and Docs as local text, searchable with your messages" },
|
|
15
|
+
{ command: "memo", shown: "memo context <person>", does: "one person across messengers, mail, notes and meetings" },
|
|
16
|
+
];
|
|
17
|
+
const WIDTH = 100;
|
|
18
|
+
const widest = (texts) => texts.reduce((most, text) => Math.max(most, text.length), 0);
|
|
19
|
+
const block = (title, rows, column) => rows.length === 0 ? [] : ["", `${title}:`, ...rows.map(([left, right]) => ` ${left.padEnd(column)}${right}`)];
|
|
20
|
+
/** Only commands the tool has: `guide` and `setup` are not in every tool. */
|
|
21
|
+
const pointers = (program, cli, start, profile) => {
|
|
22
|
+
const has = (path) => commandAt(program, path) !== undefined;
|
|
23
|
+
const next = [
|
|
24
|
+
has("commands") ? [`${cli} commands`, "every command, by section"] : undefined,
|
|
25
|
+
has("guide") ? [`${cli} guide`, "scenarios: command sequences for common tasks"] : undefined,
|
|
26
|
+
[`${cli} help options`, "output, offline, timeouts, dry run"],
|
|
27
|
+
].filter((row) => row !== undefined);
|
|
28
|
+
const rows = [
|
|
29
|
+
...(start ? [["Start here:", start, profile ? "(any profile name works)" : ""]] : []),
|
|
30
|
+
...next.map(([command = "", does = ""], at) => [at === 0 ? "Next:" : "", command, does]),
|
|
31
|
+
...(has("skill show") ? [["For agents:", `${cli} skill show`, "rules and the discovery loop"]] : []),
|
|
32
|
+
];
|
|
33
|
+
const width = widest(rows.map(([, command]) => command)) + 2;
|
|
34
|
+
return ["", ...rows.map(([label, command, does]) => `${label.padEnd(14)}${command.padEnd(width)}${does}`.trimEnd())];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The screen `<tool> --help` and a bare `<tool>` show: what the tool is for, what it does that a client would not,
|
|
38
|
+
* the other tools, and where the full list, the scenarios and the options are. It never lists every command.
|
|
39
|
+
*/
|
|
40
|
+
export const firstScreen = (program, screen = {}) => {
|
|
41
|
+
const cli = program.name();
|
|
42
|
+
const profile = screen.profile !== false;
|
|
43
|
+
const pitch = screen.pitch ?? program.description().split("\n")[0] ?? "";
|
|
44
|
+
const can = (screen.can ?? []).map(({ command, does }) => [command, does]);
|
|
45
|
+
const works = WIRECAT_TOOLS.filter((tool) => tool.command !== cli).map(({ command, shown, does }) => [shown ?? command, does]);
|
|
46
|
+
const column = Math.max(22, widest([...can, ...works].map(([left]) => left))) + 2;
|
|
47
|
+
const start = screen.start ?? (commandAt(program, "setup") ? `${cli} personal setup` : undefined);
|
|
48
|
+
return [
|
|
49
|
+
wrapAnsi(`${cli} — ${pitch}`, WIDTH),
|
|
50
|
+
"",
|
|
51
|
+
`Usage: ${cli} ${profile ? "<profile> " : ""}<command> [options]`,
|
|
52
|
+
...(profile ? ["Account commands start with your profile, a name you choose; none is ever assumed."] : []),
|
|
53
|
+
...block("What you can do", can, column),
|
|
54
|
+
...block("Works with", works, column),
|
|
55
|
+
...pointers(program, cli, start, profile),
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
};
|
|
59
|
+
const optionsScreen = (program) => {
|
|
60
|
+
const helper = program.createHelp();
|
|
61
|
+
const width = helper.longestOptionTermLength(program, helper);
|
|
62
|
+
return [
|
|
63
|
+
`Options for every ${program.name()} command:`,
|
|
64
|
+
...helper
|
|
65
|
+
.visibleOptions(program)
|
|
66
|
+
.map((option) => helper.formatItem(helper.optionTerm(option), width, helper.optionDescription(option), helper)),
|
|
67
|
+
"",
|
|
68
|
+
].join("\n");
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Puts the first screen on `--help`, a bare run and `help`, and Returns, Next usually and Example on every leaf
|
|
72
|
+
* command's `--help`. `help options` prints the options for every command, `--help-all` the full commander help;
|
|
73
|
+
* both are hidden, so `commands --json` and MCP do not change. For a tool that builds its own program;
|
|
74
|
+
* `createProgram` calls it for tg, max and wa.
|
|
75
|
+
*/
|
|
76
|
+
export const installHelp = (program, screen = {}) => {
|
|
77
|
+
let full = false;
|
|
78
|
+
const own = program.configureHelp();
|
|
79
|
+
program.configureHelp({
|
|
80
|
+
...own,
|
|
81
|
+
// Commander copies this configuration to children made with `.command()`, so only the root gets the screen.
|
|
82
|
+
formatHelp: (command, helper) => command === program && !full
|
|
83
|
+
? firstScreen(program, screen)
|
|
84
|
+
: (own.formatHelp ?? Help.prototype.formatHelp).call(helper, command, helper),
|
|
85
|
+
});
|
|
86
|
+
installCommandHelp(program);
|
|
87
|
+
program.addOption(new Option("--help-all", "the full help: every command and option").hideHelp());
|
|
88
|
+
program.on("option:help-all", () => {
|
|
89
|
+
full = true;
|
|
90
|
+
program.help();
|
|
91
|
+
});
|
|
92
|
+
if (program.commands.some((command) => command.name() === "help"))
|
|
93
|
+
return program;
|
|
94
|
+
const help = annotate(new Command("help"), { effects: NO_EFFECTS, mcp: false })
|
|
95
|
+
.description("this screen; help options: the options for every command; help <command>: that command's help")
|
|
96
|
+
.argument("[topic...]", "options, or a command path")
|
|
97
|
+
.action((topic) => {
|
|
98
|
+
if (topic.length === 0)
|
|
99
|
+
return program.outputHelp();
|
|
100
|
+
if (topic.length === 1 && topic[0] === "options")
|
|
101
|
+
return program.configureOutput().writeOut?.(optionsScreen(program));
|
|
102
|
+
const command = commandAt(program, topic.join(" "));
|
|
103
|
+
if (!command)
|
|
104
|
+
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") });
|
|
105
|
+
command.outputHelp();
|
|
106
|
+
});
|
|
107
|
+
program.addCommand(help, { hidden: true });
|
|
108
|
+
return program;
|
|
109
|
+
};
|
|
@@ -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,77 @@
|
|
|
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
|
+
guide: "setup",
|
|
65
|
+
models: "setup",
|
|
66
|
+
skill: "setup",
|
|
67
|
+
store: "setup",
|
|
68
|
+
upgrade: "setup",
|
|
69
|
+
};
|
|
70
|
+
/** Puts each of cli-core's groups mounted on `program` in its section, unless the tool already chose one. */
|
|
71
|
+
export const sectionCoreGroups = (program) => {
|
|
72
|
+
for (const command of program.commands) {
|
|
73
|
+
const section = CORE_SECTIONS[command.name()];
|
|
74
|
+
if (section && !sections.has(command))
|
|
75
|
+
inSection(command, section);
|
|
76
|
+
}
|
|
77
|
+
};
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -56,6 +56,10 @@ 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 { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, type Scenario, } from "./guide.js";
|
|
60
|
+
export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
|
|
61
|
+
export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
|
|
62
|
+
export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
|
|
59
63
|
export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
60
64
|
export { answerFields, checkJson, type EntityFields, entityFields, type JsonFieldsResolver, type JsonRequest, liftJson, } from "./json-fields.js";
|
|
61
65
|
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,10 @@ 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 { checkGuide, checkStep, guideCommand, loadScenarios, } from "./guide.js";
|
|
55
|
+
export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
|
|
56
|
+
export { firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
|
|
57
|
+
export { inSection, SECTIONS } from "./help/sections.js";
|
|
54
58
|
export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
55
59
|
export { answerFields, checkJson, entityFields, liftJson, } from "./json-fields.js";
|
|
56
60
|
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) ??
|
|
@@ -335,7 +335,9 @@ export const conversationsCommand = (messenger) => {
|
|
|
335
335
|
const consents = conversations
|
|
336
336
|
.command("consents")
|
|
337
337
|
.description("remembered analysis permissions for this account's chats and provider endpoints");
|
|
338
|
-
annotate(consents.command("list"), { effects: LOCAL_READ })
|
|
338
|
+
annotate(consents.command("list"), { effects: LOCAL_READ })
|
|
339
|
+
.description("which chats and provider endpoints you allowed to analyse, and when")
|
|
340
|
+
.action(async function () {
|
|
339
341
|
const context = messengerContext(this, messenger);
|
|
340
342
|
const items = await context.withStore((store, account) => analysisConsents(store, account).list());
|
|
341
343
|
renderAnswer(this, context, listOf("conversations consents list", {
|
|
@@ -345,6 +347,7 @@ export const conversationsCommand = (messenger) => {
|
|
|
345
347
|
}));
|
|
346
348
|
});
|
|
347
349
|
annotate(consents.command("revoke"), { effects: LOCAL_WRITE })
|
|
350
|
+
.description("forget analysis permissions so the next analysis asks again")
|
|
348
351
|
.option("--chat <chat>", "revoke only this chat's consents; defaults to every chat")
|
|
349
352
|
.option("--provider <identity>", "exact provider identity from consents list; defaults to every provider")
|
|
350
353
|
.action(async function () {
|
|
@@ -30,7 +30,7 @@ export const PARTIAL_NOTE = "the messenger did not list every chat, and did not
|
|
|
30
30
|
*/
|
|
31
31
|
export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
|
|
32
32
|
effects: REMOTE_READ,
|
|
33
|
-
output: { entity: "message", view: "
|
|
33
|
+
output: { entity: "message", view: "summary" },
|
|
34
34
|
query: INBOX_QUERY,
|
|
35
35
|
})
|
|
36
36
|
.description("other people's unread messages in every chat; --new for what arrived since the last check")
|
|
@@ -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) {
|