@wirecat/cli-core 0.461.0 → 0.463.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,47 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.463.0 — 27.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **The MCP server is `mcp serve`; `mcp` alone only shows its commands**
15
+ ([WC-CMD-3](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/commands.md#wc-cmd-3--give-a-group-without-a-leaf-only-its-help-never-a-hidden-default-action)).
16
+ `mcpCommand` gives the `mcp` group with `serve`, `revoke`, `config`, `add`, `check` and `server`; `--http`, `--port`,
17
+ `--public-url` and `--permission` move from `mcp` to `mcp serve`, and `mcp --revoke` is now its own command,
18
+ `mcp revoke`, with no flag left behind. An agent is set up with
19
+ `claude mcp add <cli> -- <cli> <profile> mcp serve`. `bot mcp` is a group too, and the bot's server is
20
+ `bot mcp serve`. `serverEntry`, so `mcp config`, `mcp add`, `mcp check` and `bot mcp config`, writes `mcp serve`,
21
+ the server of every profile starts `<cli> <profile> mcp serve` for each, and `mcp server start` runs
22
+ `mcp serve --http`. There is no alias: an old client entry that runs `<cli> <profile> mcp` gets the group's help and
23
+ exits 2 — print it again with `mcp config` or `mcp add`.
24
+ - **`mcpServerCommand` takes `--permission` only when asked.** `mcp server start` offered `--permission` to
25
+ every CLI and passed it to a server that might not take it; that server died before it listened and `start`
26
+ answered `provider_unavailable` with `retry.safe: true`, so an agent retried for ever. A CLI whose `mcp serve` takes
27
+ `--permission` passes `permission: true` (`mcpCommand` does); otherwise `start --permission` is refused as an unknown
28
+ option before anything starts. A `--permission` set on the CLI's root is still handed over. A server that stops
29
+ before it listens is now `retry.safe: false`. A CLI's own target names its server as `argv: [..., "mcp", "serve",
30
+ "--http"]` once its `mcp` is a group too.
31
+
32
+ ## 0.462.0 — 27.10.2026
33
+
34
+ ### Changed — may break callers
35
+
36
+ - **`skill show`, `skill status` and `skill install` answer `--json` in the one-thing envelope**
37
+ ([WC-OUT-4](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/output.md#wc-out-4--answer-every-json-request-with-one-envelope-a-list-or-one-thing)),
38
+ `{ item, meta }`, and `--jsonl` as one item line and the end line, like every other command and as their declared
39
+ output schema already said; they printed the bare object. MCP tool answers change the same way. `commands --json`
40
+ does not change. A caller reads the old fields under `item`: the `install/windows.ps1` installers of tg-cli, max-cli
41
+ and whatsapp-cli read `$skill.written`, and tests in tg-cli (`src/archive.test.ts`), max-cli
42
+ (`src/commands/skill.test.ts`) and cli-template (`src/roots.test.ts`) read the bare shape.
43
+
44
+ ### Fixed
45
+
46
+ - **An empty `messages` search no longer suggests `messages sync --all --background`.** `coverage.next` and the
47
+ `messages.sync` step now name one chat (`<cli> messages sync <chat>`: the selected chat, or the most active one
48
+ that was never fetched or is behind), and the search help says to offer it and run it only after the owner
49
+ approves. A sync downloads from the owner's account, so an agent must not start an unbounded one on its own.
50
+
10
51
  ## 0.461.0 — 27.10.2026
11
52
 
12
53
  ### Changed — may break callers
@@ -12,8 +12,16 @@ import { botExamples } from "./examples.js";
12
12
  /** `bot mcp`, mounted only for a messenger that hands in its `run` on `BotMessenger.mcp`. */
13
13
  export const botMcpCommand = (bot) => {
14
14
  const { app } = bot;
15
- const command = annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false })
16
- .description(`serve this bot to an agent over MCP, on stdin and stdout — \`claude mcp add sales-bot -- ${app.command} sales bot mcp\``)
15
+ const command = annotate(new Command("mcp"), { effects: LOCAL_READ, mcp: false })
16
+ .description(`serve this bot to an agent over MCP and set it up — \`claude mcp add sales-bot -- ${app.command} sales bot mcp serve\``)
17
+ .summary("serve this bot to an agent over MCP, and set that up");
18
+ command.addCommand(annotate(new Command("serve"), {
19
+ effects: REMOTE_WRITE,
20
+ lifetime: "persistent",
21
+ mcp: false,
22
+ examples: botExamples(bot, "mcp serve"),
23
+ })
24
+ .description(`serve this bot to an agent over MCP, on stdin and stdout — \`claude mcp add sales-bot -- ${app.command} sales bot mcp serve\``)
17
25
  .summary("serve this bot to an agent over MCP, on stdin and stdout")
18
26
  .action(async function () {
19
27
  const context = botContext(this, bot);
@@ -35,7 +43,7 @@ export const botMcpCommand = (bot) => {
35
43
  ...(mcp.keyOf ? { keyOf: mcp.keyOf } : {}),
36
44
  ...(mcp.skill ? { skill: mcp.skill } : {}),
37
45
  }, context.streams.diagnostic);
38
- });
46
+ }));
39
47
  command.addCommand(annotate(new Command("config"), { effects: LOCAL_READ, examples: botExamples(bot, "mcp config") })
40
48
  .description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")
41
49
  .summary("print the mcpServers entry for an MCP client; writes nothing")
package/dist/cli/guide.js CHANGED
@@ -291,7 +291,7 @@ export const checkStep = (program, run) => {
291
291
  const beforeDashes = rest.slice(index, rest.includes("--") ? rest.indexOf("--") : undefined);
292
292
  if (help && beforeDashes.some((word) => word === help.long || word === help.short))
293
293
  return [];
294
- // A group with an action of its own runs as typed: `mcp` serves, `mcp server start` is its child.
294
+ // A group with an action of its own runs as typed rather than asking for one of its children.
295
295
  const runsItself = at._actionHandler != null;
296
296
  if (at.commands.length > 0 && !runsItself)
297
297
  return [`needs one of: ${at.commands.map((one) => one.name()).join(", ")}`];
@@ -10,7 +10,7 @@ export interface McpServerTarget {
10
10
  envPrefix: string;
11
11
  env: NodeJS.ProcessEnv;
12
12
  tokenFile: string;
13
- /** The words after the CLI's own entry that run `mcp --http` in the foreground, before `--port`. */
13
+ /** The words after the CLI's own entry that run `mcp serve --http` in the foreground, before `--port`. */
14
14
  argv: string[];
15
15
  /** Added to the server's environment: the profile, for a CLI that reads it from there. */
16
16
  childEnv?: Record<string, string>;
@@ -19,17 +19,22 @@ export interface McpServerTarget {
19
19
  export interface McpServerOptions {
20
20
  /** How help shows the CLI before `mcp`: `tg personal`. */
21
21
  example: string;
22
- /** The CLI's own `mcp --http` default, when it is not 8765. */
22
+ /** The CLI's own `mcp serve --http` default, when it is not 8765. */
23
23
  defaultPort?: number;
24
+ /**
25
+ * `start` takes `--permission` and hands it to the server: only for a CLI whose `mcp serve` takes it too,
26
+ * since a server that refuses the option dies before it listens.
27
+ */
28
+ permission?: boolean;
24
29
  target: (self: Command) => McpServerTarget;
25
30
  /** The CLI's own answer: the item in `--json`, else the lines. */
26
31
  answer: (self: Command, item: object, lines: string[]) => void;
27
32
  system?: (self: Command) => ServerSystem | undefined;
28
33
  }
29
34
  /**
30
- * `mcp --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
35
+ * `mcp serve --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
31
36
  * keeps a lock beside its token file (`state.ts`) with its port, address and login code, so status
32
37
  * survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
33
38
  * None of it is offered over MCP: status holds a live login code.
34
39
  */
35
- export declare const mcpServerCommand: ({ example, defaultPort, target, answer, system, }: McpServerOptions) => Command;
40
+ export declare const mcpServerCommand: ({ example, defaultPort, permission, target, answer, system, }: McpServerOptions) => Command;
@@ -40,28 +40,29 @@ const clientLines = (clients) => clients.length === 0
40
40
  .join(", ")}.`,
41
41
  ];
42
42
  /**
43
- * `mcp --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
43
+ * `mcp serve --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
44
44
  * keeps a lock beside its token file (`state.ts`) with its port, address and login code, so status
45
45
  * survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
46
46
  * None of it is offered over MCP: status holds a live login code.
47
47
  */
48
- export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT, target, answer, system, }) => {
48
+ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT, permission = false, target, answer, system, }) => {
49
49
  const command = annotate(new Command("server"), { effects: LOCAL_READ, mcp: false })
50
50
  .summary("run the HTTP MCP server in the background")
51
- .description("start, stop and check `mcp --http` in the background; status shows the login code for a browser app");
51
+ .description("start, stop and check `mcp serve --http` in the background; status shows the login code for a browser app");
52
52
  const machine = (self) => system?.(self) ?? thisMachine();
53
- destroysNothing(annotate(command.command("start"), {
53
+ const start = destroysNothing(annotate(command.command("start"), {
54
54
  effects: LOCAL_WRITE,
55
55
  mcp: false,
56
56
  examples: [`${example} mcp server start --public-url https://laptop.example.ts.net`],
57
57
  }))
58
58
  .summary("start the HTTP MCP server in the background")
59
- .description("start `mcp --http` apart from this shell and answer once it listens, with its login code")
59
+ .description("start `mcp serve --http` apart from this shell and answer once it listens, with its login code")
60
60
  .option("--port <port>", `the local port (default ${defaultPort})`)
61
- .option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg")
62
- .option("--permission <key=level>", "make a permission stricter for this server only; repeat for more keys", (value, previous = []) => [...previous, value])
63
- .action(async function () {
64
- // A CLI whose `mcp` takes the same flags reads them first, so they are looked up on both.
61
+ .option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg");
62
+ if (permission)
63
+ start.option("--permission <key=level>", "make a permission stricter for this server only; repeat for more keys", (value, previous = []) => [...previous, value]);
64
+ start.action(async function () {
65
+ // A CLI with a root-wide `--permission` hands it over too, so the server is never looser than asked.
65
66
  const flags = this.optsWithGlobals();
66
67
  const one = target(this);
67
68
  const url = publicUrlOf(flags.publicUrl, {
@@ -102,15 +103,15 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
102
103
  }
103
104
  if (!alive(pid)) {
104
105
  const last = readableLog(tail(log, 5)).trim();
105
- throw new CliError("provider_unavailable", `mcp --http stopped before it listened${last ? `:\n${last}` : ""}`);
106
+ throw new CliError("provider_unavailable", `mcp serve --http stopped before it listened${last ? `:\n${last}` : ""}`, { retryable: false });
106
107
  }
107
108
  await sys.pause();
108
109
  }
109
- throw new CliError("timeout", `mcp --http is running but not listening yet — \`${one.invocation} mcp server status\`; its log: ${log}`);
110
+ throw new CliError("timeout", `mcp serve --http is running but not listening yet — \`${one.invocation} mcp server status\`; its log: ${log}`);
110
111
  });
111
112
  destroysNothing(annotate(command.command("stop"), { effects: LOCAL_WRITE, mcp: false, examples: [`${example} mcp server stop`] }))
112
113
  .summary("stop the background HTTP MCP server")
113
- .description("stop `mcp --http` — only one that `mcp server start` started")
114
+ .description("stop `mcp serve --http` — only one that `mcp server start` started")
114
115
  .action(async function () {
115
116
  const one = target(this);
116
117
  const lock = readHttpLock(httpLockFile(one.tokenFile));
@@ -121,14 +122,14 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
121
122
  return;
122
123
  }
123
124
  if (byOf(one, lock.pid) !== "server") {
124
- throw new CliError("validation_error", `mcp --http for ${one.invocation} (PID ${lock.pid}) was not started by \`${one.invocation} mcp server start\` — stop it where it runs`);
125
+ throw new CliError("validation_error", `mcp serve --http for ${one.invocation} (PID ${lock.pid}) was not started by \`${one.invocation} mcp server start\` — stop it where it runs`);
125
126
  }
126
127
  process.kill(lock.pid, "SIGTERM");
127
128
  const sys = machine(this);
128
129
  for (let look = 0; look < LOOKS && alive(lock.pid); look++)
129
130
  await sys.pause();
130
131
  if (alive(lock.pid))
131
- throw new CliError("timeout", `mcp --http (PID ${lock.pid}) has not stopped yet`);
132
+ throw new CliError("timeout", `mcp serve --http (PID ${lock.pid}) has not stopped yet`);
132
133
  answer(this, { profile: one.profile ?? null, stopped: true, pid: lock.pid }, [
133
134
  `Stopped serving ${one.invocation} over HTTP (PID ${lock.pid}).`,
134
135
  ]);
@@ -139,7 +140,7 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
139
140
  examples: [`${example} mcp server status --json`],
140
141
  })
141
142
  .summary("whether the HTTP MCP server runs, its login code and the apps logged in")
142
- .description("whether `mcp --http` runs, on which port and address, the login code for a new browser app, and the apps logged in")
143
+ .description("whether `mcp serve --http` runs, on which port and address, the login code for a new browser app, and the apps logged in")
143
144
  .action(function () {
144
145
  const one = target(this);
145
146
  const lock = readHttpLock(httpLockFile(one.tokenFile));
@@ -17,11 +17,10 @@ export interface McpFlags {
17
17
  http?: boolean;
18
18
  port?: string;
19
19
  publicUrl?: string;
20
- revoke?: boolean;
21
20
  }
22
- /** The token file of `<tool> mcp --http` for every profile: no profile name can start with `_`. */
21
+ /** The token file of `<tool> mcp serve --http` for every profile: no profile name can start with `_`. */
23
22
  export declare const ALL_PROFILES = "_all";
24
- /** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
23
+ /** Where `mcp serve --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
25
24
  export declare const httpTokenFile: (app: AppIdentity, profile: string, env: NodeJS.ProcessEnv) => string;
26
25
  export declare const mcpCommand: (messenger: Messenger) => Command;
27
26
  /**
@@ -44,7 +43,7 @@ export declare const redactTail: (text: string, home: string | undefined) => str
44
43
  * servers with a trimmed environment leaves it out (measured 2026-09-28). Credentials are never copied.
45
44
  */
46
45
  export declare const serverEntry: (app: AppIdentity, { profile, flags, execPath, scriptPath, env, }: {
47
- /** Left out, the entry is `<tool> mcp`, the server of every profile. */
46
+ /** Left out, the entry is `<tool> mcp serve`, the server of every profile. */
48
47
  profile?: string;
49
48
  flags?: McpFlags;
50
49
  execPath: string;
@@ -19,31 +19,36 @@ import { nextStep } from "../steps.js";
19
19
  import { recalledAccount } from "./accounts.js";
20
20
  import { messengerContext } from "./context.js";
21
21
  const DEFAULT_PORT = MCP_HTTP_DEFAULT_PORT;
22
- /** The token file of `<tool> mcp --http` for every profile: no profile name can start with `_`. */
22
+ /** The token file of `<tool> mcp serve --http` for every profile: no profile name can start with `_`. */
23
23
  export const ALL_PROFILES = "_all";
24
- /** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
24
+ /** Where `mcp serve --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
25
25
  export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "mcp-http", `${profile}.json`);
26
26
  const withFlags = (command) => command.option("--permission <key=level>", "make a permission stricter for this server only, like messages.send=deny; it never loosens the profile; repeat for more keys", (value, previous = []) => [...previous, value]);
27
27
  export const mcpCommand = (messenger) => {
28
28
  const { app } = messenger;
29
- const command = withFlags(annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false })
29
+ const command = annotate(new Command("mcp"), { effects: LOCAL_READ, mcp: false })
30
+ .summary("serve this profile's commands to an AI agent, and set that up")
31
+ .description(`serve this profile to an agent over MCP and set it up — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\``);
32
+ command.addCommand(withFlags(annotate(new Command("serve"), {
33
+ effects: REMOTE_WRITE,
34
+ lifetime: "persistent",
35
+ mcp: false,
36
+ examples: [
37
+ `${app.command} personal mcp serve`,
38
+ `${app.command} personal mcp serve --http --public-url https://laptop.example.ts.net`,
39
+ ],
40
+ })
30
41
  .summary("serve this profile's commands to an AI agent")
31
- .description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp\`; with no profile, \`${app.command} mcp\` serves every profile and each tool asks which`))
42
+ .description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\`; with no profile, \`${app.command} mcp serve\` serves every profile and each tool asks which`))
32
43
  .option("--http", "serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide")
33
44
  .option("--port <port>", `the local port for --http (default ${DEFAULT_PORT})`)
34
45
  .option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg")
35
- .option("--revoke", "forget every login given to a browser app; each must log in again");
36
- command.action(async function () {
46
+ .action(async function () {
37
47
  const flags = this.optsWithGlobals();
38
48
  const context = messengerContext(this, messenger, { needsProfile: false });
39
- if (namedProfile(context.settings) === undefined)
49
+ const profile = namedProfile(context.settings);
50
+ if (profile === undefined)
40
51
  return serveEveryProfile(this, context, messenger, flags);
41
- if (flags.revoke) {
42
- const { revokeAll } = await import("../../mcp/http/oauth.js");
43
- revokeAll(httpTokenFile(app, context.settings.profile, context.env));
44
- renderAnswer(this, context, itemOf({ revoked: true, profile: context.settings.profile }));
45
- return;
46
- }
47
52
  if (flags.http) {
48
53
  const publicUrl = publicUrlOf(flags.publicUrl, {
49
54
  missing: "--http",
@@ -51,7 +56,7 @@ export const mcpCommand = (messenger) => {
51
56
  path: true,
52
57
  });
53
58
  const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
54
- const tokenFile = httpTokenFile(app, context.settings.profile, context.env);
59
+ const tokenFile = httpTokenFile(app, profile, context.env);
55
60
  await serveOverHttpUntilStopped(this, context, messenger, {}, {
56
61
  publicUrl,
57
62
  port: portOf(flags.port, DEFAULT_PORT),
@@ -64,9 +69,24 @@ export const mcpCommand = (messenger) => {
64
69
  // Loaded here, not at the top: every other command would otherwise pay for the SDK.
65
70
  const { serveOverStdio } = await import("../../mcp/server.js");
66
71
  await serveOverStdio(this, context, messenger, {});
67
- });
72
+ }));
73
+ command.addCommand(annotate(new Command("revoke"), {
74
+ effects: LOCAL_WRITE,
75
+ mcp: false,
76
+ examples: [`${app.command} personal mcp revoke`],
77
+ })
78
+ .summary("forget every login given to a browser app")
79
+ .description(`forget every login \`mcp serve --http\` gave a browser app, of this profile or, with no profile, of the server of every profile; each app must log in again`)
80
+ .action(async function () {
81
+ const context = messengerContext(this, messenger, { needsProfile: false });
82
+ const profile = namedProfile(context.settings);
83
+ const { revokeAll } = await import("../../mcp/http/oauth.js");
84
+ revokeAll(httpTokenFile(app, profile ?? ALL_PROFILES, context.env));
85
+ renderAnswer(this, context, itemOf({ revoked: true, profile: profile ?? null }));
86
+ }));
68
87
  command.addCommand(mcpServerCommand({
69
88
  example: `${app.command} personal`,
89
+ permission: true,
70
90
  target: (self) => {
71
91
  const { settings, env } = messengerContext(self, messenger, { needsProfile: false });
72
92
  if (namedProfile(settings) === undefined)
@@ -75,7 +95,7 @@ export const mcpCommand = (messenger) => {
75
95
  envPrefix: app.envPrefix,
76
96
  env,
77
97
  tokenFile: httpTokenFile(app, ALL_PROFILES, env),
78
- argv: ["mcp", "--http"],
98
+ argv: ["mcp", "serve", "--http"],
79
99
  version: app.version,
80
100
  };
81
101
  return {
@@ -84,7 +104,7 @@ export const mcpCommand = (messenger) => {
84
104
  envPrefix: app.envPrefix,
85
105
  env,
86
106
  tokenFile: httpTokenFile(app, settings.profile, env),
87
- argv: ["mcp", "--http"],
107
+ argv: ["mcp", "serve", "--http"],
88
108
  childEnv: { [`${app.envPrefix}_PROFILE`]: settings.profile },
89
109
  version: app.version,
90
110
  };
@@ -194,22 +214,17 @@ export const mcpCommand = (messenger) => {
194
214
  return command;
195
215
  };
196
216
  /**
197
- * `<tool> mcp` with no profile: every profile on this computer, each call in that profile's own `<tool> <profile> mcp`
217
+ * `<tool> mcp serve` with no profile: every profile on this computer, each call in that profile's own
218
+ * `<tool> <profile> mcp serve`
198
219
  * (`src/mcp/all-profiles.ts`). Nothing picks a profile: each tool takes one, and the agent is told to ask.
199
220
  */
200
221
  const serveEveryProfile = async (self, context, messenger, flags) => {
201
222
  const { app } = messenger;
202
223
  const { env } = context;
203
224
  const tokenFile = httpTokenFile(app, ALL_PROFILES, env);
204
- if (flags.revoke) {
205
- const { revokeAll } = await import("../../mcp/http/oauth.js");
206
- revokeAll(tokenFile);
207
- renderAnswer(self, context, itemOf({ revoked: true, profile: null }));
208
- return;
209
- }
210
225
  const profiles = () => messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true }).profilesHere;
211
226
  if (profiles().length === 0)
212
- throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp\` again`);
227
+ throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp serve\` again`);
213
228
  const given = environmentOf(self).mcp ?? {};
214
229
  const execPath = given.execPath ?? process.execPath;
215
230
  const scriptPath = given.scriptPath ?? realpathSync(process.argv[1] ?? "");
@@ -220,7 +235,7 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
220
235
  version: app.version,
221
236
  name: messenger.name ?? app.command,
222
237
  profiles,
223
- child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", ...tightened], env }),
238
+ child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", "serve", ...tightened], env }),
224
239
  instructions: instructions({
225
240
  command: app.command,
226
241
  name: messenger.name ?? app.command,
@@ -337,6 +352,7 @@ export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, en
337
352
  scriptPath,
338
353
  ...(profile === undefined ? [] : [profile]),
339
354
  "mcp",
355
+ "serve",
340
356
  ...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
341
357
  ],
342
358
  ...(Object.keys(directories).length > 0 ? { env: directories } : {}),
@@ -43,9 +43,9 @@ export const messagesSearchCommand = (messenger) => {
43
43
  .option("--timezone <zone>", "the IANA timezone for calendar date boundaries")
44
44
  .addHelpText("after", SAVE_HELP)
45
45
  .addHelpText("after", `${syncsFirst(messenger)
46
- ? `Search reads the local archive: a chat never fetched is not searched. Prepare it once with
47
- \`${messenger.app.command} messages sync --all --background\` (the last 90 days of every chat); the answer's
48
- coverage says what was searched and coverage.next what would improve it.`
46
+ ? `Search reads the local archive: a chat never fetched is not searched. The answer's coverage says what was
47
+ searched and coverage.next names one chat that would improve it. A sync downloads from the owner's account, so
48
+ offer \`${messenger.app.command} messages sync <chat>\` and run it only once the owner approves.`
49
49
  : `Search reads the local archive: what \`${messenger.app.command} serve\` has kept since the account was linked.
50
50
  Keep serve running so new messages are searchable.`} Notes are
51
51
  \`${messenger.app.command} search notes\`, messages and notes at once \`${messenger.app.command} search all\`; for mail,
@@ -72,12 +72,9 @@ export const coverageGaps = (found, shown) => {
72
72
  const stale = found.refreshed?.complete === false;
73
73
  if (!coverage?.next && !stale)
74
74
  return { next: [], missing: [] };
75
+ const chat = coverage?.chat ?? coverage?.attention[0]?.chatId;
75
76
  return {
76
- next: [
77
- nextStep("messages.sync", {
78
- args: coverage?.chat === undefined ? { all: true, background: true } : { chat: coverage.chat },
79
- }),
80
- ],
77
+ next: chat === undefined ? [] : [nextStep("messages.sync", { args: { chat } })],
81
78
  missing: [
82
79
  {
83
80
  field: "coverage",
@@ -2,6 +2,7 @@ import { annotate } from "@wirecat/cli-base/commands";
2
2
  import { skillCommand as sharedSkillCommand } from "@wirecat/cli-base/skill";
3
3
  import { environmentOf, outputFor } from "./context.js";
4
4
  import { grantsAuthority } from "./effects.js";
5
+ import { printItem } from "./envelope.js";
5
6
  /**
6
7
  * `skill show` and `skill install`. `skill` is the CLI's own SKILL.md, shipped beside `dist/` in the
7
8
  * package and in a checkout alike, so the skill printed is always this version's.
@@ -9,7 +10,10 @@ import { grantsAuthority } from "./effects.js";
9
10
  export const skillCommand = (app, skill) => {
10
11
  const command = sharedSkillCommand(app, skill, (command) => {
11
12
  const { env } = environmentOf(command);
12
- return { ...outputFor(command), ...(env ? { env } : {}) };
13
+ const output = outputFor(command);
14
+ // cli-base prints each answer bare; the envelope is cli-core's.
15
+ const renderer = { ...output.renderer, result: (value) => printItem(output, value) };
16
+ return { ...output, renderer, ...(env ? { env } : {}) };
13
17
  }, SHARED_SKILLS);
14
18
  // An installed skill is instructions every later agent follows.
15
19
  const install = command.commands.find((sub) => sub.name() === "install");
@@ -19,8 +19,8 @@ export interface McpStdio {
19
19
  close: () => Promise<number>;
20
20
  }
21
21
  /**
22
- * A CLI's `mcp` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
23
- * `start` passes the two streams to `run(["<profile>", "mcp"], { ...yourOptions, ...streams })`. It has
22
+ * A CLI's `mcp serve` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
23
+ * `start` passes the two streams to `run(["<profile>", "mcp", "serve"], { ...yourOptions, ...streams })`. It has
24
24
  * already answered `initialize` when this resolves.
25
25
  */
26
26
  export declare const mcpOverStdio: (start: (streams: McpStdioStreams) => Promise<number>, { protocolVersion }?: {
@@ -8,8 +8,8 @@ const parsedOrText = (text) => {
8
8
  }
9
9
  };
10
10
  /**
11
- * A CLI's `mcp` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
12
- * `start` passes the two streams to `run(["<profile>", "mcp"], { ...yourOptions, ...streams })`. It has
11
+ * A CLI's `mcp serve` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
12
+ * `start` passes the two streams to `run(["<profile>", "mcp", "serve"], { ...yourOptions, ...streams })`. It has
13
13
  * already answered `initialize` when this resolves.
14
14
  */
15
15
  export const mcpOverStdio = async (start, { protocolVersion = "2025-06-18" } = {}) => {
@@ -15,7 +15,7 @@ export declare const readActivity: (path: string) => Activity;
15
15
  export declare const calledTools: (body: unknown) => string[];
16
16
  /**
17
17
  * Keeps the activity file of one running server. A call is written at once, so status shows it while the owner
18
- * watches; other requests at most every two seconds. Its own file, not the token file: `mcp --revoke` rewrites that
18
+ * watches; other requests at most every two seconds. Its own file, not the token file: `mcp revoke` rewrites that
19
19
  * from another process.
20
20
  */
21
21
  export declare const activityRecorder: (path: string, now?: () => number) => {
@@ -30,7 +30,7 @@ export const calledTools = (body) => (Array.isArray(body) ? body : [body]).flatM
30
30
  });
31
31
  /**
32
32
  * Keeps the activity file of one running server. A call is written at once, so status shows it while the owner
33
- * watches; other requests at most every two seconds. Its own file, not the token file: `mcp --revoke` rewrites that
33
+ * watches; other requests at most every two seconds. Its own file, not the token file: `mcp revoke` rewrites that
34
34
  * from another process.
35
35
  */
36
36
  export const activityRecorder = (path, now = Date.now) => {
@@ -132,7 +132,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
132
132
  while (stored.clients.length > MAX_CLIENTS) {
133
133
  const idle = stored.clients.findIndex((one) => !holding.has(one.id));
134
134
  if (idle === -1)
135
- return oauthError("invalid_client_metadata", "too many logged-in apps — run mcp --revoke first");
135
+ return oauthError("invalid_client_metadata", "too many logged-in apps — run mcp revoke first");
136
136
  stored.clients.splice(idle, 1);
137
137
  }
138
138
  store.write(stored);
@@ -235,12 +235,9 @@ export const coverageOf = async (store, { scopeAccounts, selectedChat }, messeng
235
235
  const completeness = [];
236
236
  const stats = new Map();
237
237
  let inventoryComplete = scopeAccounts.length > 0;
238
- let ownInventory = true;
239
238
  for (const selected of scopeAccounts) {
240
239
  if (!(await store.sync.syncState(selected, CHAT_LIST_KEY))) {
241
240
  inventoryComplete = false;
242
- if (messenger.provider === undefined || selected.provider === messenger.provider)
243
- ownInventory = false;
244
241
  }
245
242
  const chatIds = selectedChat
246
243
  ? selected.provider === selectedChat.account.provider && selected.account === selectedChat.account.account
@@ -274,15 +271,11 @@ export const coverageOf = async (store, { scopeAccounts, selectedChat }, messeng
274
271
  const behindCount = own.filter(behind).length;
275
272
  // A chat held back to a window stays partial for good, so only what a fetch would change is advised.
276
273
  const command = messenger.app?.command;
277
- const next = command === undefined
274
+ // One named chat, never `--all`: a download is the owner's call and stays bounded to what they approve.
275
+ const syncChat = selectedChat ? (neverFetched || behindCount ? selectedChat : null) : (attention[0] ?? null);
276
+ const next = command === undefined || !syncChat || !syncsOneChat(messenger)
278
277
  ? null
279
- : selectedChat
280
- ? (neverFetched || behindCount) && syncsOneChat(messenger)
281
- ? `${command} messages sync ${selectedChat.chatId}`
282
- : null
283
- : (neverFetched || behindCount || !ownInventory) && messenger.history !== "store"
284
- ? `${command} messages sync --all --background`
285
- : null;
278
+ : `${command} messages sync ${syncChat.chatId}`;
286
279
  const state = selectedChat && completeness.length > 0 && completeness.every(({ state }) => state === "complete")
287
280
  ? "complete"
288
281
  : completeness.some(({ state }) => state !== "unknown")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.461.0",
3
+ "version": "0.463.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",
package/parity.json CHANGED
@@ -789,7 +789,7 @@
789
789
  },
790
790
  "--port": {
791
791
  "value": "<port>",
792
- "meaning": "the local port behind the tunnel, for mcp --http or bot watch --public-url"
792
+ "meaning": "the local port behind the tunnel, for mcp serve --http or bot watch --public-url"
793
793
  },
794
794
  "--priority": {
795
795
  "value": "<n>",
@@ -804,7 +804,7 @@
804
804
  },
805
805
  "--public-url": {
806
806
  "value": "<url>",
807
- "meaning": "the tunnel's https address: the browser apps reach mcp --http through it, a messenger delivers to bot watch"
807
+ "meaning": "the tunnel's https address: the browser apps reach mcp serve --http through it, a messenger delivers to bot watch"
808
808
  },
809
809
  "--question": {
810
810
  "meaning": "match only questions"
@@ -861,9 +861,6 @@
861
861
  "value": "<n>",
862
862
  "meaning": "the revision you read before editing"
863
863
  },
864
- "--revoke": {
865
- "meaning": "forget every login given to a browser app over --http"
866
- },
867
864
  "--revote": {
868
865
  "meaning": "people may change their vote"
869
866
  },
@@ -1732,7 +1729,6 @@
1732
1729
  },
1733
1730
  "bot mcp": {
1734
1731
  "in": ["max", "tg"],
1735
- "options": {},
1736
1732
  "planned": {
1737
1733
  "wa": "whatsapp-cli: wa bot API, plans/2026-10-22-wa-bot-api"
1738
1734
  }
@@ -1744,6 +1740,13 @@
1744
1740
  "wa": "whatsapp-cli: wa bot API, plans/2026-10-22-wa-bot-api"
1745
1741
  }
1746
1742
  },
1743
+ "bot mcp serve": {
1744
+ "in": ["max", "tg"],
1745
+ "options": {},
1746
+ "planned": {
1747
+ "wa": "whatsapp-cli: wa bot API, plans/2026-10-22-wa-bot-api"
1748
+ }
1749
+ },
1747
1750
  "bot me": {
1748
1751
  "in": ["max", "tg"],
1749
1752
  "planned": {
@@ -3138,22 +3141,7 @@
3138
3141
  }
3139
3142
  },
3140
3143
  "mcp": {
3141
- "in": "all",
3142
- "options": {
3143
- "--http": "all",
3144
- "--permission": "all",
3145
- "--port": "all",
3146
- "--public-url": "all",
3147
- "--revoke": "all",
3148
- "--yes": {
3149
- "in": [],
3150
- "planned": {
3151
- "max": "P7",
3152
- "tg": "P7",
3153
- "wa": "P7"
3154
- }
3155
- }
3156
- }
3144
+ "in": "all"
3157
3145
  },
3158
3146
  "mcp add": {
3159
3147
  "in": "all",
@@ -3182,6 +3170,26 @@
3182
3170
  }
3183
3171
  }
3184
3172
  },
3173
+ "mcp revoke": {
3174
+ "in": "all"
3175
+ },
3176
+ "mcp serve": {
3177
+ "in": "all",
3178
+ "options": {
3179
+ "--http": "all",
3180
+ "--permission": "all",
3181
+ "--port": "all",
3182
+ "--public-url": "all",
3183
+ "--yes": {
3184
+ "in": [],
3185
+ "planned": {
3186
+ "max": "P7",
3187
+ "tg": "P7",
3188
+ "wa": "P7"
3189
+ }
3190
+ }
3191
+ }
3192
+ },
3185
3193
  "mcp server": {
3186
3194
  "in": "all"
3187
3195
  },