@wirecat/cli-core 0.449.0 → 0.450.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 +41 -0
- 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/http/public-url.d.ts +5 -1
- package/dist/http/public-url.js +11 -3
- 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/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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
|
+
## 0.450.0 — 27.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`<tool> mcp` with no profile serves every profile**, on stdio and with `--http`: one connector per tool in an AI
|
|
13
|
+
app instead of one per profile. Its tools are the ones `<tool> <profile> mcp` shows, each with a required `profile`
|
|
14
|
+
(every profile on this computer, no default), and the instructions tell the agent to ask when the owner did not name
|
|
15
|
+
the account. A call runs in that profile's own `<tool> <profile> mcp`, started as a child on first use and closed
|
|
16
|
+
after 10 idle minutes, so its permissions, keyring, store and audit log apply as before; `--permission` reaches
|
|
17
|
+
every child. `mcp --revoke`, `mcp config` and `mcp server start|stop|status` work without a profile too, with their
|
|
18
|
+
own login (`mcp-http/_all.json`). `<tool> <profile> mcp` is unchanged. drive and zoom wire the same module from
|
|
19
|
+
`@wirecat/cli-core/mcp`: `serveAllProfilesOverStdio`, `serveAllProfilesOverHttp`.
|
|
20
|
+
- **`mcp --http --public-url` takes a path**, `https://<name>.ts.net/tg`, so tools share one tunnel address with a
|
|
21
|
+
path each. The issuer, the login routes and the connector address (`…/tg/mcp`) carry the path; the issuer's
|
|
22
|
+
metadata is at `/.well-known/oauth-authorization-server/tg` and the resource's at
|
|
23
|
+
`/.well-known/oauth-protected-resource/tg/mcp`, where RFC 8414 and RFC 9728 put them, so the tunnel routes those
|
|
24
|
+
two paths to the server as well as `/tg` (with Tailscale: `--set-path <path> http://127.0.0.1:<port><path>` for
|
|
25
|
+
each). Without a path nothing changes.
|
|
26
|
+
|
|
27
|
+
- **A CLI that is not a messenger can use the shared `permissions`, keyed by its own command paths.** It passes
|
|
28
|
+
`settingsFor(app, { permissions: commandPermissions({ extra, defaults }) })` and hands that `configuration` to
|
|
29
|
+
`run`. Then `config set permissions.<key>` and an MCP server's `--permission` take its command paths, the groups
|
|
30
|
+
above them and its `extra` keys, such as `api.calendar.events.insert`, and refuse every other key. Before, they took
|
|
31
|
+
only a messenger's resources. Each command is refused before it runs when its key is `deny`, when it writes under
|
|
32
|
+
`readonly`, and when it writes to the service under the read-only lock. `ask` stays with the write guard
|
|
33
|
+
(`permissionDefaults`: `configuration.permissions.defaults`). MCP finds and refuses the command by the same key
|
|
34
|
+
when the server passes `keyOf: configuration.permissions.keyOf`. Messenger CLIs are unchanged. A CLI with a program
|
|
35
|
+
of its own, such as drive, uses the parts: `permissionOverrides(entries, COMMAND_PATH_KEY)`,
|
|
36
|
+
`assertOverridesTighten(overrides, saved, defaults)` from `./sends` and `knownPermissionKeys(program, permissions)`.
|
|
37
|
+
- **MCP judges a declared CLI's command as the check before a run does.** A server passes
|
|
38
|
+
`permissions: configuration.permissions` to `toolsFromCommands` and to the agent context, in place of `keyOf`. Then
|
|
39
|
+
a write to the service is unavailable under the read-only lock (`readOnly`), and the CLI's own `deny` or `readonly`
|
|
40
|
+
defaults make a command unavailable and refuse its call. Before, MCP offered both.
|
|
41
|
+
|
|
42
|
+
### Changed — may break callers
|
|
43
|
+
|
|
44
|
+
- **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
|
|
45
|
+
CLI that pins cli-base moves its pin with this release. cli-base 0.63 adds the shared Google Discovery reader
|
|
46
|
+
(`adaptDiscovery`, `syncDiscovery`, `readDiscovery` in `@wirecat/cli-base/codegen`) that drive and gcal generate their
|
|
47
|
+
API code with; nothing cli-core uses changed.
|
|
48
|
+
|
|
8
49
|
## 0.449.0 — 26.10.2026
|
|
9
50
|
|
|
10
51
|
### Changed — may break callers
|
|
@@ -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;
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { CEILING_KEY, keyForCommand, WRITE_KEYS } from "../sends/permissions.js";
|
|
2
|
+
import { AUTHORITY_KEY, CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, WRITE_KEYS, } from "../sends/permissions.js";
|
|
3
3
|
import { issueMessage, permissionIssues, publicIssue } from "./config-issues.js";
|
|
4
|
+
const HOUSEKEEPING = new Set([
|
|
5
|
+
"help",
|
|
6
|
+
"guide",
|
|
7
|
+
"config",
|
|
8
|
+
"session",
|
|
9
|
+
"doctor",
|
|
10
|
+
"commands",
|
|
11
|
+
"complete",
|
|
12
|
+
"upgrade",
|
|
13
|
+
"skill",
|
|
14
|
+
"mcp",
|
|
15
|
+
"runs",
|
|
16
|
+
]);
|
|
17
|
+
/** A command's own path is its key; the commands that look after the tool have none. */
|
|
18
|
+
export const commandPathKey = (path) => path[0] === undefined || HOUSEKEEPING.has(path[0]) ? null : path.join(".");
|
|
19
|
+
export const commandPermissions = ({ keyOf = commandPathKey, extra = [], defaults = {}, } = {}) => ({
|
|
20
|
+
keyOf,
|
|
21
|
+
extra: [AUTHORITY_KEY, ...extra],
|
|
22
|
+
defaults: { ...DEFAULT_PERMISSIONS, ...defaults },
|
|
23
|
+
});
|
|
4
24
|
const pathOf = (command) => {
|
|
5
25
|
const words = [];
|
|
6
26
|
for (let at = command; at?.parent; at = at.parent)
|
|
@@ -24,6 +44,16 @@ export const knownPermissionKeys = (command, keyOf = (one) => keyForCommand(path
|
|
|
24
44
|
for (let parts = key.split("."); parts.length > 0; parts = parts.slice(0, -1))
|
|
25
45
|
known.add(parts.join("."));
|
|
26
46
|
};
|
|
47
|
+
if (typeof keyOf !== "function") {
|
|
48
|
+
for (const one of everyCommand(rootOf(command))) {
|
|
49
|
+
const key = keyOf.keyOf(pathOf(one));
|
|
50
|
+
if (key)
|
|
51
|
+
add(key);
|
|
52
|
+
}
|
|
53
|
+
for (const key of [...keyOf.extra, ...extra])
|
|
54
|
+
add(key);
|
|
55
|
+
return known;
|
|
56
|
+
}
|
|
27
57
|
for (const one of everyCommand(rootOf(command))) {
|
|
28
58
|
const own = keyOf(one);
|
|
29
59
|
const key = own === undefined ? keyForCommand(pathOf(one)) : own;
|
package/dist/cli/preview.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { type PermissionKey } from "../sends/permissions.js";
|
|
3
|
+
import { type CommandPermissions } from "./permission-keys.js";
|
|
3
4
|
import type { Settings } from "./settings.js";
|
|
4
5
|
export declare class PreviewComplete extends Error {
|
|
5
6
|
}
|
|
6
|
-
export declare const preview: (command: Command, settings: Pick<Settings, "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "profile">, tool: string,
|
|
7
|
+
export declare const preview: (command: Command, settings: Pick<Settings, "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "profile">, tool: string, keys?: ((path: readonly string[]) => PermissionKey | null | undefined) | CommandPermissions, messenger?: boolean) => {
|
|
7
8
|
preview: boolean;
|
|
8
9
|
path: string[];
|
|
9
10
|
permission?: {
|