@wirecat/cli-core 0.465.0 → 0.466.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
@@ -5,7 +5,27 @@ 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
- ## Unreleased
8
+ ## 0.466.0 — 27.10.2026
9
+
10
+ ### Changed — may break callers
11
+
12
+ - **`account list` says whether each profile is signed in on this computer.** The `account_profile` entity gains an
13
+ optional `signed_in`, set when the messenger sets `Messenger.profileState`; a caller that checks items against an
14
+ older, strict copy of it refuses the new field.
15
+
16
+ ### Fixed
17
+
18
+ - **`<cli> mcp serve` with no profile marks the profiles that are not signed in.** A messenger sets the new optional
19
+ `Messenger.profileState(profile, env)` (`signed-in`, `signed-out` or `bot`), read from its own files without
20
+ connecting. The `profile` field keeps its `enum` and gains `oneOf` entries with a `title` per profile
21
+ (`default — not signed in`), as MCP's titled enums, and its description lists who is signed in. Bot profiles are left
22
+ out: they have their own server. A call for a profile that is not signed in is refused with how to sign it in
23
+ (`<cli> <profile> setup`, or WireCat Desktop) and starts nothing. Unset, every profile counts as signed in, as before.
24
+ - **The "no profile named" error marks the listed profiles that are not signed in** (`beta (not signed in)`), when the
25
+ messenger sets `profileState`.
26
+ - **The MCP login page says why a code that just worked is now wrong.** Each login code works once; after an app
27
+ logs in, the server shows a new one. The wrong-code message now says so and points to WireCat Desktop and
28
+ `<cli> mcp server status`, where the new code is.
9
29
 
10
30
  ## 0.465.0 — 27.10.2026
11
31
 
@@ -88,7 +88,7 @@ export { type MeetingSeriesInventoryReader, readMeetingSeriesInventory } from ".
88
88
  export { accountFileFor, profilesHere, rememberAccount } from "./messenger/accounts.js";
89
89
  export { terminalAsker } from "./messenger/ask.js";
90
90
  export type { CompletionOptions } from "./messenger/complete-command.js";
91
- export { type ConnectOptions, connectFor, type Messenger, type MessengerContext, messengerContext, type ReadConnection, type StoredMessages, } from "./messenger/context.js";
91
+ export { type ConnectOptions, connectFor, type Messenger, type MessengerContext, messengerContext, type ProfileState, type ReadConnection, type StoredMessages, } from "./messenger/context.js";
92
92
  export { COMMAND_NEEDS } from "./messenger/lacks.js";
93
93
  export { httpTokenFile, type McpEnvironment, serverEntry } from "./messenger/mcp-command.js";
94
94
  export { type MessageItem, messageItem, type ReadMessage } from "./messenger/message-items.js";
@@ -45,6 +45,9 @@ export const accountCommand = (messenger) => {
45
45
  current: profile === current,
46
46
  account: key?.account ?? null,
47
47
  name: key && store ? await store.accounts.accountName(key) : null,
48
+ ...(messenger.profileState
49
+ ? { signed_in: messenger.profileState(profile, context.env) !== "signed-out" }
50
+ : {}),
48
51
  });
49
52
  }
50
53
  renderAnswer(this, context, wholeList("account list", items));
@@ -49,6 +49,7 @@ export interface LazyStore {
49
49
  }
50
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
+ export type ProfileState = "signed-in" | "signed-out" | "bot";
52
53
  /** What one messenger CLI hands the shared commands. Everything else about it stays in its own code. */
53
54
  export interface Messenger extends ServiceMessenger {
54
55
  /**
@@ -65,6 +66,12 @@ export interface Messenger extends ServiceMessenger {
65
66
  * Called inside `--timeout`; the context tracks and closes what it returns.
66
67
  */
67
68
  connect: (command: Command, context: BaseContext, options?: ConnectOptions) => Promise<MessengerAdapter>;
69
+ /**
70
+ * A profile's state, read from the messenger's own files without connecting: `signed-in` has a session, `signed-out`
71
+ * has none yet, `bot` speaks for a bot. `<tool> mcp` with no profile offers the personal ones and marks those not
72
+ * signed in; `account list` and the profile errors say so too. Unset: every profile counts as signed in.
73
+ */
74
+ profileState?: (profile: string, env: NodeJS.ProcessEnv) => ProfileState;
68
75
  /** The help for a `<chat>` argument, in this messenger's words. */
69
76
  chatArgument: string;
70
77
  /** The group settings this messenger has, as `chats update` offers them; every one when unset. */
@@ -262,6 +269,8 @@ export declare const connectedFor: (command: Command, messenger: Pick<Messenger,
262
269
  adapter: MessengerAdapter;
263
270
  close: () => Promise<void>;
264
271
  }>;
272
+ /** `default (not signed in)`: a profile as an error lists it among the choices. */
273
+ export declare const markedProfile: (messenger: Messenger, profile: string, env: NodeJS.ProcessEnv) => string;
265
274
  /**
266
275
  * A write to the local store alone. `deny` already stopped the command (`messengerContext`); `readonly`
267
276
  * stops this write too. There is no question to put to the owner here, so `ask` refuses rather than
@@ -288,9 +288,11 @@ export const connectedFor = async (command, messenger, base, options) => {
288
288
  }),
289
289
  };
290
290
  };
291
- const unrecorded = (command, messenger, profile, { offline, profilesHere }) => {
291
+ /** `default (not signed in)`: a profile as an error lists it among the choices. */
292
+ export const markedProfile = (messenger, profile, env) => messenger.profileState?.(profile, env) === "signed-out" ? `${profile} (not signed in)` : profile;
293
+ const unrecorded = (command, messenger, profile, { offline, profilesHere }, env) => {
292
294
  if (!profilesHere.includes(profile)) {
293
- return new CliError("not_found", `no profile named "${profile}" — profiles here: ${profilesHere.length > 0 ? profilesHere.join(", ") : "none"}`, { profiles: profilesHere });
295
+ return new CliError("not_found", `no profile named "${profile}" — profiles here: ${profilesHere.length > 0 ? profilesHere.map((one) => markedProfile(messenger, one, env)).join(", ") : "none"}`, { profiles: profilesHere });
294
296
  }
295
297
  const words = `${messenger.app.command} ${asFirstWord(profile)}`;
296
298
  const setup = rootOf(command).commands.some((one) => one.name() === "setup") ? `\`${words}setup\`` : undefined;
@@ -361,7 +363,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
361
363
  const accountFor = () => {
362
364
  const account = messenger.accountOf?.(command) ?? recalledAccount(app, provider, profile, base.env);
363
365
  if (!account)
364
- throw unrecorded(command, messenger, profile, base.settings);
366
+ throw unrecorded(command, messenger, profile, base.settings, base.env);
365
367
  return account;
366
368
  };
367
369
  const guard = messenger.guard?.(command, base.settings, warn) ??
@@ -222,7 +222,11 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
222
222
  const { app } = messenger;
223
223
  const { env } = context;
224
224
  const tokenFile = httpTokenFile(app, ALL_PROFILES, env);
225
- const profiles = () => messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true }).profilesHere;
225
+ const state = (profile) => messenger.profileState?.(profile, env) ?? "signed-in";
226
+ // Read again for each session, so a profile set up while the server runs appears. A bot has its own server.
227
+ const profiles = () => messenger
228
+ .resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true })
229
+ .profilesHere.filter((profile) => state(profile) !== "bot");
226
230
  if (profiles().length === 0)
227
231
  throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp serve\` again`);
228
232
  const given = environmentOf(self).mcp ?? {};
@@ -235,6 +239,9 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
235
239
  version: app.version,
236
240
  name: messenger.name ?? app.command,
237
241
  profiles,
242
+ signedIn: (profile) => state(profile) === "signed-in",
243
+ signIn: (profile) => `profile ${profile} is not signed in — run \`${app.command} ${profile} setup\`, or sign in from WireCat Desktop, ` +
244
+ "then try again. If the owner did not choose this profile, ask which one.",
238
245
  child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", "serve", ...tightened], env }),
239
246
  instructions: instructions({
240
247
  command: app.command,
@@ -39,6 +39,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
39
39
  connection?: "exclusive";
40
40
  pace?: import("../../sends/pace.js").PaceRate;
41
41
  resolveSettings: (flags: import("../settings.js").GlobalFlags, options?: import("../settings.js").ResolveOptions) => import("../settings.js").Settings;
42
+ profileState?: (profile: string, env: NodeJS.ProcessEnv) => import("./context.js").ProfileState;
42
43
  chatArgument: string;
43
44
  groupSettings?: readonly import("../../domain/models.js").GroupSettingKey[];
44
45
  pollQuiz?: boolean;
@@ -4,4 +4,6 @@ export declare const accountProfile: v.StrictObjectSchema<{
4
4
  readonly current: v.BooleanSchema<undefined>;
5
5
  readonly account: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
6
6
  readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
7
+ /** False for a profile with no session on this computer; absent when the messenger does not say. */
8
+ readonly signed_in: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
7
9
  }, undefined>;
@@ -5,4 +5,6 @@ export const accountProfile = v.strictObject({
5
5
  current: v.boolean(),
6
6
  account: v.nullable(id),
7
7
  name: v.nullable(text),
8
+ /** False for a profile with no session on this computer; absent when the messenger does not say. */
9
+ signed_in: v.optional(v.boolean()),
8
10
  });
@@ -4177,6 +4177,7 @@ export declare const ENTITIES: {
4177
4177
  readonly current: v.BooleanSchema<undefined>;
4178
4178
  readonly account: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
4179
4179
  readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
4180
+ readonly signed_in: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
4180
4181
  }, undefined>;
4181
4182
  readonly organization: v.StrictObjectSchema<{
4182
4183
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
@@ -16,14 +16,22 @@ export interface AllProfilesOptions {
16
16
  name: string;
17
17
  /** Read again for each new session, so a profile set up while serving over HTTP shows up. */
18
18
  profiles: () => readonly string[];
19
+ /** Whether a profile has a session now; unset, every one does. A call for one without is refused with `signIn`. */
20
+ signedIn?: (profile: string) => boolean;
21
+ /** How to sign a profile in, for the refusal of a call it cannot serve yet. */
22
+ signIn?: (profile: string) => string;
19
23
  child: (profile: string) => ChildSpec;
20
24
  instructions: string;
21
25
  idleMs?: number;
22
26
  /** A child's stderr, line by line; never this process's stdout, which carries the protocol. */
23
27
  note?: (line: string) => void;
24
28
  }
25
- /** The child's tool with the one field that picks the profile, required and without a default. */
26
- export declare const withProfile: (tool: Tool, profiles: readonly string[], name: string) => Tool;
29
+ /**
30
+ * The child's tool with the one field that picks the profile, required and without a default. Each profile carries
31
+ * its state as the title an app shows in its menu (`oneOf` with `const` and `title`, as MCP's titled enums), and the
32
+ * description repeats it for an app that shows only the description.
33
+ */
34
+ export declare const withProfile: (tool: Tool, profiles: readonly string[], name: string, signedIn?: (profile: string) => boolean) => Tool;
27
35
  /**
28
36
  * **One server for every profile of a tool** (TASK-26): `<tool> mcp` shows the tools `<tool> <profile> mcp` shows,
29
37
  * each with a required `profile`, and runs a call in that profile's own server, started as a child the first time
@@ -138,9 +138,17 @@ const pool = (options) => {
138
138
  };
139
139
  return { use, close };
140
140
  };
141
- /** The child's tool with the one field that picks the profile, required and without a default. */
142
- export const withProfile = (tool, profiles, name) => {
141
+ /**
142
+ * The child's tool with the one field that picks the profile, required and without a default. Each profile carries
143
+ * its state as the title an app shows in its menu (`oneOf` with `const` and `title`, as MCP's titled enums), and the
144
+ * description repeats it for an app that shows only the description.
145
+ */
146
+ export const withProfile = (tool, profiles, name, signedIn = () => true) => {
143
147
  const schema = tool.inputSchema;
148
+ const out = profiles.filter((profile) => !signedIn(profile));
149
+ const states = out.length === 0
150
+ ? ""
151
+ : ` Signed in: ${profiles.filter(signedIn).join(", ") || "none"} · Not signed in: ${out.join(", ")}.`;
144
152
  return {
145
153
  ...tool,
146
154
  inputSchema: {
@@ -149,7 +157,11 @@ export const withProfile = (tool, profiles, name) => {
149
157
  profile: {
150
158
  type: "string",
151
159
  enum: [...profiles],
152
- description: `Which ${name} account, by its profile. If the owner did not name one, ask which — never guess.`,
160
+ oneOf: profiles.map((profile) => ({
161
+ const: profile,
162
+ title: signedIn(profile) ? profile : `${profile} — not signed in`,
163
+ })),
164
+ description: `Which ${name} account, by its profile. If the owner did not name one, ask which — never guess.${states}`,
153
165
  },
154
166
  ...schema.properties,
155
167
  },
@@ -174,24 +186,29 @@ export const allProfilesServer = (options) => {
174
186
  // list comes from whichever answers first and is kept.
175
187
  const tools = (profiles) => {
176
188
  listed ??= children
177
- .use(profiles[0], (child) => child.request("tools/list", {}))
189
+ .use(profiles.find(signedIn) ?? profiles[0], (child) => child.request("tools/list", {}))
178
190
  .then((answer) => (answer.tools ?? []));
179
191
  listed.catch(() => {
180
192
  listed = undefined;
181
193
  });
182
194
  return listed;
183
195
  };
196
+ const signedIn = (profile) => options.signedIn?.(profile) ?? true;
184
197
  const build = () => {
185
198
  const profiles = [...options.profiles()].sort();
186
199
  const server = new McpServer({ name: options.command, version: options.version }, { capabilities: { tools: {} }, instructions: options.instructions });
187
200
  server.server.setRequestHandler("tools/list", async () => ({
188
- tools: profiles.length === 0 ? [] : (await tools(profiles)).map((one) => withProfile(one, profiles, options.name)),
201
+ tools: profiles.length === 0
202
+ ? []
203
+ : (await tools(profiles)).map((one) => withProfile(one, profiles, options.name, signedIn)),
189
204
  }));
190
205
  server.server.setRequestHandler("tools/call", async (request) => {
191
206
  const { name, arguments: given = {}, _meta } = request.params;
192
207
  const { profile, ...rest } = given;
193
208
  if (typeof profile !== "string" || !profiles.includes(profile))
194
209
  return refusal(`${name} needs "profile", one of: ${profiles.join(", ")}. If the owner did not say which account, ask.`);
210
+ if (!signedIn(profile))
211
+ return refusal(options.signIn?.(profile) ?? `profile ${profile} is not signed in.`);
195
212
  try {
196
213
  return (await children.use(profile, (child) => child.request("tools/call", { name, arguments: rest, ...(_meta ? { _meta } : {}) })));
197
214
  }
@@ -201,7 +201,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
201
201
  retryAt = now() + WRONG_CODE_WAIT_MS;
202
202
  return throttled();
203
203
  }
204
- return page(params, client, `Wrong code. ${WRONG_TRIES - wrong} tries left.`);
204
+ return page(params, client, `Wrong code. Each code works for one login: after an app logs in, a new one appears in WireCat Desktop and in "${appName} mcp server status". ${WRONG_TRIES - wrong} tries left.`);
205
205
  }
206
206
  wrong = 0;
207
207
  freshCode();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.465.0",
3
+ "version": "0.466.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",