@wirecat/cli-base 0.50.3 → 0.51.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 +44 -0
- package/README.md +5 -4
- package/dist/commands/contracts.d.ts +19 -0
- package/dist/commands/contracts.js +207 -0
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/index.js +1 -0
- package/dist/dev/audit.d.ts +5 -0
- package/dist/dev/audit.js +67 -6
- package/dist/dev/capabilities.d.ts +20 -0
- package/dist/dev/capabilities.js +48 -0
- package/dist/dev/lint.js +18 -5
- package/dist/dev/main.js +76 -6
- package/dist/dev/shared-contracts.d.ts +13 -0
- package/dist/dev/shared-contracts.js +85 -0
- package/dist/standards/audit.d.ts +1 -0
- package/dist/standards/audit.js +11 -1
- package/dist/standards/flags-how.js +6 -0
- package/dist/standards/flags-what.js +35 -0
- package/dist/standards/vocabulary.js +10 -1
- package/docs/standards/README.md +1 -0
- package/docs/standards/accounts.md +1 -2
- package/docs/standards/ci.md +5 -4
- package/docs/standards/commands.md +2 -2
- package/docs/standards/comparisons.md +133 -0
- package/docs/standards/mcp.md +27 -29
- package/docs/standards/permissions.md +1 -1
- package/package.json +1 -1
package/dist/dev/main.js
CHANGED
|
@@ -11,12 +11,13 @@ import spawn from "cross-spawn";
|
|
|
11
11
|
import * as v from "valibot";
|
|
12
12
|
import { parseCommandsDocument } from "../commands/document.js";
|
|
13
13
|
import { changelogProblems, docsProblems, JOURNAL_IDS, markdownFiles, packageVersion, structureProblems, versionScript, } from "../release/index.js";
|
|
14
|
-
import { AUDIT_BLOCKS, PARITY_GROUPS, REQUIRED_ROOTS } from "../standards/audit.js";
|
|
14
|
+
import { AUDIT_BLOCKS, defineParityGroups, PARITY_GROUPS, REQUIRED_ROOTS } from "../standards/audit.js";
|
|
15
15
|
import { defineReasons } from "../standards/errors.js";
|
|
16
16
|
import { baselineProblems, baselineSchema, LINT_RULES, OVERRIDE_BUDGET, OVERRIDE_RULE_REVIEW, overridesProblems, overridesSchema, rulesForReview, } from "../standards/lint.js";
|
|
17
17
|
import { issuesOf } from "../standards/registry.js";
|
|
18
18
|
import { VOCABULARY } from "../standards/vocabulary.js";
|
|
19
19
|
import { auditClis, scorecardTable, snakeKeys } from "./audit.js";
|
|
20
|
+
import { readCapabilities } from "./capabilities.js";
|
|
20
21
|
import { changelogChange, landFold, processShell } from "./land.js";
|
|
21
22
|
import { baselineOf, judge, lintCommands } from "./lint.js";
|
|
22
23
|
import { nextVersion } from "./next-version.js";
|
|
@@ -48,7 +49,7 @@ const USAGE = `usage: cli-dev <command> [options]
|
|
|
48
49
|
lint/baseline/<rule>.json to today's violations. --schemas
|
|
49
50
|
is a JSON object of the CLI's own JSON Schemas by name
|
|
50
51
|
audit (--cli <bin> | --commands <file>)... [--schemas <cli>=<file>]... [--reasons <cli>=<file>]...
|
|
51
|
-
[--lint <cli>=<dir>]... [--baseline <dir>] [--for <cli>] [--json] [--write-baseline]
|
|
52
|
+
[--lint <cli>=<dir>]... [--capabilities <file>] [--complete] [--differences <file>] [--baseline <dir>] [--for <cli>] [--json] [--write-baseline]
|
|
52
53
|
several CLIs against every lint rule and against each other:
|
|
53
54
|
required roots, same path same contract, same flag same
|
|
54
55
|
meaning, same entity same schema, same failure same reason.
|
|
@@ -186,12 +187,12 @@ const readJson = (path) => {
|
|
|
186
187
|
throw new Error(`${path}: ${error.message}`);
|
|
187
188
|
}
|
|
188
189
|
};
|
|
189
|
-
const commandsJson = (io, cli, file) => {
|
|
190
|
+
const commandsJson = (io, cli, file, commandArgs = ["commands", "--json"]) => {
|
|
190
191
|
if (file)
|
|
191
192
|
return readJson(isAbsolute(file) ? file : join(io.cwd, file));
|
|
192
193
|
const bin = cli ?? "";
|
|
193
194
|
const script = /\.[cm]?js$/.test(bin);
|
|
194
|
-
const run = spawn.sync(script ? process.execPath : bin, [...(script ? [bin] : []),
|
|
195
|
+
const run = spawn.sync(script ? process.execPath : bin, [...(script ? [bin] : []), ...commandArgs], {
|
|
195
196
|
cwd: io.cwd,
|
|
196
197
|
encoding: "utf8",
|
|
197
198
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -331,6 +332,9 @@ const auditRun = (args, io) => {
|
|
|
331
332
|
cli: { type: "string", multiple: true },
|
|
332
333
|
commands: { type: "string", multiple: true },
|
|
333
334
|
schemas: { type: "string", multiple: true },
|
|
335
|
+
capabilities: { type: "string" },
|
|
336
|
+
complete: { type: "boolean", default: false },
|
|
337
|
+
differences: { type: "string" },
|
|
334
338
|
reasons: { type: "string", multiple: true },
|
|
335
339
|
lint: { type: "string", multiple: true },
|
|
336
340
|
baseline: { type: "string", default: "audit/baseline" },
|
|
@@ -352,18 +356,60 @@ const auditRun = (args, io) => {
|
|
|
352
356
|
const reasons = byCli(values.reasons, "--reasons");
|
|
353
357
|
const lintDirs = byCli(values.lint, "--lint");
|
|
354
358
|
const problems = [];
|
|
359
|
+
const evidence = new Map();
|
|
360
|
+
if (values.complete) {
|
|
361
|
+
for (const [index, bin] of (values.cli ?? []).entries()) {
|
|
362
|
+
const raw = raws[index];
|
|
363
|
+
const data = commandsJson(io, bin, undefined, ["commands", "info", "--json"]);
|
|
364
|
+
if (typeof raw.cli === "string")
|
|
365
|
+
evidence.set(raw.cli, data);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
355
368
|
const inputs = raws.map((raw) => {
|
|
356
369
|
const parsed = parseCommandsDocument(snakeKeys(raw));
|
|
357
370
|
if (!parsed.document)
|
|
358
371
|
throw new Error(`a commands document is not the shape cli-base's commandsDocument() writes: ${parsed.problems[0]}`);
|
|
359
372
|
const cli = parsed.document.cli;
|
|
360
|
-
const input = { document: parsed.document, raw };
|
|
373
|
+
const input = { document: parsed.document, raw, ...evidence.get(cli) };
|
|
361
374
|
const schemaFile = schemas.get(cli);
|
|
362
375
|
if (schemaFile)
|
|
363
376
|
input.schemas = readJson(at(schemaFile));
|
|
364
377
|
const reasonFile = reasons.get(cli);
|
|
365
378
|
if (reasonFile)
|
|
366
379
|
input.reasons = defineReasons(readJson(at(reasonFile)));
|
|
380
|
+
if (values.complete) {
|
|
381
|
+
if (!input.contracts ||
|
|
382
|
+
typeof input.contracts !== "object" ||
|
|
383
|
+
Array.isArray(input.contracts) ||
|
|
384
|
+
Object.keys(input.contracts).length === 0)
|
|
385
|
+
problems.push(`${cli}: complete audit needs core contract references from commands info`);
|
|
386
|
+
for (const root of REQUIRED_ROOTS) {
|
|
387
|
+
const path = root.path.join(" ");
|
|
388
|
+
if (!input.contracts?.[path])
|
|
389
|
+
problems.push(`${cli}: complete audit needs an inherited contract reference for ${path}`);
|
|
390
|
+
}
|
|
391
|
+
if (!input.schemas ||
|
|
392
|
+
typeof input.schemas !== "object" ||
|
|
393
|
+
Array.isArray(input.schemas) ||
|
|
394
|
+
Object.keys(input.schemas).length === 0)
|
|
395
|
+
problems.push(`${cli}: complete audit needs nonempty entity schemas from commands info or --schemas`);
|
|
396
|
+
if (!Array.isArray(input.reasons) || input.reasons.length === 0)
|
|
397
|
+
problems.push(`${cli}: complete audit needs declared error reasons from commands info or --reasons`);
|
|
398
|
+
if (input.reasons)
|
|
399
|
+
input.reasons = defineReasons(input.reasons);
|
|
400
|
+
if (input.schemas) {
|
|
401
|
+
const visit = (commands) => {
|
|
402
|
+
for (const command of commands) {
|
|
403
|
+
const names = [command.output?.entity, command.query?.entity].filter((name) => name !== undefined);
|
|
404
|
+
for (const name of names)
|
|
405
|
+
if (input.schemas?.[name] === undefined)
|
|
406
|
+
problems.push(`${cli}: complete audit has no schema for ${name} (${command.path.join(" ")})`);
|
|
407
|
+
visit(command.commands);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
visit(input.document.commands);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
367
413
|
const dir = lintDirs.get(cli);
|
|
368
414
|
if (dir) {
|
|
369
415
|
const overridesPath = join(at(dir), "overrides.json");
|
|
@@ -398,8 +444,32 @@ const auditRun = (args, io) => {
|
|
|
398
444
|
problems.push(`--for names ${values.for}, which is not audited`);
|
|
399
445
|
const baselineDir = at(values.baseline);
|
|
400
446
|
const baseline = readBaseline(baselineDir, values.baseline, problems);
|
|
447
|
+
let groups = PARITY_GROUPS;
|
|
448
|
+
if (values.differences) {
|
|
449
|
+
const schema = v.array(v.strictObject({
|
|
450
|
+
group: v.string(),
|
|
451
|
+
rule: v.string(),
|
|
452
|
+
target: v.string(),
|
|
453
|
+
expected: v.string(),
|
|
454
|
+
reason: v.string(),
|
|
455
|
+
}));
|
|
456
|
+
const parsed = v.safeParse(schema, readJson(at(values.differences)));
|
|
457
|
+
if (!parsed.success)
|
|
458
|
+
throw new Error(`${values.differences}: ${issuesOf(parsed.issues).join("; ")}`);
|
|
459
|
+
for (const entry of parsed.output)
|
|
460
|
+
if (!groups.some((group) => group.name === entry.group))
|
|
461
|
+
throw new Error(`${values.differences}: unknown group ${entry.group}`);
|
|
462
|
+
groups = defineParityGroups(groups.map((group) => ({
|
|
463
|
+
...group,
|
|
464
|
+
differences: [
|
|
465
|
+
...group.differences,
|
|
466
|
+
...parsed.output.filter((entry) => entry.group === group.name).map(({ group: _, ...entry }) => entry),
|
|
467
|
+
],
|
|
468
|
+
})));
|
|
469
|
+
}
|
|
401
470
|
const result = auditClis(inputs, {
|
|
402
|
-
|
|
471
|
+
...(values.capabilities ? { capabilities: readCapabilities(readJson(at(values.capabilities))) } : {}),
|
|
472
|
+
groups,
|
|
403
473
|
roots: REQUIRED_ROOTS,
|
|
404
474
|
vocabulary: VOCABULARY,
|
|
405
475
|
baseline,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CommandInfo } from "../commands/index.js";
|
|
2
|
+
export interface ContractEdit {
|
|
3
|
+
command: readonly string[];
|
|
4
|
+
field: string;
|
|
5
|
+
before: unknown;
|
|
6
|
+
after: unknown;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
/** A baseline cannot replace a core definition: only an exact, reasoned edit changes its effective value. */
|
|
10
|
+
export declare const sharedContractProblems: (commands: readonly CommandInfo[], references: Readonly<Record<string, Readonly<Record<string, unknown>>>>, edits?: readonly ContractEdit[]) => {
|
|
11
|
+
path: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const stable = (value) => {
|
|
2
|
+
if (Array.isArray(value))
|
|
3
|
+
return value.map(stable);
|
|
4
|
+
if (value && typeof value === "object")
|
|
5
|
+
return Object.fromEntries(Object.entries(value)
|
|
6
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
7
|
+
.map(([key, child]) => [key, stable(child)]));
|
|
8
|
+
return value;
|
|
9
|
+
};
|
|
10
|
+
const same = (left, right) => JSON.stringify(stable(left ?? null)) === JSON.stringify(stable(right ?? null));
|
|
11
|
+
const FIELDS = new Set([
|
|
12
|
+
"name",
|
|
13
|
+
"summary",
|
|
14
|
+
"description",
|
|
15
|
+
"origin",
|
|
16
|
+
"api_operation",
|
|
17
|
+
"effects",
|
|
18
|
+
"state",
|
|
19
|
+
"partial",
|
|
20
|
+
"lifetime",
|
|
21
|
+
"reaches",
|
|
22
|
+
"mcp",
|
|
23
|
+
"offline",
|
|
24
|
+
"cooperative",
|
|
25
|
+
"examples",
|
|
26
|
+
"output",
|
|
27
|
+
"query",
|
|
28
|
+
"runnable",
|
|
29
|
+
"arguments",
|
|
30
|
+
"options",
|
|
31
|
+
"aliases",
|
|
32
|
+
"children",
|
|
33
|
+
]);
|
|
34
|
+
/** A baseline cannot replace a core definition: only an exact, reasoned edit changes its effective value. */
|
|
35
|
+
export const sharedContractProblems = (commands, references, edits = []) => {
|
|
36
|
+
const effective = new Map(Object.entries(references).map(([path, contract]) => [path, structuredClone(contract)]));
|
|
37
|
+
const problems = [];
|
|
38
|
+
for (const edit of edits) {
|
|
39
|
+
const path = edit.command.join(" ");
|
|
40
|
+
const contract = effective.get(path);
|
|
41
|
+
if (!contract ||
|
|
42
|
+
!FIELDS.has(edit.field) ||
|
|
43
|
+
edit.reason.trim().length < 10 ||
|
|
44
|
+
!same(contract[edit.field], edit.before) ||
|
|
45
|
+
same(edit.before, edit.after)) {
|
|
46
|
+
problems.push({ path, message: `invalid or stale explicit override for ${edit.field}` });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (edit.after === null)
|
|
50
|
+
delete contract[edit.field];
|
|
51
|
+
else
|
|
52
|
+
contract[edit.field] = structuredClone(edit.after);
|
|
53
|
+
}
|
|
54
|
+
const walk = (items, inherited) => {
|
|
55
|
+
for (const command of items) {
|
|
56
|
+
const path = command.path.join(" ");
|
|
57
|
+
const expected = effective.get(path);
|
|
58
|
+
if (expected) {
|
|
59
|
+
const { path: _, usage: __, commands: nested, ...fields } = command;
|
|
60
|
+
const original = references[path]?.children;
|
|
61
|
+
const actual = {
|
|
62
|
+
...fields,
|
|
63
|
+
...(Array.isArray(original)
|
|
64
|
+
? { children: nested.filter((child) => original.includes(child.name)).map((child) => child.name) }
|
|
65
|
+
: {}),
|
|
66
|
+
};
|
|
67
|
+
const wanted = { ...expected, ...inherited };
|
|
68
|
+
const changed = [...new Set([...Object.keys(actual), ...Object.keys(wanted)])].filter((field) => field !== "aliases" &&
|
|
69
|
+
field !== "availability" &&
|
|
70
|
+
!same(actual[field], wanted[field]));
|
|
71
|
+
if (changed.length)
|
|
72
|
+
problems.push({ path, message: `core contract differs without an exact override: ${changed.join(", ")}` });
|
|
73
|
+
effective.delete(path);
|
|
74
|
+
}
|
|
75
|
+
walk(command.commands, {
|
|
76
|
+
...(inherited.mcp === false || expected?.mcp === false ? { mcp: false } : {}),
|
|
77
|
+
...(inherited.offline === false || expected?.offline === false ? { offline: false } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(commands, {});
|
|
82
|
+
for (const path of effective.keys())
|
|
83
|
+
problems.push({ path, message: "core contract names a command absent from discovery" });
|
|
84
|
+
return problems;
|
|
85
|
+
};
|
|
@@ -23,6 +23,7 @@ declare const parityGroupSchema: v.StrictObjectSchema<{
|
|
|
23
23
|
readonly differences: v.ArraySchema<v.StrictObjectSchema<{
|
|
24
24
|
readonly rule: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a lower-case word, words joined by -">]>;
|
|
25
25
|
readonly target: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
26
|
+
readonly expected: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
|
|
26
27
|
readonly reason: v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, v.MinLengthAction<string, 10, "say why, in a sentence">]>;
|
|
27
28
|
}, undefined>, undefined>;
|
|
28
29
|
}, undefined>;
|
package/dist/standards/audit.js
CHANGED
|
@@ -10,6 +10,11 @@ const auditRuleSchema = v.strictObject({ id: word, severity: v.picklist(LINT_SEV
|
|
|
10
10
|
export const defineAuditRules = (input) => parseRegistry("audit rules", v.array(auditRuleSchema), input, (rules) => duplicates(rules.map((rule) => rule.id)).map((id) => `"${id}" is listed twice`));
|
|
11
11
|
/** The rules that compare CLIs; each CLI is also judged alone by every lint rule. */
|
|
12
12
|
export const AUDIT_RULES = defineAuditRules([
|
|
13
|
+
{
|
|
14
|
+
id: "shared-command-contract",
|
|
15
|
+
severity: "high",
|
|
16
|
+
summary: "core-owned command definitions change only through exact, reasoned overrides",
|
|
17
|
+
},
|
|
13
18
|
{
|
|
14
19
|
id: "required-root",
|
|
15
20
|
severity: "high",
|
|
@@ -44,7 +49,12 @@ const parityGroupSchema = v.strictObject({
|
|
|
44
49
|
/** `tree` compares every path; otherwise the root words whose paths are compared. */
|
|
45
50
|
covers: v.union([v.literal("tree"), v.pipe(v.array(segment), v.minLength(1))]),
|
|
46
51
|
/** Permanent differences, each with its reason; they count against the override budget of the group. */
|
|
47
|
-
differences: v.array(v.strictObject({
|
|
52
|
+
differences: v.array(v.strictObject({
|
|
53
|
+
rule: word,
|
|
54
|
+
target: text,
|
|
55
|
+
expected: v.optional(text),
|
|
56
|
+
reason: v.pipe(text, v.minLength(10, "say why, in a sentence")),
|
|
57
|
+
})),
|
|
48
58
|
});
|
|
49
59
|
export const defineParityGroups = (input) => parseRegistry("parity groups", v.array(parityGroupSchema), input, (groups) => [
|
|
50
60
|
...duplicates(groups.map((group) => group.name)).map((name) => `"${name}" is listed twice`),
|
|
@@ -448,6 +448,12 @@ export const HOW_FLAGS = [
|
|
|
448
448
|
meaning: "override one permission for this run; repeat it for more",
|
|
449
449
|
},
|
|
450
450
|
{ name: "--read-only", role: "mode", type: "switch", meaning: "refuse every write, local and remote" },
|
|
451
|
+
{
|
|
452
|
+
name: "--append",
|
|
453
|
+
role: "mode",
|
|
454
|
+
type: "switch",
|
|
455
|
+
meaning: "keep existing entries and add the explicitly named one beside them",
|
|
456
|
+
},
|
|
451
457
|
{ name: "--replace", role: "mode", type: "switch", meaning: "overwrite an existing one of the same name" },
|
|
452
458
|
{
|
|
453
459
|
name: "--rescan",
|
|
@@ -323,6 +323,34 @@ export const WHAT_FLAGS = [
|
|
|
323
323
|
meaning: "look again at this many recent days, for what arrived late",
|
|
324
324
|
unit: "days",
|
|
325
325
|
},
|
|
326
|
+
{
|
|
327
|
+
name: "--meeting-id",
|
|
328
|
+
value: "<id>",
|
|
329
|
+
role: "target",
|
|
330
|
+
type: "id",
|
|
331
|
+
meaning: "one stored meeting selected for transcript search",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "--identity-id",
|
|
335
|
+
value: "<id>",
|
|
336
|
+
role: "target",
|
|
337
|
+
type: "id",
|
|
338
|
+
meaning: "a stored participant identity whose meetings are selected; does not identify the speaker of a hit",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "--person-id",
|
|
342
|
+
value: "<id>",
|
|
343
|
+
role: "target",
|
|
344
|
+
type: "id",
|
|
345
|
+
meaning: "a stored person whose linked participant identities select meetings",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "--series-id",
|
|
349
|
+
value: "<id>",
|
|
350
|
+
role: "target",
|
|
351
|
+
type: "id",
|
|
352
|
+
meaning: "one stored recurring series selected for meeting search",
|
|
353
|
+
},
|
|
326
354
|
{ name: "--meeting", value: "<id>", role: "target", type: "id", meaning: "a meeting, by its store id" },
|
|
327
355
|
{
|
|
328
356
|
name: "--message",
|
|
@@ -408,6 +436,13 @@ export const WHAT_FLAGS = [
|
|
|
408
436
|
},
|
|
409
437
|
{ name: "--task", value: "<id>", role: "target", type: "id", meaning: "a task, by id" },
|
|
410
438
|
{ name: "--to", value: "<chat>", role: "target", type: "id", meaning: "the chat it goes to" },
|
|
439
|
+
{
|
|
440
|
+
name: "--topic-name",
|
|
441
|
+
value: "<name>",
|
|
442
|
+
role: "target",
|
|
443
|
+
type: "string",
|
|
444
|
+
meaning: "a stored meeting topic label, by name or alias",
|
|
445
|
+
},
|
|
411
446
|
{ name: "--topic", value: "<id>", role: "target", type: "id", meaning: "the forum topic, in a chat that has topics" },
|
|
412
447
|
{ name: "--transcript-id", value: "<id>", role: "target", type: "id", meaning: "one transcript version, by id" },
|
|
413
448
|
{ name: "--until-id", value: "<id>", role: "target", type: "id", meaning: "up to and including this message id" },
|
|
@@ -120,7 +120,7 @@ const verbs = [
|
|
|
120
120
|
["cancel", "stop a job or a pending thing"],
|
|
121
121
|
[
|
|
122
122
|
"sync",
|
|
123
|
-
"bring the local store up to date from the source: only what changed, or the whole list where the source cannot say",
|
|
123
|
+
"bring the local store up to date from the source: only what changed, or the whole list where the source cannot say; a mirror syncs both ways",
|
|
124
124
|
],
|
|
125
125
|
["fetch", "one bounded pull of one named thing into the store, not a catch-up"],
|
|
126
126
|
["export", "write from the store to a file"],
|
|
@@ -169,6 +169,10 @@ const verbs = [
|
|
|
169
169
|
["unarchive", "bring an archived chat back to the main chat list"],
|
|
170
170
|
["reset", "replace a thing; the old one stops working"],
|
|
171
171
|
["moderate", "apply a chat's rules: delete what breaks them, act on who broke them"],
|
|
172
|
+
["assign", "name the person responsible for work, with their task role"],
|
|
173
|
+
["reverse", "record that an accepted decision no longer holds, without replacing it"],
|
|
174
|
+
["invalidate", "mark a derived record whose supporting evidence no longer holds"],
|
|
175
|
+
["acknowledge", "record successful delivery using the exact receipt issued when the item was claimed"],
|
|
172
176
|
["confirm", "the owner accepts what an agent proposed"],
|
|
173
177
|
["reject", "the owner refuses what an agent proposed"],
|
|
174
178
|
["move", "move a file in the source"],
|
|
@@ -239,6 +243,10 @@ const collections = [
|
|
|
239
243
|
["recordings", "the files a provider recorded of a meeting"],
|
|
240
244
|
["jobs", "work this tool runs in the background or on a timer"],
|
|
241
245
|
["actions", "what an agent proposes to do; the owner confirms or rejects each"],
|
|
246
|
+
["agents", "the agents whose actions are recorded in the shared store"],
|
|
247
|
+
["reminders", "scheduled notices about stored work, claimed and acknowledged with delivery receipts"],
|
|
248
|
+
["key-documents", "selected source documents that a project keeps as its starting references"],
|
|
249
|
+
["answers", "evidence-grounded answers assembled from selected stored sources"],
|
|
242
250
|
["aliases", "other names of one thing"],
|
|
243
251
|
["mentions", "phones, e-mail addresses, links and documents found in stored text, each where it was written"],
|
|
244
252
|
["images", "pictures in stored messages, searched by what they show"],
|
|
@@ -264,6 +272,7 @@ const collections = [
|
|
|
264
272
|
const singulars = [
|
|
265
273
|
["session", "this tool's login"],
|
|
266
274
|
["account", "the messenger or mail account behind a profile"],
|
|
275
|
+
["mail", "the local email archive and its mailbox import, rules and cleanup operations"],
|
|
267
276
|
["config", "a profile's settings"],
|
|
268
277
|
["server", "the long-running process of a profile"],
|
|
269
278
|
["store", "the local SQLite database the tools share"],
|
package/docs/standards/README.md
CHANGED
|
@@ -38,6 +38,7 @@ rules it keeps. A new CLI is ready for its first release when every check under
|
|
|
38
38
|
| [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
|
|
39
39
|
| [Security](security.md) | `SECR` | 20 | Secrets, logs, keyring, argv, error text, untrusted text, scanners, workflow hardening, reporting |
|
|
40
40
|
| [Documents](docs.md) | `DOC` | 20 | User pages and developer pages, generated pages, current facts, changelog |
|
|
41
|
+
| [Comparisons](comparisons.md) | `CMP` | 11 | Which tools a comparison page lists, rows as user tasks, agents and safety, where others are better, proof of marks |
|
|
41
42
|
| [Releases](releases.md) | `REL` | 16 | `bin/release`, the release lock, versions, release reports, trusted publishing |
|
|
42
43
|
| [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
|
|
43
44
|
| [Time](time.md) | `TIME` | 6 | UTC in the store and in answers, the zone at input and display edges, date-only filters, tests in another zone |
|
|
@@ -13,8 +13,7 @@ Rule ids on this page are `WC-ACCT-<n>`. How to read a rule, how ids work and wh
|
|
|
13
13
|
|
|
14
14
|
### Who follows these rules today
|
|
15
15
|
|
|
16
|
-
cli-core resolves the profile for tg, max and wa. WC-ACCT-1 needs cli-core 0.319.0 or newer, and
|
|
17
|
-
0.321.0. memo has no messenger profile: its shared commands run without one, and they pick accounts with `--provider`
|
|
16
|
+
cli-core resolves the profile for tg, max and wa. WC-ACCT-1 needs cli-core 0.319.0 or newer. memo has no messenger profile: its shared commands run without one, and they pick accounts with `--provider`
|
|
18
17
|
and `--account` (WC-ACCT-7). zoom resolves its own profile word by the same rules (`zm <profile> …`, `ZM_PROFILE`,
|
|
19
18
|
`ZM_PROFILE_LOCK`) from zoom-cli 0.35.0, with cli-core's `liftProfile` and `usableProfileName`. drive still has its own
|
|
20
19
|
profile code: it registers its own profiles. That is a known gap, not an exception.
|
package/docs/standards/ci.md
CHANGED
|
@@ -96,7 +96,7 @@ cli-testing, cli-docs, plus community itself.
|
|
|
96
96
|
cli-docs validates its site before each deployment instead, and community has no build. Third-party security apps
|
|
97
97
|
may post their own checks.
|
|
98
98
|
- **Applies to:** the CLIs, cli-base, cli-core and cli-testing. cli-base, whatsapp-cli, tg-cli, max-cli, cli-core
|
|
99
|
-
cli-memo and
|
|
99
|
+
cli-memo, zoom-cli and drive-cli have `pr.yml`; cli-testing does not have it yet. **Since:**
|
|
100
100
|
cli-base 0.41.0 (pull request validation since cli-base 0.44.6). **Rulings:** none.
|
|
101
101
|
|
|
102
102
|
### WC-CI-6 · Run the changelog guard on every pull request
|
|
@@ -168,9 +168,10 @@ cli-testing, cli-docs, plus community itself.
|
|
|
168
168
|
- **Why:** a red merge breaks `main` for every parallel branch, and an admin merge skips the only gate there is.
|
|
169
169
|
- **Enforced by:** `whatsapp-cli: main ruleset required checks` · `cli-core: main ruleset required checks` ·
|
|
170
170
|
`tg-cli: main ruleset required checks` · `max-cli: main ruleset required checks` ·
|
|
171
|
-
`cli-memo: main ruleset required checks
|
|
172
|
-
the ruleset requires the pull request validation jobs besides the
|
|
173
|
-
have no pr.yml, so their rulesets require only the standards job, and
|
|
171
|
+
`cli-memo: main ruleset required checks` · `drive-cli: main ruleset required checks`, plus the main ruleset of
|
|
172
|
+
cli-base itself. In each repository with pr.yml, the ruleset requires the pull request validation jobs besides the
|
|
173
|
+
standards job. cli-testing and cli-docs have no pr.yml, so their rulesets require only the standards job, and
|
|
174
|
+
review covers the rest. zoom-cli has pr.yml,
|
|
174
175
|
but its ruleset still requires only the standards job.
|
|
175
176
|
- **Example:** `gh pr checks <number>` shows every line as pass before `gh pr merge`. Never merge with `--admin`. A
|
|
176
177
|
release changes `main` the same way
|
|
@@ -287,10 +287,10 @@ Each of these is set with `annotate(command, { … })` from `@wirecat/cli-base/c
|
|
|
287
287
|
### WC-CMD-27 · Refuse a command path that arrived as one word
|
|
288
288
|
|
|
289
289
|
- **Why:** a shell loop that quotes `"projects add"` would otherwise get the root help and no reason.
|
|
290
|
-
- **Enforced by:** `src/commands/commands.test.ts` · `cli-memo: src/program.test.ts`.
|
|
290
|
+
- **Enforced by:** `src/commands/commands.test.ts` · `cli-memo: src/program.test.ts` · `cli-core: src/cli/program.ts`.
|
|
291
291
|
- **Example:** call `refuseJoinedPath(program, args)` before `parseAsync`. It throws a `validation_error` that names
|
|
292
292
|
the words to type.
|
|
293
|
-
- **Exceptions:** none.
|
|
293
|
+
- **Exceptions:** none. memo and cli-core call `refuseJoinedPath` before parsing.
|
|
294
294
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
295
295
|
|
|
296
296
|
## External references
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Comparisons
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
How a CLI's comparison page is written: which tools it compares, which rows it has, how a mark is proven, and what it
|
|
6
|
+
says about the places where another tool is better. The page is read by someone choosing a tool for a job, not by
|
|
7
|
+
someone reviewing our code.
|
|
8
|
+
|
|
9
|
+
The page lives in the site repository (`cli-docs`, published on wirecat.dev), not in the CLI repository, because a
|
|
10
|
+
comparison changes when the other tools change, not when we release. The CLI repository keeps a short
|
|
11
|
+
`docs/compare.md` that links to the site page, and its other docs link there too.
|
|
12
|
+
|
|
13
|
+
Rule ids on this page are `WC-CMP-<n>`. How to read a rule, how ids work and which rule wins are in the
|
|
14
|
+
[index](README.md). The general rules for user pages are in [Documents](docs.md).
|
|
15
|
+
|
|
16
|
+
## Rules
|
|
17
|
+
|
|
18
|
+
### WC-CMP-1 · Compare with the tools a reader would pick instead, for the same job
|
|
19
|
+
|
|
20
|
+
- **Why:** a table against products in another category answers a question nobody asked and makes us look either
|
|
21
|
+
better or worse than we are.
|
|
22
|
+
- **Enforced by:** review only.
|
|
23
|
+
- **Example:** zm is compared with other command lines and MCP servers for a Zoom account. Meeting assistants that
|
|
24
|
+
join calls (Otter, Fireflies) are a different job; that comparison stays private until zm does that job too.
|
|
25
|
+
- **Exceptions:** one column may be the official app or the official API, when readers would otherwise use it.
|
|
26
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
27
|
+
|
|
28
|
+
### WC-CMP-2 · A row is a task the user wants done, grouped by how people use the tool
|
|
29
|
+
|
|
30
|
+
- **Why:** people choose a tool by what they can do with it: read and send messages, send photos, find a chat, find a
|
|
31
|
+
file, keep a draft, see who is active in a group. How login works or where data is stored is rarely the reason.
|
|
32
|
+
- **Enforced by:** review only.
|
|
33
|
+
- **Example:** groups such as "Catch up", "Reply and send", "Find", "Files and media", "Organise", "Overview and
|
|
34
|
+
analytics", "Agents and safety", "Setup". Rows such as "Send several photos as one album", "Find a file someone sent
|
|
35
|
+
in any chat". Not rows such as "QR login and two-step password" or "Works without a local copy".
|
|
36
|
+
- **Exceptions:** an implementation detail gets a row only when it changes what the user can do, such as "Search
|
|
37
|
+
without internet".
|
|
38
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
39
|
+
|
|
40
|
+
### WC-CMP-3 · A missing feature gets a row only if users would choose a tool by it
|
|
41
|
+
|
|
42
|
+
- **Why:** a row we fail reads as a gap to fix. A rare account setting listed as missing makes the product look
|
|
43
|
+
incomplete for no reason.
|
|
44
|
+
- **Enforced by:** review only.
|
|
45
|
+
- **Example:** "Set or remove the cloud password" is not a row on max's page. "Drafts" is.
|
|
46
|
+
- **Exceptions:** none.
|
|
47
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
48
|
+
|
|
49
|
+
### WC-CMP-4 · Write each row so that ❌ cannot be misread
|
|
50
|
+
|
|
51
|
+
- **Why:** a short noun can name a bigger thing than the row means; with ❌ beside it the reader concludes the tool
|
|
52
|
+
cannot do the bigger thing.
|
|
53
|
+
- **Enforced by:** review only.
|
|
54
|
+
- **Example:** "Show the other person that you are typing", not "Typing". ❌ beside "Typing" reads as "cannot send
|
|
55
|
+
messages".
|
|
56
|
+
- **Exceptions:** none.
|
|
57
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
58
|
+
|
|
59
|
+
### WC-CMP-5 · Every page has an "Agents and safety" group, and it may be the largest
|
|
60
|
+
|
|
61
|
+
- **Why:** it is what the WireCat tools are built for, and what a reader cannot learn from a feature list: what an
|
|
62
|
+
agent may do without asking, how a wrong send is stopped, what the agent sees.
|
|
63
|
+
- **Enforced by:** review only.
|
|
64
|
+
- **Example:** rows such as "An agent can read but not send unless you allow it", "Ask before each send", "Limit sends
|
|
65
|
+
to a list of chats", "A send is never made twice after a retry", "Every command reachable through MCP", "Works with
|
|
66
|
+
an agent without a browser".
|
|
67
|
+
- **Exceptions:** none.
|
|
68
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
69
|
+
|
|
70
|
+
### WC-CMP-6 · Keep technical rows few, and only where they help a choice
|
|
71
|
+
|
|
72
|
+
- **Why:** a reader who wants protocol details reads the docs; a comparison full of them hides the answer.
|
|
73
|
+
- **Enforced by:** review only.
|
|
74
|
+
- **Example:** one "Setup" group: install command, platforms, licence, price, MCP over stdio or HTTP, and how the tool
|
|
75
|
+
compares with a browser agent driving the web app (speed, cost, reliability).
|
|
76
|
+
- **Exceptions:** none.
|
|
77
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
78
|
+
|
|
79
|
+
### WC-CMP-7 · Say plainly where another tool is better
|
|
80
|
+
|
|
81
|
+
- **Why:** a table that only shows our wins is not believed, and a reader who finds the gap later stops trusting the
|
|
82
|
+
rest.
|
|
83
|
+
- **Enforced by:** review only.
|
|
84
|
+
- **Example:** a section "Where another tool fits better" with two to four scenarios, each one sentence: the job, the
|
|
85
|
+
tool that does it better, why. Only gaps a reader would care about (WC-CMP-3).
|
|
86
|
+
- **Exceptions:** none.
|
|
87
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
88
|
+
|
|
89
|
+
### WC-CMP-8 · No "better" or "faster" without a published measurement
|
|
90
|
+
|
|
91
|
+
- **Why:** a quality claim against another tool is the claim a reader is most likely to test.
|
|
92
|
+
- **Enforced by:** review only.
|
|
93
|
+
- **Example:** "memo finds better answers than Hindsight" needs a result in cli-testing `performance/` and a link to
|
|
94
|
+
it. Without one, the row says what each tool does ("search by words and by meaning"), not which does it better.
|
|
95
|
+
- **Exceptions:** none.
|
|
96
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** none.
|
|
97
|
+
|
|
98
|
+
### WC-CMP-9 · Every mark is proven, and the page says when it was checked
|
|
99
|
+
|
|
100
|
+
- **Why:** a comparison goes stale faster than any other page, and a wrong mark about another project is unfair to
|
|
101
|
+
it.
|
|
102
|
+
- **Enforced by:** review only.
|
|
103
|
+
- **Example:** our marks are checked against the code on `main` or by running the tool. A competitor mark links to
|
|
104
|
+
its own documentation or source. A mark that cannot be proven is ➖ ("not in its documentation"). The intro names the
|
|
105
|
+
check date. The legend is ✅ yes · 🟡 partly · ❌ no · ➖ not in its documentation, plus "n/a" where
|
|
106
|
+
a row cannot apply to a tool (an MCP row for a connector that is itself the agent).
|
|
107
|
+
- **Exceptions:** none.
|
|
108
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** none.
|
|
109
|
+
|
|
110
|
+
### WC-CMP-10 · Keep the table short enough to read
|
|
111
|
+
|
|
112
|
+
- **Why:** past about 25 rows the reader stops scanning, and the rows that matter are lost.
|
|
113
|
+
- **Enforced by:** review only.
|
|
114
|
+
- **Example:** one feature table, with each group as a bold row inside it; 15 to 25 rows and 3 to 6 tools. A row
|
|
115
|
+
where every tool has the same mark is dropped.
|
|
116
|
+
- **Exceptions:** none.
|
|
117
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.4. **Rulings:** owner, 2026-10-24.
|
|
118
|
+
|
|
119
|
+
### WC-CMP-11 · Our marks are re-checked at each release
|
|
120
|
+
|
|
121
|
+
- **Why:** our own marks go stale with every release, and a mark that says we lack something we have shipped, or the
|
|
122
|
+
reverse, is the error a reader finds first.
|
|
123
|
+
- **Enforced by:** review only.
|
|
124
|
+
- **Example:** the release report lists the compare page as checked. Before a release, each ✅, 🟡 and ❌ for the CLI is re-proven against the release build, the check date
|
|
125
|
+
and version in the intro are updated in the site repository, and the release report names the page.
|
|
126
|
+
- **Exceptions:** none.
|
|
127
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.50.5. **Rulings:** owner, 2026-10-24.
|
|
128
|
+
|
|
129
|
+
## Sources
|
|
130
|
+
|
|
131
|
+
- Owner review of the max, zm and memo comparison pages, 2026-10-24.
|
|
132
|
+
- The tg comparison page, the first page written this way.
|
|
133
|
+
- Owner decision, 2026-10-24: comparison pages live in `cli-docs`; each CLI's docs link to them.
|