@wirecat/cli-base 0.75.2 → 0.77.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 +53 -0
- package/constants.json +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +63 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +4 -0
- package/dist/exit-codes.js +1 -0
- package/dist/failure.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/query/parser.js +11 -0
- package/dist/query/registry.d.ts +1 -1
- package/dist/standards/errors.d.ts +2 -2
- package/dist/standards/errors.js +6 -0
- package/dist/standards/output.d.ts +5 -5
- package/dist/standards/vocabulary.js +12 -0
- package/docs/standards/README.md +4 -4
- package/docs/standards/architecture.md +14 -0
- package/docs/standards/commands.md +31 -5
- package/docs/standards/docs.md +15 -0
- package/docs/standards/output.md +4 -2
- package/docs/standards/permissions.md +8 -3
- package/docs/standards/security.md +40 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,59 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
|
|
|
8
8
|
Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
|
|
9
9
|
[`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
|
|
10
10
|
|
|
11
|
+
## 0.77.0 — 05.11.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **The vocabulary has cli-core's schedule words and memo's `findings`, as the commands name them.** The resource
|
|
16
|
+
`schedule` (a profile's saved timers; the tool's own background work stays `jobs`), the verbs `agent` and `command`
|
|
17
|
+
(save a timer that starts an agent, or runs one of the tool's commands) and `exec` (what the timer itself starts;
|
|
18
|
+
a person uses `run`), and the collection `findings` (what an agent read in mail, staged for the owner; what it
|
|
19
|
+
proposes to do stays `actions`). The owner ruled to add the words as they are rather than rename the commands,
|
|
20
|
+
so the command lint checks them instead of skipping them. What to watch for: every CLI that mounts
|
|
21
|
+
`schedule` (tg, max, gcal, mail, memo, cli-core itself) and memo's `mail findings` has these in
|
|
22
|
+
`lint/baseline/unknown-verb.json` and `unknown-resource.json`; they are stale after the bump and fail the lint
|
|
23
|
+
until removed (WC-TOOL-16).
|
|
24
|
+
|
|
25
|
+
## 0.76.0 — 04.11.2026
|
|
26
|
+
|
|
27
|
+
### Changed — may break callers
|
|
28
|
+
|
|
29
|
+
- **A new error code, `conflict`, exit 15: a write refused because what it would change moved on since the caller
|
|
30
|
+
read it** (a stale `--revision`). Until now such a refusal had to borrow `configuration_error` (exit 3), which says
|
|
31
|
+
the setup is wrong and leaves an agent to read `details.reason` to tell them apart. `errorCodes` now has 15 codes,
|
|
32
|
+
`EXIT_CODES.conflict` is 15, the fallback reason is `write.conflict`, and `retry.safe` is always false: read the
|
|
33
|
+
record again and decide on the new copy. A CLI keeps its own reason (`task.stale`) and details. What to watch for:
|
|
34
|
+
anything that lists the codes itself, or maps every code or exit number, must add this one. Every CLI's
|
|
35
|
+
`contracts/mcp-tools.json` names the codes and is regenerated at its bump; braze-cli keeps its own copy of the list;
|
|
36
|
+
the desktop app branches on codes for sync failures. cli-core moves `task.stale` (and later the other `*.stale`
|
|
37
|
+
reasons) to this code after a release.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`loadConstants(layers, schema)`: one way to ship values inside the packages.** Each layer (cli-base, cli-core,
|
|
42
|
+
the tool) keeps a `constants.json` at its package root; `loadConstants([BASE_CONSTANTS, core, tool], schema)` merges
|
|
43
|
+
them in that order (objects key by key, a later layer winning; arrays and scalars replaced), treats a missing file as
|
|
44
|
+
empty, validates against the caller's schema built on `baseConstantsSchema`, names the dotted path and file of a bad
|
|
45
|
+
value, and freezes the result. cli-base's own layer is `{}`. Rule
|
|
46
|
+
[WC-ARCH-23](docs/standards/architecture.md#wc-arch-23--a-value-that-ships-with-the-package-lives-in-the-layers-constantsjson):
|
|
47
|
+
such a value is never a scattered constant or a file CI writes from a secret. cli-core builds the OAuth apps on it.
|
|
48
|
+
- **A query value takes a ref unquoted: `about:project:1` and `about:chat:max/1/2` are one value.** Every WireCat
|
|
49
|
+
CLI prints refs this way, and a list refused them unless the caller quoted them. A field value made of a lower-case
|
|
50
|
+
word, a colon and more text with no space now reads as one term, so `url:https://example.com` works unquoted too.
|
|
51
|
+
`about:project` alone and `project:1` as a field and value read as before. A CLI gets it by updating cli-base.
|
|
52
|
+
- **Rule WC-CMD-31: accept every ref any WireCat CLI prints, and never ask the caller to translate it**
|
|
53
|
+
([WC-CMD-31](docs/standards/commands.md#wc-cmd-31--accept-every-ref-any-wirecat-cli-prints-and-never-ask-the-caller-to-translate-it)).
|
|
54
|
+
Arguments, options and query values take a stored thing's ref as printed; a ref for another account or provider
|
|
55
|
+
fails with an error that names the right one.
|
|
56
|
+
- **Rule WC-CMD-30: every setting is in the config file with its default, and `config show` lists them all**
|
|
57
|
+
([WC-CMD-30](docs/standards/commands.md#wc-cmd-30--write-every-setting-into-the-config-file-with-its-default-and-list-them-all-in-config-show)).
|
|
58
|
+
A CLI declares each scalar setting with its default and a description through cli-core's `DeclaredSetting`. It
|
|
59
|
+
fills the missing ones whenever it writes the file anyway, and on `config migrate`. A plain read never rewrites the
|
|
60
|
+
file. The owner then finds and edits every setting in the file without the docs. memo follows it first. The known
|
|
61
|
+
gaps now name what cli-core's shared settings cover (tg, max, wa, mail, gcal after their cli-core bump) and what is
|
|
62
|
+
left: those CLIs' own settings, drive's own `config` (no `migrate`, writes do not fill) and zm.
|
|
63
|
+
|
|
11
64
|
## 0.75.2 — 03.11.2026
|
|
12
65
|
|
|
13
66
|
### Added
|
package/constants.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
/** cli-base's own layer, the first one every program passes to `loadConstants`. */
|
|
3
|
+
export declare const BASE_CONSTANTS: import("url").URL;
|
|
4
|
+
/** What cli-base's layer holds; a layer above extends it with `v.object({ ...baseConstantsSchema.entries, … })`. */
|
|
5
|
+
export declare const baseConstantsSchema: v.ObjectSchema<{}, undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* Values that ship inside the packages — never the owner's settings. Each layer is a JSON file, base first; a file
|
|
8
|
+
* that does not exist is an empty layer, so a tool without constants of its own passes its URL anyway. Plain objects
|
|
9
|
+
* merge key by key and a later layer wins; an array or a scalar replaces what was below it. The result is checked
|
|
10
|
+
* against the caller's schema and frozen.
|
|
11
|
+
*/
|
|
12
|
+
export declare const loadConstants: <TSchema extends v.GenericSchema>(layers: readonly URL[], schema: TSchema) => v.InferOutput<TSchema>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import { loadConfigFile } from "./config.js";
|
|
4
|
+
/** cli-base's own layer, the first one every program passes to `loadConstants`. */
|
|
5
|
+
export const BASE_CONSTANTS = new URL("../constants.json", import.meta.url);
|
|
6
|
+
/** What cli-base's layer holds; a layer above extends it with `v.object({ ...baseConstantsSchema.entries, … })`. */
|
|
7
|
+
export const baseConstantsSchema = v.object({});
|
|
8
|
+
const LayerSchema = v.record(v.string(), v.unknown());
|
|
9
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
const merge = (into, from, file, setBy, prefix = "") => {
|
|
11
|
+
const out = { ...into };
|
|
12
|
+
for (const [key, value] of Object.entries(from)) {
|
|
13
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
14
|
+
const current = out[key];
|
|
15
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
16
|
+
out[key] = merge(current, value, file, setBy, path);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
out[key] = value;
|
|
20
|
+
setBy.set(path, file);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
const deepFreeze = (value) => {
|
|
26
|
+
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
27
|
+
for (const child of Object.values(value))
|
|
28
|
+
deepFreeze(child);
|
|
29
|
+
Object.freeze(value);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
};
|
|
33
|
+
const fileOf = (path, setBy, files) => {
|
|
34
|
+
for (let at = path.split("."); at.length > 0; at = at.slice(0, -1)) {
|
|
35
|
+
const file = setBy.get(at.join("."));
|
|
36
|
+
if (file)
|
|
37
|
+
return `set in ${file}`;
|
|
38
|
+
}
|
|
39
|
+
return `not set in ${files.join(", ")}`;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Values that ship inside the packages — never the owner's settings. Each layer is a JSON file, base first; a file
|
|
43
|
+
* that does not exist is an empty layer, so a tool without constants of its own passes its URL anyway. Plain objects
|
|
44
|
+
* merge key by key and a later layer wins; an array or a scalar replaces what was below it. The result is checked
|
|
45
|
+
* against the caller's schema and frozen.
|
|
46
|
+
*/
|
|
47
|
+
export const loadConstants = (layers, schema) => {
|
|
48
|
+
const files = layers.map((layer) => fileURLToPath(layer));
|
|
49
|
+
const setBy = new Map();
|
|
50
|
+
let merged = {};
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
merged = merge(merged, loadConfigFile(file, LayerSchema, () => ({})), file, setBy);
|
|
53
|
+
}
|
|
54
|
+
const result = v.safeParse(schema, merged);
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
const problems = result.issues.map((issue) => {
|
|
57
|
+
const path = v.getDotPath(issue) ?? "(root)";
|
|
58
|
+
return `${path}: ${issue.message} (${fileOf(path, setBy, files)})`;
|
|
59
|
+
});
|
|
60
|
+
throw new Error(`the packaged constants are not valid:\n ${problems.join("\n ")}`);
|
|
61
|
+
}
|
|
62
|
+
return deepFreeze(result.output);
|
|
63
|
+
};
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* Every failure a CLI built on this package, and anything consuming its output, can see. The list
|
|
3
3
|
* is closed on purpose: an agent branches on `code`, and a new code is a contract change.
|
|
4
4
|
*
|
|
5
|
+
* `conflict` is a write refused because what it would change moved on since the caller read it (a stale revision):
|
|
6
|
+
* nothing is wrong with the input or the setup, and the same write on a fresh copy may work.
|
|
7
|
+
*
|
|
5
8
|
* "provider" is whatever the CLI talks to — an HTTP API, a socket, a messenger. The word is
|
|
6
9
|
* deliberately not "server": a provider that answered with a refusal is not the same as one that
|
|
7
10
|
* never answered.
|
|
8
11
|
*/
|
|
9
|
-
export declare const errorCodes: readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"];
|
|
12
|
+
export declare const errorCodes: readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"];
|
|
10
13
|
export type ErrorCode = (typeof errorCodes)[number];
|
|
11
14
|
/**
|
|
12
15
|
* Anything a caller may attach. Everything is optional because these are diagnostics, not a
|
package/dist/errors.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Every failure a CLI built on this package, and anything consuming its output, can see. The list
|
|
3
3
|
* is closed on purpose: an agent branches on `code`, and a new code is a contract change.
|
|
4
4
|
*
|
|
5
|
+
* `conflict` is a write refused because what it would change moved on since the caller read it (a stale revision):
|
|
6
|
+
* nothing is wrong with the input or the setup, and the same write on a fresh copy may work.
|
|
7
|
+
*
|
|
5
8
|
* "provider" is whatever the CLI talks to — an HTTP API, a socket, a messenger. The word is
|
|
6
9
|
* deliberately not "server": a provider that answered with a refusal is not the same as one that
|
|
7
10
|
* never answered.
|
|
@@ -20,6 +23,7 @@ export const errorCodes = [
|
|
|
20
23
|
"provider_unavailable",
|
|
21
24
|
"invalid_response",
|
|
22
25
|
"outcome_unknown",
|
|
26
|
+
"conflict",
|
|
23
27
|
"cancelled",
|
|
24
28
|
];
|
|
25
29
|
// Symbol.for, so two copies of this package — one CLI's and one dependency's, during a pin bump —
|
package/dist/exit-codes.js
CHANGED
package/dist/failure.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export declare const mcpErrorResult: (object: ErrorObject) => {
|
|
|
58
58
|
}[];
|
|
59
59
|
structuredContent: {
|
|
60
60
|
error: {
|
|
61
|
-
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
61
|
+
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "conflict" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
62
62
|
reason: string;
|
|
63
63
|
message: string;
|
|
64
64
|
param?: string | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { BATCH_LIMITS, type BatchContext, type BatchHandler, type BatchOperation, type BatchReport, type BatchResult, type PreparedBatchRequest, prepareBatch, projectBatchAnswers, readBatchInput, runBatch, } from "./batch.js";
|
|
2
2
|
export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
|
|
3
|
+
export { BASE_CONSTANTS, baseConstantsSchema, loadConstants } from "./constants.js";
|
|
3
4
|
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential, WriteOptions, } from "./credentials.js";
|
|
4
5
|
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
5
6
|
export { createDeadline, type Deadline, type DeadlineOptions } from "./deadline.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { BATCH_LIMITS, prepareBatch, projectBatchAnswers, readBatchInput, runBatch, } from "./batch.js";
|
|
2
2
|
export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
|
|
3
|
+
export { BASE_CONSTANTS, baseConstantsSchema, loadConstants } from "./constants.js";
|
|
3
4
|
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
4
5
|
export { createDeadline } from "./deadline.js";
|
|
5
6
|
export { CliError, errorCodes, isCliError } from "./errors.js";
|
package/dist/query/parser.js
CHANGED
|
@@ -115,6 +115,17 @@ const tokensOf = (text, fail = queryError) => {
|
|
|
115
115
|
break;
|
|
116
116
|
at++;
|
|
117
117
|
}
|
|
118
|
+
// A ref after a field (`about:project:1`, `about:chat:max/1/2`) is one value: every WireCat CLI prints refs
|
|
119
|
+
// unquoted, and Lucene would read the second colon as a stray field.
|
|
120
|
+
const [field, colon] = tokens.slice(-2);
|
|
121
|
+
if (field?.kind === "term" &&
|
|
122
|
+
colon?.kind === ":" &&
|
|
123
|
+
colon.span.end === start &&
|
|
124
|
+
text[at] === ":" &&
|
|
125
|
+
/^[a-z][a-z_-]*$/u.test(text.slice(start, at)) &&
|
|
126
|
+
/^:[^\s()"]/u.test(text.slice(at)))
|
|
127
|
+
while (at < text.length && !whitespace(text[at] ?? "") && !["(", ")", '"'].includes(text[at] ?? ""))
|
|
128
|
+
at++;
|
|
118
129
|
const raw = text.slice(start, at);
|
|
119
130
|
kind =
|
|
120
131
|
{ AND: "AND", OR: "OR", NOT: "NOT", "&&": "AND", "||": "OR" }[raw] ??
|
package/dist/query/registry.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare const defineQuery: (registry: QueryRegistry) => QueryRegistry;
|
|
|
15
15
|
/** The query reasons every CLI shares; a CLI adds its own (`query.invalid-tag`) with `defineReasons`. */
|
|
16
16
|
export declare const QUERY_REASONS: {
|
|
17
17
|
reason: string;
|
|
18
|
-
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
18
|
+
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "conflict" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
19
19
|
message: string;
|
|
20
20
|
details: string[];
|
|
21
21
|
step?: string | undefined;
|
|
@@ -8,7 +8,7 @@ export declare const CRASH_REASON = "command.crashed";
|
|
|
8
8
|
/** The one error object, on stderr in machine mode and as MCP's `isError` content alike. */
|
|
9
9
|
export declare const errorObjectSchema: v.StrictObjectSchema<{
|
|
10
10
|
readonly error: v.StrictObjectSchema<{
|
|
11
|
-
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled", "internal_error"], undefined>;
|
|
11
|
+
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled", "internal_error"], undefined>;
|
|
12
12
|
readonly reason: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>;
|
|
13
13
|
readonly message: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
14
14
|
/** The option or argument at fault. */
|
|
@@ -55,7 +55,7 @@ export declare const errorObjectSchema: v.StrictObjectSchema<{
|
|
|
55
55
|
export type ErrorObject = v.InferOutput<typeof errorObjectSchema>;
|
|
56
56
|
declare const reasonSchema: v.StrictObjectSchema<{
|
|
57
57
|
readonly reason: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>;
|
|
58
|
-
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled", "internal_error"], undefined>;
|
|
58
|
+
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled", "internal_error"], undefined>;
|
|
59
59
|
/** `{name}` placeholders, each one of `details`. */
|
|
60
60
|
readonly message: v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, v.MaxLengthAction<string, 120, undefined>]>;
|
|
61
61
|
readonly details: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a snake_case key">]>, undefined>;
|
package/dist/standards/errors.js
CHANGED
|
@@ -97,6 +97,12 @@ export const BASE_REASONS = defineReasons([
|
|
|
97
97
|
details: [],
|
|
98
98
|
},
|
|
99
99
|
{ reason: "outcome.unknown", code: "outcome_unknown", message: "it is not known whether it worked", details: [] },
|
|
100
|
+
{
|
|
101
|
+
reason: "write.conflict",
|
|
102
|
+
code: "conflict",
|
|
103
|
+
message: "it changed since it was read; read it again",
|
|
104
|
+
details: [],
|
|
105
|
+
},
|
|
100
106
|
{ reason: "command.cancelled", code: "cancelled", message: "the command was cancelled", details: [] },
|
|
101
107
|
{ reason: CRASH_REASON, code: CRASH_CODE, message: "the command failed; this is a bug", details: [] },
|
|
102
108
|
]);
|
|
@@ -65,7 +65,7 @@ export declare const pageSchema: v.VariantSchema<"mode", [v.StrictObjectSchema<{
|
|
|
65
65
|
export type Total = v.InferOutput<typeof totalSchema>;
|
|
66
66
|
/** A source that failed or was skipped: the answer holds what the others gave. */
|
|
67
67
|
export declare const issueSchema: v.StrictObjectSchema<{
|
|
68
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
68
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
69
69
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
70
70
|
/** The account, provider or source that is missing from the answer. */
|
|
71
71
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -80,7 +80,7 @@ export declare const metaSchema: v.StrictObjectSchema<{
|
|
|
80
80
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
81
81
|
readonly complete: v.BooleanSchema<undefined>;
|
|
82
82
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
83
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
83
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
84
84
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
85
85
|
/** The account, provider or source that is missing from the answer. */
|
|
86
86
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -172,7 +172,7 @@ export declare const listEnvelopeSchema: <T extends v.GenericSchema>(item: T) =>
|
|
|
172
172
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
173
173
|
readonly complete: v.BooleanSchema<undefined>;
|
|
174
174
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
175
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
175
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
176
176
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
177
177
|
/** The account, provider or source that is missing from the answer. */
|
|
178
178
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -237,7 +237,7 @@ export declare const itemEnvelopeSchema: <T extends v.GenericSchema>(item: T) =>
|
|
|
237
237
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
238
238
|
readonly complete: v.BooleanSchema<undefined>;
|
|
239
239
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
240
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
240
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
241
241
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
242
242
|
/** The account, provider or source that is missing from the answer. */
|
|
243
243
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -280,7 +280,7 @@ export declare const jsonlEndSchema: v.StrictObjectSchema<{
|
|
|
280
280
|
readonly type: v.LiteralSchema<"end", undefined>;
|
|
281
281
|
readonly complete: v.BooleanSchema<undefined>;
|
|
282
282
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
283
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
283
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
284
284
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
285
285
|
/** The account, provider or source that is missing from the answer. */
|
|
286
286
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -192,6 +192,10 @@ const verbs = [
|
|
|
192
192
|
["move", "move a thing to another place in the source: a file to a folder, an event to a calendar"],
|
|
193
193
|
["copy", "copy a file in the source"],
|
|
194
194
|
["run", "run a saved job once, now, as its timer would"],
|
|
195
|
+
["exec", "what a timer itself starts: one saved schedule, from its file; a person uses run"],
|
|
196
|
+
// schedule's creators are named for what the timer starts; the owner kept these names over `create --kind`.
|
|
197
|
+
["agent", "save a timer that starts an agent with a prompt, read-only; a verb under schedule, never a resource"],
|
|
198
|
+
["command", "save a timer that runs one of this tool's own commands; a verb under schedule"],
|
|
195
199
|
["claim", "take a thing for a while so no other agent works it at once; the claim lapses by itself"],
|
|
196
200
|
["release", "give back what claim took, before it lapses"],
|
|
197
201
|
["merge", "fold a duplicate into the thing that stays; nothing is deleted"],
|
|
@@ -262,6 +266,10 @@ const collections = [
|
|
|
262
266
|
["recordings", "the files a provider recorded of a meeting"],
|
|
263
267
|
["jobs", "work this tool runs in the background or on a timer"],
|
|
264
268
|
["actions", "what an agent proposes to do; the owner confirms or rejects each"],
|
|
269
|
+
[
|
|
270
|
+
"findings",
|
|
271
|
+
"what an agent read in stored mail, staged in batches with citations; nothing is recorded until the owner confirms. What it proposes to do is actions",
|
|
272
|
+
],
|
|
265
273
|
["agents", "the agents whose actions are recorded in the shared store"],
|
|
266
274
|
["reminders", "scheduled notices about stored work, claimed and acknowledged with delivery receipts"],
|
|
267
275
|
["key-documents", "selected source documents that a project keeps as its starting references"],
|
|
@@ -304,6 +312,10 @@ const singulars = [
|
|
|
304
312
|
["privacy", "who may find, see, call or add the account"],
|
|
305
313
|
["metadata", "a profile's cached source attributes"],
|
|
306
314
|
["auth", "the credentials a bot profile uses: its token and the secrets beside it"],
|
|
315
|
+
[
|
|
316
|
+
"schedule",
|
|
317
|
+
"a profile's timers the owner saved, each running a command of this tool or an agent; the tool's own background work is jobs",
|
|
318
|
+
],
|
|
307
319
|
];
|
|
308
320
|
const views = [
|
|
309
321
|
["tree", "the parent and child hierarchy of stored sources"],
|
package/docs/standards/README.md
CHANGED
|
@@ -29,18 +29,18 @@ the four steps that need a person. A new CLI is ready for its first release when
|
|
|
29
29
|
|---|---|---:|---|
|
|
30
30
|
| [Toolchain](toolchain.md) | `TOOL` | 20 | TypeScript, Biome, grit plugins, size caps, import cycles, spelling, Markdown, Go |
|
|
31
31
|
| [Layout](layout.md) | `LAY` | 10 | Repository layout, entry points, exports, file size, one job per file, splitting a unit, the bundled bin |
|
|
32
|
-
| [Commands](commands.md) | `CMD` |
|
|
32
|
+
| [Commands](commands.md) | `CMD` | 31 | Paths, vocabulary, flags, arguments, effects, lifetime, query, help, external references, joining `wcat`, settings in the config file, refs |
|
|
33
33
|
| [Output](output.md) | `OUT` | 24 | Data on stdout only, envelopes, errors, exit codes, partial answers, projection |
|
|
34
34
|
| [MCP](mcp.md) | `MCP` | 13 | The MCP tools, how they come from commands, what each permission shows |
|
|
35
35
|
| [Accounts](accounts.md) | `ACCT` | 12 | The explicit profile, reads across accounts, scope flags, linked identities, sign-in commands |
|
|
36
36
|
| [Permissions](permissions.md) | `PERM` | 22 | Levels, keys, defaults, hard limits, the write guard and its journal |
|
|
37
|
-
| [Architecture](architecture.md) | `ARCH` |
|
|
37
|
+
| [Architecture](architecture.md) | `ARCH` | 19 | Layers, dependency direction, vendor types, what goes in which package, the command tree built once |
|
|
38
38
|
| [Lifecycle](lifecycle.md) | `LIFE` | 15 | Opening late, who closes, deadlines, long-running commands, the `serve` relay, resumable syncs and their request budget, the run log |
|
|
39
39
|
| [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
|
|
40
40
|
| [Data model](data-model.md) | `DATA` | 9 | Core entities, entities and source records, `external_id`, field names, links and duplicates, files, calendar entries |
|
|
41
41
|
| [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
|
|
42
|
-
| [Security](security.md) | `SECR` |
|
|
43
|
-
| [Documents](docs.md) | `DOC` |
|
|
42
|
+
| [Security](security.md) | `SECR` | 24 | Secrets, logs, keyring, argv, error text, untrusted text, scanners, workflow hardening, reporting, OAuth sign-in and which app it uses, meeting secrets, keyring in moved-folder runs |
|
|
43
|
+
| [Documents](docs.md) | `DOC` | 21 | User pages and developer pages, generated pages, current facts, changelog, repository page |
|
|
44
44
|
| [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 |
|
|
45
45
|
| [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions, the owner's yes, trusted publishing |
|
|
46
46
|
| [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
|
|
@@ -249,6 +249,20 @@ options, its hooks) lives in a per-call context that the action receives, never
|
|
|
249
249
|
[CORE-41](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/BACKLOG_LATER.md). New code does not add to it.
|
|
250
250
|
- **Applies to:** cli-core's command skeleton and every CLI built on it. **Since:** cli-base 0.65.3. **Rulings:** none.
|
|
251
251
|
|
|
252
|
+
### WC-ARCH-23 · A value that ships with the package lives in the layer's constants.json
|
|
253
|
+
|
|
254
|
+
A value that ships inside a package and is the same for every owner — an OAuth app's client id, a fixed URL — lives
|
|
255
|
+
in that layer's `constants.json` and is read through `loadConstants` (cli-base, then cli-core, then the tool). It is
|
|
256
|
+
never a constant scattered through the source, and never a file CI writes from a secret.
|
|
257
|
+
|
|
258
|
+
- **Why:** a CI-written file goes missing when the secret does, and the release then cannot sign in (gcal 0.19.0);
|
|
259
|
+
scattered constants are copied between CLIs and drift apart.
|
|
260
|
+
- **Enforced by:** review only.
|
|
261
|
+
- **Example:** cli-core's `constants.json` holds the WireCat Google desktop app's `google.app.client_id`; drive and
|
|
262
|
+
gcal read it through cli-core instead of each keeping a copy. The owner's own choices stay in `config.json`.
|
|
263
|
+
- **Exceptions:** none.
|
|
264
|
+
- **Applies to:** cli-base, cli-core and every CLI. **Since:** cli-base 0.76.0. **Rulings:** none.
|
|
265
|
+
|
|
252
266
|
## Sources
|
|
253
267
|
|
|
254
268
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
|
@@ -22,6 +22,13 @@ A "messenger CLI" below is one of the CLIs that share cli-core's messenger comma
|
|
|
22
22
|
catalogue and parity manifest stay in cli-core
|
|
23
23
|
[`docs/dev/STANDARD.md`](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/STANDARD.md).
|
|
24
24
|
|
|
25
|
+
### Who follows these rules today
|
|
26
|
+
|
|
27
|
+
WC-CMD-30: cli-core's shared settings declare every shared scalar setting and give `config migrate`, so tg, max, wa,
|
|
28
|
+
mail and gcal follow once they take the cli-core release after 0.561.0. Their own settings (max's `serve` and
|
|
29
|
+
`transcribe_model`, for example) are not declared yet. drive's first-run file holds them all, but drive's own
|
|
30
|
+
`config` has no `migrate` and its writes do not fill the file. zm declares nothing yet.
|
|
31
|
+
|
|
25
32
|
## Paths and words
|
|
26
33
|
|
|
27
34
|
### WC-CMD-1 · Build every path as resource first, then a verb or a view
|
|
@@ -392,19 +399,38 @@ The references, each for its own part:
|
|
|
392
399
|
A CLI declares each scalar setting once, with its default and one line that says what it does, through cli-core's
|
|
393
400
|
`DeclaredSetting`. `config show` lists every declared setting with its value, its default and whether the file sets
|
|
394
401
|
it. Whenever the CLI writes its config file anyway, and on an explicit `config migrate`, it adds each missing setting
|
|
395
|
-
at its default with `withSettingDefaults`. It keeps the owner's values, their order and unknown keys
|
|
396
|
-
section
|
|
402
|
+
at its default with `withSettingDefaults`. It keeps the owner's values, their order and unknown keys. It may create
|
|
403
|
+
the top-level `defaults` section, which needs no field of its own, so every key shows; it creates no other section. A
|
|
404
|
+
plain read never rewrites the file.
|
|
397
405
|
|
|
398
406
|
- **Why:** the owner finds and edits every setting in the file, without reading the docs to learn that it exists.
|
|
399
407
|
- **Enforced by:** review only.
|
|
400
408
|
- **Example:** `memo config migrate` adds `"agent_decisions": false` to a file that lacks it; `memo config show` lists
|
|
401
409
|
it as `false (default)`.
|
|
402
410
|
- **Exceptions:** lists and sections that need fields no default can give (accounts, folders, permissions) get no
|
|
403
|
-
placeholder. A secret is never declared, since `config show` prints every value.
|
|
404
|
-
|
|
405
|
-
only five of them and nothing fills it later; zm declares nothing yet.
|
|
411
|
+
placeholder. A secret is never declared, since `config show` prints every value. A setting whose absence means
|
|
412
|
+
something no value says (no timeout, the terminal decides colour) is not declared either.
|
|
406
413
|
- **Applies to:** all CLIs. **Since:** cli-base 0.76.0. **Rulings:** none.
|
|
407
414
|
|
|
415
|
+
## References
|
|
416
|
+
|
|
417
|
+
### WC-CMD-31 · Accept every ref any WireCat CLI prints, and never ask the caller to translate it
|
|
418
|
+
|
|
419
|
+
A command argument, an option and a query value that take a stored thing accept its ref exactly as any WireCat CLI
|
|
420
|
+
prints it ([WC-OUT-13](output.md#wc-out-13--write-times-durations-references-and-empty-values-one-way)), unquoted.
|
|
421
|
+
The CLI that owns the thing opens it; a ref for another account or provider fails with an error that names the
|
|
422
|
+
right one, never `not_found`.
|
|
423
|
+
|
|
424
|
+
- **Why:** agents copy refs from one CLI's output into the next command. Each ref that fails costs a retry, a guess
|
|
425
|
+
or a correction from the owner: memo printed `chat:max/…`, and max read it as a chat title.
|
|
426
|
+
- **Enforced by:** `src/query/parser.test.ts` · `cli-core: src/domain/references.test.ts`.
|
|
427
|
+
- **Example:** `memo relationships list about:project:1` and `about:chat:max/1/2` read the ref as one value;
|
|
428
|
+
`max work messages list chat:max/<account>/<id>` answers as the bare id does.
|
|
429
|
+
- **Exceptions:** none. Known gaps, as of 2026-11-03: the query value works from cli-base 0.75.3 in each CLI that
|
|
430
|
+
takes it; chat and message arguments in tg, max and wa, and `document:N` in drive's `knowledge` commands, follow
|
|
431
|
+
cli-core's reference slices.
|
|
432
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.75.3. **Rulings:** none.
|
|
433
|
+
|
|
408
434
|
## Sources
|
|
409
435
|
|
|
410
436
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
package/docs/standards/docs.md
CHANGED
|
@@ -217,6 +217,21 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
217
217
|
Sections that already differed from their tag on 25.10.2026 stay as they are, and nothing reports them.
|
|
218
218
|
- **Applies to:** every repository that calls the changelog guard. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
219
219
|
|
|
220
|
+
### WC-DOC-21 · A public repository has a banner, a link preview, a homepage and topics
|
|
221
|
+
|
|
222
|
+
- **Why:** the GitHub page and a shared link are often the first thing a person sees. A tool without them looks
|
|
223
|
+
unfinished next to the others, and one without topics is not found in GitHub search.
|
|
224
|
+
- **Enforced by:** review only.
|
|
225
|
+
- **Example:** `docs/images/social-preview.png` is 1280×640 in the shared style: the command name in the
|
|
226
|
+
cyan-violet-magenta gradient, a one-line tagline and the "part of WireCat · wirecat.dev" footer. `README.md` opens
|
|
227
|
+
with it through an absolute `raw.githubusercontent.com` URL, so it also renders on npmjs.com, with the tagline as
|
|
228
|
+
its alt text. The same file is the repository's social preview (Settings → Social preview; GitHub has no API for
|
|
229
|
+
it). The homepage is the tool's wirecat.dev docs page, and the topics name the service, `cli`, `ai-agents`, `mcp`
|
|
230
|
+
where it serves MCP, and `wirecat`.
|
|
231
|
+
- **Exceptions:** private repositories, and repositories that only support the tools (`.github`, `community`,
|
|
232
|
+
`cli-docs`, `cli-testing`).
|
|
233
|
+
- **Applies to:** every public WireCat repository. **Since:** cli-base 0.75.3. **Rulings:** none.
|
|
234
|
+
|
|
220
235
|
## Sources
|
|
221
236
|
|
|
222
237
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
package/docs/standards/output.md
CHANGED
|
@@ -216,7 +216,8 @@ whole or partial.
|
|
|
216
216
|
- **Enforced by:** `src/exit-codes.test.ts`.
|
|
217
217
|
- **Example:** 0 ok · 1 internal error · 2 validation error · 3 configuration error · 4 authentication error ·
|
|
218
218
|
5 permission error · 6 not found · 7 confirmation required · 8 rate limited · 9 timeout · 10 network error ·
|
|
219
|
-
11 provider error · 12 provider unavailable · 13 invalid response · 14 outcome unknown ·
|
|
219
|
+
11 provider error · 12 provider unavailable · 13 invalid response · 14 outcome unknown · 15 conflict ·
|
|
220
|
+
130 cancelled.
|
|
220
221
|
- **Exceptions:** on cli-core's `run()`, a one-shot command stopped by SIGTERM exits 143. A `persistent` command
|
|
221
222
|
stopped by Ctrl-C or SIGTERM exits 0. A closed pipe exits 0. See the [lifecycle](lifecycle.md) page.
|
|
222
223
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
@@ -237,7 +238,8 @@ whole or partial.
|
|
|
237
238
|
- **Example:** every error has `retry: { safe, after_ms? }`. A transient failure (`timeout`, `rate_limited`,
|
|
238
239
|
`network_error`, `provider_unavailable`) is safe for a command that writes nothing on the service, or for a write
|
|
239
240
|
that never left (`sent: false`). A sync that only writes the local store is safe to repeat. `outcome_unknown` is
|
|
240
|
-
never safe: repeat that write only with its `--operation-id`.
|
|
241
|
+
never safe: repeat that write only with its `--operation-id`. `conflict` (the record changed since the caller read
|
|
242
|
+
it, as a `--revision` check finds) is never safe either: read the record again and decide on the new copy.
|
|
241
243
|
- **Exceptions:** cli-base's `retryOf`, used on its own, also counts a local write as a write, so it is stricter
|
|
242
244
|
than cli-core's runner.
|
|
243
245
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
@@ -194,8 +194,12 @@ An agent never deletes a message for everyone, and never ends the owner's other
|
|
|
194
194
|
### WC-PERM-12 · `<PREFIX>_READ_ONLY` refuses every write
|
|
195
195
|
|
|
196
196
|
When this variable is set, every write answers `permission_error`, whatever `permissions` or a flag say. So does a
|
|
197
|
-
command that writes only on this machine, unless cli-core marks it `destroysNothing
|
|
198
|
-
until someone marks it.
|
|
197
|
+
command that writes only on this machine, unless cli-core marks it `destroysNothing` or it is of the propose tier; a
|
|
198
|
+
new local write is refused until someone marks it. The propose tier is a closed list, cli-core's `PROPOSE_COMMANDS`:
|
|
199
|
+
`messages acknowledge` and `mail findings add`. A command passes as a proposal only when it is marked `proposesOnly`
|
|
200
|
+
and its path ends with a listed one; it stages something the owner decides, or moves its caller's own processing
|
|
201
|
+
mark, and nothing else. Adding a path widens what a read-only agent can write, so each addition is reviewed on its
|
|
202
|
+
own. Logging in or out and a change to what a later run may do (a schedule, a setting, the
|
|
199
203
|
recipient list) are refused too. A `--dry-run` preview still runs.
|
|
200
204
|
|
|
201
205
|
- **Why:** an unattended agent needs one switch that the config file and `--permission` cannot undo, and that also
|
|
@@ -209,7 +213,8 @@ recipient list) are refused too. A `--dry-run` preview still runs.
|
|
|
209
213
|
other command, an MCP session included (cli-core 0.343.0). Local writes marked `destroysNothing` run: what the store
|
|
210
214
|
rebuilds (`store reindex`, `mentions rebuild`), a new file (`store backup`, `store export`, `stats charts`), and the
|
|
211
215
|
task writes the follow-ups skill makes on a timer, where `tasks close` only suggests the close to the owner.
|
|
212
|
-
|
|
216
|
+
A read-only scheduled mail agent stages what it read with `mail findings add` and moves its mark with
|
|
217
|
+
`messages acknowledge`; nothing it staged is recorded until the owner confirms. Embedding and analysis on this machine run; sending text to an outside service for them is refused.
|
|
213
218
|
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
214
219
|
|
|
215
220
|
### WC-PERM-13 · Every write passes the write guard before it leaves
|
|
@@ -373,6 +373,46 @@ store and runs no search. A CLI declares its provider and may replace any step.
|
|
|
373
373
|
owner, 2026-10-30 (share the keyring entry, do not multiply it); owner, 2026-10-31 (do not replace it: a test of a
|
|
374
374
|
new session writes a temporary entry and cleans it up).
|
|
375
375
|
|
|
376
|
+
### WC-SECR-25 · Sign in with WireCat's app by default, and let the owner choose their own
|
|
377
|
+
|
|
378
|
+
Every provider sign-in uses WireCat's OAuth app unless the owner names another. That app is read from
|
|
379
|
+
`constants.json` ([WC-ARCH-23](architecture.md#wc-arch-23--a-value-that-ships-with-the-package-lives-in-the-layers-constantsjson)):
|
|
380
|
+
cli-core's layer for Google and Microsoft, a tool's own layer for a provider only that tool uses, such as Zoom.
|
|
381
|
+
|
|
382
|
+
- **Which app, first match wins:**
|
|
383
|
+
1. `session start --client-file <json>` or `--client-id <id>`;
|
|
384
|
+
2. `<TOOL>_<PROVIDER>_CLIENT_ID`, plus `<TOOL>_<PROVIDER>_CLIENT_SECRET` where the provider needs a secret. A
|
|
385
|
+
secret without an id is a `configuration_error`;
|
|
386
|
+
3. the account's config key `<provider>.client_id`;
|
|
387
|
+
4. WireCat's app.
|
|
388
|
+
- **Stored with the account.** The app a sign-in used is saved with that account, so refresh and revoke use the same
|
|
389
|
+
app even after the default changes.
|
|
390
|
+
- **Reported by `doctor`,** per provider: `oauth_client: { client: "wirecat" | "own" | null, source: "flag" | "env" |
|
|
391
|
+
"config" | "constants" | null }`. A null client comes with a one-line reason.
|
|
392
|
+
- **Desktop.** A sign-in from wirecat-desktop offers WireCat's app, with a "Use my own app" choice that writes the same
|
|
393
|
+
override as step 2 or 3.
|
|
394
|
+
- **A shipped secret** is allowed only where the provider documents the client as public, and the tool's gitleaks
|
|
395
|
+
allowlist names it by value ([WC-SECR-17](#wc-secr-17--a-gitleaks-rule-covers-each-secret-shape-a-cli-handles)).
|
|
396
|
+
Google's token endpoint refuses a Desktop app client without its secret (tested 2026-11-04), although
|
|
397
|
+
[Google's guide](https://developers.google.com/identity/protocols/oauth2/native-app) calls desktop apps public
|
|
398
|
+
clients that cannot keep secrets. So a Google app ships its id and secret; a Microsoft public client ships its id
|
|
399
|
+
only.
|
|
400
|
+
- **A provider with no WireCat app yet** says so in `doctor` (a null client and its reason) and in its docs, and still
|
|
401
|
+
keeps the order, the stored app and the report above.
|
|
402
|
+
|
|
403
|
+
- **Why:** an owner should sign in without creating an app first, and still be able to use their own; a token
|
|
404
|
+
refreshed with a different app than the one that issued it fails.
|
|
405
|
+
- **Enforced by:** `cli-core: src/oauth/client-file.test.ts`.
|
|
406
|
+
- **Example:** `gcal personal session start` uses WireCat's Google app and `doctor` reports
|
|
407
|
+
`{ client: "wirecat", source: "constants" }`; with `GCAL_GOOGLE_CLIENT_ID` and `GCAL_GOOGLE_CLIENT_SECRET` set it
|
|
408
|
+
reports `{ client: "own", source: "env" }`.
|
|
409
|
+
- **Exceptions:** known gaps, as of 2026-11-04: mail has no WireCat default for Google; zoom has no WireCat app until
|
|
410
|
+
Zoom Marketplace approves one; no tool reads the per-account `google.client_id` key yet; mail's Microsoft key is
|
|
411
|
+
named `microsoft_client_id`; `doctor` reports no `source` yet. gcal's default came from a file CI wrote from a
|
|
412
|
+
secret; cli-core's `constants.json` replaces it from its next release.
|
|
413
|
+
- **Applies to:** every CLI that signs in with OAuth, and wirecat-desktop. **Since:** cli-base 0.76.1.
|
|
414
|
+
**Rulings:** owner, 2026-11-04.
|
|
415
|
+
|
|
376
416
|
## Sources
|
|
377
417
|
|
|
378
418
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.0",
|
|
4
4
|
"description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
22
|
"config",
|
|
23
|
+
"constants.json",
|
|
23
24
|
"docs/standards",
|
|
24
25
|
"README.md",
|
|
25
26
|
"CHANGELOG.md",
|