@wirecat/cli-core 0.484.0 → 0.486.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 CHANGED
@@ -7,6 +7,25 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.486.0 — 30.10.2026
11
+
12
+ ### Fixed
13
+
14
+ - **A messenger's `mcp revoke`, `mcp server stop` and `mcp server status` with no profile say what the profile is
15
+ for.** They refused with "name the profiles to serve", the words of `mcp serve`. Now: "name the profiles whose
16
+ browser logins to forget", "… whose server to stop", "… whose server to check". `mcp serve`, `mcp server start` and
17
+ `mcp config` keep "to serve". Same exit 2 and `details.profiles`.
18
+
19
+ ## 0.485.0 — 30.10.2026
20
+
21
+ ### Fixed
22
+
23
+ - **A store write no longer fails with "database is locked" while another process writes steadily.** SQLite's own wait
24
+ for the write lock sleeps up to 100 ms between tries, so a process that commits and begins again at once was never
25
+ seen free, and the waiter failed after the 5 s `busy_timeout` (`mail sync` against `memo sync`). A write now tries
26
+ for the lock every millisecond within the same `busy_timeout`. `saveAccount` takes the lock the same way instead
27
+ of as a bare statement.
28
+
10
29
  ## 0.484.0 — 30.10.2026
11
30
 
12
31
  ### Fixed
@@ -42,7 +42,7 @@ export interface ProfileSet {
42
42
  export declare const servedProfiles: (self: Command, messenger: Pick<Messenger, "app" | "resolveSettings">, { settings, env }: {
43
43
  settings: Pick<Settings, "profile" | "sources" | "profilesHere">;
44
44
  env: NodeJS.ProcessEnv;
45
- }, given: string | undefined, words: string) => {
45
+ }, given: string | undefined, words: string, purpose?: string) => {
46
46
  profile: string;
47
47
  } | {
48
48
  set: ProfileSet;
@@ -23,13 +23,17 @@ const DEFAULT_PORT = MCP_HTTP_DEFAULT_PORT;
23
23
  export const ALL_PROFILES = "_all";
24
24
  /** `--profiles all`; a profile named `all` is served alone, as `<tool> all mcp serve`. */
25
25
  export const EVERY_PROFILE = "all";
26
+ const SERVER_PURPOSE = {
27
+ stop: "whose server to stop",
28
+ status: "whose server to check",
29
+ };
26
30
  /**
27
31
  * The profile or profiles an MCP server is for, always named (NEED-1881, WC-MCP-9): a leading profile, or
28
32
  * `--profiles` with a list or `all`. Neither, or both, is refused with the profiles on this computer. `--profiles`
29
33
  * takes the place of `<PREFIX>_PROFILE`; under `<PREFIX>_PROFILE_LOCK` it may name the locked profile only
30
34
  * (WC-ACCT-5).
31
35
  */
32
- export const servedProfiles = (self, messenger, { settings, env }, given, words) => {
36
+ export const servedProfiles = (self, messenger, { settings, env }, given, words, purpose = "to serve") => {
33
37
  const { app } = messenger;
34
38
  const here = settings.profilesHere;
35
39
  const listed = `profiles here: ${here.length > 0 ? here.join(", ") : "none"}`;
@@ -42,7 +46,7 @@ export const servedProfiles = (self, messenger, { settings, env }, given, words)
42
46
  if (given === undefined) {
43
47
  if (here.length === 0)
44
48
  throw new NoProfileHere(app);
45
- throw new CliError("validation_error", `name the profiles to serve: \`${app.command} <profile> ${words}\`, or \`${app.command} ${words} --profiles ` +
49
+ throw new CliError("validation_error", `name the profiles ${purpose}: \`${app.command} <profile> ${words}\`, or \`${app.command} ${words} --profiles ` +
46
50
  `<a,b>\` or \`--profiles all\` — ${listed}`, { profiles: here });
47
51
  }
48
52
  const names = [...new Set(given.split(",").map((one) => one.trim()))].filter(Boolean).sort();
@@ -149,7 +153,7 @@ export const mcpGroup = (host) => {
149
153
  .summary("forget every login given to a browser app")
150
154
  .description("forget every login `mcp serve --http` gave a browser app, of this profile or of the server `--profiles` names; each app must log in again")).action(async function () {
151
155
  const context = host.context(this, { needsProfile: false });
152
- const served = servedProfiles(this, host, context, this.opts().profiles, "mcp revoke");
156
+ const served = servedProfiles(this, host, context, this.opts().profiles, "mcp revoke", "whose browser logins to forget");
153
157
  const { revokeAll } = await import("../../mcp/http/oauth.js");
154
158
  revokeAll(httpTokenFile(app, "set" in served ? served.set.key : served.profile, context.env));
155
159
  renderAnswer(this, context, itemOf("set" in served
@@ -163,7 +167,7 @@ export const mcpGroup = (host) => {
163
167
  target: (self) => {
164
168
  const context = host.context(self, { needsProfile: false });
165
169
  const { env } = context;
166
- const served = servedProfiles(self, host, context, self.opts().profiles, `mcp server ${self.name()}`);
170
+ const served = servedProfiles(self, host, context, self.opts().profiles, `mcp server ${self.name()}`, SERVER_PURPOSE[self.name()]);
167
171
  if ("set" in served) {
168
172
  const { set } = served;
169
173
  return {
@@ -11,8 +11,11 @@ export const accountAccountsOver = (context) => {
11
11
  const { accountPk, findAccountPk } = keysOver(context);
12
12
  return {
13
13
  saveAccount: async (key, { name }) => {
14
- const pk = accountPk(key, name);
15
- linkInbox(database, `${key.provider}:${key.account}`, pk);
14
+ const pk = write(database, () => {
15
+ const made = accountPk(key, name);
16
+ linkInbox(database, `${key.provider}:${key.account}`, made);
17
+ return made;
18
+ });
16
19
  linkOwnIdentities(context);
17
20
  return pk;
18
21
  },
@@ -1,6 +1,39 @@
1
1
  import { drainInvolvementQueue } from "./involvement-queue.js";
2
2
  import { drainMentions } from "./mention-drain.js";
3
3
  import { drainStems, stemmerCache } from "./stems.js";
4
+ const SQLITE_BUSY = 5;
5
+ const pause = new Int32Array(new SharedArrayBuffer(4));
6
+ const isBusy = (error) => {
7
+ const { errcode, errno } = (error ?? {});
8
+ const code = errcode ?? errno;
9
+ return typeof code === "number" && (code & 0xff) === SQLITE_BUSY;
10
+ };
11
+ /**
12
+ * Takes the write lock, waiting as long as the connection's `busy_timeout`. SQLite's own wait sleeps 1 ms, then up to
13
+ * 100 ms between tries, so a process that commits and begins again at once is never seen free and the waiter fails
14
+ * with "database is locked" after the full timeout. Trying every millisecond sees the gap between two writes.
15
+ */
16
+ const beginImmediate = (database) => {
17
+ const timeout = Number(database.prepare("PRAGMA busy_timeout").get()?.timeout ?? 0);
18
+ const deadline = Date.now() + timeout;
19
+ database.exec("PRAGMA busy_timeout = 0");
20
+ try {
21
+ for (;;) {
22
+ try {
23
+ database.exec("BEGIN IMMEDIATE");
24
+ return;
25
+ }
26
+ catch (error) {
27
+ if (!isBusy(error) || Date.now() >= deadline)
28
+ throw error;
29
+ Atomics.wait(pause, 0, 0, 1);
30
+ }
31
+ }
32
+ }
33
+ finally {
34
+ database.exec(`PRAGMA busy_timeout = ${timeout}`);
35
+ }
36
+ };
4
37
  const connections = new WeakMap();
5
38
  const connectionOf = (database) => {
6
39
  let connection = connections.get(database);
@@ -29,7 +62,10 @@ const transact = (database, body, drain, beforeCommit) => {
29
62
  throw new Error("a store write cannot run inside a read");
30
63
  const outer = connection.depth === 0;
31
64
  const name = `w${connection.depth + 1}`;
32
- database.exec(outer ? "BEGIN IMMEDIATE" : `SAVEPOINT ${name}`);
65
+ if (outer)
66
+ beginImmediate(database);
67
+ else
68
+ database.exec(`SAVEPOINT ${name}`);
33
69
  connection.depth += 1;
34
70
  try {
35
71
  const result = body();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.484.0",
3
+ "version": "0.486.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",