@wirecat/cli-core 0.401.0 → 0.403.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 +24 -0
- package/dist/cli/bot/api.js +5 -2
- package/dist/cli/config-command.js +13 -7
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/first-run.d.ts +68 -0
- package/dist/cli/first-run.js +224 -0
- package/dist/cli/help/first-screen.d.ts +2 -0
- package/dist/cli/help/first-screen.js +31 -6
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/messenger/context.js +4 -2
- package/dist/cli/messenger/store-maintenance-command.d.ts +10 -0
- package/dist/cli/messenger/store-maintenance-command.js +1 -1
- package/dist/cli/permission-keys.js +2 -2
- package/dist/cli/program.js +16 -1
- package/dist/cli/settings.js +15 -2
- package/dist/cli/setup-answers.d.ts +24 -0
- package/dist/cli/setup-answers.js +47 -0
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/mcp/agent.js +4 -2
- package/dist/search/dictionary.d.ts +20 -0
- package/dist/search/dictionary.js +106 -0
- package/dist/sends/guard.d.ts +2 -0
- package/dist/sends/guard.js +8 -3
- package/dist/sends/permissions.d.ts +14 -0
- package/dist/sends/permissions.js +23 -1
- package/dist/services/documents/mirror-watch.d.ts +39 -0
- package/dist/services/documents/mirror-watch.js +155 -0
- package/dist/services/documents/push.d.ts +2 -0
- package/dist/services/documents/push.js +2 -0
- package/dist/store/shared-config.d.ts +4 -0
- package/dist/store/shared-config.js +10 -6
- package/dist/store/sqlite/transcript-query.js +15 -5
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.403.0 — 24.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`setupAtStart` runs the shared first-run questions for a tool with its own program** (zm, memo, drive): it asks only a person at a terminal, and the command runs whatever happens to the questions.
|
|
15
|
+
|
|
16
|
+
- **The first command at a terminal asks three questions once for every WireCat tool.** Which languages you write and speak (English and the system's language ticked on a new store; a store in use keeps its own, so Enter changes nothing), which other WireCat tools to install (installed ones are marked), and whether commands and agents may only read. The answers sit in the `setup` section of `~/.config/WireCat/config.json`; "read only" caps every profile of every tool, over MCP too. Agents, pipes, `--json`, `--no-input` and `CI` are never asked. `askSetupQuestions` lets a tool's `setup` ask them again.
|
|
17
|
+
|
|
18
|
+
## 0.402.0 — 24.10.2026
|
|
19
|
+
|
|
20
|
+
### Changed — may break callers
|
|
21
|
+
|
|
22
|
+
- **MCP offers `bot api`, behind `bot.api`.** The generated `bot api <operation>` commands (raw calls to Bot API methods, which skip the send guard and the bot's recipient list) are bot MCP tools again, offered only where the profile sets `permissions.bot.api` to `allow`; it is `deny` by default for an MCP server, so nothing changes until the owner turns it on, and a broader `bot: allow` does not lift it. A call made after the owner turned it off is refused naming the key and the terminal command; an agent cannot set the key through `config set` (the guard on `permissions.mcp…` covers `permissions.bot.api`). Breaking for a CLI: `generatedApiCommand` no longer marks the group `mcp: false`, and each operation now declares effects (`remote: read` or `write`), so `commands --json` shows them and a catalogue built with `toolsFromCommands` without `levels` lists the operations; update a pinned bot catalogue and a document that lists what MCP leaves out. Under `<PREFIX>_READ_ONLY` a write operation now refuses on the command line too. The command line's `bot api` is otherwise unchanged.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Voice-note search no longer turns an English word into another real English word.** A word in a transcript widens to the spellings speech recognition garbles only past a dictionary of its language: Russian as before, and now English and Spanish, following the store's word-form languages (`search_stemmers`). Before, «cost» also found «cast». A Latin word with no dictionary for its language, and English and Spanish number words, no longer widen.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`watchMirrors` keeps folder mirrors in step.** One cycle per local save (after a quiet period, and only when the
|
|
31
|
+
files differ from the last sync), per remote change the CLI reports, per periodic local check and per full run;
|
|
32
|
+
each cycle syncs only the mirrors that need it, and a failed one is retried at the next poll.
|
|
33
|
+
|
|
10
34
|
## 0.401.0 — 24.10.2026
|
|
11
35
|
|
|
12
36
|
### Changed — may break callers
|
package/dist/cli/bot/api.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
|
+
import { REMOTE_READ, REMOTE_WRITE } from "../effects.js";
|
|
4
5
|
import { apiFlagOf, apiOptionKey, readApiBody } from "./api-input.js";
|
|
5
6
|
export const generatedApiCommand = (api) => {
|
|
6
|
-
const group =
|
|
7
|
+
const group = new Command("api").description(api.description ?? "every operation of the official Bot API, generated from its schema");
|
|
7
8
|
group.option("--store-token <profile>", "keep a returned authentication token only in this bot profile's OS keyring; never print it");
|
|
8
9
|
for (const operation of api.operations) {
|
|
9
10
|
const binding = operation.binding.kind === "http"
|
|
10
11
|
? `${operation.binding.method} ${operation.binding.path}`
|
|
11
12
|
: operation.binding.name;
|
|
12
|
-
const command = new Command(operation.command)
|
|
13
|
+
const command = annotate(new Command(operation.command), {
|
|
14
|
+
effects: operation.effect === "read" ? REMOTE_READ : REMOTE_WRITE,
|
|
15
|
+
}).description(`${operation.summary ?? operation.id} — ${operation.effect} (${binding})`);
|
|
13
16
|
for (const parameter of operation.parameters) {
|
|
14
17
|
if (parameter.sensitive)
|
|
15
18
|
continue;
|
|
@@ -2,7 +2,7 @@ import { CliError, pathsAreOverridden } from "@wirecat/cli-base";
|
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { envName } from "../ports/app.js";
|
|
5
|
-
import { AUTHORITY_KEY } from "../sends/permissions.js";
|
|
5
|
+
import { AUTHORITY_KEY, BOT_API_KEY } from "../sends/permissions.js";
|
|
6
6
|
import { loadSnakeCaseConfig } from "./config-keys.js";
|
|
7
7
|
import { baseContext, environmentOf } from "./context.js";
|
|
8
8
|
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
|
|
@@ -96,7 +96,7 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
|
96
96
|
throw new CliError("permission_error", `this process is locked to profile ${settings.profile} (${lock}) — ${setting === "default_profile" ? setting : "--defaults"} changes other profiles`);
|
|
97
97
|
}
|
|
98
98
|
if (environmentOf(this).agent)
|
|
99
|
-
|
|
99
|
+
refuseGuardedKey(setting, action === "set" ? String(given) : undefined);
|
|
100
100
|
if (action === "set")
|
|
101
101
|
refuseUnknownKey(this, setting, String(given), permissionKey);
|
|
102
102
|
const saved = config.changeSetting(settings.configPath, {
|
|
@@ -137,10 +137,11 @@ export const refuseUnknownKey = (command, setting, value, permissionKey) => {
|
|
|
137
137
|
}
|
|
138
138
|
};
|
|
139
139
|
/**
|
|
140
|
-
* An agent may hold `mcp.authority` and change other settings, but never the key that let it in: on any
|
|
141
|
-
* the defaults, or by replacing `permissions` whole. Only the owner, in a terminal, turns it on or off
|
|
140
|
+
* An agent may hold `mcp.authority` or `bot.api` and change other settings, but never the key that let it in: on any
|
|
141
|
+
* profile, in the defaults, or by replacing `permissions` whole. Only the owner, in a terminal, turns it on or off
|
|
142
|
+
* (NEED-1662, NEED-1679).
|
|
142
143
|
*/
|
|
143
|
-
const
|
|
144
|
+
const refuseGuardedKey = (setting, value) => {
|
|
144
145
|
const keys = setting.startsWith("permissions.")
|
|
145
146
|
? [setting.slice("permissions.".length)]
|
|
146
147
|
: setting === "permissions"
|
|
@@ -148,9 +149,14 @@ const refuseAuthorityKey = (setting, value) => {
|
|
|
148
149
|
? ["mcp"]
|
|
149
150
|
: objectKeys(value)
|
|
150
151
|
: [];
|
|
151
|
-
|
|
152
|
+
const guarded = keys.some((key) => key === BOT_API_KEY || key.startsWith(`${BOT_API_KEY}.`))
|
|
153
|
+
? BOT_API_KEY
|
|
154
|
+
: keys.some((key) => key === "mcp" || key.startsWith("mcp."))
|
|
155
|
+
? AUTHORITY_KEY
|
|
156
|
+
: undefined;
|
|
157
|
+
if (!guarded)
|
|
152
158
|
return;
|
|
153
|
-
throw new CliError("permission_error", `an agent cannot change permissions.${
|
|
159
|
+
throw new CliError("permission_error", `an agent cannot change permissions.${guarded} — the owner sets it in a terminal`, { permission: guarded });
|
|
154
160
|
};
|
|
155
161
|
/** The keys of a `permissions` object given whole; anything else is left to the schema to refuse. */
|
|
156
162
|
const objectKeys = (value) => {
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -87,6 +87,8 @@ export interface AgentRun {
|
|
|
87
87
|
overrides?: readonly string[];
|
|
88
88
|
/** The server's levels allow `mcp.authority`: a command that changes what a later run may do may run. */
|
|
89
89
|
authority?: boolean;
|
|
90
|
+
/** The server's levels allow `bot.api`: a raw Bot API call, past the send guard, may run. */
|
|
91
|
+
botApi?: boolean;
|
|
90
92
|
}
|
|
91
93
|
export interface RelayedRun {
|
|
92
94
|
/** The profile serve holds; a command for another is refused before it touches the connection. */
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Installer } from "@wirecat/cli-base/update";
|
|
2
|
+
import { type Question } from "./setup-answers.js";
|
|
3
|
+
/** The languages cli-core stems and has a dictionary for; each is one stemmer of its alphabet. */
|
|
4
|
+
export declare const LANGUAGES: readonly [{
|
|
5
|
+
readonly name: "russian";
|
|
6
|
+
readonly code: "ru";
|
|
7
|
+
readonly label: "Russian";
|
|
8
|
+
readonly script: "cyrillic";
|
|
9
|
+
}, {
|
|
10
|
+
readonly name: "english";
|
|
11
|
+
readonly code: "en";
|
|
12
|
+
readonly label: "English";
|
|
13
|
+
readonly script: "latin";
|
|
14
|
+
}, {
|
|
15
|
+
readonly name: "spanish";
|
|
16
|
+
readonly code: "es";
|
|
17
|
+
readonly label: "Spanish";
|
|
18
|
+
readonly script: "latin";
|
|
19
|
+
}];
|
|
20
|
+
export interface FirstRunIo {
|
|
21
|
+
ask: (prompt: string) => Promise<string>;
|
|
22
|
+
say: (text: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface FirstRun {
|
|
25
|
+
command: string;
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
io: FirstRunIo;
|
|
28
|
+
/** Every question, as `setup` asks them; otherwise only those no WireCat tool asked yet. */
|
|
29
|
+
all?: boolean;
|
|
30
|
+
/** Where tools are looked for and how one is installed; injected by tests. */
|
|
31
|
+
installed?: (command: string) => boolean;
|
|
32
|
+
installer?: Installer;
|
|
33
|
+
install?: (argv: string[]) => boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Only a person at a terminal is asked. An agent in a real terminal passes every terminal check and would wait on
|
|
37
|
+
* the question forever; a process locked read-only must write nothing, and every answer writes something.
|
|
38
|
+
*/
|
|
39
|
+
export declare const asksAtStart: (start: {
|
|
40
|
+
headless: boolean;
|
|
41
|
+
terminal: boolean;
|
|
42
|
+
env: NodeJS.ProcessEnv;
|
|
43
|
+
readOnlyLock: string | undefined;
|
|
44
|
+
path: readonly string[];
|
|
45
|
+
}) => boolean;
|
|
46
|
+
/** The system's language, as the first locale variable set names it: `LC_ALL`, `LC_MESSAGES`, `LANG`, `LANGUAGE`. */
|
|
47
|
+
export declare const systemLanguage: (env: NodeJS.ProcessEnv) => string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* The questions every WireCat tool shares, asked once for all of them: the answers go to the shared config
|
|
50
|
+
* file, the languages to the store. Only this, at a terminal, writes the permission answer.
|
|
51
|
+
*/
|
|
52
|
+
export declare const askSetupQuestions: (run: FirstRun) => Promise<Question[]>;
|
|
53
|
+
/**
|
|
54
|
+
* The first-run step a tool runs before each command: asks when `asksAtStart` says so, and lets the command run
|
|
55
|
+
* whatever happens to the questions. `setup` asks all three again, since a refusal sends the owner there.
|
|
56
|
+
* A tool that builds its own program (zm, memo, drive) calls this with its command path.
|
|
57
|
+
*/
|
|
58
|
+
export declare const setupAtStart: (start: {
|
|
59
|
+
command: string;
|
|
60
|
+
env: NodeJS.ProcessEnv;
|
|
61
|
+
path: readonly string[];
|
|
62
|
+
headless: boolean;
|
|
63
|
+
readOnlyLock?: string | undefined;
|
|
64
|
+
input?: NodeJS.ReadableStream & {
|
|
65
|
+
isTTY?: boolean;
|
|
66
|
+
};
|
|
67
|
+
terminal?: boolean;
|
|
68
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { installerOf, updateCommand } from "@wirecat/cli-base/update";
|
|
5
|
+
import { DEFAULT_STEMMERS } from "../search/stem.js";
|
|
6
|
+
import { openCache } from "../store/open.js";
|
|
7
|
+
import { storePath } from "../store/path.js";
|
|
8
|
+
import { sharedConfigPath } from "../store/shared-config.js";
|
|
9
|
+
import { openStore } from "../store/store.js";
|
|
10
|
+
import { readSecret } from "../terminal/prompt.js";
|
|
11
|
+
import { WIRECAT_TOOLS } from "./help/first-screen.js";
|
|
12
|
+
import { buildStems } from "./messenger/store-maintenance-command.js";
|
|
13
|
+
import { QUESTIONS, readSetupAnswers, unanswered, writeSetupAnswers, } from "./setup-answers.js";
|
|
14
|
+
/** The languages cli-core stems and has a dictionary for; each is one stemmer of its alphabet. */
|
|
15
|
+
export const LANGUAGES = [
|
|
16
|
+
{ name: "russian", code: "ru", label: "Russian", script: "cyrillic" },
|
|
17
|
+
{ name: "english", code: "en", label: "English", script: "latin" },
|
|
18
|
+
{ name: "spanish", code: "es", label: "Spanish", script: "latin" },
|
|
19
|
+
];
|
|
20
|
+
/** Commands that run without first-run questions: they serve agents, help, or open the store themselves. */
|
|
21
|
+
const SKIPPED = new Set(["help", "guide", "completion", "mcp", "serve", "server", "update", "store"]);
|
|
22
|
+
/**
|
|
23
|
+
* Only a person at a terminal is asked. An agent in a real terminal passes every terminal check and would wait on
|
|
24
|
+
* the question forever; a process locked read-only must write nothing, and every answer writes something.
|
|
25
|
+
*/
|
|
26
|
+
export const asksAtStart = (start) => !start.headless &&
|
|
27
|
+
start.terminal &&
|
|
28
|
+
!start.env.AI_AGENT &&
|
|
29
|
+
!start.env.CLAUDECODE &&
|
|
30
|
+
start.readOnlyLock === undefined &&
|
|
31
|
+
!SKIPPED.has(start.path[0] ?? "");
|
|
32
|
+
/** The system's language, as the first locale variable set names it: `LC_ALL`, `LC_MESSAGES`, `LANG`, `LANGUAGE`. */
|
|
33
|
+
export const systemLanguage = (env) => {
|
|
34
|
+
const named = [env.LC_ALL, env.LC_MESSAGES, env.LANG, ...(env.LANGUAGE ?? "").split(":")]
|
|
35
|
+
.map((value) => value?.trim())
|
|
36
|
+
.find((value) => value && value !== "C" && value !== "POSIX" && !value.startsWith("C."));
|
|
37
|
+
return (named ?? Intl.DateTimeFormat().resolvedOptions().locale).slice(0, 2).toLowerCase();
|
|
38
|
+
};
|
|
39
|
+
const stemmersOf = (languages) => {
|
|
40
|
+
const latin = LANGUAGES.filter(({ name, script }) => script === "latin" && languages.includes(name));
|
|
41
|
+
return {
|
|
42
|
+
cyrillic: languages.includes("russian") ? "russian" : "none",
|
|
43
|
+
latin: latin.length ? latin.map(({ name }) => name).join(",") : "none",
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const languagesOf = (stemmers) => LANGUAGES.filter(({ name }) => stemmers.cyrillic === name || stemmers.latin.split(",").includes(name)).map(({ name }) => name);
|
|
47
|
+
/** Numbers from a list, as `1,3` or `1 3`; Enter keeps `chosen`. A wrong answer is asked again, then left as it was. */
|
|
48
|
+
const choose = async (io, prompt, count, chosen) => {
|
|
49
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
50
|
+
const answer = (await io.ask(prompt)).trim();
|
|
51
|
+
if (!answer)
|
|
52
|
+
return chosen;
|
|
53
|
+
if (/^(none|-)$/i.test(answer))
|
|
54
|
+
return [];
|
|
55
|
+
const numbers = answer
|
|
56
|
+
.split(/[\s,]+/)
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map(Number);
|
|
59
|
+
if (numbers.every((number) => Number.isInteger(number) && number >= 1 && number <= count))
|
|
60
|
+
return [...new Set(numbers)].sort((a, b) => a - b);
|
|
61
|
+
io.say(`Choose numbers from 1 to ${count}, separated by commas.`);
|
|
62
|
+
}
|
|
63
|
+
return chosen;
|
|
64
|
+
};
|
|
65
|
+
const yes = async (io, prompt) => /^y(es)?$/i.test((await io.ask(prompt)).trim());
|
|
66
|
+
const onPath = (command, env) => {
|
|
67
|
+
const names = process.platform === "win32" ? [".cmd", ".exe", ""].map((ending) => command + ending) : [command];
|
|
68
|
+
return (env.PATH ?? env.Path ?? "").split(delimiter).some((folder) => names.some((name) => {
|
|
69
|
+
try {
|
|
70
|
+
accessSync(join(folder, name), process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
/** With the installer that installed this tool, its output on stderr so stdout stays data. */
|
|
79
|
+
const installWith = (argv) => spawnSync(argv[0], argv.slice(1), { stdio: ["inherit", 2, 2] }).status === 0;
|
|
80
|
+
const installerHere = () => {
|
|
81
|
+
try {
|
|
82
|
+
return installerOf(realpathSync(process.argv[1] ?? ""));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const askLanguages = async ({ command, env, io }) => {
|
|
89
|
+
const store = await openStore({ env });
|
|
90
|
+
let rebuild = false;
|
|
91
|
+
try {
|
|
92
|
+
const saved = (await store.searchIndex.stemmers()) ?? undefined;
|
|
93
|
+
// A store already in use keeps the languages it was built with, so Enter changes nothing (RISK-1612).
|
|
94
|
+
const inUse = saved !== undefined || (await store.accounts.accounts()).length > 0;
|
|
95
|
+
const system = systemLanguage(env);
|
|
96
|
+
const ticked = inUse
|
|
97
|
+
? languagesOf(saved ?? DEFAULT_STEMMERS)
|
|
98
|
+
: LANGUAGES.filter(({ code }) => code === "en" || code === system).map(({ name }) => name);
|
|
99
|
+
const numbers = ticked.map((name) => LANGUAGES.findIndex((one) => one.name === name) + 1);
|
|
100
|
+
io.say("Languages you write and speak — search finds every form of their words, and spellings speech mangles:");
|
|
101
|
+
LANGUAGES.forEach(({ name, label }, at) => {
|
|
102
|
+
io.say(` ${at + 1}. ${label}${ticked.includes(name) ? " ✓" : ""}`);
|
|
103
|
+
});
|
|
104
|
+
const chosen = await choose(io, `Numbers (${numbers.join(",")}): `, LANGUAGES.length, numbers);
|
|
105
|
+
const next = stemmersOf(chosen.map((number) => LANGUAGES[number - 1].name));
|
|
106
|
+
const before = saved ?? DEFAULT_STEMMERS;
|
|
107
|
+
if (inUse && before.cyrillic === next.cyrillic && before.latin === next.latin)
|
|
108
|
+
return;
|
|
109
|
+
// Word search refuses until stems built by the old choice are rebuilt, so the change and the rebuild go together.
|
|
110
|
+
if (inUse &&
|
|
111
|
+
!(await yes(io, "Rebuild the word forms of every stored message now? Word search waits for it; a large store takes minutes. [y/N] "))) {
|
|
112
|
+
io.say(`Languages kept as they were; \`${command} setup\` asks again.`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await store.searchIndex.saveStemmers(next);
|
|
116
|
+
rebuild = inUse;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await store.close();
|
|
120
|
+
}
|
|
121
|
+
if (!rebuild)
|
|
122
|
+
return;
|
|
123
|
+
const database = await openCache(storePath(env));
|
|
124
|
+
try {
|
|
125
|
+
buildStems(database, (note) => io.say(` ${note}`), { force: true });
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
database.close();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const askTools = async (run) => {
|
|
132
|
+
const { command, env, io } = run;
|
|
133
|
+
const installed = run.installed ?? ((name) => onPath(name, env));
|
|
134
|
+
const others = WIRECAT_TOOLS.filter((tool) => tool.command !== command);
|
|
135
|
+
const missing = others.filter((tool) => !installed(tool.command));
|
|
136
|
+
io.say("Other WireCat tools — they share this store, so one search finds everything:");
|
|
137
|
+
for (const tool of others) {
|
|
138
|
+
const at = missing.indexOf(tool);
|
|
139
|
+
io.say(` ${at < 0 ? "✓ " : `${at + 1}.`} ${tool.command} — ${tool.does}`);
|
|
140
|
+
}
|
|
141
|
+
if (!missing.length)
|
|
142
|
+
return;
|
|
143
|
+
const chosen = await choose(io, "Numbers to install (none): ", missing.length, []);
|
|
144
|
+
const installer = run.installer ?? installerHere();
|
|
145
|
+
for (const number of chosen) {
|
|
146
|
+
const tool = missing[number - 1];
|
|
147
|
+
const argv = updateCommand(installer, tool.package);
|
|
148
|
+
if (!argv) {
|
|
149
|
+
io.say(`Install ${tool.command} with \`npm install -g ${tool.package}\`.`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
io.say(`Installing ${tool.command}: ${argv.join(" ")}`);
|
|
153
|
+
if (!(run.install ?? installWith)(argv))
|
|
154
|
+
io.say(`${tool.command} was not installed; run \`${argv.join(" ")}\`.`);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const PERMISSION_CHOICES = [
|
|
158
|
+
{ answer: "readonly", text: "read only — search and read; nothing sends or changes, bots and agents included" },
|
|
159
|
+
{ answer: "allow", text: "as now — each profile's own permissions" },
|
|
160
|
+
];
|
|
161
|
+
const askPermissions = async ({ io }, answers) => {
|
|
162
|
+
const now = answers.permissions ?? "allow";
|
|
163
|
+
const current = Math.max(1, PERMISSION_CHOICES.findIndex(({ answer }) => answer === now) + 1);
|
|
164
|
+
io.say("What may commands and agents do, in every WireCat tool?");
|
|
165
|
+
PERMISSION_CHOICES.forEach(({ text }, at) => {
|
|
166
|
+
io.say(` ${at + 1}. ${text}`);
|
|
167
|
+
});
|
|
168
|
+
const [chosen = current] = await choose(io, `Number (${current}): `, PERMISSION_CHOICES.length, [current]);
|
|
169
|
+
return PERMISSION_CHOICES[chosen - 1].answer;
|
|
170
|
+
};
|
|
171
|
+
/** A section another tool wrote wrongly is asked again and written over, rather than stopping the command. */
|
|
172
|
+
const answersAt = (path) => {
|
|
173
|
+
try {
|
|
174
|
+
return readSetupAnswers(path);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* The questions every WireCat tool shares, asked once for all of them: the answers go to the shared config
|
|
182
|
+
* file, the languages to the store. Only this, at a terminal, writes the permission answer.
|
|
183
|
+
*/
|
|
184
|
+
export const askSetupQuestions = async (run) => {
|
|
185
|
+
const path = sharedConfigPath(storePath(run.env), run.env);
|
|
186
|
+
const answers = answersAt(path);
|
|
187
|
+
const asking = run.all ? [...QUESTIONS] : unanswered(answers);
|
|
188
|
+
for (const question of asking) {
|
|
189
|
+
run.io.say("");
|
|
190
|
+
if (question === "languages") {
|
|
191
|
+
await askLanguages(run);
|
|
192
|
+
writeSetupAnswers(path, { languages: true });
|
|
193
|
+
}
|
|
194
|
+
else if (question === "tools") {
|
|
195
|
+
await askTools(run);
|
|
196
|
+
writeSetupAnswers(path, { tools: true });
|
|
197
|
+
}
|
|
198
|
+
else
|
|
199
|
+
writeSetupAnswers(path, { permissions: await askPermissions(run, answers) });
|
|
200
|
+
}
|
|
201
|
+
return asking;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* The first-run step a tool runs before each command: asks when `asksAtStart` says so, and lets the command run
|
|
205
|
+
* whatever happens to the questions. `setup` asks all three again, since a refusal sends the owner there.
|
|
206
|
+
* A tool that builds its own program (zm, memo, drive) calls this with its command path.
|
|
207
|
+
*/
|
|
208
|
+
export const setupAtStart = async (start) => {
|
|
209
|
+
const input = start.input ?? process.stdin;
|
|
210
|
+
const terminal = start.terminal ?? (input.isTTY === true && process.stderr.isTTY === true);
|
|
211
|
+
if (!asksAtStart({ ...start, terminal, readOnlyLock: start.readOnlyLock }))
|
|
212
|
+
return;
|
|
213
|
+
await askSetupQuestions({
|
|
214
|
+
command: start.command,
|
|
215
|
+
env: start.env,
|
|
216
|
+
all: start.path[0] === "setup",
|
|
217
|
+
io: {
|
|
218
|
+
ask: (prompt) => readSecret(prompt, { input, echo: true }),
|
|
219
|
+
say: (text) => process.stderr.write(`${text}\n`),
|
|
220
|
+
},
|
|
221
|
+
}).catch((error) => {
|
|
222
|
+
process.stderr.write(`setup questions skipped: ${error instanceof Error ? error.message : String(error)} — \`${start.command} setup\` asks them again\n`);
|
|
223
|
+
});
|
|
224
|
+
};
|
|
@@ -7,12 +7,37 @@ import { commandAt } from "../steps.js";
|
|
|
7
7
|
import { EXAMPLE_PROFILE, installCommandHelp } from "./command-help.js";
|
|
8
8
|
/** Every WireCat tool, as "Works with" names it; a tool's own entry is left out of its screen. */
|
|
9
9
|
export const WIRECAT_TOOLS = [
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
10
|
+
{
|
|
11
|
+
command: "tg",
|
|
12
|
+
package: "@wirecat/tg-cli",
|
|
13
|
+
does: "Telegram in the same local store: search, review and tasks offline",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
command: "max",
|
|
17
|
+
package: "@wirecat/max-cli",
|
|
18
|
+
does: "MAX in the same local store: search, review and tasks offline",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
command: "wa",
|
|
22
|
+
package: "@wirecat/whatsapp-cli",
|
|
23
|
+
does: "WhatsApp in the same local store: search, review and tasks offline",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
command: "zm",
|
|
27
|
+
package: "@wirecat/zoom-cli",
|
|
28
|
+
does: "Zoom meetings: transcripts and summaries, searchable with your messages",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
command: "drive",
|
|
32
|
+
package: "@wirecat/drive-cli",
|
|
33
|
+
does: "Google Drive and Docs as local text, searchable with your messages",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: "memo",
|
|
37
|
+
package: "@wirecat/cli-memo",
|
|
38
|
+
shown: "memo context <person>",
|
|
39
|
+
does: "one person across messengers, mail, notes and meetings",
|
|
40
|
+
},
|
|
16
41
|
];
|
|
17
42
|
const WIDTH = 100;
|
|
18
43
|
const widest = (texts) => texts.reduce((most, text) => Math.max(most, text.length), 0);
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
|
56
56
|
export { changesAuthority, changesAuthorityAt, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
|
|
57
57
|
export { type AnswerOutput, answerSchema, type Body, type ItemBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
58
58
|
export { withAbort } from "./execution.js";
|
|
59
|
+
export { askSetupQuestions, asksAtStart, type FirstRun, type FirstRunIo, LANGUAGES, setupAtStart, systemLanguage, } from "./first-run.js";
|
|
59
60
|
export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, MEMO_SCENARIOS, type Scenario, shellProblems, } from "./guide.js";
|
|
60
61
|
export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
|
|
61
62
|
export { commandsText } from "./help/commands-text.js";
|
|
@@ -128,6 +129,7 @@ export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOpt
|
|
|
128
129
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
129
130
|
export { runsCommand } from "./runs/command.js";
|
|
130
131
|
export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
|
|
132
|
+
export { type PermissionAnswer, readSetupAnswers, type SetupAnswers } from "./setup-answers.js";
|
|
131
133
|
export { AGENTS, type Agent, agentFor, screenFor } from "./setup-screen.js";
|
|
132
134
|
export { skillCommand } from "./skill-command.js";
|
|
133
135
|
export { runnable, withSafety } from "./steps.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -51,6 +51,7 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
|
51
51
|
export { changesAuthority, changesAuthorityAt, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
|
|
52
52
|
export { answerSchema, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
53
53
|
export { withAbort } from "./execution.js";
|
|
54
|
+
export { askSetupQuestions, asksAtStart, LANGUAGES, setupAtStart, systemLanguage, } from "./first-run.js";
|
|
54
55
|
export { checkGuide, checkStep, guideCommand, loadScenarios, MEMO_SCENARIOS, shellProblems, } from "./guide.js";
|
|
55
56
|
export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
|
|
56
57
|
export { commandsText } from "./help/commands-text.js";
|
|
@@ -123,6 +124,7 @@ export { createProgram, run } from "./program.js";
|
|
|
123
124
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
124
125
|
export { runsCommand } from "./runs/command.js";
|
|
125
126
|
export { count, first, flag, fromFile, NONE_NAMED, plain, settingsFor, } from "./settings.js";
|
|
127
|
+
export { readSetupAnswers } from "./setup-answers.js";
|
|
126
128
|
export { AGENTS, agentFor, screenFor } from "./setup-screen.js";
|
|
127
129
|
export { skillCommand } from "./skill-command.js";
|
|
128
130
|
export { runnable, withSafety } from "./steps.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { servingProfiles } from "../../background/lock.js";
|
|
3
3
|
import { FloodMemory, floodPathFor } from "../../sends/flood.js";
|
|
4
|
-
import { guardFor } from "../../sends/guard.js";
|
|
4
|
+
import { ceilingRefusal, guardFor } from "../../sends/guard.js";
|
|
5
5
|
import { DEFAULT_PACE, Pacer, pacePathFor } from "../../sends/pace.js";
|
|
6
|
-
import { assertStatsPermissionsCurrent, levelFor, readKeysForCommand, } from "../../sends/permissions.js";
|
|
6
|
+
import { assertStatsPermissionsCurrent, CEILING_KEY, levelFor, readKeysForCommand, } from "../../sends/permissions.js";
|
|
7
7
|
import { RecipientList, recipientsPathFor } from "../../sends/recipients.js";
|
|
8
8
|
import { OFFLINE, servicesFor } from "../../services/index.js";
|
|
9
9
|
import { openStore } from "../../store/store.js";
|
|
@@ -338,6 +338,8 @@ export const refuseLocalWrite = (context, command, permission) => {
|
|
|
338
338
|
const { level, key } = levelFor(settings.permissions, permission);
|
|
339
339
|
if (level === "allow" || (level === "ask" && context.agent))
|
|
340
340
|
return;
|
|
341
|
+
if (key === CEILING_KEY)
|
|
342
|
+
throw ceilingRefusal(permission, command);
|
|
341
343
|
throw new CliError(level === "ask" ? "confirmation_required" : "permission_error", `profile ${settings.profile} does not let ${permission} write (permissions.${key} is ${level}, from the ` +
|
|
342
344
|
`${settings.permissionSources[key ?? ""] ?? "default"}); to allow it: ` +
|
|
343
345
|
`${command} ${settings.profile} config set permissions.${permission} allow`, { permission });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import type { CacheDatabase } from "../../store/driver.js";
|
|
2
3
|
import type { Messenger } from "./context.js";
|
|
3
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
4
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
@@ -19,3 +20,12 @@ export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
|
19
20
|
error: string;
|
|
20
21
|
}>;
|
|
21
22
|
export declare const storeMaintenanceCommands: (messenger: Messenger) => Command[];
|
|
23
|
+
/**
|
|
24
|
+
* Stems what is left, after rebuilding stems made by other stemmer choices than the store's setting —
|
|
25
|
+
* only here and in `store reindex`, never in a search's quick fill.
|
|
26
|
+
*/
|
|
27
|
+
export declare const buildStems: (database: CacheDatabase, note: (text: string) => void, { force }?: {
|
|
28
|
+
force?: boolean | undefined;
|
|
29
|
+
}) => {
|
|
30
|
+
stemmed: number;
|
|
31
|
+
};
|
|
@@ -409,7 +409,7 @@ const buildWordIndex = (database, note) => {
|
|
|
409
409
|
* Stems what is left, after rebuilding stems made by other stemmer choices than the store's setting —
|
|
410
410
|
* only here and in `store reindex`, never in a search's quick fill.
|
|
411
411
|
*/
|
|
412
|
-
const buildStems = (database, note, { force = false } = {}) => {
|
|
412
|
+
export const buildStems = (database, note, { force = false } = {}) => {
|
|
413
413
|
const before = stemsState(database);
|
|
414
414
|
if (!before)
|
|
415
415
|
return { stemmed: 0 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { keyForCommand, WRITE_KEYS } from "../sends/permissions.js";
|
|
1
|
+
import { CEILING_KEY, keyForCommand, WRITE_KEYS } from "../sends/permissions.js";
|
|
2
2
|
const pathOf = (command) => {
|
|
3
3
|
const words = [];
|
|
4
4
|
for (let at = command; at?.parent; at = at.parent)
|
|
@@ -39,4 +39,4 @@ export const knownBeside = (key, known) => {
|
|
|
39
39
|
.filter((one) => one.split(".").length === depth && (parent === "" || one.startsWith(`${parent}.`)))
|
|
40
40
|
.sort();
|
|
41
41
|
};
|
|
42
|
-
export const unknownPermissionKeys = (levels, known) => Object.keys(levels).filter((key) => !known.has(key));
|
|
42
|
+
export const unknownPermissionKeys = (levels, known) => Object.keys(levels).filter((key) => key !== CEILING_KEY && !known.has(key));
|
package/dist/cli/program.js
CHANGED
|
@@ -11,7 +11,7 @@ import { failureContext } from "../errors/reasons.js";
|
|
|
11
11
|
import { envName, readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
|
|
12
12
|
import { recorded, wasSettled } from "../runs/recording.js";
|
|
13
13
|
import { processRun, useAsProcessRun } from "../runs/scope.js";
|
|
14
|
-
import { AUTHORITY_KEY, keyForCommand } from "../sends/permissions.js";
|
|
14
|
+
import { AUTHORITY_KEY, BOT_API_KEY, isBotApi, keyForCommand } from "../sends/permissions.js";
|
|
15
15
|
import { logParsedArgv, logVersion } from "./argv-log.js";
|
|
16
16
|
import { commandPathOf } from "./command-contract.js";
|
|
17
17
|
import { outputFor, provide } from "./context.js";
|
|
@@ -19,6 +19,7 @@ import { refuseWriteBeforeConnecting } from "./early-refusal.js";
|
|
|
19
19
|
import { changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, STORE_CHANGE_REFUSAL, } from "./effects.js";
|
|
20
20
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
21
21
|
import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
|
|
22
|
+
import { setupAtStart } from "./first-run.js";
|
|
22
23
|
import { installHelp } from "./help/first-screen.js";
|
|
23
24
|
import { showInputFields } from "./input-fields.js";
|
|
24
25
|
import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
|
|
@@ -237,6 +238,16 @@ const runOnce = async (argv, definition, options) => {
|
|
|
237
238
|
action.setOptionValue("dryRun", true);
|
|
238
239
|
const path = commandPathOf(action);
|
|
239
240
|
const lock = readOnlyLockOf(definition.app, commandEnv);
|
|
241
|
+
const input = options.stdin ?? process.stdin;
|
|
242
|
+
await setupAtStart({
|
|
243
|
+
command,
|
|
244
|
+
env: commandEnv,
|
|
245
|
+
path,
|
|
246
|
+
headless,
|
|
247
|
+
readOnlyLock: lock,
|
|
248
|
+
input,
|
|
249
|
+
terminal: !options.streams && input.isTTY === true && process.stderr.isTTY === true,
|
|
250
|
+
});
|
|
240
251
|
if (lock && isLogin(action) && !flags.dryRun)
|
|
241
252
|
throw readOnlyRefusal(lock, "log in or out");
|
|
242
253
|
if (lock && isStoreChange(action) && !flags.dryRun)
|
|
@@ -258,6 +269,10 @@ const runOnce = async (argv, definition, options) => {
|
|
|
258
269
|
throw new CliError("permission_error", `\`${command} ${path.join(" ")}\` changes what a later run may do, and this profile keeps that from agents ` +
|
|
259
270
|
`(${AUTHORITY_KEY}) — the owner turns it on in a terminal: ` +
|
|
260
271
|
`\`${command} <profile> config set${path[0] === "bot" ? " --bot" : ""} permissions.${AUTHORITY_KEY} allow\``, { permission: AUTHORITY_KEY, sent: false });
|
|
272
|
+
if (options.agent && options.agent.botApi !== true && isBotApi(path))
|
|
273
|
+
throw new CliError("permission_error", `\`${command} ${path.join(" ")}\` calls the Bot API directly, past the send guard and the recipient list, ` +
|
|
274
|
+
`and this profile keeps that from agents (${BOT_API_KEY}) — the owner turns it on in a terminal: ` +
|
|
275
|
+
`\`${command} <profile> config set --bot permissions.${BOT_API_KEY} allow\``, { permission: BOT_API_KEY, sent: false });
|
|
261
276
|
const { lifetime, cooperative: stopsAtItsPace } = metaOf(action);
|
|
262
277
|
const persistent = lifetime === "persistent";
|
|
263
278
|
listens = persistent;
|
package/dist/cli/settings.js
CHANGED
|
@@ -4,15 +4,28 @@ import * as v from "valibot";
|
|
|
4
4
|
import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
|
|
5
5
|
import { parseDuration } from "../domain/duration.js";
|
|
6
6
|
import { envName, readOnlyLockOf } from "../ports/app.js";
|
|
7
|
-
import { assertOverridesTighten, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
|
|
7
|
+
import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
|
|
8
|
+
import { storePath } from "../store/path.js";
|
|
9
|
+
import { sharedConfigPath } from "../store/shared-config.js";
|
|
8
10
|
import { loadSnakeCaseConfig } from "./config-keys.js";
|
|
9
11
|
import { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
10
12
|
import { profilesHere } from "./messenger/accounts.js";
|
|
11
13
|
import { usableProfileName } from "./profile.js";
|
|
14
|
+
import { permissionCeiling } from "./setup-answers.js";
|
|
12
15
|
const DEFAULT_LIMIT = DEFAULT_CONFIG.defaults.limit;
|
|
13
16
|
const DEFAULT_KEEP_RUNS_FOR_DAYS = DEFAULT_CONFIG.defaults.keep_runs_for_days;
|
|
14
17
|
/** On by default: a limit that is off protects nobody from a loop. */
|
|
15
18
|
const DEFAULT_SENDS_PER_HOUR = DEFAULT_CONFIG.defaults.sends_per_hour;
|
|
19
|
+
/** The first-run answer caps every profile; a config file cannot name its key, so only `setup` writes it. */
|
|
20
|
+
const withCeiling = (permissions, env) => {
|
|
21
|
+
const ceiling = permissionCeiling(sharedConfigPath(storePath(env), env));
|
|
22
|
+
return ceiling
|
|
23
|
+
? {
|
|
24
|
+
levels: { ...permissions.levels, [CEILING_KEY]: ceiling },
|
|
25
|
+
sources: { ...permissions.sources, [CEILING_KEY]: "setup answer" },
|
|
26
|
+
}
|
|
27
|
+
: permissions;
|
|
28
|
+
};
|
|
16
29
|
export const plain = (rule) => (issue) => `${rule}, not ${issue.received}`;
|
|
17
30
|
const wholeNumber = plain("has to be a whole number, 1 or more");
|
|
18
31
|
export const count = v.pipe(v.number(wholeNumber), v.integer(wholeNumber), v.minValue(1, wholeNumber));
|
|
@@ -221,7 +234,7 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
221
234
|
]);
|
|
222
235
|
const overrides = permissionOverrides(flags.permission);
|
|
223
236
|
assertOverridesTighten(overrides, layerPermissions(fileLayers).levels);
|
|
224
|
-
const permissions = layerPermissions([["flag", overrides], ...fileLayers]);
|
|
237
|
+
const permissions = withCeiling(layerPermissions([["flag", overrides], ...fileLayers]), env);
|
|
225
238
|
const updateCheck = first([
|
|
226
239
|
[
|
|
227
240
|
extension.sourcePaths ? "config file: defaults" : "config defaults",
|