@wirecat/cli-core 0.449.0 → 0.451.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 +64 -0
- package/README.md +7 -5
- package/dist/cli/command-availability.d.ts +2 -1
- package/dist/cli/command-availability.js +8 -5
- package/dist/cli/config-command.d.ts +3 -3
- package/dist/cli/config-command.js +2 -2
- package/dist/cli/early-refusal.d.ts +13 -0
- package/dist/cli/early-refusal.js +44 -2
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/mcp-server-command.js +9 -5
- package/dist/cli/messenger/mcp-command.d.ts +4 -1
- package/dist/cli/messenger/mcp-command.js +108 -12
- package/dist/cli/permission-keys.d.ts +21 -4
- package/dist/cli/permission-keys.js +31 -1
- package/dist/cli/preview.d.ts +2 -1
- package/dist/cli/preview.js +8 -6
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +10 -3
- package/dist/cli/settings.d.ts +9 -2
- package/dist/cli/settings.js +21 -6
- package/dist/cli/shared-command-exports.d.ts +7 -5
- package/dist/embeddings/embed.js +4 -21
- package/dist/embeddings/onnx-runtime.d.ts +14 -1
- package/dist/embeddings/onnx-runtime.js +50 -2
- package/dist/http/public-url.d.ts +5 -1
- package/dist/http/public-url.js +11 -3
- package/dist/images/picture/embedder.js +9 -8
- package/dist/mcp/agent.d.ts +5 -0
- package/dist/mcp/agent.js +7 -4
- package/dist/mcp/all-profiles.d.ts +47 -0
- package/dist/mcp/all-profiles.js +257 -0
- package/dist/mcp/http/oauth.d.ts +2 -2
- package/dist/mcp/http/oauth.js +8 -7
- package/dist/mcp/http/serve.d.ts +6 -1
- package/dist/mcp/http/serve.js +24 -7
- package/dist/mcp/index.d.ts +2 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/instructions.d.ts +2 -1
- package/dist/mcp/instructions.js +35 -29
- package/dist/mcp/server.js +3 -73
- package/dist/mcp/stdio-wire.d.ts +14 -0
- package/dist/mcp/stdio-wire.js +72 -0
- package/dist/sends/index.d.ts +1 -1
- package/dist/sends/index.js +1 -1
- package/dist/sends/permissions.d.ts +10 -2
- package/dist/sends/permissions.js +15 -7
- package/dist/store/sqlite/note-search.js +66 -22
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,70 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.451.0 — 27.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Picture models run on the native ONNX Runtime too**, where text models do: on one machine CLIP embedded 12
|
|
15
|
+
pictures in 0.5 s against 1.1 s through WebAssembly, and text queries in 0.15 s against 0.26 s, with vectors at
|
|
16
|
+
cosine 0.99 or more to WebAssembly's, so pictures embedded before still match. `openSession` in
|
|
17
|
+
`src/embeddings/onnx-runtime.ts` is now the one place that picks the runtime and falls back per model.
|
|
18
|
+
- **Text models and rerankers run natively on Windows too** (x64 and arm64): `models download` fetches
|
|
19
|
+
`onnxruntime.dll` out of `onnxruntime-node`'s npm tarball (113.5 MB once, checked by sha256), because the Windows
|
|
20
|
+
binding needs its DirectML build, which Microsoft's release archive lacks. Needs `@wirecat/cli-core-onnx` 1.2.0.
|
|
21
|
+
A new native-runtime workflow downloads and runs the tiny test models natively on Linux x64 and arm64, macOS,
|
|
22
|
+
and Windows x64 and arm64, and checks that Alpine stays on WebAssembly.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Meaning search over documents and notes uses the vector index**, as message search already did: it scanned every
|
|
27
|
+
chunk of the store in pages (on one archive 600,000 chunks, mostly mail, for 23,000 document chunks), 7–18 s a
|
|
28
|
+
query; now about 0.05 s, with the same results, ties included. It still scans when the index is not ready or cannot
|
|
29
|
+
give an exact answer. `memo search all` gets most of the gain.
|
|
30
|
+
|
|
31
|
+
## 0.450.0 — 27.10.2026
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`<tool> mcp` with no profile serves every profile**, on stdio and with `--http`: one connector per tool in an AI
|
|
36
|
+
app instead of one per profile. Its tools are the ones `<tool> <profile> mcp` shows, each with a required `profile`
|
|
37
|
+
(every profile on this computer, no default), and the instructions tell the agent to ask when the owner did not name
|
|
38
|
+
the account. A call runs in that profile's own `<tool> <profile> mcp`, started as a child on first use and closed
|
|
39
|
+
after 10 idle minutes, so its permissions, keyring, store and audit log apply as before; `--permission` reaches
|
|
40
|
+
every child. `mcp --revoke`, `mcp config` and `mcp server start|stop|status` work without a profile too, with their
|
|
41
|
+
own login (`mcp-http/_all.json`). `<tool> <profile> mcp` is unchanged. drive and zoom wire the same module from
|
|
42
|
+
`@wirecat/cli-core/mcp`: `serveAllProfilesOverStdio`, `serveAllProfilesOverHttp`.
|
|
43
|
+
- **`mcp --http --public-url` takes a path**, `https://<name>.ts.net/tg`, so tools share one tunnel address with a
|
|
44
|
+
path each. The issuer, the login routes and the connector address (`…/tg/mcp`) carry the path; the issuer's
|
|
45
|
+
metadata is at `/.well-known/oauth-authorization-server/tg` and the resource's at
|
|
46
|
+
`/.well-known/oauth-protected-resource/tg/mcp`, where RFC 8414 and RFC 9728 put them, so the tunnel routes those
|
|
47
|
+
two paths to the server as well as `/tg` (with Tailscale: `--set-path <path> http://127.0.0.1:<port><path>` for
|
|
48
|
+
each). Without a path nothing changes.
|
|
49
|
+
|
|
50
|
+
- **A CLI that is not a messenger can use the shared `permissions`, keyed by its own command paths.** It passes
|
|
51
|
+
`settingsFor(app, { permissions: commandPermissions({ extra, defaults }) })` and hands that `configuration` to
|
|
52
|
+
`run`. Then `config set permissions.<key>` and an MCP server's `--permission` take its command paths, the groups
|
|
53
|
+
above them and its `extra` keys, such as `api.calendar.events.insert`, and refuse every other key. Before, they took
|
|
54
|
+
only a messenger's resources. Each command is refused before it runs when its key is `deny`, when it writes under
|
|
55
|
+
`readonly`, and when it writes to the service under the read-only lock. `ask` stays with the write guard
|
|
56
|
+
(`permissionDefaults`: `configuration.permissions.defaults`). MCP finds and refuses the command by the same key
|
|
57
|
+
when the server passes `keyOf: configuration.permissions.keyOf`. Messenger CLIs are unchanged. A CLI with a program
|
|
58
|
+
of its own, such as drive, uses the parts: `permissionOverrides(entries, COMMAND_PATH_KEY)`,
|
|
59
|
+
`assertOverridesTighten(overrides, saved, defaults)` from `./sends` and `knownPermissionKeys(program, permissions)`.
|
|
60
|
+
- **MCP judges a declared CLI's command as the check before a run does.** A server passes
|
|
61
|
+
`permissions: configuration.permissions` to `toolsFromCommands` and to the agent context, in place of `keyOf`. Then
|
|
62
|
+
a write to the service is unavailable under the read-only lock (`readOnly`), and the CLI's own `deny` or `readonly`
|
|
63
|
+
defaults make a command unavailable and refuse its call. Before, MCP offered both.
|
|
64
|
+
|
|
65
|
+
### Changed — may break callers
|
|
66
|
+
|
|
67
|
+
- **cli-core needs `@wirecat/cli-base` 0.63.** The peer range moves from `>=0.62.0 <0.63.0` to `>=0.63.0 <0.64.0`. A
|
|
68
|
+
CLI that pins cli-base moves its pin with this release. cli-base 0.63 adds the shared Google Discovery reader
|
|
69
|
+
(`adaptDiscovery`, `syncDiscovery`, `readDiscovery` in `@wirecat/cli-base/codegen`) that drive and gcal generate their
|
|
70
|
+
API code with; nothing cli-core uses changed.
|
|
71
|
+
|
|
8
72
|
## 0.449.0 — 26.10.2026
|
|
9
73
|
|
|
10
74
|
### Changed — may break callers
|
package/README.md
CHANGED
|
@@ -131,8 +131,8 @@ code and its document must agree. A search plan that reads an index once per mes
|
|
|
131
131
|
- **Presets for what people actually look for.** `preset:password`, `api-key`, `card`, `phone`, `email`, `url` and
|
|
132
132
|
seven more find candidates, labelled as candidates.
|
|
133
133
|
- **Search by meaning, opt in.** Conversations are split into chunks and embedded locally by ONNX Runtime — native
|
|
134
|
-
on Linux (glibc)
|
|
135
|
-
Bun alike — e5-small by default, or any OpenAI-compatible API with your own key, after
|
|
134
|
+
on Linux (glibc), Apple-silicon Macs and Windows once `models download` has fetched it, WebAssembly elsewhere, on
|
|
135
|
+
Node and Bun alike — e5-small by default, or any OpenAI-compatible API with your own key, after
|
|
136
136
|
you see the cost. Results by meaning and by words are merged by reciprocal rank fusion.
|
|
137
137
|
- **The archive and the server together.** `--backend both` also asks the messenger's own search; its hits are saved
|
|
138
138
|
and filtered by the same query, so nothing is counted twice. `--thread` brings the reply thread around a hit.
|
|
@@ -226,9 +226,11 @@ they deliberately differ. A rule changes in its own pull request, before the cod
|
|
|
226
226
|
over an in-memory fake or a real client.
|
|
227
227
|
- **Checked across tools and runtimes.** CI runs on Linux: the test suite on Node 24, smoke checks on Bun and on
|
|
228
228
|
Node 22.16, a parity workflow that builds the Telegram, MAX and WhatsApp CLIs against this repository, and
|
|
229
|
-
gitleaks over the full history. macOS and Windows run only
|
|
230
|
-
the ONNX runtime probe (macOS and Windows) run monthly and when started by hand; the
|
|
231
|
-
|
|
229
|
+
gitleaks over the full history. macOS and Windows run only four workflows: the SQLite library build (macOS) and
|
|
230
|
+
the ONNX runtime probe (macOS and Windows) run monthly and when started by hand; the native-runtime check (Linux,
|
|
231
|
+
macOS and Windows, x64 and arm64, and Alpine) runs monthly and on every pull request that changes how models
|
|
232
|
+
load; the scheduler test (macOS and Windows) runs only when started by hand. The rest of the suite does not run on
|
|
233
|
+
macOS or Windows.
|
|
232
234
|
- **One shared toolchain.** Biome, markdownlint, CSpell and Lefthook configs come from the
|
|
233
235
|
[WireCat standards](https://github.com/WireCatLabs/community/blob/main/standards/README.md), and CI fails when a
|
|
234
236
|
copy drifts.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CommandInfo } from "@wirecat/cli-base/commands";
|
|
2
2
|
import { type Level, type PermissionKey } from "../sends/permissions.js";
|
|
3
|
+
import type { CommandPermissions } from "./permission-keys.js";
|
|
3
4
|
export type AvailabilityCommand = Pick<CommandInfo, "path" | "effects" | "lifetime" | "reaches">;
|
|
4
5
|
export interface DiscoveredCommand extends CommandInfo {
|
|
5
6
|
availability?: CommandAvailability;
|
|
@@ -11,4 +12,4 @@ export interface CommandAvailability {
|
|
|
11
12
|
permission?: string;
|
|
12
13
|
}
|
|
13
14
|
/** Discovery and execution use the same permission decision. */
|
|
14
|
-
export declare const commandAvailability: (info: AvailabilityCommand, levels: Readonly<Record<PermissionKey, Level>>, keyOf?: (path: readonly string[]) => PermissionKey | null | undefined, authority?: (path: readonly string[]) => boolean, readOnly?: boolean, mcp?: boolean) => CommandAvailability;
|
|
15
|
+
export declare const commandAvailability: (info: AvailabilityCommand, levels: Readonly<Record<PermissionKey, Level>>, keyOf?: (path: readonly string[]) => PermissionKey | null | undefined, authority?: (path: readonly string[]) => boolean, readOnly?: boolean, mcp?: boolean, declared?: CommandPermissions) => CommandAvailability;
|
|
@@ -2,24 +2,27 @@ import { writes } from "@wirecat/cli-base/standards";
|
|
|
2
2
|
import { AUTHORITY_KEY, authorityAllowed, BOT_API_KEY, botApiAllowed, isBotApi, keyForCommand, levelFor, readKeysForCommand, } from "../sends/permissions.js";
|
|
3
3
|
import { changesMessenger } from "./effects.js";
|
|
4
4
|
/** Discovery and execution use the same permission decision. */
|
|
5
|
-
export const commandAvailability = (info, levels, keyOf, authority, readOnly = false, mcp = true) => {
|
|
5
|
+
export const commandAvailability = (info, levels, keyOf, authority, readOnly = false, mcp = true, declared) => {
|
|
6
6
|
const blocked = (reason, permission) => ({
|
|
7
7
|
available: false,
|
|
8
8
|
reason,
|
|
9
9
|
...(permission ? { permission } : {}),
|
|
10
10
|
});
|
|
11
|
-
|
|
11
|
+
const declaredKey = declared?.keyOf(info.path);
|
|
12
|
+
// As `refuseByCommandPermissions` judges it before the run: a keyed write to the service.
|
|
13
|
+
const declaredChange = Boolean(declaredKey) && info.effects?.remote === "write";
|
|
14
|
+
if (readOnly && (changesMessenger(info) || declaredChange || authority?.(info.path)))
|
|
12
15
|
return blocked("the read-only lock blocks this change");
|
|
13
16
|
if (mcp && authority?.(info.path) && !authorityAllowed(levels))
|
|
14
17
|
return blocked(`the profile keeps ${AUTHORITY_KEY} off`, AUTHORITY_KEY);
|
|
15
18
|
if (mcp && isBotApi(info.path) && !botApiAllowed(levels))
|
|
16
19
|
return blocked(`the profile requires ${BOT_API_KEY}: allow`, BOT_API_KEY);
|
|
17
|
-
const key = keyOf?.(info.path) ?? keyForCommand(info.path);
|
|
18
|
-
const level = key ? levelFor(levels, key).level : "allow";
|
|
20
|
+
const key = declared ? declaredKey : (keyOf?.(info.path) ?? keyForCommand(info.path));
|
|
21
|
+
const level = key ? levelFor(levels, key, declared?.defaults).level : "allow";
|
|
19
22
|
if (level === "deny")
|
|
20
23
|
return blocked(`the profile denies ${key}`, key ?? undefined);
|
|
21
24
|
if (level === "readonly" && writes(info.effects))
|
|
22
25
|
return blocked(`${key} is readonly in this profile`, key ?? undefined);
|
|
23
|
-
const denied = readKeysForCommand(info.path).find((read) => levelFor(levels, read).level === "deny");
|
|
26
|
+
const denied = (declared ? [] : readKeysForCommand(info.path)).find((read) => levelFor(levels, read).level === "deny");
|
|
24
27
|
return denied ? blocked(`the profile denies ${denied}`, denied) : { available: true };
|
|
25
28
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type AppIdentity } from "../ports/app.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type PermissionKeySource } from "./permission-keys.js";
|
|
4
4
|
import { type Configuration } from "./settings.js";
|
|
5
5
|
export declare const configCommand: (app: AppIdentity, config: Configuration, { permissionKey }?: {
|
|
6
|
-
permissionKey?:
|
|
6
|
+
permissionKey?: PermissionKeySource | undefined;
|
|
7
7
|
}) => Command;
|
|
8
8
|
/**
|
|
9
9
|
* A key no command or write is checked against would be saved and do nothing (BUG-137) — whether it
|
|
10
10
|
* comes as `permissions.<key>` or inside a whole `permissions` object.
|
|
11
11
|
*/
|
|
12
|
-
export declare const refuseUnknownKey: (command: Command, setting: string, value: string, permissionKey?:
|
|
12
|
+
export declare const refuseUnknownKey: (command: Command, setting: string, value: string, permissionKey?: PermissionKeySource) => void;
|
|
@@ -6,7 +6,7 @@ import { AUTHORITY_KEY, BOT_API_KEY } from "../sends/permissions.js";
|
|
|
6
6
|
import { baseContext, environmentOf } from "./context.js";
|
|
7
7
|
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
|
|
8
8
|
import { itemOf, renderAnswer } from "./envelope.js";
|
|
9
|
-
import { knownBeside, knownPermissionKeys, unknownPermissionIssues } from "./permission-keys.js";
|
|
9
|
+
import { knownBeside, knownPermissionKeys, unknownPermissionIssues, } from "./permission-keys.js";
|
|
10
10
|
import { fromFile, NONE_NAMED } from "./settings.js";
|
|
11
11
|
import { nextStep } from "./steps.js";
|
|
12
12
|
import { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } from "./store-settings.js";
|
|
@@ -16,7 +16,7 @@ import { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } fro
|
|
|
16
16
|
*/
|
|
17
17
|
/** `sender_colors` colours who wrote a message; a CLI without messages has nothing to colour. */
|
|
18
18
|
const shownSettings = (app, config) => app.messenger === false ? config.allSettings.filter((setting) => setting !== "sender_colors") : config.allSettings;
|
|
19
|
-
export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
19
|
+
export const configCommand = (app, config, { permissionKey = config.permissions } = {}) => {
|
|
20
20
|
const command = new Command("config").description("the settings in force, and where each one came from");
|
|
21
21
|
annotate(command.command("show"), { effects: LOCAL_READ, examples: [`${app.command} personal config show`] })
|
|
22
22
|
.description("the profile, the profiles that exist, and each setting with where it came from")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { type AppIdentity } from "../ports/app.js";
|
|
3
|
+
import { type CommandPermissions } from "./permission-keys.js";
|
|
3
4
|
type Resolve = (flags: object, options: {
|
|
4
5
|
env: NodeJS.ProcessEnv;
|
|
5
6
|
needsProfile: false;
|
|
@@ -17,4 +18,16 @@ export declare const refuseWriteBeforeConnecting: (action: Command, { app, env,
|
|
|
17
18
|
dryRun: boolean;
|
|
18
19
|
resolve?: Resolve;
|
|
19
20
|
}) => void;
|
|
21
|
+
/**
|
|
22
|
+
* A command of a CLI that declares its own keys (`commandPermissions`), refused by its key before anything connects:
|
|
23
|
+
* `deny` stops it, `readonly` stops a write, and the read-only lock stops a write to the service. `ask` is left to
|
|
24
|
+
* the write guard, which can put the question. A saved or `--permission` key that names no command stops every keyed
|
|
25
|
+
* command; housekeeping still runs, so `config unset` can remove it.
|
|
26
|
+
*/
|
|
27
|
+
export declare const refuseByCommandPermissions: (action: Command, permissions: CommandPermissions, { app, env, dryRun, resolve }: {
|
|
28
|
+
app: AppIdentity;
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
dryRun: boolean;
|
|
31
|
+
resolve: Resolve;
|
|
32
|
+
}) => void;
|
|
20
33
|
export {};
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { metaOf } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { writes } from "@wirecat/cli-base/standards";
|
|
1
4
|
import { MESSENGER_CHANGE_REFUSAL, readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
|
|
2
|
-
import { levelRefusal } from "../sends/guard.js";
|
|
5
|
+
import { ceilingRefusal, levelRefusal } from "../sends/guard.js";
|
|
3
6
|
import { SendJournal, sendsPathFor } from "../sends/journal.js";
|
|
4
|
-
import { keyForCommand, levelFor, writeOfKey } from "../sends/permissions.js";
|
|
7
|
+
import { CEILING_KEY, keyForCommand, levelFor, writeOfKey } from "../sends/permissions.js";
|
|
5
8
|
import { newOperationId } from "../sends/send-id.js";
|
|
6
9
|
import { commandPathOf } from "./command-contract.js";
|
|
10
|
+
import { environmentOf } from "./context.js";
|
|
7
11
|
import { changesMessenger } from "./effects.js";
|
|
12
|
+
import { refuseUnknownPermissionKeys } from "./permission-keys.js";
|
|
8
13
|
import { NONE_NAMED, settingsFor } from "./settings.js";
|
|
9
14
|
/**
|
|
10
15
|
* Refuses a change on the messenger the read-only lock or the profile's level stops, before anything connects
|
|
@@ -70,3 +75,40 @@ export const refuseWriteBeforeConnecting = (action, { app, env, dryRun, resolve
|
|
|
70
75
|
}
|
|
71
76
|
throw refusal;
|
|
72
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* A command of a CLI that declares its own keys (`commandPermissions`), refused by its key before anything connects:
|
|
80
|
+
* `deny` stops it, `readonly` stops a write, and the read-only lock stops a write to the service. `ask` is left to
|
|
81
|
+
* the write guard, which can put the question. A saved or `--permission` key that names no command stops every keyed
|
|
82
|
+
* command; housekeeping still runs, so `config unset` can remove it.
|
|
83
|
+
*/
|
|
84
|
+
export const refuseByCommandPermissions = (action, permissions, { app, env, dryRun, resolve }) => {
|
|
85
|
+
const key = permissions.keyOf(commandPathOf(action));
|
|
86
|
+
if (!key)
|
|
87
|
+
return;
|
|
88
|
+
const { effects } = metaOf(action);
|
|
89
|
+
const lock = readOnlyLockOf(app, env);
|
|
90
|
+
if (lock && effects?.remote === "write")
|
|
91
|
+
throw readOnlyRefusal(lock, `run ${commandPathOf(action).join(" ")}`);
|
|
92
|
+
const flags = action.optsWithGlobals();
|
|
93
|
+
const overrides = environmentOf(action).agent?.overrides ?? [];
|
|
94
|
+
const settings = resolve(overrides.length > 0 ? { ...flags, permission: [...overrides, ...(flags.permission ?? [])] } : flags, { env, needsProfile: false, readOnly: true });
|
|
95
|
+
refuseUnknownPermissionKeys(action, settings, app.command, permissions);
|
|
96
|
+
if (dryRun || settings.sources?.profile === NONE_NAMED)
|
|
97
|
+
return;
|
|
98
|
+
const { level, key: named } = levelFor(settings.permissions, key, permissions.defaults);
|
|
99
|
+
if (level === "deny") {
|
|
100
|
+
if (named === CEILING_KEY)
|
|
101
|
+
throw ceilingRefusal(key, app.command);
|
|
102
|
+
throw new CliError("permission_error", `profile ${settings.profile} denies ${key} (permissions.${named} is deny, from the ` +
|
|
103
|
+
`${settings.permissionSources[named ?? ""] ?? "default"})`, { permission: key });
|
|
104
|
+
}
|
|
105
|
+
if (level === "readonly" && effects !== undefined && writes(effects))
|
|
106
|
+
throw levelRefusal({
|
|
107
|
+
profile: settings.profile,
|
|
108
|
+
command: app.command,
|
|
109
|
+
key,
|
|
110
|
+
level,
|
|
111
|
+
named,
|
|
112
|
+
permissionSources: settings.permissionSources,
|
|
113
|
+
});
|
|
114
|
+
};
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
|
103
103
|
export { type Saving, stored } from "./messenger/stored.js";
|
|
104
104
|
export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
|
|
105
105
|
export { window, withPaging } from "./paging.js";
|
|
106
|
-
export { knownBeside, knownPermissionKeys, type PermissionKeyOf, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
106
|
+
export { type CommandPermissions, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
107
107
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
108
108
|
export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
|
|
109
109
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -94,7 +94,7 @@ export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
|
94
94
|
export { stored } from "./messenger/stored.js";
|
|
95
95
|
export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
|
|
96
96
|
export { window, withPaging } from "./paging.js";
|
|
97
|
-
export { knownBeside, knownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
97
|
+
export { commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
98
98
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
99
99
|
export { createProgram, run } from "./program.js";
|
|
100
100
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
@@ -4,7 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
import { alive, carries } from "../background/processes.js";
|
|
5
5
|
import { thisMachine } from "../background/system.js";
|
|
6
6
|
import { readableLog, tail } from "../background/units.js";
|
|
7
|
-
import { portOf, publicUrlOf } from "../http/public-url.js";
|
|
7
|
+
import { mcpUrlOf, portOf, publicUrlOf } from "../http/public-url.js";
|
|
8
8
|
import { httpLockFile, httpLogFile, mcpHttpMarker, readHttpLock, signedInClients, } from "../mcp/http/state.js";
|
|
9
9
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
|
|
10
10
|
export const MCP_HTTP_DEFAULT_PORT = 8765;
|
|
@@ -14,7 +14,7 @@ const clock = (iso) => {
|
|
|
14
14
|
const time = when.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
15
15
|
return when.toDateString() === new Date().toDateString() ? time : `${when.toLocaleDateString("sv-SE")} ${time}`;
|
|
16
16
|
};
|
|
17
|
-
const connectorUrl = (lock) => new URL(
|
|
17
|
+
const connectorUrl = (lock) => mcpUrlOf(new URL(lock.publicUrl)).href;
|
|
18
18
|
const liveCode = (lock, now) => lock.loginCode && lock.loginCodeExpiresAt && Date.parse(lock.loginCodeExpiresAt) > now
|
|
19
19
|
? { code: lock.loginCode, expires_at: lock.loginCodeExpiresAt }
|
|
20
20
|
: null;
|
|
@@ -58,13 +58,17 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
|
|
|
58
58
|
.summary("start the HTTP MCP server in the background")
|
|
59
59
|
.description("start `mcp --http` apart from this shell and answer once it listens, with its login code")
|
|
60
60
|
.option("--port <port>", `the local port (default ${defaultPort})`)
|
|
61
|
-
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net")
|
|
61
|
+
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg")
|
|
62
62
|
.option("--permission <key=level>", "make a permission stricter for this server only; repeat for more keys", (value, previous = []) => [...previous, value])
|
|
63
63
|
.action(async function () {
|
|
64
64
|
// A CLI whose `mcp` takes the same flags reads them first, so they are looked up on both.
|
|
65
65
|
const flags = this.optsWithGlobals();
|
|
66
66
|
const one = target(this);
|
|
67
|
-
const url = publicUrlOf(flags.publicUrl, {
|
|
67
|
+
const url = publicUrlOf(flags.publicUrl, {
|
|
68
|
+
missing: "mcp server start",
|
|
69
|
+
reaches: "the browser apps reach it",
|
|
70
|
+
path: true,
|
|
71
|
+
});
|
|
68
72
|
const port = portOf(flags.port, defaultPort);
|
|
69
73
|
const lockFile = httpLockFile(one.tokenFile);
|
|
70
74
|
const before = readHttpLock(lockFile);
|
|
@@ -82,7 +86,7 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
|
|
|
82
86
|
"--port",
|
|
83
87
|
String(port),
|
|
84
88
|
"--public-url",
|
|
85
|
-
url.origin,
|
|
89
|
+
url.pathname === "/" ? url.origin : url.href,
|
|
86
90
|
...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
|
|
87
91
|
], { ...inherited, ...one.childEnv, [mcpHttpMarker(one.envPrefix)]: one.profile ?? "default" }, log);
|
|
88
92
|
for (let look = 0; look < LOOKS; look++) {
|
|
@@ -19,6 +19,8 @@ export interface McpFlags {
|
|
|
19
19
|
publicUrl?: string;
|
|
20
20
|
revoke?: boolean;
|
|
21
21
|
}
|
|
22
|
+
/** The token file of `<tool> mcp --http` for every profile: no profile name can start with `_`. */
|
|
23
|
+
export declare const ALL_PROFILES = "_all";
|
|
22
24
|
/** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
23
25
|
export declare const httpTokenFile: (app: AppIdentity, profile: string, env: NodeJS.ProcessEnv) => string;
|
|
24
26
|
export declare const mcpCommand: (messenger: Messenger) => Command;
|
|
@@ -42,7 +44,8 @@ export declare const redactTail: (text: string, home: string | undefined) => str
|
|
|
42
44
|
* servers with a trimmed environment leaves it out (measured 2026-09-28). Credentials are never copied.
|
|
43
45
|
*/
|
|
44
46
|
export declare const serverEntry: (app: AppIdentity, { profile, flags, execPath, scriptPath, env, }: {
|
|
45
|
-
profile
|
|
47
|
+
/** Left out, the entry is `<tool> mcp`, the server of every profile. */
|
|
48
|
+
profile?: string;
|
|
46
49
|
flags?: McpFlags;
|
|
47
50
|
execPath: string;
|
|
48
51
|
scriptPath: string;
|
|
@@ -6,7 +6,7 @@ import { annotate } from "@wirecat/cli-base/commands";
|
|
|
6
6
|
import { installerOf } from "@wirecat/cli-base/update";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { ATTACHMENT_LIMIT_ENV } from "../../attachments/limits.js";
|
|
9
|
-
import { portOf, publicUrlOf } from "../../http/public-url.js";
|
|
9
|
+
import { mcpUrlOf, portOf, publicUrlOf } from "../../http/public-url.js";
|
|
10
10
|
import { httpLockFile } from "../../mcp/http/state.js";
|
|
11
11
|
import { envName } from "../../ports/app.js";
|
|
12
12
|
import { environmentOf } from "../context.js";
|
|
@@ -14,10 +14,13 @@ import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effec
|
|
|
14
14
|
import { itemOf, renderAnswer } from "../envelope.js";
|
|
15
15
|
import { MCP_HTTP_DEFAULT_PORT, mcpServerCommand } from "../mcp-server-command.js";
|
|
16
16
|
import { rootOf } from "../profile.js";
|
|
17
|
+
import { namedProfile } from "../settings.js";
|
|
17
18
|
import { nextStep } from "../steps.js";
|
|
18
19
|
import { recalledAccount } from "./accounts.js";
|
|
19
20
|
import { messengerContext } from "./context.js";
|
|
20
21
|
const DEFAULT_PORT = MCP_HTTP_DEFAULT_PORT;
|
|
22
|
+
/** The token file of `<tool> mcp --http` for every profile: no profile name can start with `_`. */
|
|
23
|
+
export const ALL_PROFILES = "_all";
|
|
21
24
|
/** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
22
25
|
export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "mcp-http", `${profile}.json`);
|
|
23
26
|
const withFlags = (command) => command.option("--permission <key=level>", "make a permission stricter for this server only, like messages.send=deny; it never loosens the profile; repeat for more keys", (value, previous = []) => [...previous, value]);
|
|
@@ -25,14 +28,16 @@ export const mcpCommand = (messenger) => {
|
|
|
25
28
|
const { app } = messenger;
|
|
26
29
|
const command = withFlags(annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false })
|
|
27
30
|
.summary("serve this profile's commands to an AI agent")
|
|
28
|
-
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp
|
|
31
|
+
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp\`; with no profile, \`${app.command} mcp\` serves every profile and each tool asks which`))
|
|
29
32
|
.option("--http", "serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide")
|
|
30
33
|
.option("--port <port>", `the local port for --http (default ${DEFAULT_PORT})`)
|
|
31
|
-
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net")
|
|
34
|
+
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg")
|
|
32
35
|
.option("--revoke", "forget every login given to a browser app; each must log in again");
|
|
33
36
|
command.action(async function () {
|
|
34
37
|
const flags = this.optsWithGlobals();
|
|
35
|
-
const context = messengerContext(this, messenger);
|
|
38
|
+
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
39
|
+
if (namedProfile(context.settings) === undefined)
|
|
40
|
+
return serveEveryProfile(this, context, messenger, flags);
|
|
36
41
|
if (flags.revoke) {
|
|
37
42
|
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
38
43
|
revokeAll(httpTokenFile(app, context.settings.profile, context.env));
|
|
@@ -43,6 +48,7 @@ export const mcpCommand = (messenger) => {
|
|
|
43
48
|
const publicUrl = publicUrlOf(flags.publicUrl, {
|
|
44
49
|
missing: "--http",
|
|
45
50
|
reaches: `the browser apps reach ${app.command}`,
|
|
51
|
+
path: true,
|
|
46
52
|
});
|
|
47
53
|
const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
|
|
48
54
|
const tokenFile = httpTokenFile(app, context.settings.profile, context.env);
|
|
@@ -62,7 +68,16 @@ export const mcpCommand = (messenger) => {
|
|
|
62
68
|
command.addCommand(mcpServerCommand({
|
|
63
69
|
example: `${app.command} personal`,
|
|
64
70
|
target: (self) => {
|
|
65
|
-
const { settings, env } = messengerContext(self, messenger);
|
|
71
|
+
const { settings, env } = messengerContext(self, messenger, { needsProfile: false });
|
|
72
|
+
if (namedProfile(settings) === undefined)
|
|
73
|
+
return {
|
|
74
|
+
invocation: app.command,
|
|
75
|
+
envPrefix: app.envPrefix,
|
|
76
|
+
env,
|
|
77
|
+
tokenFile: httpTokenFile(app, ALL_PROFILES, env),
|
|
78
|
+
argv: ["mcp", "--http"],
|
|
79
|
+
version: app.version,
|
|
80
|
+
};
|
|
66
81
|
return {
|
|
67
82
|
invocation: `${app.command} ${settings.profile}`,
|
|
68
83
|
profile: settings.profile,
|
|
@@ -74,21 +89,22 @@ export const mcpCommand = (messenger) => {
|
|
|
74
89
|
version: app.version,
|
|
75
90
|
};
|
|
76
91
|
},
|
|
77
|
-
answer: (self, item, lines) => renderAnswer(self, messengerContext(self, messenger), itemOf(item), () => lines.join("\n")),
|
|
92
|
+
answer: (self, item, lines) => renderAnswer(self, messengerContext(self, messenger, { needsProfile: false }), itemOf(item), () => lines.join("\n")),
|
|
78
93
|
system: (self) => environmentOf(self).system,
|
|
79
94
|
}));
|
|
80
95
|
command.addCommand(withFlags(annotate(new Command("config"), {
|
|
81
96
|
effects: LOCAL_READ,
|
|
82
97
|
examples: [`${app.command} personal mcp config`],
|
|
83
98
|
})
|
|
84
|
-
.summary("print this profile
|
|
99
|
+
.summary("print the local MCP configuration, of this profile or of every profile")
|
|
85
100
|
.description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")).action(async function () {
|
|
86
101
|
const flags = this.optsWithGlobals();
|
|
87
|
-
const context = messengerContext(this, messenger);
|
|
102
|
+
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
88
103
|
const { settings, env } = context;
|
|
89
104
|
const given = environmentOf(this).mcp ?? {};
|
|
105
|
+
const named = namedProfile(settings);
|
|
90
106
|
const entry = serverEntry(app, {
|
|
91
|
-
profile:
|
|
107
|
+
...(named === undefined ? {} : { profile: named }),
|
|
92
108
|
flags,
|
|
93
109
|
execPath: given.execPath ?? process.execPath,
|
|
94
110
|
scriptPath: given.scriptPath ?? realpathSync(process.argv[1] ?? ""),
|
|
@@ -177,6 +193,81 @@ export const mcpCommand = (messenger) => {
|
|
|
177
193
|
}));
|
|
178
194
|
return command;
|
|
179
195
|
};
|
|
196
|
+
/**
|
|
197
|
+
* `<tool> mcp` with no profile: every profile on this computer, each call in that profile's own `<tool> <profile> mcp`
|
|
198
|
+
* (`src/mcp/all-profiles.ts`). Nothing picks a profile: each tool takes one, and the agent is told to ask.
|
|
199
|
+
*/
|
|
200
|
+
const serveEveryProfile = async (self, context, messenger, flags) => {
|
|
201
|
+
const { app } = messenger;
|
|
202
|
+
const { env } = context;
|
|
203
|
+
const tokenFile = httpTokenFile(app, ALL_PROFILES, env);
|
|
204
|
+
if (flags.revoke) {
|
|
205
|
+
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
206
|
+
revokeAll(tokenFile);
|
|
207
|
+
renderAnswer(self, context, itemOf({ revoked: true, profile: null }));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const profiles = () => messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true }).profilesHere;
|
|
211
|
+
if (profiles().length === 0)
|
|
212
|
+
throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp\` again`);
|
|
213
|
+
const given = environmentOf(self).mcp ?? {};
|
|
214
|
+
const execPath = given.execPath ?? process.execPath;
|
|
215
|
+
const scriptPath = given.scriptPath ?? realpathSync(process.argv[1] ?? "");
|
|
216
|
+
const tightened = (flags.permission ?? []).flatMap((entry) => ["--permission", entry]);
|
|
217
|
+
const { instructions } = await import("../../mcp/instructions.js");
|
|
218
|
+
const options = {
|
|
219
|
+
command: app.command,
|
|
220
|
+
version: app.version,
|
|
221
|
+
name: messenger.name ?? app.command,
|
|
222
|
+
profiles,
|
|
223
|
+
child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", ...tightened], env }),
|
|
224
|
+
instructions: instructions({
|
|
225
|
+
command: app.command,
|
|
226
|
+
name: messenger.name ?? app.command,
|
|
227
|
+
profile: profiles(),
|
|
228
|
+
writes: [],
|
|
229
|
+
}),
|
|
230
|
+
note: (line) => context.renderer.note(line),
|
|
231
|
+
};
|
|
232
|
+
if (flags.http) {
|
|
233
|
+
const publicUrl = publicUrlOf(flags.publicUrl, {
|
|
234
|
+
missing: "--http",
|
|
235
|
+
reaches: `the browser apps reach ${app.command}`,
|
|
236
|
+
path: true,
|
|
237
|
+
});
|
|
238
|
+
const { serveAllProfilesOverHttp } = await import("../../mcp/all-profiles.js");
|
|
239
|
+
const { mcpHttpMarker } = await import("../../mcp/http/state.js");
|
|
240
|
+
const port = portOf(flags.port, DEFAULT_PORT);
|
|
241
|
+
const listening = await serveAllProfilesOverHttp(options, {
|
|
242
|
+
publicUrl,
|
|
243
|
+
port,
|
|
244
|
+
tokenFile,
|
|
245
|
+
lockFile: httpLockFile(tokenFile),
|
|
246
|
+
version: app.version,
|
|
247
|
+
onCode: (code, expires) => context.renderer.note(env[mcpHttpMarker(app.envPrefix)] === undefined
|
|
248
|
+
? `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`
|
|
249
|
+
: `a new login code for a browser app: \`${app.command} mcp server status\``),
|
|
250
|
+
}).catch((error) => {
|
|
251
|
+
if (error.code === "EADDRINUSE")
|
|
252
|
+
throw new CliError("configuration_error", `port ${port} is in use — pass another with --port`);
|
|
253
|
+
throw error;
|
|
254
|
+
});
|
|
255
|
+
context.renderer.note(`serving every profile on ${listening.url.href} — point your tunnel at it; connectors use ${mcpUrlOf(publicUrl).href}`);
|
|
256
|
+
await new Promise((resolve) => {
|
|
257
|
+
process.once("SIGINT", resolve);
|
|
258
|
+
process.once("SIGTERM", resolve);
|
|
259
|
+
});
|
|
260
|
+
await listening.close();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const { serveAllProfilesOverStdio } = await import("../../mcp/all-profiles.js");
|
|
264
|
+
const { stdin, stdout, signal } = environmentOf(self);
|
|
265
|
+
await serveAllProfilesOverStdio(options, {
|
|
266
|
+
...(stdin ? { stdin: stdin } : {}),
|
|
267
|
+
...(stdout ? { stdout: stdout } : {}),
|
|
268
|
+
...(signal ? { signal } : {}),
|
|
269
|
+
});
|
|
270
|
+
};
|
|
180
271
|
const TAIL_LINES = 20;
|
|
181
272
|
const TAIL_BYTES = 2_000;
|
|
182
273
|
const TAIL_WAIT_MS = 500;
|
|
@@ -230,7 +321,7 @@ const VERSION_MANAGER = /[\\/](\.nvm|nvm|\.fnm|fnm|fnm_multishells|\.volta|volta
|
|
|
230
321
|
export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, env, }) => {
|
|
231
322
|
if (installerOf(scriptPath) === "npx") {
|
|
232
323
|
throw new CliError("validation_error", `this ${app.command} runs from npx's cache, which gets cleared, and the path would stop working — ` +
|
|
233
|
-
`install it globally, then run \`${app.command} ${profile} mcp config\` again`);
|
|
324
|
+
`install it globally, then run \`${app.command} ${profile === undefined ? "" : `${profile} `}mcp config\` again`);
|
|
234
325
|
}
|
|
235
326
|
const names = [
|
|
236
327
|
...["CONFIG_DIR", "STATE_DIR", "CACHE_DIR"].map((name) => envName(app, name)),
|
|
@@ -242,11 +333,16 @@ export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, en
|
|
|
242
333
|
const server = {
|
|
243
334
|
type: "stdio",
|
|
244
335
|
command: execPath,
|
|
245
|
-
args: [
|
|
336
|
+
args: [
|
|
337
|
+
scriptPath,
|
|
338
|
+
...(profile === undefined ? [] : [profile]),
|
|
339
|
+
"mcp",
|
|
340
|
+
...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
|
|
341
|
+
],
|
|
246
342
|
...(Object.keys(directories).length > 0 ? { env: directories } : {}),
|
|
247
343
|
};
|
|
248
344
|
return {
|
|
249
|
-
config: { mcpServers: { [`${app.command}-${profile}`]: server } },
|
|
345
|
+
config: { mcpServers: { [profile === undefined ? app.command : `${app.command}-${profile}`]: server } },
|
|
250
346
|
/** The path belongs to one Node version of a version manager; switching Node breaks the entry. */
|
|
251
347
|
nodeBound: VERSION_MANAGER.test(execPath),
|
|
252
348
|
};
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { type PermissionKey } from "../sends/permissions.js";
|
|
2
|
+
import { type Level, type PermissionKey } from "../sends/permissions.js";
|
|
3
3
|
import { type ConfigIssue } from "./config-issues.js";
|
|
4
4
|
import type { Settings } from "./settings.js";
|
|
5
5
|
export type PermissionKeyOf = (command: Command) => PermissionKey | null | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* A CLI that is not a messenger names its own keys: each command is checked against its own path, and the
|
|
8
|
+
* messenger's resources and write keys mean nothing to it (drive's model). `settingsFor(app, { permissions })` takes it.
|
|
9
|
+
*/
|
|
10
|
+
export interface CommandPermissions {
|
|
11
|
+
/** The key a command path is checked against; `null` for housekeeping, which no level stops. */
|
|
12
|
+
keyOf: (path: readonly string[]) => PermissionKey | null;
|
|
13
|
+
/** Keys a command checks inside itself, beyond its own path: `api.calendar.events.insert`. */
|
|
14
|
+
extra: readonly PermissionKey[];
|
|
15
|
+
/** cli-core's built-in levels and the CLI's own; they only ever tighten. */
|
|
16
|
+
defaults: Readonly<Record<PermissionKey, Level>>;
|
|
17
|
+
}
|
|
18
|
+
/** Either way of naming a program's keys: a messenger's per-command mapping, or a declared CLI's own. */
|
|
19
|
+
export type PermissionKeySource = PermissionKeyOf | CommandPermissions;
|
|
20
|
+
/** A command's own path is its key; the commands that look after the tool have none. */
|
|
21
|
+
export declare const commandPathKey: (path: readonly string[]) => PermissionKey | null;
|
|
22
|
+
export declare const commandPermissions: ({ keyOf, extra, defaults, }?: Partial<CommandPermissions>) => CommandPermissions;
|
|
6
23
|
/**
|
|
7
24
|
* The keys `permissions` may name: every command of the program, every write key, and each of their
|
|
8
25
|
* parents. A key outside it is a typo that would otherwise do nothing (BUG-137).
|
|
9
26
|
*/
|
|
10
|
-
export declare const knownPermissionKeys: (command: Command, keyOf?:
|
|
27
|
+
export declare const knownPermissionKeys: (command: Command, keyOf?: PermissionKeySource, extra?: readonly PermissionKey[]) => Set<PermissionKey>;
|
|
11
28
|
/** The known keys one level under the unknown key's parent, to name in the refusal. */
|
|
12
29
|
export declare const knownBeside: (key: PermissionKey, known: ReadonlySet<PermissionKey>) => PermissionKey[];
|
|
13
30
|
export declare const unknownPermissionKeys: (levels: Readonly<Record<PermissionKey, unknown>>, known: ReadonlySet<PermissionKey>) => PermissionKey[];
|
|
14
31
|
/** Every unknown permission key saved in the file, in every section and profile, with the unset that removes it. */
|
|
15
|
-
export declare const unknownPermissionIssues: (command: Command, settings: Pick<Settings, "configPath" | "savedPermissions" | "permissions" | "permissionSources" | "profile">, tool: string, keyOf?:
|
|
32
|
+
export declare const unknownPermissionIssues: (command: Command, settings: Pick<Settings, "configPath" | "savedPermissions" | "permissions" | "permissionSources" | "profile">, tool: string, keyOf?: PermissionKeySource) => ConfigIssue[];
|
|
16
33
|
/**
|
|
17
34
|
* A saved key no command checks would do nothing, so the command refuses to run and names the unset that removes
|
|
18
35
|
* it. A `--permission` key is checked too: it is no file's, so the flag is the fix.
|
|
19
36
|
*/
|
|
20
|
-
export declare const refuseUnknownPermissionKeys: (command: Command, settings: Pick<Settings, "configPath" | "savedPermissions" | "permissions" | "permissionSources" | "profile">, tool: string, keyOf?:
|
|
37
|
+
export declare const refuseUnknownPermissionKeys: (command: Command, settings: Pick<Settings, "configPath" | "savedPermissions" | "permissions" | "permissionSources" | "profile">, tool: string, keyOf?: PermissionKeySource) => void;
|