@wirecat/cli-core 0.402.0 → 0.404.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 +20 -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 +11 -0
- package/dist/cli/settings.js +15 -2
- package/dist/cli/setup-answers.d.ts +26 -0
- package/dist/cli/setup-answers.js +50 -0
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/sends/guard.d.ts +2 -0
- package/dist/sends/guard.js +8 -3
- package/dist/sends/index.d.ts +1 -1
- package/dist/sends/index.js +1 -1
- package/dist/sends/permissions.d.ts +5 -0
- package/dist/sends/permissions.js +11 -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/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.404.0 — 24.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`setupPermissionCeiling(env)` and `CEILING_KEY`** let a tool that keeps its own permission levels (zm, drive, memo) honour the shared first-run "read only" answer: add the ceiling to its levels under `CEILING_KEY`, and `levelFor` caps every key.
|
|
15
|
+
|
|
16
|
+
## 0.403.0 — 24.10.2026
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`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.
|
|
21
|
+
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
10
24
|
## 0.402.0 — 24.10.2026
|
|
11
25
|
|
|
12
26
|
### Changed — may break callers
|
|
@@ -17,6 +31,12 @@ shape of this file.
|
|
|
17
31
|
|
|
18
32
|
- **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.
|
|
19
33
|
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`watchMirrors` keeps folder mirrors in step.** One cycle per local save (after a quiet period, and only when the
|
|
37
|
+
files differ from the last sync), per remote change the CLI reports, per periodic local check and per full run;
|
|
38
|
+
each cycle syncs only the mirrors that need it, and a failed one is retried at the next poll.
|
|
39
|
+
|
|
20
40
|
## 0.401.0 — 24.10.2026
|
|
21
41
|
|
|
22
42
|
### Changed — may break callers
|
|
@@ -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, setupPermissionCeiling } 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, setupPermissionCeiling } 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
|
@@ -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)
|
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",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Level } from "../sends/permissions.js";
|
|
2
|
+
/** "allow" leaves every profile as its own configuration says. */
|
|
3
|
+
export declare const PERMISSION_ANSWERS: readonly ["readonly", "ask", "allow"];
|
|
4
|
+
export type PermissionAnswer = (typeof PERMISSION_ANSWERS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* The first-run answers every WireCat CLI shares. Languages live in the store (`search_stemmers`), so only the
|
|
7
|
+
* fact that they were asked is kept here.
|
|
8
|
+
*/
|
|
9
|
+
export interface SetupAnswers {
|
|
10
|
+
languages?: true;
|
|
11
|
+
tools?: true;
|
|
12
|
+
permissions?: PermissionAnswer;
|
|
13
|
+
}
|
|
14
|
+
export declare const QUESTIONS: readonly ["languages", "tools", "permissions"];
|
|
15
|
+
export type Question = (typeof QUESTIONS)[number];
|
|
16
|
+
export declare const readSetupAnswers: (path: string) => SetupAnswers;
|
|
17
|
+
export declare const unanswered: (answers: SetupAnswers) => Question[];
|
|
18
|
+
/** A section written wrongly is replaced; a file that is not JSON at all still refuses, naming itself. */
|
|
19
|
+
export declare const writeSetupAnswers: (path: string, answers: SetupAnswers) => void;
|
|
20
|
+
/**
|
|
21
|
+
* The level no command of any WireCat tool goes above, or `undefined` when the owner chose to leave them as they
|
|
22
|
+
* are. A file that cannot be read keeps everything read-only: reads still work, and `setup` writes it again.
|
|
23
|
+
*/
|
|
24
|
+
export declare const permissionCeiling: (path: string) => Level | undefined;
|
|
25
|
+
/** `permissionCeiling` of this machine's shared config, for a tool that keeps its own levels: add it as `CEILING_KEY`. */
|
|
26
|
+
export declare const setupPermissionCeiling: (env?: NodeJS.ProcessEnv) => Level | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { storePath } from "../store/path.js";
|
|
3
|
+
import { readSharedSection, sharedConfigPath, writeSharedSection } from "../store/shared-config.js";
|
|
4
|
+
/** "allow" leaves every profile as its own configuration says. */
|
|
5
|
+
export const PERMISSION_ANSWERS = ["readonly", "ask", "allow"];
|
|
6
|
+
export const QUESTIONS = ["languages", "tools", "permissions"];
|
|
7
|
+
const SECTION = "setup";
|
|
8
|
+
export const readSetupAnswers = (path) => {
|
|
9
|
+
const section = readSharedSection(path, SECTION);
|
|
10
|
+
if (section === undefined)
|
|
11
|
+
return {};
|
|
12
|
+
const answers = section;
|
|
13
|
+
if (section === null ||
|
|
14
|
+
typeof section !== "object" ||
|
|
15
|
+
Array.isArray(section) ||
|
|
16
|
+
(answers.languages !== undefined && answers.languages !== true) ||
|
|
17
|
+
(answers.tools !== undefined && answers.tools !== true) ||
|
|
18
|
+
(answers.permissions !== undefined && !PERMISSION_ANSWERS.includes(answers.permissions)))
|
|
19
|
+
throw new CliError("configuration_error", `${path}: ${SECTION} holds what setup asked — languages: true, tools: true, permissions: ${PERMISSION_ANSWERS.join(" | ")}`);
|
|
20
|
+
return answers;
|
|
21
|
+
};
|
|
22
|
+
export const unanswered = (answers) => QUESTIONS.filter((question) => answers[question] === undefined);
|
|
23
|
+
/** A section written wrongly is replaced; a file that is not JSON at all still refuses, naming itself. */
|
|
24
|
+
export const writeSetupAnswers = (path, answers) => {
|
|
25
|
+
let kept = {};
|
|
26
|
+
try {
|
|
27
|
+
kept = readSetupAnswers(path);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!(error instanceof CliError) || !error.message.includes(`${SECTION} holds`))
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
writeSharedSection(path, SECTION, { ...kept, ...answers });
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The level no command of any WireCat tool goes above, or `undefined` when the owner chose to leave them as they
|
|
37
|
+
* are. A file that cannot be read keeps everything read-only: reads still work, and `setup` writes it again.
|
|
38
|
+
*/
|
|
39
|
+
export const permissionCeiling = (path) => {
|
|
40
|
+
let answer;
|
|
41
|
+
try {
|
|
42
|
+
answer = readSetupAnswers(path).permissions;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return "readonly";
|
|
46
|
+
}
|
|
47
|
+
return answer === "allow" ? undefined : answer;
|
|
48
|
+
};
|
|
49
|
+
/** `permissionCeiling` of this machine's shared config, for a tool that keeps its own levels: add it as `CEILING_KEY`. */
|
|
50
|
+
export const setupPermissionCeiling = (env = process.env) => permissionCeiling(sharedConfigPath(storePath(env), env));
|
|
@@ -3,6 +3,7 @@ export { type Complete, extractMemories } from "../services/documents/extraction
|
|
|
3
3
|
export { documentIngestion, type IngestionOptions } from "../services/documents/ingestion.js";
|
|
4
4
|
export { type DocumentKnowledge, documentKnowledge } from "../services/documents/knowledge.js";
|
|
5
5
|
export { blocksMarkdown, inlineMarkdown, spliceMarkdown } from "../services/documents/markdown-splice.js";
|
|
6
|
+
export { type MirrorTrigger, type MirrorWatchEvent, type MirrorWatchOptions, watchMirrors, } from "../services/documents/mirror-watch.js";
|
|
6
7
|
export { conflictReport, fileMirrorState, type Mirror, type MirrorSyncOptions, mirrorId, mirrorStatus, syncMirrors, } from "../services/documents/mirrors.js";
|
|
7
8
|
export type { SyncOptions } from "../services/documents/ports.js";
|
|
8
9
|
export { extractDocumentMemories, type ImportedSource, type ProposalOptions } from "../services/documents/proposals.js";
|
package/dist/documents/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { extractMemories } from "../services/documents/extraction.js";
|
|
|
3
3
|
export { documentIngestion } from "../services/documents/ingestion.js";
|
|
4
4
|
export { documentKnowledge } from "../services/documents/knowledge.js";
|
|
5
5
|
export { blocksMarkdown, inlineMarkdown, spliceMarkdown } from "../services/documents/markdown-splice.js";
|
|
6
|
+
export { watchMirrors, } from "../services/documents/mirror-watch.js";
|
|
6
7
|
export { conflictReport, fileMirrorState, mirrorId, mirrorStatus, syncMirrors, } from "../services/documents/mirrors.js";
|
|
7
8
|
export { extractDocumentMemories } from "../services/documents/proposals.js";
|
|
8
9
|
export { planPush, pushDocuments, scanLocal, } from "../services/documents/push.js";
|
package/dist/sends/guard.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface SendGuardOptions {
|
|
|
71
71
|
now?: () => Date;
|
|
72
72
|
}
|
|
73
73
|
/** A write the profile's level refuses, worded alike by the guard and by the check before connecting. */
|
|
74
|
+
/** A write the first-run answer stops, whatever the profile allows. */
|
|
75
|
+
export declare const ceilingRefusal: (key: PermissionKey, command: string) => CliError;
|
|
74
76
|
export declare const levelRefusal: (refused: {
|
|
75
77
|
profile: string;
|
|
76
78
|
command: string;
|
package/dist/sends/guard.js
CHANGED
|
@@ -3,7 +3,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
3
3
|
import { MESSENGER_CHANGE_REFUSAL, readOnlyRefusal } from "../ports/app.js";
|
|
4
4
|
import { FloodMemory, floodPathFor, holdEnd } from "./flood.js";
|
|
5
5
|
import { SendJournal, sendsPathFor, } from "./journal.js";
|
|
6
|
-
import { keyForWrite, levelFor, skipFlagFor } from "./permissions.js";
|
|
6
|
+
import { CEILING_KEY, keyForWrite, levelFor, skipFlagFor } from "./permissions.js";
|
|
7
7
|
import { RecipientList, recipientsPathFor } from "./recipients.js";
|
|
8
8
|
const HOUR_MS = 60 * 60 * 1000;
|
|
9
9
|
/**
|
|
@@ -51,8 +51,13 @@ const weightOf = ({ kind = "message", action, count, people }) => kind === "dele
|
|
|
51
51
|
/** A scheduled message counts in the hour the provider sends it. */
|
|
52
52
|
const timeOf = (entry) => Date.parse(entry.scheduledFor ?? entry.at);
|
|
53
53
|
/** A write the profile's level refuses, worded alike by the guard and by the check before connecting. */
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/** A write the first-run answer stops, whatever the profile allows. */
|
|
55
|
+
export const ceilingRefusal = (key, command) => new CliError("permission_error", `${key} is not allowed: setup's answer keeps every WireCat tool read-only; ` +
|
|
56
|
+
`the owner changes it at a terminal with \`${command} setup\``, { permission: key });
|
|
57
|
+
export const levelRefusal = (refused, fix = `${refused.command} ${refused.profile} config set permissions.${refused.key} allow`) => refused.named === CEILING_KEY
|
|
58
|
+
? ceilingRefusal(refused.key, refused.command)
|
|
59
|
+
: new CliError("permission_error", `profile ${refused.profile} does not let ${refused.key} write (permissions.${refused.named} is ${refused.level}, ` +
|
|
60
|
+
`from the ${refused.permissionSources[refused.named ?? ""] ?? "default"}); to allow it: ${fix}`, { permission: refused.key });
|
|
56
61
|
/**
|
|
57
62
|
* These stop a model that was talked into sending by a message it read. They do **not** stop an
|
|
58
63
|
* agent that edits the configuration itself — that needs a boundary outside this process
|
package/dist/sends/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { type FloodDeadline, FloodMemory, type FloodState, FROZEN_HOLD_MS, flood
|
|
|
3
3
|
export { type Asker, type GuardRequest, guardFor, type SendGuard, type SendGuardOptions, sendGuard, sharedJournal, } from "./guard.js";
|
|
4
4
|
export { currentOperation, guardedWrite, type Operated } from "./guarded.js";
|
|
5
5
|
export { type AccountAction, type ChatAction, type SendEntry, SendJournal, type SendKind, type SendOutcome, sendsPathFor, } from "./journal.js";
|
|
6
|
-
export { assertStatsPermissionsCurrent, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, type Level, layerPermissions, levelFor, PERMISSIONS, type Permission, type PermissionKey, permissionFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
6
|
+
export { assertStatsPermissionsCurrent, CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, type Level, layerPermissions, levelFor, PERMISSIONS, type Permission, type PermissionKey, permissionFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
7
7
|
export { type Recipient, RecipientList, recipientsPathFor } from "./recipients.js";
|
|
8
8
|
export { newOperationId, newSendId } from "./send-id.js";
|
|
9
9
|
export { MAX_BUFFERED_UPLOAD_BYTES, readUpload, refusedPlace, uploadBlob, uploadBytes, uploadSize, } from "./upload.js";
|
package/dist/sends/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { FloodMemory, FROZEN_HOLD_MS, floodPathFor, LIMITED_HOLD_MS, } from "./f
|
|
|
2
2
|
export { guardFor, sendGuard, sharedJournal, } from "./guard.js";
|
|
3
3
|
export { currentOperation, guardedWrite } from "./guarded.js";
|
|
4
4
|
export { SendJournal, sendsPathFor, } from "./journal.js";
|
|
5
|
-
export { assertStatsPermissionsCurrent, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, layerPermissions, levelFor, PERMISSIONS, permissionFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
5
|
+
export { assertStatsPermissionsCurrent, CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, layerPermissions, levelFor, PERMISSIONS, permissionFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
6
6
|
export { RecipientList, recipientsPathFor } from "./recipients.js";
|
|
7
7
|
export { newOperationId, newSendId } from "./send-id.js";
|
|
8
8
|
export { MAX_BUFFERED_UPLOAD_BYTES, readUpload, refusedPlace, uploadBlob, uploadBytes, uploadSize, } from "./upload.js";
|
|
@@ -43,6 +43,11 @@ export declare const layerPermissions: (layers: readonly [string, Readonly<Recor
|
|
|
43
43
|
levels: Record<string, "allow" | "ask" | "deny" | "readonly">;
|
|
44
44
|
sources: Record<string, string>;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The owner's first-run answer, held among a profile's levels: no key of any profile goes above it. Only `setup`
|
|
48
|
+
* at a terminal writes it, so an agent cannot lift it with `config set`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const CEILING_KEY = "*";
|
|
46
51
|
/**
|
|
47
52
|
* The most specific key the owner set wins. A built-in default (`DEFAULT_PERMISSIONS`) only ever
|
|
48
53
|
* tightens: against a broader key of the owner's, the stricter of the two holds — so
|
|
@@ -207,6 +207,11 @@ export const layerPermissions = (layers) => {
|
|
|
207
207
|
}
|
|
208
208
|
return { levels, sources };
|
|
209
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* The owner's first-run answer, held among a profile's levels: no key of any profile goes above it. Only `setup`
|
|
212
|
+
* at a terminal writes it, so an agent cannot lift it with `config set`.
|
|
213
|
+
*/
|
|
214
|
+
export const CEILING_KEY = "*";
|
|
210
215
|
const ALLOWED = { level: "allow", key: null };
|
|
211
216
|
const ORDER = { deny: 0, readonly: 1, ask: 2, allow: 3 };
|
|
212
217
|
const nearest = (levels, key) => {
|
|
@@ -230,9 +235,14 @@ const FORMER_KEYS = { "chats.unmute": "chats.mute" };
|
|
|
230
235
|
* A path nothing names is allowed.
|
|
231
236
|
*/
|
|
232
237
|
export const levelFor = (permissions, key, defaults = DEFAULT_PERMISSIONS) => {
|
|
238
|
+
const found = profileLevel(permissions, key, defaults);
|
|
239
|
+
const ceiling = permissions[CEILING_KEY];
|
|
240
|
+
return ceiling && ORDER[ceiling] < ORDER[found.level] ? { level: ceiling, key: CEILING_KEY } : found;
|
|
241
|
+
};
|
|
242
|
+
const profileLevel = (permissions, key, defaults) => {
|
|
233
243
|
const former = FORMER_KEYS[key];
|
|
234
244
|
if (former !== undefined && permissions[key] === undefined)
|
|
235
|
-
return
|
|
245
|
+
return profileLevel(permissions, former, defaults);
|
|
236
246
|
const own = nearest(permissions, key);
|
|
237
247
|
const builtIn = nearest(defaults, key);
|
|
238
248
|
if (!builtIn || (own && own.depth >= builtIn.depth))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import type { PushStatePort } from "../../documents/push-model.js";
|
|
3
|
+
import { type Mirror } from "./mirrors.js";
|
|
4
|
+
/** What started a cycle: the first run, a local save, a remote change, the periodic local check, or a full run. */
|
|
5
|
+
export type MirrorTrigger = "start" | "local" | "remote" | "check" | "full";
|
|
6
|
+
export type MirrorWatchEvent<T> = {
|
|
7
|
+
triggers: MirrorTrigger[];
|
|
8
|
+
mirrors: string[];
|
|
9
|
+
result: T;
|
|
10
|
+
} | {
|
|
11
|
+
triggers: MirrorTrigger[];
|
|
12
|
+
mirrors: string[];
|
|
13
|
+
error: CliError;
|
|
14
|
+
};
|
|
15
|
+
export interface MirrorWatchOptions<S> {
|
|
16
|
+
mirrors: Mirror[];
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
kindOf(path: string): string | undefined;
|
|
19
|
+
stateOf(mirror: Mirror): PushStatePort<S>;
|
|
20
|
+
/** Ids of the mirrors whose remote files changed since the last call, or `all` when the provider cannot tell. */
|
|
21
|
+
remoteChanges?(signal: AbortSignal): Promise<string[] | "all">;
|
|
22
|
+
pollMs?: number;
|
|
23
|
+
quietMs?: number;
|
|
24
|
+
/** How often local files are compared with the state, for file events the system dropped. */
|
|
25
|
+
checkMs?: number;
|
|
26
|
+
/** How often every mirror is synced whatever the triggers said, for remote changes the provider listed late. */
|
|
27
|
+
fullMs?: number;
|
|
28
|
+
watchFolder?(root: string, changed: () => void): () => void;
|
|
29
|
+
sleep?(milliseconds: number, signal: AbortSignal): Promise<void>;
|
|
30
|
+
now?(): number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Keeps mirrors in sync until `signal` aborts: a cycle at start, then one for each mirror whose files were saved
|
|
34
|
+
* (after `quietMs` without another save), whose remote files changed, or whose files differ from the state at the
|
|
35
|
+
* periodic check; every mirror at each full run. A local trigger syncs only when the files differ from the state, so
|
|
36
|
+
* the files a pull writes do not start another cycle. A failed cycle is reported and its mirrors tried again at the
|
|
37
|
+
* next poll; the watch goes on.
|
|
38
|
+
*/
|
|
39
|
+
export declare function watchMirrors<S, T>(sync: (mirrors: Mirror[], signal: AbortSignal) => Promise<T>, options: MirrorWatchOptions<S>): AsyncGenerator<MirrorWatchEvent<T>, void, void>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
3
|
+
import { mirrorStatus } from "./mirrors.js";
|
|
4
|
+
import { skippedName } from "./push.js";
|
|
5
|
+
const sleep = (milliseconds, signal) => new Promise((resolve) => {
|
|
6
|
+
const finish = () => {
|
|
7
|
+
clearTimeout(timer);
|
|
8
|
+
signal.removeEventListener("abort", finish);
|
|
9
|
+
resolve();
|
|
10
|
+
};
|
|
11
|
+
const timer = setTimeout(finish, milliseconds);
|
|
12
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
13
|
+
});
|
|
14
|
+
const watchFolder = (root, changed) => {
|
|
15
|
+
try {
|
|
16
|
+
const watcher = watch(root, { recursive: true }, (_event, name) => {
|
|
17
|
+
if (name === null || !name.split(/[\\/]/).some(skippedName))
|
|
18
|
+
changed();
|
|
19
|
+
});
|
|
20
|
+
// A lost watcher only delays a sync until the periodic check; it never stops the watch.
|
|
21
|
+
watcher.on("error", () => watcher.close());
|
|
22
|
+
return () => watcher.close();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return () => { };
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const failure = (error) => isCliError(error) ? error : new CliError("provider_error", "the sync cycle failed");
|
|
29
|
+
/**
|
|
30
|
+
* Keeps mirrors in sync until `signal` aborts: a cycle at start, then one for each mirror whose files were saved
|
|
31
|
+
* (after `quietMs` without another save), whose remote files changed, or whose files differ from the state at the
|
|
32
|
+
* periodic check; every mirror at each full run. A local trigger syncs only when the files differ from the state, so
|
|
33
|
+
* the files a pull writes do not start another cycle. A failed cycle is reported and its mirrors tried again at the
|
|
34
|
+
* next poll; the watch goes on.
|
|
35
|
+
*/
|
|
36
|
+
export async function* watchMirrors(sync, options) {
|
|
37
|
+
const { mirrors, signal } = options;
|
|
38
|
+
const pollMs = options.pollMs ?? 60_000;
|
|
39
|
+
const quietMs = options.quietMs ?? 2_000;
|
|
40
|
+
const checkMs = options.checkMs ?? 300_000;
|
|
41
|
+
const fullMs = options.fullMs ?? 1_800_000;
|
|
42
|
+
const now = options.now ?? Date.now;
|
|
43
|
+
const wait = options.sleep ?? sleep;
|
|
44
|
+
const byId = new Map(mirrors.map((mirror) => [mirror.id, mirror]));
|
|
45
|
+
const saved = new Map();
|
|
46
|
+
const retry = new Set();
|
|
47
|
+
let wake = new AbortController();
|
|
48
|
+
const closers = mirrors.map((mirror) => (options.watchFolder ?? watchFolder)(mirror.path, () => {
|
|
49
|
+
saved.set(mirror.id, now());
|
|
50
|
+
wake.abort();
|
|
51
|
+
}));
|
|
52
|
+
const differs = async (ids) => {
|
|
53
|
+
const chosen = [...ids].flatMap((id) => byId.get(id) ?? []);
|
|
54
|
+
try {
|
|
55
|
+
const items = await mirrorStatus(chosen, options.kindOf, options.stateOf);
|
|
56
|
+
return items.filter((item) => item.create.length || item.update.length).map((item) => item.id);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// An unreadable folder is for the sync to report.
|
|
60
|
+
return chosen.map((mirror) => mirror.id);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const run = async (triggers, ids) => {
|
|
64
|
+
const chosen = mirrors.filter((mirror) => ids.has(mirror.id));
|
|
65
|
+
const names = chosen.map((mirror) => mirror.id);
|
|
66
|
+
try {
|
|
67
|
+
return { triggers, mirrors: names, result: await sync(chosen, signal) };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
for (const id of names)
|
|
71
|
+
retry.add(id);
|
|
72
|
+
return { triggers, mirrors: names, error: failure(error) };
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
if (signal.aborted)
|
|
77
|
+
return;
|
|
78
|
+
const first = await run(["start"], new Set(byId.keys()));
|
|
79
|
+
// A cycle cut short by the stop is the stop, not a failure.
|
|
80
|
+
if (signal.aborted && "error" in first)
|
|
81
|
+
return;
|
|
82
|
+
yield first;
|
|
83
|
+
let nextPoll = now() + pollMs;
|
|
84
|
+
let nextCheck = now() + checkMs;
|
|
85
|
+
let nextFull = now() + fullMs;
|
|
86
|
+
while (!signal.aborted) {
|
|
87
|
+
let due = Math.min(nextPoll, nextCheck, nextFull);
|
|
88
|
+
for (const at of saved.values())
|
|
89
|
+
due = Math.min(due, at + quietMs);
|
|
90
|
+
const until = due - now();
|
|
91
|
+
if (until > 0) {
|
|
92
|
+
await wait(until, AbortSignal.any([signal, wake.signal]));
|
|
93
|
+
if (wake.signal.aborted)
|
|
94
|
+
wake = new AbortController();
|
|
95
|
+
if (signal.aborted)
|
|
96
|
+
return;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const at = now();
|
|
100
|
+
const triggers = [];
|
|
101
|
+
const ids = new Set();
|
|
102
|
+
const add = (trigger, found) => {
|
|
103
|
+
const before = ids.size;
|
|
104
|
+
for (const id of found)
|
|
105
|
+
ids.add(id);
|
|
106
|
+
if (ids.size > before && !triggers.includes(trigger))
|
|
107
|
+
triggers.push(trigger);
|
|
108
|
+
};
|
|
109
|
+
if (at >= nextFull) {
|
|
110
|
+
add("full", byId.keys());
|
|
111
|
+
nextFull = at + fullMs;
|
|
112
|
+
nextCheck = at + checkMs;
|
|
113
|
+
nextPoll = at + pollMs;
|
|
114
|
+
saved.clear();
|
|
115
|
+
retry.clear();
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const settled = [...saved].filter(([, saveAt]) => saveAt + quietMs <= at).map(([id]) => id);
|
|
119
|
+
for (const id of settled)
|
|
120
|
+
saved.delete(id);
|
|
121
|
+
if (settled.length)
|
|
122
|
+
add("local", await differs(settled));
|
|
123
|
+
if (at >= nextCheck) {
|
|
124
|
+
add("check", await differs(byId.keys()));
|
|
125
|
+
nextCheck = at + checkMs;
|
|
126
|
+
}
|
|
127
|
+
if (at >= nextPoll) {
|
|
128
|
+
nextPoll = at + pollMs;
|
|
129
|
+
add("remote", retry);
|
|
130
|
+
retry.clear();
|
|
131
|
+
if (options.remoteChanges)
|
|
132
|
+
try {
|
|
133
|
+
const changed = await options.remoteChanges(signal);
|
|
134
|
+
add("remote", changed === "all" ? byId.keys() : changed.filter((id) => byId.has(id)));
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (signal.aborted)
|
|
138
|
+
return;
|
|
139
|
+
yield { triggers: ["remote"], mirrors: [], error: failure(error) };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (signal.aborted || !ids.size)
|
|
144
|
+
continue;
|
|
145
|
+
const event = await run(triggers, ids);
|
|
146
|
+
if (signal.aborted && "error" in event)
|
|
147
|
+
return;
|
|
148
|
+
yield event;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
for (const close of closers)
|
|
153
|
+
close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -60,6 +60,8 @@ export interface PushResult {
|
|
|
60
60
|
};
|
|
61
61
|
stopped?: true;
|
|
62
62
|
}
|
|
63
|
+
/** Hidden files, `node_modules` and editor leftovers: never mirrored, and never a reason to sync. */
|
|
64
|
+
export declare const skippedName: (name: string) => boolean;
|
|
63
65
|
/** Files under `root` the target pushes. Hidden files, editor leftovers and symbolic links are never followed. */
|
|
64
66
|
export declare const scanLocal: (root: string, kindOf: (path: string) => string | undefined, maxFiles?: number) => Promise<{
|
|
65
67
|
files: LocalDocument[];
|
|
@@ -4,6 +4,8 @@ import { chmod, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { basename, dirname, join, posix } from "node:path";
|
|
5
5
|
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
6
6
|
const SKIPPED = /^(\.|node_modules$|~\$)|(~|\.swp|\.tmp)$/;
|
|
7
|
+
/** Hidden files, `node_modules` and editor leftovers: never mirrored, and never a reason to sync. */
|
|
8
|
+
export const skippedName = (name) => SKIPPED.test(name);
|
|
7
9
|
const hashFile = (path) => new Promise((done, fail) => {
|
|
8
10
|
const hash = createHash("sha256");
|
|
9
11
|
createReadStream(path)
|
|
@@ -12,6 +12,10 @@ export declare const readTaskCategoryEdits: (path: string) => TaskCategoryEdits
|
|
|
12
12
|
export declare const applyTaskCategoryEdits: (base: readonly TaskCategory[], edits: TaskCategoryEdits, path: string) => readonly TaskCategory[];
|
|
13
13
|
/** The edits that turn the default list into `list`, for moving a list an older build saved in the store. */
|
|
14
14
|
export declare const editsFrom: (list: readonly TaskCategory[]) => TaskCategoryEdits;
|
|
15
|
+
/** One section of the file, or `undefined` when the file or the section is missing. */
|
|
16
|
+
export declare const readSharedSection: (path: string, section: string) => unknown;
|
|
17
|
+
/** Writes one section (or drops it, with `undefined`), keeping every other key of the file. */
|
|
18
|
+
export declare const writeSharedSection: (path: string, section: string, value: unknown) => void;
|
|
15
19
|
/** Writes the section (or drops it, with `undefined`), keeping every other key of the file. */
|
|
16
20
|
export declare const writeTaskCategoryEdits: (path: string, edits: TaskCategoryEdits | undefined) => void;
|
|
17
21
|
/** Read again only when the file changed: a sync asks for the list once per message. */
|
|
@@ -39,7 +39,7 @@ const readJson = (path) => {
|
|
|
39
39
|
};
|
|
40
40
|
/** The `task_categories` section, or `undefined` when the file or the section is missing. */
|
|
41
41
|
export const readTaskCategoryEdits = (path) => {
|
|
42
|
-
const section =
|
|
42
|
+
const section = readSharedSection(path, SECTION);
|
|
43
43
|
if (section === undefined)
|
|
44
44
|
return undefined;
|
|
45
45
|
if (section === null || typeof section !== "object" || Array.isArray(section))
|
|
@@ -77,18 +77,22 @@ export const editsFrom = (list) => {
|
|
|
77
77
|
}
|
|
78
78
|
return edits;
|
|
79
79
|
};
|
|
80
|
-
/**
|
|
81
|
-
export const
|
|
80
|
+
/** One section of the file, or `undefined` when the file or the section is missing. */
|
|
81
|
+
export const readSharedSection = (path, section) => readJson(path)?.[section];
|
|
82
|
+
/** Writes one section (or drops it, with `undefined`), keeping every other key of the file. */
|
|
83
|
+
export const writeSharedSection = (path, section, value) => {
|
|
82
84
|
const file = readJson(path) ?? {};
|
|
83
|
-
if (
|
|
84
|
-
delete file[
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
delete file[section];
|
|
85
87
|
else
|
|
86
|
-
file[
|
|
88
|
+
file[section] = value;
|
|
87
89
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
88
90
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
89
91
|
writeFileSync(temporary, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
90
92
|
renameSync(temporary, path);
|
|
91
93
|
};
|
|
94
|
+
/** Writes the section (or drops it, with `undefined`), keeping every other key of the file. */
|
|
95
|
+
export const writeTaskCategoryEdits = (path, edits) => writeSharedSection(path, SECTION, edits === undefined || Object.keys(edits).length === 0 ? undefined : edits);
|
|
92
96
|
/** Read again only when the file changed: a sync asks for the list once per message. */
|
|
93
97
|
export const taskCategoriesReader = (path) => {
|
|
94
98
|
let seen;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.404.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|