@wirecat/cli-core 0.413.0 → 0.415.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 +27 -1
- package/dist/cli/entry.js +1 -0
- package/dist/cli/first-run.d.ts +3 -0
- package/dist/cli/first-run.js +8 -5
- package/dist/cli/help/command-help.js +8 -5
- package/dist/cli/help/first-screen.js +3 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/messenger/context.d.ts +1 -1
- package/dist/cli/messenger/context.js +6 -6
- package/dist/cli/messenger/store-maintenance-command.js +1 -1
- package/dist/cli/moved.d.ts +4 -0
- package/dist/cli/moved.js +16 -0
- package/dist/cli/program.js +4 -12
- package/dist/http/public-url.js +2 -0
- package/dist/mcp/agent.js +4 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/server.js +1 -1
- package/dist/ports/app.d.ts +5 -0
- package/dist/store/migrations.d.ts +4 -2
- package/dist/store/migrations.js +16 -2
- package/dist/store/people.js +4 -1
- package/dist/store/sqlite/clients.d.ts +26 -0
- package/dist/store/sqlite/clients.js +79 -0
- package/dist/store/sqlite/open-store.d.ts +5 -1
- package/dist/store/sqlite/open-store.js +5 -3
- package/dist/store/sqlite/person-links.d.ts +6 -0
- package/dist/store/sqlite/person-links.js +27 -0
- package/dist/store/sqlite/read-compatibility.d.ts +1 -1
- package/dist/store/sqlite/read-compatibility.js +3 -2
- package/dist/store/types.d.ts +2 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,33 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 0.415.0 — 25.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`movedCommandError(program, args)` from `/cli`** answers a renamed or moved command path with the pointer `run` gives ("`drive models text download` is not here — use `models download`"), for a CLI that parses without `run`. drive is the one today.
|
|
13
|
+
- **The store records which CLIs open it, so an upgrade that locks some out names them** (available from cli-core 0.415.0). `openStore({ command, version })` saves each CLI's command, version, cli-core version and the newest store schema it speaks in `store_settings` (`clients`). A migration that raises the store's minimum prints one line to stderr naming each recorded CLI it locks out and the command to run (`memo upgrade`); a CLI refused by a newer store now says `run \`<its command> upgrade\`` and which CLI moved the store. The messenger context, the MCP server and `store migrate` pass the CLI's version already; other CLIs pass `version` to `openStore`.
|
|
14
|
+
|
|
15
|
+
### Changed — may break callers
|
|
16
|
+
|
|
17
|
+
- **cli-core needs cli-base 0.53.** The peer range is now `>=0.53.0 <0.54.0`, so a CLI moves its cli-base pin to 0.53.0 together with this release. cli-base 0.53.0 makes the command lint require a query declaration on every handwritten list (`WC-CMD-20`); its runtime API is unchanged.
|
|
18
|
+
|
|
19
|
+
## 0.414.0 — 25.10.2026
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `AppIdentity.contentLanguages`: ISO 639-1 codes of the languages a CLI's content is in. On a new store the first run pre-ticks the ones search can stem in place of English and the system's language; the person still confirms the shared choice. Unset, nothing changes.
|
|
24
|
+
- Export `publicUrlOf` and `portOf` through `/mcp`, so non-messenger consumers reuse the same HTTP listener option validation as the messenger commands.
|
|
25
|
+
- **A contact saved with an address joins the mail contact of that exact address.** `savePeople` takes an optional
|
|
26
|
+
`email`; the provider's contact and the mail contact become one person (`same-email`), whichever is saved first.
|
|
27
|
+
A name never joins anyone, and two identities the owner unlinked stay apart.
|
|
28
|
+
- **The first screen of `tg --help`, `max --help` and `wa --help` points at `account list`.** A row under "Start here" names `<tool> account list`, which lists every profile on this computer without a profile in front, so a reader who does not know their profile names finds them. A tool without `account list` shows no row.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Generated MCP commands accept a positional argument or its same-named flag alternative once. A single `text` input no longer emits both the argument and `--text`, which made Memo note creation fail. Positional requiredness is retained.
|
|
33
|
+
- Public listener origins refuse embedded credentials before startup diagnostics can expose them.
|
|
34
|
+
- **"Returns" lists the fields of commands whose output has several shapes.** A command's help and the generated `docs/commands.md` showed `message send — ; every field: …` with no fields for every output declared as a union (`message_send`, `store_info`, `chat_group_change` and others; 41 lines in tg-cli's page). The line now lists the fields of every shape, required first, as for any other output.
|
|
9
35
|
|
|
10
36
|
## 0.413.0 — 25.10.2026
|
|
11
37
|
|
package/dist/cli/entry.js
CHANGED
|
@@ -61,6 +61,7 @@ export const startCli = async (entry) => {
|
|
|
61
61
|
atStart: (command) => setupAtStart({
|
|
62
62
|
command: entry.app.command,
|
|
63
63
|
env: io.env,
|
|
64
|
+
contentLanguages: entry.app.contentLanguages,
|
|
64
65
|
path: commandPathOf(command),
|
|
65
66
|
headless: headless(command),
|
|
66
67
|
readOnlyLock: readOnlyLockOf(entry.app, io.env),
|
package/dist/cli/first-run.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface FirstRun {
|
|
|
25
25
|
command: string;
|
|
26
26
|
env: NodeJS.ProcessEnv;
|
|
27
27
|
io: FirstRunIo;
|
|
28
|
+
/** `AppIdentity.contentLanguages`: ticked on a new store in place of English and the system's language. */
|
|
29
|
+
contentLanguages?: readonly string[] | undefined;
|
|
28
30
|
/** Every question, as `setup` asks them; otherwise only those no WireCat tool asked yet. */
|
|
29
31
|
all?: boolean;
|
|
30
32
|
/** Where tools are looked for and how one is installed; injected by tests. */
|
|
@@ -58,6 +60,7 @@ export declare const askSetupQuestions: (run: FirstRun) => Promise<Question[]>;
|
|
|
58
60
|
export declare const setupAtStart: (start: {
|
|
59
61
|
command: string;
|
|
60
62
|
env: NodeJS.ProcessEnv;
|
|
63
|
+
contentLanguages?: readonly string[] | undefined;
|
|
61
64
|
path: readonly string[];
|
|
62
65
|
headless: boolean;
|
|
63
66
|
readOnlyLock?: string | undefined;
|
package/dist/cli/first-run.js
CHANGED
|
@@ -85,17 +85,19 @@ const installerHere = () => {
|
|
|
85
85
|
return "unknown";
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
|
-
const
|
|
88
|
+
const suggested = (env, contentLanguages) => {
|
|
89
|
+
const own = LANGUAGES.filter(({ code }) => contentLanguages?.includes(code));
|
|
90
|
+
const system = systemLanguage(env);
|
|
91
|
+
return (own.length ? own : LANGUAGES.filter(({ code }) => code === "en" || code === system)).map(({ name }) => name);
|
|
92
|
+
};
|
|
93
|
+
const askLanguages = async ({ command, env, io, contentLanguages }) => {
|
|
89
94
|
const store = await openStore({ env });
|
|
90
95
|
let rebuild = false;
|
|
91
96
|
try {
|
|
92
97
|
const saved = (await store.searchIndex.stemmers()) ?? undefined;
|
|
93
98
|
// A store already in use keeps the languages it was built with, so Enter changes nothing (RISK-1612).
|
|
94
99
|
const inUse = saved !== undefined || (await store.accounts.accounts()).length > 0;
|
|
95
|
-
const
|
|
96
|
-
const ticked = inUse
|
|
97
|
-
? languagesOf(saved ?? DEFAULT_STEMMERS)
|
|
98
|
-
: LANGUAGES.filter(({ code }) => code === "en" || code === system).map(({ name }) => name);
|
|
100
|
+
const ticked = inUse ? languagesOf(saved ?? DEFAULT_STEMMERS) : suggested(env, contentLanguages);
|
|
99
101
|
const numbers = ticked.map((name) => LANGUAGES.findIndex((one) => one.name === name) + 1);
|
|
100
102
|
io.say("Languages you write and speak — search finds every form of their words, and spellings speech mangles:");
|
|
101
103
|
LANGUAGES.forEach(({ name, label }, at) => {
|
|
@@ -213,6 +215,7 @@ export const setupAtStart = async (start) => {
|
|
|
213
215
|
await askSetupQuestions({
|
|
214
216
|
command: start.command,
|
|
215
217
|
env: start.env,
|
|
218
|
+
contentLanguages: start.contentLanguages,
|
|
216
219
|
all: start.path[0] === "setup",
|
|
217
220
|
io: {
|
|
218
221
|
ask: (prompt) => readSecret(prompt, { input, echo: true }),
|
|
@@ -100,16 +100,19 @@ const call = (command) => [
|
|
|
100
100
|
...commandPathOf(command),
|
|
101
101
|
...command.registeredArguments.map((one) => (one.required ? `<${one.name()}>` : `[${one.name()}]`)),
|
|
102
102
|
].join(" ");
|
|
103
|
+
const fieldsOf = (schema) => {
|
|
104
|
+
const variants = schema.anyOf;
|
|
105
|
+
if (variants)
|
|
106
|
+
return variants.flatMap(fieldsOf);
|
|
107
|
+
const required = schema.required ?? [];
|
|
108
|
+
return [...required, ...Object.keys(schema.properties ?? {})];
|
|
109
|
+
};
|
|
103
110
|
const returnsOf = (command) => {
|
|
104
111
|
const output = metaOf(command).output;
|
|
105
112
|
const { item, coverage } = outputOf(command);
|
|
106
113
|
if (!output || coverage !== "declared-entity")
|
|
107
114
|
return short(command.summary() || command.description()) || "not declared";
|
|
108
|
-
const
|
|
109
|
-
const fields = [
|
|
110
|
-
...required,
|
|
111
|
-
...Object.keys(item.properties ?? {}).filter((name) => !required.includes(name)),
|
|
112
|
-
];
|
|
115
|
+
const fields = [...new Set(fieldsOf(item))];
|
|
113
116
|
const noun = (output.view ? `${output.entity} ${output.view}` : output.entity).replaceAll("_", " ");
|
|
114
117
|
const more = fields.length > 8 ? `, and ${fields.length - 8} more` : "";
|
|
115
118
|
const root = rootOf(command);
|
|
@@ -64,6 +64,9 @@ const pointers = (program, cli, start, profile) => {
|
|
|
64
64
|
].filter((row) => row !== undefined);
|
|
65
65
|
const rows = [
|
|
66
66
|
...(start ? [["Start here:", start, profile ? "(any profile name works)" : ""]] : []),
|
|
67
|
+
...(has("account list")
|
|
68
|
+
? [["", `${cli} account list`, "your profiles and their logged-in accounts"]]
|
|
69
|
+
: []),
|
|
67
70
|
...next.map(([command = "", does = ""], at) => [at === 0 ? "Next:" : "", command, does]),
|
|
68
71
|
...(has("skill show") ? [["For agents:", `${cli} skill show`, "rules and the discovery loop"]] : []),
|
|
69
72
|
];
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export type { Running, ServerOptions, ServerProcess, ServerSystem } from "./mess
|
|
|
96
96
|
export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
97
97
|
export { type Saving, stored } from "./messenger/stored.js";
|
|
98
98
|
export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
|
|
99
|
+
export { movedCommandError } from "./moved.js";
|
|
99
100
|
export { window, withPaging } from "./paging.js";
|
|
100
101
|
export { knownBeside, knownPermissionKeys, type PermissionKeyOf, unknownPermissionKeys } from "./permission-keys.js";
|
|
101
102
|
export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -87,6 +87,7 @@ export { STEMS_EVERY_MS, STEMS_SLICE_MS, stemFills } from "./messenger/serve-ste
|
|
|
87
87
|
export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
88
88
|
export { stored } from "./messenger/stored.js";
|
|
89
89
|
export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
|
|
90
|
+
export { movedCommandError } from "./moved.js";
|
|
90
91
|
export { window, withPaging } from "./paging.js";
|
|
91
92
|
export { knownBeside, knownPermissionKeys, unknownPermissionKeys } from "./permission-keys.js";
|
|
92
93
|
export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
|
|
@@ -47,7 +47,7 @@ export interface LazyStore {
|
|
|
47
47
|
open: () => Promise<MessageStore>;
|
|
48
48
|
close: () => Promise<void>;
|
|
49
49
|
}
|
|
50
|
-
export declare const lazyStore: (env: NodeJS.ProcessEnv, command: string) => LazyStore;
|
|
50
|
+
export declare const lazyStore: (env: NodeJS.ProcessEnv, command: string, version?: string) => LazyStore;
|
|
51
51
|
export declare const storedMessages: (provider: Provider, store: () => Promise<MessageStore>, account: () => Id | null | undefined) => StoredMessages;
|
|
52
52
|
/** What one messenger CLI hands the shared commands. Everything else about it stays in its own code. */
|
|
53
53
|
export interface Messenger extends ServiceMessenger {
|
|
@@ -17,11 +17,11 @@ import { flooded } from "./flooded.js";
|
|
|
17
17
|
import { refuseLacking } from "./lacks.js";
|
|
18
18
|
import { observed } from "./observed.js";
|
|
19
19
|
import { stored } from "./stored.js";
|
|
20
|
-
export const lazyStore = (env, command) => {
|
|
20
|
+
export const lazyStore = (env, command, version) => {
|
|
21
21
|
let opened;
|
|
22
22
|
return {
|
|
23
23
|
open: () => {
|
|
24
|
-
opened ??= openStore({ env, command });
|
|
24
|
+
opened ??= openStore({ env, command, ...(version ? { version } : {}) });
|
|
25
25
|
return opened;
|
|
26
26
|
},
|
|
27
27
|
close: async () => {
|
|
@@ -195,7 +195,7 @@ const waitsOf = (command) => {
|
|
|
195
195
|
* does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
|
|
196
196
|
* A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
|
|
197
197
|
*/
|
|
198
|
-
export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, unreadCounts, }, { settings, env, renderer }, events, store = lazyStore(env, app.command), { listen: watching = false, closeWaitMs = SAVES_WAIT_MS, rekeyPacing, } = {}) => {
|
|
198
|
+
export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, unreadCounts, }, { settings, env, renderer }, events, store = lazyStore(env, app.command, app.version), { listen: watching = false, closeWaitMs = SAVES_WAIT_MS, rekeyPacing, } = {}) => {
|
|
199
199
|
const self = connection.self();
|
|
200
200
|
if (self !== null)
|
|
201
201
|
rememberAccount(app, settings.profile, self, env);
|
|
@@ -268,7 +268,7 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
|
|
|
268
268
|
};
|
|
269
269
|
/** `connectFor` and then `connected`, over one store handle: what the reads save and what the adapter looks up. */
|
|
270
270
|
export const connectedFor = async (command, messenger, base, options) => {
|
|
271
|
-
const store = lazyStore(base.env, messenger.app.command);
|
|
271
|
+
const store = lazyStore(base.env, messenger.app.command, messenger.app.version);
|
|
272
272
|
let connection;
|
|
273
273
|
try {
|
|
274
274
|
connection = await connectFor(command, messenger, base, {
|
|
@@ -392,7 +392,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
|
|
|
392
392
|
}, { unbounded: options.listen === true }),
|
|
393
393
|
withStore: (work, { name } = {}) => base.run(async () => {
|
|
394
394
|
const account = accountFor();
|
|
395
|
-
const store = await openStore({ env: base.env, command: app.command });
|
|
395
|
+
const store = await openStore({ env: base.env, command: app.command, version: app.version });
|
|
396
396
|
try {
|
|
397
397
|
return await work(store, account);
|
|
398
398
|
}
|
|
@@ -406,7 +406,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
|
|
|
406
406
|
let warned;
|
|
407
407
|
let released;
|
|
408
408
|
const releaseConnection = () => (released ??= held?.close() ?? Promise.resolve());
|
|
409
|
-
const openOnce = () => (store ??= openStore({ env: base.env, command: app.command }));
|
|
409
|
+
const openOnce = () => (store ??= openStore({ env: base.env, command: app.command, version: app.version }));
|
|
410
410
|
const deps = {
|
|
411
411
|
messenger,
|
|
412
412
|
profile,
|
|
@@ -375,7 +375,7 @@ const migrateCommand = (messenger) => annotate(new Command("migrate"), { output:
|
|
|
375
375
|
}
|
|
376
376
|
const answer = await reading(path, (database) => {
|
|
377
377
|
const from = schemaOf(database).version;
|
|
378
|
-
migrate(database, { command: messenger.app.command });
|
|
378
|
+
migrate(database, { command: messenger.app.command, version: messenger.app.version });
|
|
379
379
|
const { normalized, indexed, terms } = buildWordIndex(database, (note) => renderer.note(note));
|
|
380
380
|
const stemmed = buildStems(database, (note) => renderer.note(note));
|
|
381
381
|
const notes = buildNoteIndex(database, (note) => renderer.note(note));
|
package/dist/cli/moved.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import type { Command } from "commander";
|
|
1
3
|
/**
|
|
2
4
|
* Commands a source tool no longer has, each with the memo command that does the job
|
|
3
5
|
* (docs/dev/TOOL-BOUNDARIES.md). The CLI and the MCP server answer with the pointer instead of
|
|
@@ -12,3 +14,5 @@ export declare const RENAMED_COMMANDS: Readonly<Record<string, string>>;
|
|
|
12
14
|
export declare const movedPointer: (path: string) => string | undefined;
|
|
13
15
|
/** The longest moved path the words start with, unless the program still has it. */
|
|
14
16
|
export declare const movedPath: (words: readonly string[], has: (path: string) => boolean) => string | undefined;
|
|
17
|
+
/** The pointer for a moved or renamed path in `args`, for a CLI that parses without `run`. */
|
|
18
|
+
export declare const movedCommandError: (program: Command, args: readonly string[]) => CliError | undefined;
|
package/dist/cli/moved.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { commandAt } from "./steps.js";
|
|
1
3
|
/**
|
|
2
4
|
* Commands a source tool no longer has, each with the memo command that does the job
|
|
3
5
|
* (docs/dev/TOOL-BOUNDARIES.md). The CLI and the MCP server answer with the pointer instead of
|
|
@@ -54,3 +56,17 @@ export const movedPath = (words, has) => [...Object.keys(MOVED_TO_MEMO), ...Obje
|
|
|
54
56
|
.filter((path) => path.split(" ").every((word, at) => words[at] === word))
|
|
55
57
|
.sort((a, b) => b.length - a.length)
|
|
56
58
|
.find((path) => !has(path));
|
|
59
|
+
const mountedOrArgument = (program, path) => {
|
|
60
|
+
const words = path.split(" ");
|
|
61
|
+
return words.some((_, at) => {
|
|
62
|
+
const command = commandAt(program, words.slice(0, at + 1).join(" "));
|
|
63
|
+
return command !== undefined && (at === words.length - 1 || command.commands.length === 0);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
/** The pointer for a moved or renamed path in `args`, for a CLI that parses without `run`. */
|
|
67
|
+
export const movedCommandError = (program, args) => {
|
|
68
|
+
const moved = movedPath(args.filter((word) => !word.startsWith("-")), (path) => mountedOrArgument(program, path));
|
|
69
|
+
return moved === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: new CliError("validation_error", `\`${program.name()} ${moved}\` is not here — ${movedPointer(moved)}`);
|
|
72
|
+
};
|
package/dist/cli/program.js
CHANGED
|
@@ -25,7 +25,7 @@ import { installHelp } from "./help/first-screen.js";
|
|
|
25
25
|
import { showInputFields } from "./input-fields.js";
|
|
26
26
|
import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
|
|
27
27
|
import { checkJson, liftJson } from "./json-fields.js";
|
|
28
|
-
import {
|
|
28
|
+
import { movedCommandError } from "./moved.js";
|
|
29
29
|
import { PreviewComplete, preview } from "./preview.js";
|
|
30
30
|
import { commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
|
|
31
31
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
@@ -267,6 +267,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
267
267
|
await setupAtStart({
|
|
268
268
|
command,
|
|
269
269
|
env: commandEnv,
|
|
270
|
+
contentLanguages: definition.app.contentLanguages,
|
|
270
271
|
path,
|
|
271
272
|
headless,
|
|
272
273
|
readOnlyLock: lock,
|
|
@@ -420,10 +421,8 @@ const runOnce = async (argv, definition, options) => {
|
|
|
420
421
|
const settled = await settleFailure(failure, { definition, program, rest, profile, options });
|
|
421
422
|
return report(streams, reporting, failed(definition, program, rest, failure, settled, debug));
|
|
422
423
|
}
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
if (moved !== undefined) {
|
|
426
|
-
const failure = new CliError("validation_error", `\`${command} ${moved}\` is not here — ${movedPointer(moved)}`);
|
|
424
|
+
const failure = movedCommandError(program, rest);
|
|
425
|
+
if (failure !== undefined) {
|
|
427
426
|
const settled = await settleFailure(failure, { definition, program, rest, profile, options });
|
|
428
427
|
return report(streams, reporting, failed(definition, program, rest, failure, settled, debug));
|
|
429
428
|
}
|
|
@@ -590,13 +589,6 @@ const keepFailure = async (failure, { definition, program, rest, profile, option
|
|
|
590
589
|
}).catch(() => { });
|
|
591
590
|
};
|
|
592
591
|
/** Mounted, or reached through a command that takes no subcommand, where the rest of the path is its argument. */
|
|
593
|
-
const mountedOrArgument = (program, path) => {
|
|
594
|
-
const words = path.split(" ");
|
|
595
|
-
return words.some((_, at) => {
|
|
596
|
-
const command = commandAt(program, words.slice(0, at + 1).join(" "));
|
|
597
|
-
return command !== undefined && (at === words.length - 1 || command.commands.length === 0);
|
|
598
|
-
});
|
|
599
|
-
};
|
|
600
592
|
/** `messages list` from `messages list 111 --limit 5`: the words that name commands, up to the first that does not. */
|
|
601
593
|
const commandPath = (program, rest) => {
|
|
602
594
|
const words = [];
|
package/dist/http/public-url.js
CHANGED
|
@@ -14,6 +14,8 @@ export const publicUrlOf = (given, { missing, reaches }) => {
|
|
|
14
14
|
catch {
|
|
15
15
|
throw new CliError("validation_error", `--public-url is not an address: ${EXAMPLE}`);
|
|
16
16
|
}
|
|
17
|
+
if (url.username || url.password)
|
|
18
|
+
throw new CliError("validation_error", "--public-url must not contain login credentials");
|
|
17
19
|
const local = url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
|
|
18
20
|
if (url.protocol !== "https:" && !local)
|
|
19
21
|
throw new CliError("validation_error", `--public-url must be https — ${reaches} through it`);
|
package/dist/mcp/agent.js
CHANGED
|
@@ -111,7 +111,10 @@ const fieldsOf = (info, { omit = [], offline = false, stdin }) => {
|
|
|
111
111
|
const long = longOf(option);
|
|
112
112
|
if (!long || option.mcp === false)
|
|
113
113
|
continue;
|
|
114
|
-
|
|
114
|
+
const field = optionField(option, long, longs.has(`--no-${long.slice(2)}`));
|
|
115
|
+
// A positional and its flag alternative share one MCP input; emitting both supplies the value twice.
|
|
116
|
+
if (!fields.some((existing) => existing.name === field.name))
|
|
117
|
+
fields.push(field);
|
|
115
118
|
}
|
|
116
119
|
if (offline && info.effects?.remote === "read" && info.offline !== false)
|
|
117
120
|
fields.push(OFFLINE);
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { portOf, publicUrlOf } from "../http/public-url.js";
|
|
1
2
|
export { type AgentAnswer, type AgentContext, type AgentProgram, type AgentTool, type CatalogueOptions, type CommandAvailability, runCommandForAgent, toolsFromCommands, } from "./agent.js";
|
|
2
3
|
export { type BotServerOptions, createBotServer, type RunBotCommand } from "./bot/server.js";
|
|
3
4
|
export { revokeAll } from "./http/oauth.js";
|
package/dist/mcp/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Apart from the `cli` entry, so a CLI loads the MCP SDK and zod only when it serves MCP.
|
|
2
|
+
export { portOf, publicUrlOf } from "../http/public-url.js";
|
|
2
3
|
export { runCommandForAgent, toolsFromCommands, } from "./agent.js";
|
|
3
4
|
export { createBotServer } from "./bot/server.js";
|
|
4
5
|
export { revokeAll } from "./http/oauth.js";
|
package/dist/mcp/server.js
CHANGED
|
@@ -176,7 +176,7 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
176
176
|
}),
|
|
177
177
|
});
|
|
178
178
|
const skill = messenger.skill ? skillResource(app, messenger.skill) : undefined;
|
|
179
|
-
const log = agentLog(() => openStore({ env: context.env, command: app.command }));
|
|
179
|
+
const log = agentLog(() => openStore({ env: context.env, command: app.command, version: app.version }));
|
|
180
180
|
const audit = async (call) => {
|
|
181
181
|
if (!recalledAccount(app, provider, settings.profile, context.env))
|
|
182
182
|
return;
|
package/dist/ports/app.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export interface AppIdentity {
|
|
|
14
14
|
issues?: string;
|
|
15
15
|
/** The language of the transcript a person reads: its day headings and the word for "you". `ru-RU` when unset. */
|
|
16
16
|
locale?: string;
|
|
17
|
+
/**
|
|
18
|
+
* ISO 639-1 codes of the languages its content is mostly in (`["ru"]`). The first run pre-ticks the ones search
|
|
19
|
+
* can stem in place of English and the system's language; the person still confirms, since the choice is shared.
|
|
20
|
+
*/
|
|
21
|
+
contentLanguages?: readonly string[];
|
|
17
22
|
/** Error reasons and detail keys of its own — max's `max_code`, tg's `migrated_chat_id` — kept in every error it prints. */
|
|
18
23
|
errors?: AppErrors;
|
|
19
24
|
}
|
|
@@ -24,9 +24,11 @@ export declare const MIGRATIONS: Migration[];
|
|
|
24
24
|
* that fails names `store reset` when the app has one: without it the owner is stuck until a build that can migrate
|
|
25
25
|
* the file. An app without that command (drive) passes no `command`, and the advice names none.
|
|
26
26
|
*/
|
|
27
|
-
export declare const migrate: (database: CacheDatabase, { migrations, now, command, ownTable, }?: {
|
|
27
|
+
export declare const migrate: (database: CacheDatabase, { migrations, now, command, version, ownTable, }?: {
|
|
28
28
|
migrations?: Migration[];
|
|
29
29
|
now?: () => number;
|
|
30
30
|
command?: string;
|
|
31
|
+
/** The CLI's own version: with `command`, it is recorded so a later upgrade can name who it locks out. */
|
|
32
|
+
version?: string;
|
|
31
33
|
ownTable?: string | null;
|
|
32
|
-
}) =>
|
|
34
|
+
}) => string | undefined;
|
package/dist/store/migrations.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { lockedOutNotice, recordClient, refusalAdvice, storeClients } from "./sqlite/clients.js";
|
|
2
3
|
import { generatedMigrations } from "./sqlite/manifest.js";
|
|
3
4
|
/**
|
|
4
5
|
* Every migration is generated by drizzle-kit and numbered in `sqlite/manifest.ts`. Version 1 creates the whole
|
|
@@ -36,7 +37,7 @@ const fileState = (database) => {
|
|
|
36
37
|
* that fails names `store reset` when the app has one: without it the owner is stuck until a build that can migrate
|
|
37
38
|
* the file. An app without that command (drive) passes no `command`, and the advice names none.
|
|
38
39
|
*/
|
|
39
|
-
export const migrate = (database, { migrations = MIGRATIONS, now = Date.now, command, ownTable = OWN_TABLE, } = {}) => {
|
|
40
|
+
export const migrate = (database, { migrations = MIGRATIONS, now = Date.now, command, version, ownTable = OWN_TABLE, } = {}) => {
|
|
40
41
|
const ours = latest(migrations);
|
|
41
42
|
database.exec("BEGIN IMMEDIATE");
|
|
42
43
|
try {
|
|
@@ -48,8 +49,18 @@ export const migrate = (database, { migrations = MIGRATIONS, now = Date.now, com
|
|
|
48
49
|
}
|
|
49
50
|
if (file.version > ours && file.minCompatible > ours) {
|
|
50
51
|
throw new CliError("configuration_error", `the message store was written by a newer version (schema ${file.version}, needs at least ` +
|
|
51
|
-
`${file.minCompatible}; this one speaks ${ours}) —
|
|
52
|
+
`${file.minCompatible}; this one speaks ${ours}) — ${refusalAdvice(storeClients(database), command, file.version)}`);
|
|
52
53
|
}
|
|
54
|
+
const raisedTo = migrations
|
|
55
|
+
.filter((migration) => migration.version > file.version)
|
|
56
|
+
.reduce((highest, { minCompatible }) => Math.max(highest, minCompatible), file.minCompatible);
|
|
57
|
+
const notice = file.version > 0 && raisedTo > file.minCompatible
|
|
58
|
+
? lockedOutNotice(storeClients(database), {
|
|
59
|
+
...(command ? { command } : {}),
|
|
60
|
+
schema: ours,
|
|
61
|
+
minCompatible: raisedTo,
|
|
62
|
+
})
|
|
63
|
+
: undefined;
|
|
53
64
|
for (const migration of migrations) {
|
|
54
65
|
if (migration.version <= file.version)
|
|
55
66
|
continue;
|
|
@@ -60,7 +71,10 @@ export const migrate = (database, { migrations = MIGRATIONS, now = Date.now, com
|
|
|
60
71
|
.prepare("INSERT INTO schema_migrations (version, min_compatible, applied_at) VALUES (?, ?, ?)")
|
|
61
72
|
.run(migration.version, migration.minCompatible, now());
|
|
62
73
|
}
|
|
74
|
+
if (command && version)
|
|
75
|
+
recordClient(database, { command, version, speaks: ours }, now());
|
|
63
76
|
database.exec("COMMIT");
|
|
77
|
+
return notice;
|
|
64
78
|
}
|
|
65
79
|
catch (error) {
|
|
66
80
|
database.exec("ROLLBACK");
|
package/dist/store/people.js
CHANGED
|
@@ -12,8 +12,11 @@ export const accountPeopleOver = (context) => {
|
|
|
12
12
|
return {
|
|
13
13
|
savePeople: async (key, people) => write(database, () => {
|
|
14
14
|
const accountKey = accountPk(key);
|
|
15
|
-
for (const person of people)
|
|
15
|
+
for (const person of people) {
|
|
16
16
|
identityPk(accountKey, key.provider, person.id, person.name, person);
|
|
17
|
+
if (person.email)
|
|
18
|
+
personLinks.joinByAddress(context, { provider: key.provider, id: person.id }, person.email, key.provider);
|
|
19
|
+
}
|
|
17
20
|
}),
|
|
18
21
|
contacts: async (key, options) => {
|
|
19
22
|
const accountKey = findAccountPk(key);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
export declare const CLIENTS_SETTING = "clients";
|
|
3
|
+
/** A CLI that opened the store for writing: what it is and the newest store schema it speaks. */
|
|
4
|
+
export interface StoreClient {
|
|
5
|
+
command: string;
|
|
6
|
+
version: string;
|
|
7
|
+
core: string;
|
|
8
|
+
speaks: number;
|
|
9
|
+
seenAt: number;
|
|
10
|
+
}
|
|
11
|
+
/** Readable from any schema: `store_settings` is in the initial migration. */
|
|
12
|
+
export declare const storeClients: (database: CacheDatabase) => StoreClient[];
|
|
13
|
+
/** Writes only when the CLI's version, cli-core or schema changed, or a day passed: every run calls it. */
|
|
14
|
+
export declare const recordClient: (database: CacheDatabase, { command, version, speaks }: {
|
|
15
|
+
command: string;
|
|
16
|
+
version: string;
|
|
17
|
+
speaks: number;
|
|
18
|
+
}, now: number) => void;
|
|
19
|
+
/** The sentence an upgrade that raised the store's minimum prints: who can no longer open it, and what to run. */
|
|
20
|
+
export declare const lockedOutNotice: (clients: readonly StoreClient[], { command, schema, minCompatible }: {
|
|
21
|
+
command?: string;
|
|
22
|
+
schema: number;
|
|
23
|
+
minCompatible: number;
|
|
24
|
+
}) => string | undefined;
|
|
25
|
+
/** Added to a refusal: this tool's own command, and the CLI that moved the store, so the owner knows what to run. */
|
|
26
|
+
export declare const refusalAdvice: (clients: readonly StoreClient[], command: string | undefined, schema: number) => string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export const CLIENTS_SETTING = "clients";
|
|
3
|
+
const coreVersion = (() => {
|
|
4
|
+
try {
|
|
5
|
+
const manifest = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8"));
|
|
6
|
+
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return "unknown";
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
const clientOf = (value) => {
|
|
13
|
+
if (typeof value !== "object" || value === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
const { command, version, core, speaks, seenAt } = value;
|
|
16
|
+
return typeof command === "string" &&
|
|
17
|
+
typeof version === "string" &&
|
|
18
|
+
typeof core === "string" &&
|
|
19
|
+
Number.isSafeInteger(speaks) &&
|
|
20
|
+
Number.isSafeInteger(seenAt)
|
|
21
|
+
? { command, version, core, speaks: speaks, seenAt: seenAt }
|
|
22
|
+
: undefined;
|
|
23
|
+
};
|
|
24
|
+
// A store built from a test's own migrations may have no settings table.
|
|
25
|
+
const hasSettings = (database) => database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'store_settings'").get() !== undefined;
|
|
26
|
+
/** Readable from any schema: `store_settings` is in the initial migration. */
|
|
27
|
+
export const storeClients = (database) => {
|
|
28
|
+
if (!hasSettings(database))
|
|
29
|
+
return [];
|
|
30
|
+
const row = database.prepare("SELECT value FROM store_settings WHERE key = ?").get(CLIENTS_SETTING);
|
|
31
|
+
if (!row)
|
|
32
|
+
return [];
|
|
33
|
+
try {
|
|
34
|
+
const value = JSON.parse(String(row.value));
|
|
35
|
+
return Array.isArray(value) ? value.flatMap((one) => clientOf(one) ?? []) : [];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/** Writes only when the CLI's version, cli-core or schema changed, or a day passed: every run calls it. */
|
|
42
|
+
export const recordClient = (database, { command, version, speaks }, now) => {
|
|
43
|
+
if (!hasSettings(database))
|
|
44
|
+
return;
|
|
45
|
+
const stored = storeClients(database);
|
|
46
|
+
const same = stored.find((one) => one.command === command);
|
|
47
|
+
if (same &&
|
|
48
|
+
same.version === version &&
|
|
49
|
+
same.core === coreVersion &&
|
|
50
|
+
same.speaks === speaks &&
|
|
51
|
+
now - same.seenAt < 86_400_000)
|
|
52
|
+
return;
|
|
53
|
+
const entry = { command, version, core: coreVersion, speaks, seenAt: now };
|
|
54
|
+
database
|
|
55
|
+
.prepare("INSERT OR REPLACE INTO store_settings (key, value, at) VALUES (?, ?, ?)")
|
|
56
|
+
.run(CLIENTS_SETTING, JSON.stringify([...stored.filter((one) => one.command !== command), entry]), now);
|
|
57
|
+
};
|
|
58
|
+
const upgradeOf = (clients) => clients.map(({ command }) => `\`${command} upgrade\``).join(", ");
|
|
59
|
+
/** The sentence an upgrade that raised the store's minimum prints: who can no longer open it, and what to run. */
|
|
60
|
+
export const lockedOutNotice = (clients, { command, schema, minCompatible }) => {
|
|
61
|
+
const others = clients.filter((one) => one.command !== command);
|
|
62
|
+
const behind = others.filter(({ speaks }) => speaks < minCompatible);
|
|
63
|
+
const named = behind.map(({ command, version }) => `${command} ${version}`).join(", ");
|
|
64
|
+
if (behind.length)
|
|
65
|
+
return `the store moved to schema ${schema}; ${named} can no longer open it until upgraded — run ${upgradeOf(behind)}`;
|
|
66
|
+
// A CLI that has not opened the store since clients were recorded is unknown here, but may still be behind.
|
|
67
|
+
if (!others.length)
|
|
68
|
+
return (`the store moved to schema ${schema}; every other WireCat CLI that uses it needs a version that speaks it — ` +
|
|
69
|
+
"run `<cli> upgrade` for each");
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
/** Added to a refusal: this tool's own command, and the CLI that moved the store, so the owner knows what to run. */
|
|
73
|
+
export const refusalAdvice = (clients, command, schema) => {
|
|
74
|
+
const mover = clients
|
|
75
|
+
.filter(({ speaks, command: other }) => speaks >= schema && other !== command)
|
|
76
|
+
.sort((a, b) => b.seenAt - a.seenAt)[0];
|
|
77
|
+
const own = command ? `run \`${command} upgrade\`` : "upgrade this tool";
|
|
78
|
+
return mover ? `${own} (${mover.command} ${mover.version} moved the store to schema ${schema})` : own;
|
|
79
|
+
};
|
|
@@ -7,6 +7,10 @@ export interface StoreOptions {
|
|
|
7
7
|
now?: () => number;
|
|
8
8
|
/** The CLI's own name, for the advice a failed migration gives. */
|
|
9
9
|
command?: string;
|
|
10
|
+
/** The CLI's own version: with `command`, recorded in the store so an upgrade can name the CLIs it locks out. */
|
|
11
|
+
version?: string;
|
|
12
|
+
/** Where the notice of an upgrade that locks other CLIs out goes; stderr, since stdout may carry JSON or MCP. */
|
|
13
|
+
warn?: (line: string) => void;
|
|
10
14
|
}
|
|
11
15
|
export declare const BACKFILL_ON_OPEN = 5000;
|
|
12
|
-
export declare const openStoreContext: ({ path, env, now, command, readOnly, }?: StoreOptions) => Promise<StoreContext>;
|
|
16
|
+
export declare const openStoreContext: ({ path, env, now, command, version, warn, readOnly, }?: StoreOptions) => Promise<StoreContext>;
|
|
@@ -28,7 +28,7 @@ const createWritable = (file) => {
|
|
|
28
28
|
throw new CliError("permission_error", "the message store cannot be written from here; run where it is writable, or set MESSAGING_STORE", { reason: "store.not-writable" });
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
|
-
export const openStoreContext = async ({ path, env, now = Date.now, command, readOnly = false, } = {}) => {
|
|
31
|
+
export const openStoreContext = async ({ path, env, now = Date.now, command, version, warn = (line) => process.stderr.write(`${line}\n`), readOnly = false, } = {}) => {
|
|
32
32
|
if (typeof readOnly !== "boolean")
|
|
33
33
|
throw new CliError("validation_error", "readOnly must be a boolean");
|
|
34
34
|
await storeCapable();
|
|
@@ -43,9 +43,11 @@ export const openStoreContext = async ({ path, env, now = Date.now, command, rea
|
|
|
43
43
|
const database = opened.database;
|
|
44
44
|
try {
|
|
45
45
|
if (readOnly)
|
|
46
|
-
assertReadCompatible(database);
|
|
46
|
+
assertReadCompatible(database, command);
|
|
47
47
|
else {
|
|
48
|
-
migrate(database, { now, command });
|
|
48
|
+
const notice = migrate(database, { now, command, ...(version ? { version } : {}) });
|
|
49
|
+
if (notice)
|
|
50
|
+
warn(notice);
|
|
49
51
|
fillFolded(database);
|
|
50
52
|
const pending = pendingNormalization(database);
|
|
51
53
|
if (pending > 0 && pending <= BACKFILL_ON_OPEN)
|
|
@@ -14,6 +14,12 @@ export declare const linkIdentityToPerson: (context: StoreContext, identityPk: n
|
|
|
14
14
|
export declare const linkIdentities: (context: StoreContext, person: IdentityRef, other: IdentityRef, options: LinkOptions) => PersonRecord;
|
|
15
15
|
/** Moves every identity of `from` to `to`, as `linkIdentities` does once it has found both people. */
|
|
16
16
|
export declare const mergePerson: (context: StoreContext, from: number, to: number, options: LinkOptions) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Joins the identity to the person of the mail contact with this exact address, making that contact when mail has
|
|
19
|
+
* not seen it yet, so whichever import comes second lands on one person. The contact is seen by no account, so it is
|
|
20
|
+
* no one's mail correspondent. An owner's unlink wins: two identities the owner separated stay apart.
|
|
21
|
+
*/
|
|
22
|
+
export declare const joinByAddress: (context: StoreContext, ref: IdentityRef, address: string, by: string) => void;
|
|
17
23
|
/** Gives the identity a person of its own again; the ones left keep theirs. */
|
|
18
24
|
export declare const unlinkIdentity: (context: StoreContext, ref: IdentityRef, options: LinkOptions) => PersonRecord;
|
|
19
25
|
/** Messages that name the person: by id where the messenger said so, or by `@username` in the text. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { and, eq, inArray, isNull, sql } from "./drizzle/core.js";
|
|
3
|
+
import { identityOf } from "./identities.js";
|
|
3
4
|
import { resolvePersonLinks } from "./person-resolution.js";
|
|
4
5
|
import { accountIdentities, accounts, chats, identities, identityLinkEvents, identityLinks, messages, persons, } from "./schema.js";
|
|
5
6
|
import { hitsWhere } from "./search.js";
|
|
@@ -135,6 +136,32 @@ export const mergePerson = (context, from, to, options) => {
|
|
|
135
136
|
for (const { identityPk } of together)
|
|
136
137
|
move(context, identityPk, from, to, options);
|
|
137
138
|
};
|
|
139
|
+
/** The owner once moved one of the two identities away from a person the other belonged to. */
|
|
140
|
+
const separatedByOwner = ({ database }, one, other) => database
|
|
141
|
+
.prepare(`SELECT 1 FROM identity_link_events moved
|
|
142
|
+
JOIN identity_link_events held ON held.to_person_id = moved.from_person_id
|
|
143
|
+
WHERE moved.author = 'owner' AND ((moved.identity_id = ? AND held.identity_id = ?)
|
|
144
|
+
OR (moved.identity_id = ? AND held.identity_id = ?))
|
|
145
|
+
LIMIT 1`)
|
|
146
|
+
.get(one, other, other, one) !== undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Joins the identity to the person of the mail contact with this exact address, making that contact when mail has
|
|
149
|
+
* not seen it yet, so whichever import comes second lands on one person. The contact is seen by no account, so it is
|
|
150
|
+
* no one's mail correspondent. An owner's unlink wins: two identities the owner separated stay apart.
|
|
151
|
+
*/
|
|
152
|
+
export const joinByAddress = (context, ref, address, by) => {
|
|
153
|
+
const key = address.trim().toLowerCase();
|
|
154
|
+
if (!/^[^\s@]+@[^\s@]+$/.test(key) || ref.provider === "email")
|
|
155
|
+
return;
|
|
156
|
+
const found = required(context, ref);
|
|
157
|
+
if (!identityRow(context, { provider: "email", id: key }))
|
|
158
|
+
identityOf(context, "email", key, null);
|
|
159
|
+
const contact = required(context, { provider: "email", id: key });
|
|
160
|
+
if (contact.personPk === found.personPk || separatedByOwner(context, found.pk, contact.pk))
|
|
161
|
+
return;
|
|
162
|
+
mergePerson(context, found.personPk, contact.personPk, { method: "same-email", by });
|
|
163
|
+
resolvePersonLinks(context.database, recordOf(context, contact.personPk).identities.flatMap(({ name, username }) => [name, username]));
|
|
164
|
+
};
|
|
138
165
|
/** Gives the identity a person of its own again; the ones left keep theirs. */
|
|
139
166
|
export const unlinkIdentity = (context, ref, options) => {
|
|
140
167
|
const found = required(context, ref);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CacheDatabase } from "../driver.js";
|
|
2
|
-
export declare const assertReadCompatible: (database: CacheDatabase) => void;
|
|
2
|
+
export declare const assertReadCompatible: (database: CacheDatabase, command?: string) => void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { MIGRATIONS } from "../migrations.js";
|
|
3
|
-
|
|
3
|
+
import { refusalAdvice, storeClients } from "./clients.js";
|
|
4
|
+
export const assertReadCompatible = (database, command) => {
|
|
4
5
|
const ours = MIGRATIONS.at(-1)?.version ?? 0;
|
|
5
6
|
if (!database.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'").get())
|
|
6
7
|
throw new CliError("configuration_error", "The store has no schema history; initialize it explicitly before a read-only open");
|
|
@@ -17,5 +18,5 @@ export const assertReadCompatible = (database) => {
|
|
|
17
18
|
if (version < ours)
|
|
18
19
|
throw new CliError("configuration_error", `The store needs schema ${ours} maintenance before a read-only open (found ${version})`);
|
|
19
20
|
if (minCompatible > ours)
|
|
20
|
-
throw new CliError("configuration_error", `The store requires schema ${minCompatible}, but this tool supports ${ours};
|
|
21
|
+
throw new CliError("configuration_error", `The store requires schema ${minCompatible}, but this tool supports ${ours}; ${refusalAdvice(storeClients(database), command, version)}`);
|
|
21
22
|
};
|
package/dist/store/types.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface PersonFacts {
|
|
|
32
32
|
username?: string | null;
|
|
33
33
|
isBot?: boolean | null;
|
|
34
34
|
description?: string | null;
|
|
35
|
+
/** An address the provider shows for them: they become one person with the mail contact of that exact address. */
|
|
36
|
+
email?: string | null;
|
|
35
37
|
}
|
|
36
38
|
/** One identity in one messenger, by the messenger's own id. `provider` is any string: `email` is one. */
|
|
37
39
|
export interface IdentityRef {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.415.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",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"@types/node": "^22.20.4",
|
|
214
214
|
"@types/pngjs": "6.0.5",
|
|
215
215
|
"@vitest/coverage-v8": "^5.0.2",
|
|
216
|
-
"@wirecat/cli-base": "0.
|
|
216
|
+
"@wirecat/cli-base": "0.53.0",
|
|
217
217
|
"commander": "15.0.0",
|
|
218
218
|
"cspell": "10.3.6",
|
|
219
219
|
"drizzle-kit": "1.0.0-rc.4",
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
},
|
|
234
234
|
"peerDependencies": {
|
|
235
235
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
236
|
-
"@wirecat/cli-base": ">=0.
|
|
236
|
+
"@wirecat/cli-base": ">=0.53.0 <0.54.0",
|
|
237
237
|
"commander": ">=15"
|
|
238
238
|
},
|
|
239
239
|
"bin": {
|