@wirecat/cli-base 0.65.2 → 0.65.4
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 +31 -0
- package/README.md +2 -2
- package/dist/credentials.d.ts +10 -10
- package/dist/credentials.js +18 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +27 -1
- package/docs/standards/README.md +1 -1
- package/docs/standards/accounts.md +0 -1
- package/docs/standards/architecture.md +19 -0
- package/docs/standards/lifecycle.md +5 -5
- package/docs/standards/mcp.md +2 -3
- package/docs/standards/permissions.md +1 -2
- package/docs/standards/releases.md +3 -2
- package/docs/standards/security.md +23 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,37 @@ 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.65.4 — 31.10.2026
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A run with moved folders uses the app's own keyring entry again** ([WC-SECR-24](docs/standards/security.md#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it),
|
|
16
|
+
rewritten). `Credentials` with `isolated` reads and writes service `<app>`, not `<app>:<config folder>`, and no
|
|
17
|
+
longer saves to the moved folder's file first (the 0.65.1 redirect is gone). `keyringService` returns the app's
|
|
18
|
+
service whatever its other arguments say, so cli-core's sign-in key names the shared entry. Per-folder entries made
|
|
19
|
+
before are no longer read; they were copies made by throwaway runs. A secret 0.65.1 saved in a moved folder's file
|
|
20
|
+
is still read when the keyring has none.
|
|
21
|
+
- **The handbook's statements about gcal match gcal-cli 0.2.0:** it has `session list`, its `api` checks
|
|
22
|
+
`events.notify` and `events.delete.series`, its `api` answers drop the passcode lines of a description and a
|
|
23
|
+
location, it serves MCP as `mcp serve`, it sets `requestMs` to 1.5 s, and it packs 144 files.
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- **A moved run never deletes the owner's keyring entry.** A security review found that
|
|
28
|
+
`HOME=/tmp/x gcal personal session end` deleted the owner's real Google sign-in. With the system keyring,
|
|
29
|
+
`Credentials.remove` from a moved run keeps the entry, warns on stderr and leaves `"keyring"` out of what it
|
|
30
|
+
returns. A run counts as moved with `isolated`, or when the `env` it is given moves `HOME` or an XDG folder away from
|
|
31
|
+
the account's own home. The new `isMovedRun({ appName, prefix, env })` says the same for a CLI: pass it as
|
|
32
|
+
`isolated`, and check it before revoking a sign-in at the provider. A test with `memoryKeyring()` is unchanged.
|
|
33
|
+
|
|
34
|
+
## 0.65.3 — 30.10.2026
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **A rule against per-call state in the command tree** (`WC-ARCH-22`, [Architecture](docs/standards/architecture.md)):
|
|
39
|
+
the tree is built once per process and a call's state lives in a per-call context. cli-core's `run()` still
|
|
40
|
+
rebuilds the tree per call and is the named exception until that is reworked.
|
|
41
|
+
|
|
11
42
|
## 0.65.2 — 29.10.2026
|
|
12
43
|
|
|
13
44
|
### Fixed
|
package/README.md
CHANGED
|
@@ -150,8 +150,8 @@ on macOS and Windows through `node-ci.yml` with `platforms: true`; here, Full va
|
|
|
150
150
|
spawning `npm` and `tsc`). In every test the keyring is a fake, so no test reaches a real Keychain or Credential
|
|
151
151
|
Manager.
|
|
152
152
|
|
|
153
|
-
Two traps worth designing for: the keyring is global, so a throwaway config directory
|
|
154
|
-
|
|
153
|
+
Two traps worth designing for: the keyring is global, so a throwaway config directory shares the owner's entry (pass
|
|
154
|
+
`isolated: isMovedRun(…)` so it never deletes it); and a locked Linux keyring can block on a prompt rather than fail.
|
|
155
155
|
|
|
156
156
|
### Time, retries and deadlines
|
|
157
157
|
|
package/dist/credentials.d.ts
CHANGED
|
@@ -12,16 +12,12 @@ export interface CredentialsOptions {
|
|
|
12
12
|
/** The keyring service name — the CLI's app name, e.g. `max-cli`, not its command word. */
|
|
13
13
|
service: string;
|
|
14
14
|
/**
|
|
15
|
-
* **True when the
|
|
15
|
+
* **True when the run's folders are not the owner's own** — pass `isMovedRun(…)`.
|
|
16
16
|
*
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* service name to the directory, making the isolation people already assume they have real.
|
|
22
|
-
*
|
|
23
|
-
* With the system keyring it also keeps new secrets out of it: they go to the file in the moved directory, so a test
|
|
24
|
-
* or worktree run leaves nothing in the owner's keyring (WC-SECR-24). A secret saved there before is still read.
|
|
17
|
+
* The run still uses the owner's keyring entry (`service`, the same account names): one sign-in per computer, not
|
|
18
|
+
* one per temp folder. With the system keyring, `remove` then keeps that entry instead of deleting it, so a test or
|
|
19
|
+
* worktree run cannot end the owner's real sign-in (WC-SECR-24). A moved `HOME` or XDG folder in `env` counts as
|
|
20
|
+
* moved even when this is false.
|
|
25
21
|
*/
|
|
26
22
|
isolated?: boolean;
|
|
27
23
|
/** Read before the keyring, for CI. */
|
|
@@ -33,7 +29,11 @@ export interface CredentialsOptions {
|
|
|
33
29
|
/** Where the one-line warning goes when the keyring is unavailable. Never stdout. */
|
|
34
30
|
warn?: (message: string) => void;
|
|
35
31
|
}
|
|
36
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The keyring service a `Credentials` uses: the app's own, for a moved run too. The other two arguments are ignored
|
|
34
|
+
* and stay only so that callers written against 0.65.0 still compile.
|
|
35
|
+
*/
|
|
36
|
+
export declare const keyringService: (service: string, _configDir?: string, _isolated?: boolean) => string;
|
|
37
37
|
/**
|
|
38
38
|
* Environment first, then the OS keyring, then a file.
|
|
39
39
|
*
|
package/dist/credentials.js
CHANGED
|
@@ -3,8 +3,13 @@ import { join } from "node:path";
|
|
|
3
3
|
import { writeSecurely } from "./config.js";
|
|
4
4
|
import { CliError } from "./errors.js";
|
|
5
5
|
import { systemKeyring } from "./keyring.js";
|
|
6
|
+
import { homeIsMoved } from "./paths.js";
|
|
6
7
|
export const CREDENTIAL_STORAGE = ["auto", "keyring", "file"];
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The keyring service a `Credentials` uses: the app's own, for a moved run too. The other two arguments are ignored
|
|
10
|
+
* and stay only so that callers written against 0.65.0 still compile.
|
|
11
|
+
*/
|
|
12
|
+
export const keyringService = (service, _configDir, _isolated) => service;
|
|
8
13
|
/**
|
|
9
14
|
* Environment first, then the OS keyring, then a file.
|
|
10
15
|
*
|
|
@@ -21,7 +26,7 @@ export class Credentials {
|
|
|
21
26
|
#envVar;
|
|
22
27
|
#fileName;
|
|
23
28
|
#warn;
|
|
24
|
-
#
|
|
29
|
+
#keepsSharedEntry;
|
|
25
30
|
#warned = false;
|
|
26
31
|
constructor(options) {
|
|
27
32
|
this.#configDir = options.configDir;
|
|
@@ -31,31 +36,24 @@ export class Credentials {
|
|
|
31
36
|
this.#envVar = options.envVar;
|
|
32
37
|
this.#fileName = options.fileName ?? "credentials.json";
|
|
33
38
|
this.#warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
|
|
34
|
-
this.#service =
|
|
35
|
-
this.#
|
|
39
|
+
this.#service = options.service;
|
|
40
|
+
this.#keepsSharedEntry =
|
|
41
|
+
this.#keyring === systemKeyring && (options.isolated === true || homeIsMoved({ env: this.#env }));
|
|
36
42
|
}
|
|
37
43
|
read(account) {
|
|
38
44
|
const fromEnv = this.#envVar ? this.#env[this.#envVar]?.trim() : undefined;
|
|
39
45
|
if (fromEnv)
|
|
40
46
|
return { secret: fromEnv, source: "environment" };
|
|
41
|
-
const fromFile = () => {
|
|
42
|
-
const secret = this.#readFile()?.[account]?.secret;
|
|
43
|
-
return secret ? { secret, source: "file" } : undefined;
|
|
44
|
-
};
|
|
45
|
-
if (this.#fileFirst) {
|
|
46
|
-
const found = fromFile();
|
|
47
|
-
if (found)
|
|
48
|
-
return found;
|
|
49
|
-
}
|
|
50
47
|
if (this.#storage !== "file") {
|
|
51
48
|
const fromKeyring = this.#tryKeyring(() => this.#keyring.get(this.#service, account));
|
|
52
49
|
if (fromKeyring)
|
|
53
50
|
return { secret: fromKeyring, source: "keyring" };
|
|
54
51
|
}
|
|
55
|
-
|
|
52
|
+
const secret = this.#readFile()?.[account]?.secret;
|
|
53
|
+
return secret ? { secret, source: "file" } : undefined;
|
|
56
54
|
}
|
|
57
55
|
write(account, secret) {
|
|
58
|
-
if (this.#storage !== "file"
|
|
56
|
+
if (this.#storage !== "file") {
|
|
59
57
|
const stored = this.#tryKeyring(() => {
|
|
60
58
|
this.#keyring.set(this.#service, account, secret);
|
|
61
59
|
return true;
|
|
@@ -74,7 +72,11 @@ export class Credentials {
|
|
|
74
72
|
}
|
|
75
73
|
remove(account) {
|
|
76
74
|
const removed = [];
|
|
77
|
-
if (this.#storage !== "file" && this.#
|
|
75
|
+
if (this.#storage !== "file" && this.#keepsSharedEntry) {
|
|
76
|
+
this.#warn(`kept ${account} in the OS keyring: it is the owner's sign-in, shared with every run, and a run with moved ` +
|
|
77
|
+
"folders never removes it");
|
|
78
|
+
}
|
|
79
|
+
else if (this.#storage !== "file" && this.#tryKeyring(() => this.#keyring.delete(this.#service, account))) {
|
|
78
80
|
removed.push("keyring");
|
|
79
81
|
}
|
|
80
82
|
const store = this.#readFile();
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { brokenKeyring, type KeyringStore, memoryKeyring, systemKeyring } from "
|
|
|
10
10
|
export type { Logger } from "./logger.js";
|
|
11
11
|
export { createRecordLog, logRecordOf, NOT_ALLOWED, REDACTED_FIELDS, type RecordInput, type RecordLogOptions, } from "./logging.js";
|
|
12
12
|
export { createOutputBuffer, type OutputBuffer, type OutputBufferOptions } from "./output-buffer.js";
|
|
13
|
-
export { configFilePath, type Paths, type PathsOptions, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
13
|
+
export { configFilePath, isMovedRun, type Paths, type PathsOptions, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
14
14
|
export { indent, type PrettyOptions, renderPretty } from "./pretty.js";
|
|
15
15
|
export { createRenderer, RENDER_FORMATS, type Renderer, type RendererOptions, type RenderFormat, } from "./renderer.js";
|
|
16
16
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export { catchCrashes, errorObjectOf, errorRecordOf, exitCodeOfAnswer, exitCodeO
|
|
|
8
8
|
export { brokenKeyring, memoryKeyring, systemKeyring } from "./keyring.js";
|
|
9
9
|
export { createRecordLog, logRecordOf, NOT_ALLOWED, REDACTED_FIELDS, } from "./logging.js";
|
|
10
10
|
export { createOutputBuffer } from "./output-buffer.js";
|
|
11
|
-
export { configFilePath, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
11
|
+
export { configFilePath, isMovedRun, pathsAreOverridden, resolvePaths } from "./paths.js";
|
|
12
12
|
export { indent, renderPretty } from "./pretty.js";
|
|
13
13
|
export { createRenderer, RENDER_FORMATS, } from "./renderer.js";
|
|
14
14
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
package/dist/paths.d.ts
CHANGED
|
@@ -31,4 +31,21 @@ export declare const defaultPaths: ({ appName, env, platform }: PathsOptions) =>
|
|
|
31
31
|
export declare const resolvePaths: (options: PathsOptions) => Paths;
|
|
32
32
|
/** Whether any of the three directories came from the environment rather than the OS convention. */
|
|
33
33
|
export declare const pathsAreOverridden: ({ appName, prefix, env }: PathsOptions) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether `HOME` or an XDG (or Windows) folder in `env` puts the default folders anywhere but under the account's own
|
|
36
|
+
* home, which comes from the user database: `os.homedir()` returns `$HOME` and would miss exactly this. An account with
|
|
37
|
+
* no home entry counts as moved.
|
|
38
|
+
*/
|
|
39
|
+
export declare const homeIsMoved: ({ env, platform, home, }?: {
|
|
40
|
+
env?: NodeJS.ProcessEnv;
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
home?: string | undefined;
|
|
43
|
+
}) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* **A run whose folders are not the owner's own**: a `<PREFIX>_*_DIR` override, or a moved `HOME` or XDG folder.
|
|
46
|
+
* It still shares the owner's keyring entry, so it must not delete or revoke it (WC-SECR-24).
|
|
47
|
+
*/
|
|
48
|
+
export declare const isMovedRun: (options: PathsOptions & {
|
|
49
|
+
home?: string | undefined;
|
|
50
|
+
}) => boolean;
|
|
34
51
|
export declare const configFilePath: (configDir: string, fileName?: string) => string;
|
package/dist/paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { homedir, userInfo } from "node:os";
|
|
2
2
|
import { join, posix, win32 } from "node:path";
|
|
3
3
|
export const PATH_KINDS = ["config", "state", "cache"];
|
|
4
4
|
const OVERRIDES = { config: "CONFIG_DIR", state: "STATE_DIR", cache: "CACHE_DIR" };
|
|
@@ -41,4 +41,30 @@ export const resolvePaths = (options) => {
|
|
|
41
41
|
};
|
|
42
42
|
/** Whether any of the three directories came from the environment rather than the OS convention. */
|
|
43
43
|
export const pathsAreOverridden = ({ appName, prefix, env = process.env }) => PATH_KINDS.some((kind) => env[overrideName(appName, prefix, kind)] !== undefined);
|
|
44
|
+
const accountHome = () => {
|
|
45
|
+
try {
|
|
46
|
+
return userInfo().homedir;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Whether `HOME` or an XDG (or Windows) folder in `env` puts the default folders anywhere but under the account's own
|
|
54
|
+
* home, which comes from the user database: `os.homedir()` returns `$HOME` and would miss exactly this. An account with
|
|
55
|
+
* no home entry counts as moved.
|
|
56
|
+
*/
|
|
57
|
+
export const homeIsMoved = ({ env = process.env, platform = process.platform, home = accountHome(), } = {}) => {
|
|
58
|
+
if (home === undefined)
|
|
59
|
+
return true;
|
|
60
|
+
const appName = "app";
|
|
61
|
+
const actual = defaultPaths({ appName, env, platform });
|
|
62
|
+
const owners = defaultPaths({ appName, env: { HOME: home, USERPROFILE: home }, platform });
|
|
63
|
+
return PATH_KINDS.some((kind) => actual[kind] !== owners[kind]);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* **A run whose folders are not the owner's own**: a `<PREFIX>_*_DIR` override, or a moved `HOME` or XDG folder.
|
|
67
|
+
* It still shares the owner's keyring entry, so it must not delete or revoke it (WC-SECR-24).
|
|
68
|
+
*/
|
|
69
|
+
export const isMovedRun = (options) => pathsAreOverridden(options) || homeIsMoved(options);
|
|
44
70
|
export const configFilePath = (configDir, fileName = "config.json") => join(configDir, fileName);
|
package/docs/standards/README.md
CHANGED
|
@@ -34,7 +34,7 @@ the four steps that need a person. A new CLI is ready for its first release when
|
|
|
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` | 18 | 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 |
|
|
@@ -180,7 +180,6 @@ Every CLI that signs in has four commands under `session`:
|
|
|
180
180
|
- tg, max and wa have `session start` and `session end`, no `session status`, and list their profiles with
|
|
181
181
|
`account list` instead of `session list`.
|
|
182
182
|
- drive has `session start`, `session list` and `session end`, and no `session status`.
|
|
183
|
-
- gcal has `session start`, `session status` and `session end`, and no `session list`.
|
|
184
183
|
- zm has `session start` and `session status`, and no `session end` or `session list`.
|
|
185
184
|
- memo signs in per mail account with `mail add` and has no `session` group.
|
|
186
185
|
|
|
@@ -230,6 +230,25 @@ out of search and listed under `skipped`, with the hint to run that CLI once.
|
|
|
230
230
|
- **Applies to:** cli-core and every CLI with a provider of its own. **Since:** cli-base 0.53.0. **Rulings:** none
|
|
231
231
|
(Wave 3 requirements 3.2).
|
|
232
232
|
|
|
233
|
+
### WC-ARCH-22 · Build the command tree once; keep each call's state out of it
|
|
234
|
+
|
|
235
|
+
The Commander tree — commands, options, help, actions — describes the CLI and is the same for every call, so it is
|
|
236
|
+
built once per process. What belongs to one call (its streams, signal, deadline, environment, profile and parsed
|
|
237
|
+
options, its hooks) lives in a per-call context that the action receives, never on the `Command` objects.
|
|
238
|
+
|
|
239
|
+
- **Why:** state written into a shared tree leaks into the next call. Commander restores option values to what they
|
|
240
|
+
were before the *first* parse, so a profile set on the tree before parsing becomes every later call's starting
|
|
241
|
+
value, and a hook added per call runs once more each time. Building a fresh tree per call avoids the leak but costs
|
|
242
|
+
an MCP server ~16 ms per tool call for tg.
|
|
243
|
+
- **Enforced by:** review only.
|
|
244
|
+
- **Example:** a check that always gives the same answer for the same definition (`defineQuery`'s examples,
|
|
245
|
+
`assertSharedContracts`) runs once per definition, not per build. A new per-call value goes into the call's context,
|
|
246
|
+
not into `setOptionValue`, `hook` or `provide` on the tree.
|
|
247
|
+
- **Exceptions:** cli-core's `run()` still builds a fresh tree per call, because it writes the call's environment,
|
|
248
|
+
a `preAction` hook and the profile into it; moving that to a per-call context is
|
|
249
|
+
[CORE-41](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/BACKLOG_LATER.md). New code does not add to it.
|
|
250
|
+
- **Applies to:** cli-core's command skeleton and every CLI built on it. **Since:** cli-base 0.65.3. **Rulings:** none.
|
|
251
|
+
|
|
233
252
|
## Sources
|
|
234
253
|
|
|
235
254
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
|
@@ -23,10 +23,10 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
|
|
|
23
23
|
- **WC-LIFE-15:** tg, max, wa, gcal and the template. drive, memo and zm do not keep runs yet; each is adding them on a
|
|
24
24
|
`feat/runs` branch (2026-10-25). zm's `resolveSettings` also returns `keepFailedRuns: false`, which turns off the
|
|
25
25
|
failure log cli-core's `run` would keep.
|
|
26
|
-
- **WC-LIFE-16:** cli-core's shared calendar `sync`, from cli-core 0.464.0 (cli-core #2118); gcal
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
- **WC-LIFE-16:** cli-core's shared calendar `sync`, from cli-core 0.464.0 (cli-core #2118); gcal has it. The
|
|
27
|
+
deadlines of WC-LIFE-5 come with the same release. The other syncs (`messages sync` in tg, max and wa, drive's
|
|
28
|
+
`sync`, memo's mail import, zm's `recordings sync`) are cooperative but spend no request budget yet. gcal sets
|
|
29
|
+
`requestMs` to 1.5 s.
|
|
30
30
|
|
|
31
31
|
## One-shot commands
|
|
32
32
|
|
|
@@ -271,7 +271,7 @@ status command answers how far it is
|
|
|
271
271
|
and the run loses its round; with one, every run ends with an answer, saves its work and says how to go on. One
|
|
272
272
|
`requestMs` per CLI, in one place, keeps the arithmetic the same in every sync of that CLI.
|
|
273
273
|
- **Enforced by:** `cli-core: src/background/budget.test.ts` · `cli-core: src/calendar/testing/sync-contract.ts`.
|
|
274
|
-
- **Example:** `gcal personal sync` over MCP has 45 s: margin 6.75 s, so with
|
|
274
|
+
- **Example:** `gcal personal sync` over MCP has 45 s: margin 6.75 s, so with gcal's 1.5 s about 25 requests. It
|
|
275
275
|
answers `state: "partial"`, `stopped: "requests"`, `progress: { calendars, calendarsDone, requests, saved }`, and
|
|
276
276
|
`next` with `gcal personal sync`; `gcal personal status` shows each calendar's progress. cli-core builds the budget
|
|
277
277
|
with `runBudgetOf(command, maxPages)` from `@wirecat/cli-core/cli` over `runBudget` in
|
package/docs/standards/mcp.md
CHANGED
|
@@ -19,9 +19,8 @@ tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp serve`) an
|
|
|
19
19
|
`mcp serve --profiles` ([WC-MCP-9](#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)) is not
|
|
20
20
|
built yet: cli-core serves one named profile.
|
|
21
21
|
zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp serve`, cli-core's
|
|
22
|
-
`toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp
|
|
23
|
-
|
|
24
|
-
drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
|
|
22
|
+
`toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp serve`). memo and drive have their own servers: memo has hand-written
|
|
23
|
+
tools, and drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
|
|
25
24
|
gaps against these rules.
|
|
26
25
|
|
|
27
26
|
WC-MCP-2 and WC-MCP-8 need cli-core 0.321.0, which tg, max and wa pin.
|
|
@@ -346,8 +346,7 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
|
|
|
346
346
|
- **Exceptions:** known gaps, as of 2026-10-27:
|
|
347
347
|
- cli-core's `CALENDAR_WRITE_DEFAULTS` (`src/calendar/writer.ts`) still has `events create`, `update` and
|
|
348
348
|
`respond` at `ask`.
|
|
349
|
-
- gcal's generated `api` commands default every write to `ask`, ordinary changes included
|
|
350
|
-
`events.notify` nor `events.delete.series`.
|
|
349
|
+
- gcal's generated `api` commands default every write to `ask`, ordinary changes included.
|
|
351
350
|
- drive keeps its own policy and `--read-only` flag, zoom reads its own `remote-meetings` key with `levelFor`, and
|
|
352
351
|
memo uses cli-core's permissions for the shared commands only. None of them declares `commandPermissions` yet.
|
|
353
352
|
- **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0 (the levels and the shared
|
|
@@ -196,8 +196,9 @@ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/
|
|
|
196
196
|
`@wirecat/cli-base/release` fails on any path in `npm pack --dry-run` outside `allowed`. It checks the pack against
|
|
197
197
|
the list it is given, not against this rule: a CLI that passes its own `files` list as `allowed`, as the template's
|
|
198
198
|
`scripts/smoke.ts` does, checks only that npm packs what `files` says, and cannot fail on a wrong `files` list.
|
|
199
|
-
`npm pack --dry-run` in gcal-cli
|
|
200
|
-
`contracts/`, `lint/` and `template.json` among them, because its `files` list
|
|
199
|
+
`npm pack --dry-run` in gcal-cli packed 256 files on 2026-10-27, `src/`, `scripts/`, `.github/`, `config/`,
|
|
200
|
+
`contracts/`, `lint/` and `template.json` among them, because its `files` list named them; its check could not see
|
|
201
|
+
it. On 2026-10-31 (gcal-cli 0.2.0) it packs 144. The check this rule needs
|
|
201
202
|
refuses the forbidden paths above whatever `files` says; it belongs in cli-template, so every CLI gets it by
|
|
202
203
|
`upgrade` ([cli-template#77](https://github.com/WireCatLabs/cli-template/issues/77)).
|
|
203
204
|
- **Exceptions:** cli-template's own package ships the skeleton's sources and configs, since `new` copies them; the
|
|
@@ -68,11 +68,10 @@ name. Any other top-level key is written as `[not allowed]`. A key that names co
|
|
|
68
68
|
|
|
69
69
|
cli-base's `Credentials` reads the environment variable first (for CI), then the OS keyring, then a file with mode
|
|
70
70
|
`0600`. The keyring service is the CLI's app name, `appName` in its identity (`max-cli`, `gcal-cli`, `cli-memo`), not
|
|
71
|
-
the command word people type (`max`, `gcal`).
|
|
72
|
-
|
|
71
|
+
the command word people type (`max`, `gcal`). A run with moved folders uses the same entry and never deletes it
|
|
72
|
+
([WC-SECR-24](#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it)).
|
|
73
73
|
|
|
74
|
-
- **Why:** the keyring is the safest store on the computer
|
|
75
|
-
owner's real keyring entry.
|
|
74
|
+
- **Why:** the keyring is the safest store on the computer.
|
|
76
75
|
- **Enforced by:** `src/credentials.test.ts`.
|
|
77
76
|
- **Example:** a bot token is read from `MAX_BOT_TOKEN`, then the keyring entry `bot:<profile>`, then the file.
|
|
78
77
|
- **Exceptions:** `auto` storage tries the keyring and, when it fails, warns once on stderr and uses the file. A
|
|
@@ -322,28 +321,29 @@ store and runs no search. A CLI declares its provider and may replace any step.
|
|
|
322
321
|
the provider and never touched the store: a generic API command removes the provider's passcode fields (Google's
|
|
323
322
|
`conferenceData.entryPoints[].passcode`, `pin`, `accessCode`, `password`), the query of every join link and the
|
|
324
323
|
dial sequence after a phone number, and the passcode lines of a description or a location.
|
|
325
|
-
- **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name.
|
|
326
|
-
2026-10-27: gcal's `api` answers drop the conference entry points' secrets (`src/conference.ts`) but keep the
|
|
327
|
-
description and location as Google sent them.
|
|
324
|
+
- **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name.
|
|
328
325
|
- **Applies to:** cli-core and every CLI that stores or shows meetings or calendar entries. **Since:** cli-base 0.57.1
|
|
329
326
|
(output and agent answers: cli-base 0.65.0). **Rulings:** none (Wave 3 requirements 3.4).
|
|
330
327
|
|
|
331
|
-
### WC-SECR-24 · A run with moved folders
|
|
332
|
-
|
|
333
|
-
- **Why:**
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
-
|
|
337
|
-
- **
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
328
|
+
### WC-SECR-24 · A run with moved folders shares the owner's keyring entry and never deletes it
|
|
329
|
+
|
|
330
|
+
- **Why:** a per-folder keyring entry (`<app>:<config folder>`) left one copy of a sign-in for every test run, agent
|
|
331
|
+
check or worktree, about 1,500 of them in the owner's keyring, and nothing removed them. One entry per computer is
|
|
332
|
+
enough. But a moved run then reaches the owner's real sign-in: `HOME=/tmp/x gcal personal session end` deleted the
|
|
333
|
+
owner's Google sign-in, because only `<PREFIX>_*_DIR` counted as moved (a security review, 2026-10-30).
|
|
334
|
+
- **Enforced by:** `src/credentials.test.ts` · `src/paths.test.ts`.
|
|
335
|
+
- **Example:** a run is moved when `isMovedRun({ appName, prefix, env })` is true: a `<PREFIX>_CONFIG_DIR`,
|
|
336
|
+
`_STATE_DIR` or `_CACHE_DIR` override, or a `HOME` or XDG folder that puts the default folders anywhere but under
|
|
337
|
+
the account's own home. A CLI passes that as `isolated`. `Credentials` then reads and writes the app's own entry
|
|
338
|
+
(`<app>` / `oauth:<provider>:<profile>`), as a run with the default folders does. With the system keyring, its
|
|
339
|
+
`remove` keeps that entry, warns on stderr and leaves `"keyring"` out of what it returns; a moved `HOME` or XDG
|
|
340
|
+
folder in its `env` counts even when `isolated` is false. A CLI checks `isMovedRun` before it revokes a sign-in at
|
|
341
|
+
the provider, and says so when `session end` removed nothing. A test passes `memoryKeyring()`; a run that must not
|
|
342
|
+
see the keyring at all also runs with `DBUS_SESSION_BUS_ADDRESS` removed, since the keyring is reached over D-Bus.
|
|
343
|
+
- **Exceptions:** a write from a moved run, such as `session start` in a worktree, replaces the owner's entry.
|
|
344
|
+
- **Applies to:** every CLI, its tests and every agent that runs a CLI. **Since:** cli-base 0.65.4.
|
|
345
|
+
**Rulings:** owner, 2026-10-29 (sign-ins copied per work folder should have reused the existing entry); owner,
|
|
346
|
+
2026-10-30 (share the keyring entry, do not multiply it).
|
|
347
347
|
|
|
348
348
|
## Sources
|
|
349
349
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.65.
|
|
3
|
+
"version": "0.65.4",
|
|
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",
|